125 lines
3.4 KiB
Dart
125 lines
3.4 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../game/engine/game_engine.dart';
|
|
import '../game/engine/piece_generator.dart';
|
|
import '../game/models/grid.dart';
|
|
import '../game/models/objective.dart';
|
|
import '../game/models/piece.dart';
|
|
import '../game/models/stage.dart';
|
|
|
|
/// Immutable snapshot of one engine moment; the only game state the UI sees.
|
|
class GameViewState {
|
|
const GameViewState({
|
|
required this.grid,
|
|
required this.tray,
|
|
required this.score,
|
|
required this.comboStreak,
|
|
required this.movesLeft,
|
|
required this.moveLimit,
|
|
required this.phase,
|
|
required this.stuckReason,
|
|
required this.objectives,
|
|
required this.starsEarned,
|
|
required this.objectiveProgress,
|
|
required this.lastPlacement,
|
|
required this.fxTick,
|
|
required this.endless,
|
|
});
|
|
|
|
final GridState grid;
|
|
final List<Piece> tray;
|
|
final int score;
|
|
final int comboStreak;
|
|
final int movesLeft;
|
|
final int moveLimit;
|
|
final GamePhase phase;
|
|
final StuckReason? stuckReason;
|
|
final List<Objective> objectives;
|
|
final int starsEarned;
|
|
final double objectiveProgress;
|
|
final PlacementResult? lastPlacement;
|
|
|
|
/// Increments on every accepted placement so animations can retrigger.
|
|
final int fxTick;
|
|
|
|
final bool endless;
|
|
}
|
|
|
|
/// Bridges the pure-Dart [GameEngine] to the widget tree. The engine object
|
|
/// never leaks past this class.
|
|
class GameSessionNotifier extends Notifier<GameViewState?> {
|
|
GameEngine? _engine;
|
|
StageConfig? _stage;
|
|
PieceGenerator? _generatorOverride;
|
|
int _attempt = 0;
|
|
int _fxTick = 0;
|
|
|
|
@override
|
|
GameViewState? build() => null;
|
|
|
|
void startStage(StageConfig stage,
|
|
{int attempt = 0, PieceGenerator? generator}) {
|
|
_stage = stage;
|
|
_attempt = attempt;
|
|
_generatorOverride = generator;
|
|
_engine = GameEngine(stage, attempt: attempt, generator: generator);
|
|
_fxTick = 0;
|
|
_publish(lastPlacement: null);
|
|
}
|
|
|
|
/// Restarts the current stage as a new attempt (fresh piece sequence).
|
|
void restart() {
|
|
final stage = _stage;
|
|
if (stage == null) throw StateError('no stage to restart');
|
|
startStage(stage, attempt: _attempt + 1, generator: _generatorOverride);
|
|
}
|
|
|
|
bool tryPlace(int trayIndex, int x, int y) {
|
|
final engine = _engine;
|
|
if (engine == null) return false;
|
|
final result = engine.tryPlace(trayIndex, x, y);
|
|
if (result == null) return false;
|
|
_fxTick++;
|
|
_publish(lastPlacement: result);
|
|
return true;
|
|
}
|
|
|
|
bool canPlaceAt(int trayIndex, int x, int y) =>
|
|
_engine?.tryPlaceWouldSucceed(trayIndex, x, y) ?? false;
|
|
|
|
void useContinue() {
|
|
_engine?.useContinue();
|
|
_publish(lastPlacement: null);
|
|
}
|
|
|
|
void addExtraMoves() {
|
|
_engine?.addExtraMoves();
|
|
_publish(lastPlacement: null);
|
|
}
|
|
|
|
void declineAndLose() {
|
|
_engine?.declineAndLose();
|
|
_publish(lastPlacement: null);
|
|
}
|
|
|
|
void _publish({required PlacementResult? lastPlacement}) {
|
|
final engine = _engine!;
|
|
state = GameViewState(
|
|
grid: engine.grid,
|
|
tray: engine.tray,
|
|
score: engine.score,
|
|
comboStreak: engine.combo.streak,
|
|
movesLeft: engine.movesLeft,
|
|
endless: engine.endless,
|
|
moveLimit: engine.endless ? 0 : engine.movesLeft + engine.movesUsed,
|
|
phase: engine.phase,
|
|
stuckReason: engine.stuckReason,
|
|
objectives: engine.objectives,
|
|
starsEarned: engine.starsEarned,
|
|
objectiveProgress: engine.objectiveProgress,
|
|
lastPlacement: lastPlacement,
|
|
fxTick: _fxTick,
|
|
);
|
|
}
|
|
}
|