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 categories; Category? currentCategory; final int gameWidth = 10; final int gameHeight = 14; final VoidCallback onGameOver; List gameItems = []; List 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 collectedLetters = []; // Letters collected so far // Add snake positions reference List 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 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 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 updatedItems = []; List 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(); } }