diff --git a/lib/game/engine/game_engine.dart b/lib/game/engine/game_engine.dart index 83f10d3..d467094 100644 --- a/lib/game/engine/game_engine.dart +++ b/lib/game/engine/game_engine.dart @@ -82,6 +82,7 @@ class GameEngine { List get objectives => List.unmodifiable(_objectives); GamePhase get phase => _phase; StuckReason? get stuckReason => _stuckReason; + bool get rescueUsed => _rescueUsed; int get starsEarned => _phase == GamePhase.won ? _stage.stars.starsFor(movesLeft: movesLeft) diff --git a/lib/state/game_session_notifier.dart b/lib/state/game_session_notifier.dart index b0d8858..1823578 100644 --- a/lib/state/game_session_notifier.dart +++ b/lib/state/game_session_notifier.dart @@ -6,6 +6,7 @@ import '../game/models/grid.dart'; import '../game/models/objective.dart'; import '../game/models/piece.dart'; import '../game/models/stage.dart'; +import 'providers.dart'; /// Immutable snapshot of one engine moment; the only game state the UI sees. class GameViewState { @@ -24,6 +25,7 @@ class GameViewState { required this.lastPlacement, required this.fxTick, required this.endless, + required this.rescueUsed, }); final GridState grid; @@ -43,6 +45,7 @@ class GameViewState { final int fxTick; final bool endless; + final bool rescueUsed; } /// Bridges the pure-Dart [GameEngine] to the widget tree. The engine object @@ -72,6 +75,17 @@ class GameSessionNotifier extends Notifier { final stage = _stage; if (stage == null) throw StateError('no stage to restart'); startStage(stage, attempt: _attempt + 1, generator: _generatorOverride); + if (stage.endless) { + ref.read(analyticsProvider).endlessStart(); + } else { + final flow = ref.read(seasonFlowProvider); + if (flow != null) { + ref.read(analyticsProvider).stageStart( + seasonId: flow.pack.seasonId, + stageId: flow.stage.id, + ); + } + } } bool tryPlace(int trayIndex, int x, int y) { @@ -119,6 +133,7 @@ class GameSessionNotifier extends Notifier { objectiveProgress: engine.objectiveProgress, lastPlacement: lastPlacement, fxTick: _fxTick, + rescueUsed: engine.rescueUsed, ); } } diff --git a/lib/ui/screens/game_screen.dart b/lib/ui/screens/game_screen.dart index 618f152..8c58745 100644 --- a/lib/ui/screens/game_screen.dart +++ b/lib/ui/screens/game_screen.dart @@ -391,33 +391,47 @@ class _GameScreenState extends ConsumerState (GamePhase.stuck, StuckReason.outOfMoves) => ( l10n.outOfMoves, [ - FilledButton( - onPressed: () { - ref.read(analyticsProvider).rescueUsed(type: 'extra_moves'); - notifier.addExtraMoves(); - }, - child: Text(l10n.plusFiveMoves), - ), - TextButton( - onPressed: notifier.declineAndLose, - child: Text(l10n.giveUp), - ), + if (!view.rescueUsed) + FilledButton( + onPressed: () { + ref.read(analyticsProvider).rescueUsed(type: 'extra_moves'); + notifier.addExtraMoves(); + }, + child: Text(l10n.plusFiveMoves), + ), + if (view.rescueUsed) + FilledButton( + onPressed: notifier.declineAndLose, + child: Text(l10n.giveUp), + ) + else + TextButton( + onPressed: notifier.declineAndLose, + child: Text(l10n.giveUp), + ), ], ), (GamePhase.stuck, _) => ( l10n.boardFull, [ - FilledButton( - onPressed: () { - ref.read(analyticsProvider).rescueUsed(type: 'continue'); - notifier.useContinue(); - }, - child: Text(l10n.watchAdContinue), - ), - TextButton( - onPressed: notifier.declineAndLose, - child: Text(l10n.giveUp), - ), + if (!view.rescueUsed) + FilledButton( + onPressed: () { + ref.read(analyticsProvider).rescueUsed(type: 'continue'); + notifier.useContinue(); + }, + child: Text(l10n.watchAdContinue), + ), + if (view.rescueUsed) + FilledButton( + onPressed: notifier.declineAndLose, + child: Text(l10n.giveUp), + ) + else + TextButton( + onPressed: notifier.declineAndLose, + child: Text(l10n.giveUp), + ), ], ), (GamePhase.lost, _) when view.endless => ( diff --git a/test/game/engine/game_engine_test.dart b/test/game/engine/game_engine_test.dart index 88d507a..6784e31 100644 --- a/test/game/engine/game_engine_test.dart +++ b/test/game/engine/game_engine_test.dart @@ -193,6 +193,21 @@ void main() { }); }); + group('rescueUsed getter', () { + test('rescueUsed flag is exposed and flips after a rescue', () { + final engine = GameEngine( + _stage(moveLimit: 1), + generator: _smallPool(1), + ); + expect(engine.rescueUsed, isFalse); + engine.tryPlace(0, 0, 0); + expect(engine.phase, GamePhase.stuck); + expect(engine.rescueUsed, isFalse); + engine.addExtraMoves(); + expect(engine.rescueUsed, isTrue); + }); + }); + group('dead board and continue', () { StageConfig deadStage() { // Checkerboard: only monos fit, and the injected pool has none small