430 lines
12 KiB
Dart
430 lines
12 KiB
Dart
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;
|
|
}
|
|
}
|