mnemo_cards/funny_letters/lib/runner/game_manager.dart

431 lines
12 KiB
Dart
Raw Normal View History

2025-11-10 23:55:41 +00:00
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<Bonus> bonuses = [];
List<Obstacle> 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<String> 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<void> _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<void> _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<void> 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<Bonus> 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<Obstacle> 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<void> 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<LogicalKeyboardKey> keysPressed,
) {
if (event is RawKeyDownEvent) {
pressedKey = event.logicalKey;
} else if (event is RawKeyUpEvent) {
pressedKey = null;
}
return KeyEventResult.handled;
}
}