572 lines
15 KiB
Dart
572 lines
15 KiB
Dart
|
|
import 'dart:math';
|
||
|
|
import 'dart:async';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import '../main.dart';
|
||
|
|
import '../models/game_items.dart';
|
||
|
|
|
||
|
|
class GameManager {
|
||
|
|
final Random random = Random();
|
||
|
|
final List<Category> categories;
|
||
|
|
Category? currentCategory;
|
||
|
|
|
||
|
|
final int gameWidth = 10;
|
||
|
|
final int gameHeight = 14;
|
||
|
|
final VoidCallback onGameOver;
|
||
|
|
|
||
|
|
List<GameItem> gameItems = [];
|
||
|
|
List<PowerUp> activePowerUps = [];
|
||
|
|
|
||
|
|
double baseSpeed = 1.0;
|
||
|
|
int score = 0;
|
||
|
|
int combo = 0;
|
||
|
|
int healthPoints = 5; // Increased initial health points
|
||
|
|
int spawnCooldown = 0;
|
||
|
|
|
||
|
|
String targetWord = ''; // Current word to collect
|
||
|
|
List<String> collectedLetters = []; // Letters collected so far
|
||
|
|
|
||
|
|
// Add snake positions reference
|
||
|
|
List<Offset> snakePositions = [];
|
||
|
|
|
||
|
|
// Add snake direction reference
|
||
|
|
Direction snakeDirection = Direction.right;
|
||
|
|
|
||
|
|
// Independent magnet effect timer
|
||
|
|
Timer? _magnetTimer;
|
||
|
|
|
||
|
|
GameManager({
|
||
|
|
required this.categories,
|
||
|
|
required this.onGameOver,
|
||
|
|
}) {
|
||
|
|
// Start with the default category
|
||
|
|
currentCategory = categories.firstWhere((c) => c.name == 'Default');
|
||
|
|
_selectNewTargetWord();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _selectNewTargetWord() {
|
||
|
|
if (currentCategory == null) {
|
||
|
|
currentCategory = categories.firstWhere((c) => c.name == 'Default');
|
||
|
|
}
|
||
|
|
|
||
|
|
targetWord = currentCategory!
|
||
|
|
.correctAnswers[random.nextInt(currentCategory!.correctAnswers.length)];
|
||
|
|
|
||
|
|
collectedLetters = [];
|
||
|
|
}
|
||
|
|
|
||
|
|
String get nextRequiredLetter {
|
||
|
|
if (targetWord.isEmpty) return '';
|
||
|
|
return targetWord[collectedLetters.length];
|
||
|
|
}
|
||
|
|
|
||
|
|
void spawnGameItem() {
|
||
|
|
// Remove expired items first
|
||
|
|
gameItems.removeWhere((item) => item.isExpired);
|
||
|
|
|
||
|
|
// Decrease remaining cycles for all items
|
||
|
|
for (var item in gameItems) {
|
||
|
|
item.decrementCycle();
|
||
|
|
}
|
||
|
|
if (random.nextDouble() <
|
||
|
|
gameItems.length.toDouble() / (gameWidth * gameHeight)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Spawn category challenge less frequently
|
||
|
|
if (currentCategory?.name == 'Default' && random.nextDouble() < 0.1) {
|
||
|
|
return _spawnCategoryChallenge();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Spawn power-ups
|
||
|
|
if (random.nextDouble() < 0.25) {
|
||
|
|
return _spawnPowerUp();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Always try to spawn a target
|
||
|
|
if (random.nextDouble() < 0.3) {
|
||
|
|
return _spawnTarget();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper method to check if position is available and safe
|
||
|
|
bool _isPositionAvailable(Offset position) {
|
||
|
|
// Check if position is occupied by snake
|
||
|
|
if (snakePositions.contains(position)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if position is occupied by other items
|
||
|
|
if (gameItems.any((item) => item.position == position)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Don't spawn directly in front of the snake
|
||
|
|
if (snakePositions.isNotEmpty) {
|
||
|
|
Offset snakeHead = snakePositions.first;
|
||
|
|
|
||
|
|
// Check positions in front of the snake
|
||
|
|
switch (snakeDirection) {
|
||
|
|
case Direction.up:
|
||
|
|
if (position.dx == snakeHead.dx && position.dy < snakeHead.dy) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
case Direction.down:
|
||
|
|
if (position.dx == snakeHead.dx && position.dy > snakeHead.dy) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
case Direction.left:
|
||
|
|
if (position.dy == snakeHead.dy && position.dx < snakeHead.dx) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
case Direction.right:
|
||
|
|
if (position.dy == snakeHead.dy && position.dx > snakeHead.dx) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Don't spawn too close to snake head
|
||
|
|
double distance = (position - snakeHead).distance;
|
||
|
|
if (distance < 2) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
Offset _getRandomPosition() {
|
||
|
|
int attempts = 0;
|
||
|
|
Offset position;
|
||
|
|
List<Offset> availablePositions = [];
|
||
|
|
|
||
|
|
// Generate all possible positions
|
||
|
|
for (int x = 1; x <= gameWidth; x++) {
|
||
|
|
for (int y = 1; y <= gameHeight; y++) {
|
||
|
|
Offset pos = Offset(x.toDouble(), y.toDouble());
|
||
|
|
if (_isPositionAvailable(pos)) {
|
||
|
|
availablePositions.add(pos);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If no positions available, return invalid position
|
||
|
|
if (availablePositions.isEmpty) {
|
||
|
|
return const Offset(-1, -1);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Return random available position
|
||
|
|
return availablePositions[random.nextInt(availablePositions.length)];
|
||
|
|
}
|
||
|
|
|
||
|
|
void _spawnTarget() {
|
||
|
|
final position = _getRandomPosition();
|
||
|
|
if (position.dx < 0) return; // No free position found
|
||
|
|
|
||
|
|
final isCorrect = random.nextDouble() < 0.5;
|
||
|
|
String content;
|
||
|
|
|
||
|
|
if (isCorrect) {
|
||
|
|
content = nextRequiredLetter;
|
||
|
|
} else {
|
||
|
|
// Generate more diverse wrong letters
|
||
|
|
final availableLetters =
|
||
|
|
List.generate(26, (i) => String.fromCharCode(65 + i))
|
||
|
|
..remove(nextRequiredLetter); // Remove the correct letter
|
||
|
|
|
||
|
|
// Bias towards letters from the target word for more challenge
|
||
|
|
if (random.nextDouble() < 0.3 && targetWord.isNotEmpty) {
|
||
|
|
final targetLetters = targetWord.split('')
|
||
|
|
..remove(nextRequiredLetter); // Remove the next required letter
|
||
|
|
if (targetLetters.isNotEmpty) {
|
||
|
|
content = targetLetters[random.nextInt(targetLetters.length)];
|
||
|
|
} else {
|
||
|
|
content = availableLetters[random.nextInt(availableLetters.length)];
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
content = availableLetters[random.nextInt(availableLetters.length)];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_resetSpawnCooldown();
|
||
|
|
gameItems.add(GameItem(
|
||
|
|
position: position,
|
||
|
|
type: ItemType.letter,
|
||
|
|
content: content,
|
||
|
|
isCorrect: isCorrect,
|
||
|
|
value: currentCategory!.name == 'Default' ? 1.0 : 2.0,
|
||
|
|
maxCycles: 40,
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
void _spawnPowerUp() {
|
||
|
|
final position = _getRandomPosition();
|
||
|
|
if (position.dx < 0) return; // No free position found
|
||
|
|
|
||
|
|
final powerUpTypes = [
|
||
|
|
ItemType.slowTime,
|
||
|
|
ItemType.scoreMultiplier,
|
||
|
|
ItemType.shield,
|
||
|
|
ItemType.magnet,
|
||
|
|
ItemType.health
|
||
|
|
];
|
||
|
|
|
||
|
|
final type = powerUpTypes[random.nextInt(powerUpTypes.length)];
|
||
|
|
|
||
|
|
String content;
|
||
|
|
int maxCycles;
|
||
|
|
|
||
|
|
switch (type) {
|
||
|
|
case ItemType.slowTime:
|
||
|
|
content = '🐌';
|
||
|
|
maxCycles = 25;
|
||
|
|
case ItemType.health:
|
||
|
|
content = '❤️';
|
||
|
|
maxCycles = 25;
|
||
|
|
case ItemType.scoreMultiplier:
|
||
|
|
content = '2x';
|
||
|
|
maxCycles = 25;
|
||
|
|
case ItemType.shield:
|
||
|
|
content = '🛡️';
|
||
|
|
maxCycles = 30;
|
||
|
|
case ItemType.magnet:
|
||
|
|
content = '🧲';
|
||
|
|
maxCycles = 35;
|
||
|
|
default:
|
||
|
|
content = '?';
|
||
|
|
maxCycles = 25;
|
||
|
|
}
|
||
|
|
|
||
|
|
_resetSpawnCooldown();
|
||
|
|
gameItems.add(GameItem(
|
||
|
|
position: position,
|
||
|
|
type: type,
|
||
|
|
content: content,
|
||
|
|
maxCycles: maxCycles,
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
void _resetSpawnCooldown() {
|
||
|
|
spawnCooldown = 5;
|
||
|
|
}
|
||
|
|
|
||
|
|
void _spawnCategoryChallenge() {
|
||
|
|
final position = _getRandomPosition();
|
||
|
|
if (position.dx < 0) return; // No free position found
|
||
|
|
|
||
|
|
Category newCategory;
|
||
|
|
do {
|
||
|
|
newCategory = categories[random.nextInt(categories.length)];
|
||
|
|
} while (newCategory.name == currentCategory?.name);
|
||
|
|
|
||
|
|
_resetSpawnCooldown();
|
||
|
|
gameItems.add(GameItem(
|
||
|
|
position: position,
|
||
|
|
type: ItemType.categoryChallenge,
|
||
|
|
content: newCategory.name,
|
||
|
|
maxCycles: 60,
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
bool collectItem(GameItem item) {
|
||
|
|
gameItems.remove(item);
|
||
|
|
|
||
|
|
switch (item.type) {
|
||
|
|
case ItemType.letter:
|
||
|
|
case ItemType.word:
|
||
|
|
return _handleTargetCollection(item);
|
||
|
|
case ItemType.slowTime:
|
||
|
|
_activateSlowTime();
|
||
|
|
return true;
|
||
|
|
case ItemType.scoreMultiplier:
|
||
|
|
_activateScoreMultiplier();
|
||
|
|
return true;
|
||
|
|
case ItemType.categoryChallenge:
|
||
|
|
_startCategoryChallenge(item.content);
|
||
|
|
return true;
|
||
|
|
case ItemType.health:
|
||
|
|
_collectHealth();
|
||
|
|
return true;
|
||
|
|
case ItemType.shield:
|
||
|
|
_activateShield();
|
||
|
|
return true;
|
||
|
|
case ItemType.magnet:
|
||
|
|
_activateMagnet();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool _handleTargetCollection(GameItem item) {
|
||
|
|
// Check if shield is active
|
||
|
|
bool isShieldActive = activePowerUps
|
||
|
|
.where((p) => p.type == PowerUpType.shield && p.isActive)
|
||
|
|
.isNotEmpty;
|
||
|
|
|
||
|
|
// Check if this is the next required letter
|
||
|
|
if (item.content == nextRequiredLetter) {
|
||
|
|
collectedLetters.add(item.content);
|
||
|
|
combo++;
|
||
|
|
score += ((combo * item.value) * _getCurrentMultiplier()).round();
|
||
|
|
|
||
|
|
// Check if word is completed
|
||
|
|
if (collectedLetters.length == targetWord.length) {
|
||
|
|
score +=
|
||
|
|
(50 * _getCurrentMultiplier()).round(); // Bonus for completing word
|
||
|
|
_selectNewTargetWord();
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
} else {
|
||
|
|
// Wrong letter collected
|
||
|
|
combo = 0;
|
||
|
|
if (!isShieldActive) {
|
||
|
|
healthPoints--;
|
||
|
|
}
|
||
|
|
return healthPoints > 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _activateSlowTime() {
|
||
|
|
final slowPowerUp = PowerUp(
|
||
|
|
type: PowerUpType.slowTime,
|
||
|
|
duration: const Duration(seconds: 5),
|
||
|
|
value: 0.5,
|
||
|
|
);
|
||
|
|
slowPowerUp.activate();
|
||
|
|
activePowerUps.add(slowPowerUp);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _activateScoreMultiplier() {
|
||
|
|
final multiplierPowerUp = PowerUp(
|
||
|
|
type: PowerUpType.scoreMultiplier,
|
||
|
|
duration: const Duration(seconds: 10),
|
||
|
|
value: 2.0,
|
||
|
|
);
|
||
|
|
multiplierPowerUp.activate();
|
||
|
|
activePowerUps.add(multiplierPowerUp);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _startCategoryChallenge(String categoryName) {
|
||
|
|
currentCategory = categories.firstWhere((c) => c.name == categoryName);
|
||
|
|
_selectNewTargetWord();
|
||
|
|
Future.delayed(const Duration(seconds: 30), () {
|
||
|
|
currentCategory = categories.firstWhere((c) => c.name == 'Default');
|
||
|
|
_selectNewTargetWord();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
double getCurrentSpeed() {
|
||
|
|
double speed = baseSpeed;
|
||
|
|
|
||
|
|
// Apply slow time power-up if active
|
||
|
|
final slowPowerUp = activePowerUps
|
||
|
|
.where((p) => p.type == PowerUpType.slowTime && p.isActive)
|
||
|
|
.firstOrNull;
|
||
|
|
|
||
|
|
if (slowPowerUp != null) {
|
||
|
|
speed *= slowPowerUp.value;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remove expired power-ups
|
||
|
|
activePowerUps.removeWhere((p) => !p.isActive);
|
||
|
|
|
||
|
|
return speed;
|
||
|
|
}
|
||
|
|
|
||
|
|
double _getCurrentMultiplier() {
|
||
|
|
double multiplier = 1.0;
|
||
|
|
|
||
|
|
final multiplierPowerUp = activePowerUps
|
||
|
|
.where((p) => p.type == PowerUpType.scoreMultiplier && p.isActive)
|
||
|
|
.firstOrNull;
|
||
|
|
|
||
|
|
if (multiplierPowerUp != null) {
|
||
|
|
multiplier *= multiplierPowerUp.value;
|
||
|
|
}
|
||
|
|
|
||
|
|
return multiplier;
|
||
|
|
}
|
||
|
|
|
||
|
|
void updateBaseSpeed() {
|
||
|
|
baseSpeed = 1.0 + (score / 1000); // Gradually increase speed with score
|
||
|
|
}
|
||
|
|
|
||
|
|
void _collectHealth() {
|
||
|
|
healthPoints =
|
||
|
|
(healthPoints + 1).clamp(0, 8); // Increased max health points
|
||
|
|
}
|
||
|
|
|
||
|
|
void _activateShield() {
|
||
|
|
final shieldPowerUp = PowerUp(
|
||
|
|
type: PowerUpType.shield,
|
||
|
|
duration: const Duration(seconds: 8),
|
||
|
|
value: 1.0,
|
||
|
|
);
|
||
|
|
shieldPowerUp.activate();
|
||
|
|
activePowerUps.add(shieldPowerUp);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _activateMagnet() {
|
||
|
|
final magnetPowerUp = PowerUp(
|
||
|
|
type: PowerUpType.magnet,
|
||
|
|
duration: const Duration(seconds: 20),
|
||
|
|
value: 10.0,
|
||
|
|
);
|
||
|
|
magnetPowerUp.activate();
|
||
|
|
activePowerUps.add(magnetPowerUp);
|
||
|
|
|
||
|
|
// Cancel existing timer if any
|
||
|
|
_magnetTimer?.cancel();
|
||
|
|
|
||
|
|
// Start continuous attraction updates
|
||
|
|
_magnetTimer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
|
||
|
|
if (!magnetPowerUp.isActive) {
|
||
|
|
timer.cancel();
|
||
|
|
_magnetTimer = null;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
_attractNearbyItems();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method to check if an item can be affected by magnet
|
||
|
|
bool _isAttractableItem(ItemType type) {
|
||
|
|
return type == ItemType.slowTime ||
|
||
|
|
type == ItemType.scoreMultiplier ||
|
||
|
|
type == ItemType.shield ||
|
||
|
|
type == ItemType.magnet ||
|
||
|
|
type == ItemType.health;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Method to update snake positions and direction
|
||
|
|
void updateSnakePositions(List<Offset> positions, Direction direction) {
|
||
|
|
snakePositions = List.from(positions);
|
||
|
|
snakeDirection = direction;
|
||
|
|
}
|
||
|
|
|
||
|
|
void _attractNearbyItems() {
|
||
|
|
if (snakePositions.isEmpty) return;
|
||
|
|
|
||
|
|
final magnetPowerUp = activePowerUps
|
||
|
|
.where((p) => p.type == PowerUpType.magnet && p.isActive)
|
||
|
|
.firstOrNull;
|
||
|
|
|
||
|
|
if (magnetPowerUp == null) return;
|
||
|
|
|
||
|
|
final head = snakePositions.first;
|
||
|
|
final attractionRadius = magnetPowerUp.value;
|
||
|
|
|
||
|
|
List<GameItem> updatedItems = [];
|
||
|
|
List<GameItem> itemsToRemove = [];
|
||
|
|
|
||
|
|
for (var item in gameItems) {
|
||
|
|
if (!_isAttractableItem(item.type)) {
|
||
|
|
updatedItems.add(item);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calculate shortest path considering screen wrapping
|
||
|
|
double dx = item.position.dx - head.dx;
|
||
|
|
double dy = item.position.dy - head.dy;
|
||
|
|
|
||
|
|
// Handle screen wrapping for distance calculation
|
||
|
|
if (dx > gameWidth / 2) dx -= gameWidth;
|
||
|
|
if (dx < -gameWidth / 2) dx += gameWidth;
|
||
|
|
if (dy > gameHeight / 2) dy -= gameHeight;
|
||
|
|
if (dy < -gameHeight / 2) dy += gameHeight;
|
||
|
|
|
||
|
|
double distance = sqrt(dx * dx + dy * dy);
|
||
|
|
|
||
|
|
// Check for direct collision with snake head
|
||
|
|
if (distance < 0.5) {
|
||
|
|
itemsToRemove.add(item);
|
||
|
|
bool success = collectItem(item);
|
||
|
|
if (!success) {
|
||
|
|
onGameOver();
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (distance <= attractionRadius) {
|
||
|
|
// Enhanced smooth movement with easing
|
||
|
|
double attractionStrength = _easeInOutQuad(1.0 - (distance / attractionRadius));
|
||
|
|
|
||
|
|
// Smoother base movement speed
|
||
|
|
double moveSpeed = 0.15;
|
||
|
|
double moveX = dx / distance * moveSpeed * attractionStrength;
|
||
|
|
double moveY = dy / distance * moveSpeed * attractionStrength;
|
||
|
|
|
||
|
|
// Calculate new position
|
||
|
|
double newX = item.position.dx - moveX;
|
||
|
|
double newY = item.position.dy - moveY;
|
||
|
|
|
||
|
|
// Handle screen wrapping properly
|
||
|
|
if (newX < 0) {
|
||
|
|
newX = gameWidth - 0.01; // Slightly inside the right edge
|
||
|
|
} else if (newX >= gameWidth) {
|
||
|
|
newX = 0.01; // Slightly inside the left edge
|
||
|
|
}
|
||
|
|
|
||
|
|
if (newY < 0) {
|
||
|
|
newY = gameHeight - 0.01; // Slightly inside the bottom edge
|
||
|
|
} else if (newY >= gameHeight) {
|
||
|
|
newY = 0.01; // Slightly inside the top edge
|
||
|
|
}
|
||
|
|
|
||
|
|
updatedItems.add(
|
||
|
|
GameItem(
|
||
|
|
position: Offset(newX, newY),
|
||
|
|
type: item.type,
|
||
|
|
content: item.content,
|
||
|
|
isCorrect: item.isCorrect,
|
||
|
|
value: item.value,
|
||
|
|
maxCycles: item.maxCycles,
|
||
|
|
remainingCycles: item.remainingCycles,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
updatedItems.add(item);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remove collected items and update positions
|
||
|
|
gameItems.removeWhere((item) => itemsToRemove.contains(item));
|
||
|
|
gameItems = updatedItems;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Smooth easing function for magnet effect
|
||
|
|
double _easeInOutQuad(double t) {
|
||
|
|
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_magnetTimer?.cancel();
|
||
|
|
}
|
||
|
|
|
||
|
|
void resetGame() {
|
||
|
|
// Clear all game items
|
||
|
|
gameItems.clear();
|
||
|
|
|
||
|
|
// Reset power-ups and cancel magnet timer
|
||
|
|
activePowerUps.clear();
|
||
|
|
_magnetTimer?.cancel();
|
||
|
|
_magnetTimer = null;
|
||
|
|
|
||
|
|
// Reset score and combo
|
||
|
|
score = 0;
|
||
|
|
combo = 0;
|
||
|
|
|
||
|
|
// Reset speed
|
||
|
|
baseSpeed = 1.0;
|
||
|
|
|
||
|
|
// Reset health points to maximum
|
||
|
|
healthPoints = 8;
|
||
|
|
|
||
|
|
// Reset spawn cooldown
|
||
|
|
spawnCooldown = 0;
|
||
|
|
|
||
|
|
// Reset category to default and generate new target word
|
||
|
|
currentCategory = categories.firstWhere((c) => c.name == 'Default');
|
||
|
|
_selectNewTargetWord();
|
||
|
|
}
|
||
|
|
}
|