import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:audioplayers/audioplayers.dart'; import 'package:flame/components.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:funny_letters/main.dart'; import 'package:funny_letters/runner/bonuses/health_bonus.dart'; import 'package:funny_letters/runner/obstacles/obstacle.dart'; import 'package:funny_letters/runner/player.dart'; import 'package:funny_letters/runner/ui/back_button.dart'; import 'package:funny_letters/runner/ui/score_bar.dart'; import 'package:flame/input.dart'; import 'package:flame/events.dart'; import 'package:flame/text.dart' as flameText; import 'package:flame/game.dart'; import 'package:flame/flame.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:funny_letters/runner/words.dart'; import 'package:funny_letters/gen/assets.gen.dart'; import 'bonuses/bonus.dart'; import 'bonuses/letter_bonus.dart'; import 'ui/health_bar_v2.dart'; enum Difficulty { easy, medium, hard } enum Level { noun, verb, adverb, adjective } Difficulty difficulty = Difficulty.hard; String? background; String target = ''; String targetTranslated = ''; String targetLetters = ''; String goText = ''; class GameManager extends FlameGame with KeyboardEvents, TapCallbacks, DragCallbacks, HasCollisionDetection { double get playerHorizontalSpeed => switch (difficulty) { Difficulty.easy => size.x, Difficulty.medium => size.x, Difficulty.hard => size.x * 1.1, } * (1.0); double get playerVerticalSpeed => switch (difficulty) { Difficulty.easy => size.y / 2, Difficulty.medium => size.y / 2 * 1.1, Difficulty.hard => size.y / 2 * 1.2, } * (1.0); @override void onDragUpdate(DragUpdateEvent event) { player.position.x += event.localDelta.x; print(player.position.x); if (player.position.x < player.size.x / 2) { player.position.x = player.size.x / 2; } else if (player.position.x + player.size.x / 2 >= size.x) { player.position.x = size.x - player.size.x / 2; } } double get hitRadiusSq => player.width * player.width / 2; double get playerWidth => size.x / 5; double get playerHeight => playerWidth; Vector2 get playerSize => Vector2(playerWidth, playerHeight); late Player player; LogicalKeyboardKey? pressedKey; List bonuses = []; List obstacles = []; late HealthBarV2 healthBar; late BackButtonComponent backButton; late ScoreBar scoreBar; TextComponent? targetText; TextComponent? translatedText; TextComponent? scoreText; SpriteComponent? backgroundComponent; int score = 0; String _nextBonus() { final alphabet = 'abcdefghijklmnopqrstuvwxyz'; List letters = [ ...targetLetters.characters, alphabet[Random().nextInt(alphabet.length)], alphabet[Random().nextInt(alphabet.length)], if (difficulty != Difficulty.easy) alphabet[Random().nextInt(alphabet.length)], ]; return letters[Random().nextInt(letters.length)]; } Future _updateNextWord() async { final cache = await words; final next = cache[Random().nextInt(cache.length)]; // Check if there's a custom background for this word final customBg = await assetLoader.getCustomBackground(next); background = customBg; if (background != null) { updateBackground(); } assetLoader.playWord(next, lang: 'es'); target = next; targetTranslated = await translate(target); targetLetters = target.replaceAll(' ', ''); } Future _init() async { score = 0; player = Player(Vector2(playerWidth, playerHeight * 0.25)); player.y = size.y - playerHeight; player.x = size.x / 2; healthBar = HealthBarV2( health: player.health, size: Vector2(size.x * 0.4, playerHeight * 0.3), position: Vector2(38, 40), ); backButton = BackButtonComponent( size: Vector2(20, 27), position: Vector2(8, 40), onPressed: () => Navigator.of(menuKey.currentContext!).pop(), ); scoreBar = ScoreBar(playerSize, tag: 'score') ..y = 200 ..anchor = Anchor.centerLeft ..x = size.x / 2; await _updateNextWord(); } Future updateBackground() async { if (backgroundComponent != null) { backgroundComponent?.removeFromParent(); } if (background != null) { try { final bytes = await assetLoader.loadFileBytes(background!); final codec = await ui.instantiateImageCodec(bytes); final frame = await codec.getNextFrame(); Flame.images.add(background!, frame.image); final sprite = await loadSprite(background!); backgroundComponent = SpriteComponent( sprite: sprite, anchor: Anchor.center, position: size / 2, size: Vector2(size.x, size.x), ) ..opacity = 0.5 ..priority = -1; await add(backgroundComponent!); } catch (e) { print('Error loading background: $e'); } } } @override void resumeEngine() { // target = _nextWord(); // targetLetters = target.replaceAll(' ', ''); // updateTargetText(); // updateBackground(); super.resumeEngine(); } void updateTargetText(BuildContext context) { if (targetText != null) { remove(targetText!); remove(translatedText!); remove(scoreText!); } final theme = Theme.of(context); final paint = TextPaint(style: theme.textTheme.titleLarge!); targetText = TextComponent( text: target.toUpperCase(), textRenderer: paint, ) ..y = 100 ..anchor = Anchor.center ..x = size.x / 2; translatedText = TextComponent( text: targetTranslated.toUpperCase(), textRenderer: paint, ) ..y = 150 ..anchor = Anchor.center ..x = size.x / 2; scoreText = TextComponent( text: score.toString(), textRenderer: paint, ) ..y = playerHeight ..anchor = Anchor.centerRight ..x = size.x - 10; add(targetText!); add(translatedText!); add(scoreText!); } void dispose() { if (backgroundComponent != null) { remove(backgroundComponent!); backgroundComponent = null; } remove(player); removeAll(bonuses); removeAll(obstacles); remove(healthBar); remove(backButton); remove(scoreBar); if (targetText != null) remove(targetText!); if (scoreText != null) remove(scoreText!); bonuses.clear(); obstacles.clear(); } double spawnTime = 0.0; void gameOver() { goText = target; dispose(); onLoad(); pauseEngine(); if (gameSettings.soundEnabled) { effectPlayer.stop().then((value) async { audioPlayer.pause(); // Play game over sound through the main app await assetLoader.playWord('game_over'); audioPlayer.resume(); }); } (menuKey.currentState as MenuState).openGameOver(); } @override void update(double dt) { super.update(dt); if (player.health <= 0) { gameOver(); return; } int bonusLength; switch (difficulty) { case Difficulty.easy: bonusLength = 7; case Difficulty.medium: bonusLength = 8; case Difficulty.hard: bonusLength = 9; } spawnTime += dt; double spawnCooldown; switch (difficulty) { case Difficulty.easy: spawnCooldown = playerVerticalSpeed / size.y / 1.3; case Difficulty.medium: spawnCooldown = playerVerticalSpeed / size.y / 1.6; case Difficulty.hard: spawnCooldown = playerVerticalSpeed / size.y / 2; } if (bonuses.length < bonusLength && spawnTime > spawnCooldown) { spawnTime = 0; Bonus bonus; final r = Random().nextInt(8); if (r == 0) { bonus = HealthBonus(playerSize * 0.8) ..x = Random().nextDouble() * (size.x - playerWidth) + playerWidth / 2 ..y = -(Random().nextDouble() + 1) * playerSize.y; } else { bonus = LetterBonus(playerSize * 0.8, _nextBonus()) ..x = Random().nextDouble() * (size.x - playerWidth) + playerWidth / 2 ..y = -(Random().nextDouble() * 2 + 1) * playerSize.y; } bonuses.add(bonus); add(bonus); } if (obstacles.length < 3) { // for (int i = 0; i < Random().nextInt(3 - obstacles.length); i++) { // final moon = Obstacle(playerSize) // ..x = Random().nextDouble() * (size.x - playerWidth) + playerWidth / 2 // ..y = 0; // obstacles.add(moon); // add(moon); // } } if (pressedKey == LogicalKeyboardKey.arrowLeft) { player.x -= dt * playerHorizontalSpeed; if (player.x < player.width / 2) { player.x = player.width / 2; } } else if (pressedKey == LogicalKeyboardKey.arrowRight) { player.x += dt * playerHorizontalSpeed; if (player.x > size.x - player.width / 2) { player.x = size.x - player.width / 2; } } List bonusesToRemove = []; bonuses.where((bonus) => !bonus.activated).forEach((bonus) { bonus.y += dt * playerVerticalSpeed; if (bonus.y > size.y + bonus.height) { bonusesToRemove.add(bonus); } else { if (bonus.collidingWith(player)) { bonus.activate(player); if (bonus is HealthBonus) { effectPlayer.stop().then((value) async { if (gameSettings.soundEnabled) { await assetLoader.playWord('health_bonus'); } }); healthBar.updateHealth(player); } else if (bonus is LetterBonus) { if (gameSettings.soundEnabled) { effectPlayer.stop().then((value) async { await assetLoader.playWord('letter_bonus'); }); } if (bonus.letter != targetLetters[scoreBar.score.length]) { player.hit(); healthBar.updateHealth(player); bonusesToRemove.add(bonus); return; } scoreBar.addScore(bonus.letter, targetLetters.length, target); if (scoreBar.score.length == targetLetters.length) { bool same = true; for (int i = 0; i < targetLetters.length; i++) { if (scoreBar.score[i] != targetLetters[i]) { same = false; break; } } if (same) { score++; scoreBar.updateScore([], target); _updateNextWord(); updateTargetText(buildContext!); } } } bonusesToRemove.add(bonus); } } }); for (var bonus in bonusesToRemove) { // Not hitting player for missed letters if (bonus is LetterBonus && bonus.letter == targetLetters[scoreBar.score.length] && false) { player.hit(); healthBar.updateHealth(player); } bonuses.remove(bonus); remove(bonus); } List obstaclesToRemove = []; for (var obstacle in obstacles) { if (obstacle.isHit) { continue; } obstacle.y += dt * playerVerticalSpeed; if (obstacle.y > size.y + obstacle.height) { obstaclesToRemove.add(obstacle); } else { if (obstacle.collidingWith(player)) { obstacle.hit(player); healthBar.updateHealth(player); obstaclesToRemove.add(obstacle); } } } for (var element in obstaclesToRemove) { obstacles.remove(element); remove(element); } scoreBar.updateIfNeeded(); } @override FutureOr onLoad() async { await super.onLoad(); await _init(); add(player); add(backButton); addAll(bonuses); addAll(obstacles); add(healthBar); add(scoreBar); WidgetsBinding.instance.addPostFrameCallback((_) { if (buildContext != null) { updateTargetText(buildContext!); } }); // await updateBackground(); } @override KeyEventResult onKeyEvent( KeyEvent event, Set keysPressed, ) { if (event is RawKeyDownEvent) { pressedKey = event.logicalKey; } else if (event is RawKeyUpEvent) { pressedKey = null; } return KeyEventResult.handled; } }