Skip to content

feat: replace trail run with summit camp game - #71

Merged
YKDBontekoe merged 4 commits into
mainfrom
cursor/summit-camp-game-redesign-1edb
Jul 6, 2026
Merged

YKDBontekoe merged 4 commits into
mainfrom
cursor/summit-camp-game-redesign-1edb

Conversation

@YKDBontekoe

@YKDBontekoe YKDBontekoe commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces the Trail Run board-game/combat loop with Summit Camp — a health-fueled base builder on the Character tab.

What changed

  • Four health resources: Momentum (steps + exercise minutes), Fuel (active calories), Focus (sleep + readiness), Spirit (runs)
  • Camp actions: expand tiles, build/upgrade structures, overnight Rest, run Expeditions
  • Weekly summit meter: persistent altitude progress toward a 100 m weekly goal
  • Daily quests: three fully measurable pillar missions (momentum, fuel, focus)
  • Dashboard: glance card shows summit altitude and top resource

Removed

  • Trail Run daily board, turn-based combat, and adventure session persistence
  • Manual-only stat honor quests

Validation

  • flutter analyze — 0 errors (4 info-level style hints)
  • flutter test — 148 tests passing
  • flutter build web — succeeds
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added a new camp-based progression experience with camp quests, weekly summit tracking, camp building/expansion, rest, and expedition events.
    • Updated character and dashboard screens to show camp progress, camp resources, and expedition status.
    • Added new measurable quest goals for exercise minutes and sleep hours.
  • Bug Fixes

    • Replaced the older trail-run and encounter flow with the new camp system for a more consistent progression experience.
    • Improved persistence so camp progress is saved and restored correctly.

Replace the Trail Run board-game/combat loop with Summit Camp, a health-fueled
base builder where steps, calories, sleep, and runs power four distinct resources.

- Add camp domain entities, use cases, and persistence
- Build camp grid UI with expand, build, rest, and expedition actions
- Redesign daily quests to three measurable pillar missions
- Update dashboard glance card with summit altitude and resources
- Remove trail/combat code and tests

Co-authored-by: Youri Bontekoe <YKDBontekoe@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 82805bd3-eeac-43f0-aac8-727cc3f40147

📥 Commits

Reviewing files that changed from the base of the PR and between 2badae6 and 942ffe9.

📒 Files selected for processing (1)
  • CODEMAP.md
📝 Walkthrough

Walkthrough

This PR replaces the "Trail Run" gamification mini-game with a new "Summit Camp" system. New constants, entities (CampBuildingType, PlacedBuilding, CampResources, CampTile, CampState, ExpeditionEvent), use cases, providers, and UI widgets are introduced, while trail/encounter/adventure entities and use cases are removed. Repository, persistence, and dashboard/character page integrations are updated accordingly.

Changes

Summit Camp Feature

Layer / File(s) Summary
Camp constants and core domain entities
lib/core/constants/gamification_constants.dart, lib/domain/entities/gamification/camp_building.dart, camp_resources.dart, camp_tile.dart, camp_state.dart, expedition_event.dart, quest.dart
New Summit Camp tuning constants replace trail-run constants; adds CampBuildingType/PlacedBuilding, CampResources, CampTile, CampState, ExpeditionEvent entities, and new QuestObjectiveKind values.
Repository contract and persistence for CampState
lib/domain/repositories/character_repository.dart, lib/infrastructure/gamification/character_persistence_repository.dart, test/infrastructure/.../character_persistence_repository_test.dart
Repository interface and persistence implementation switch from AdventureSession to CampState load/save, with legacy key cleanup.
Camp resource, tile, and structure use cases
lib/domain/usecases/gamification/compute_camp_resources_usecase.dart, expand_camp_tile_usecase.dart, build_camp_structure_usecase.dart, rest_camp_usecase.dart, advance_weekly_summit_usecase.dart, test/.../camp_actions_usecase_test.dart, compute_camp_resources_usecase_test.dart
New use cases compute camp resources, expand tiles, build/upgrade structures, rest, and advance weekly summit progress.
Camp quest generation, expedition resolution, and quest progress
generate_camp_quests_usecase.dart, resolve_expedition_usecase.dart, evaluate_quest_progress_usecase.dart, related tests
New quest generation and expedition resolution use cases; quest progress evaluation extended for exercise minutes and sleep hours.
Camp and gamification provider wiring
lib/shared/providers/camp_providers.dart, camp_providers.g.dart, gamification_providers.dart, daily_quests_provider.dart
New CampSessionNotifier manages camp state and actions; gamification providers rewired to camp use cases.
Camp UI widgets
camp_grid.dart, camp_build_sheet.dart, camp_resources_bar.dart, expedition_card.dart, summit_progress_card.dart, camp_game_panel.dart, tests
New widgets render the camp grid, build sheet, resources bar, expedition card, summit progress, and main game panel.
Character and dashboard page integration
character_page.dart, dashboard_page.dart, character_glance_card.dart, daily_quest_teaser.dart
Pages wire in campSessionProvider and render camp/summit UI; glance card and quest teaser copy updated.
Codebase map documentation update
CODEMAP.md
Layer map and hot files list updated to reflect new camp-related files.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • YKDBontekoe/KYNOS#55: Directly related as it replaces the Trail Run gamification implementation that this PR removes and supersedes with Summit Camp.
  • YKDBontekoe/KYNOS#63: Both PRs modify character_glance_card.dart, overlapping in semantics and rendered UI logic.
  • YKDBontekoe/KYNOS#65: Both PRs touch character_glance_card.dart visuals, with this PR adding camp/summit rendering.

Poem

A rabbit hops up a snowy peak,
Trading trail maps for tents this week 🏕️
Momentum, fuel, focus, spirit bright,
Camp tiles unlock in the morning light,
Summit reached — thump goes my foot in delight! 🐰🚩

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: replacing the Trail Run gameplay with Summit Camp.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

cursoragent and others added 2 commits July 6, 2026 20:25
Co-authored-by: Youri Bontekoe <YKDBontekoe@users.noreply.github.com>
Co-authored-by: Youri Bontekoe <YKDBontekoe@users.noreply.github.com>
@YKDBontekoe
YKDBontekoe marked this pull request as ready for review July 6, 2026 20:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/features/character/camp_game_panel_test.dart (1)

1-35: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Test file doesn't test CampGamePanel.

Despite the filename, this only tests CampResourcesBar in isolation. CampGamePanel itself — which drives campSessionProvider, healthSummaryProvider, recentRunsProvider, tile selection, and the build sheet flow — has no widget test with a ProviderScope overriding fakes, as required by the coding guidelines: "widget tests must use ProviderScope with overridden fakes rather than real repositories." The AI summary/commit message describes this as "a corresponding widget test" for the panel, but coverage doesn't match that scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/features/character/camp_game_panel_test.dart` around lines 1 - 35, The
test coverage is for CampResourcesBar, not CampGamePanel, so add a widget test
that targets CampGamePanel itself. Build the panel under a ProviderScope and
override the relevant providers such as campSessionProvider,
healthSummaryProvider, and recentRunsProvider with fakes/mocks so it does not
hit real repositories. Verify the panel’s key UI and flow behavior, including
tile selection and the build sheet interaction, using the CampGamePanel widget
rather than the CampResourcesBar widget.

Source: Coding guidelines

🧹 Nitpick comments (13)
lib/features/character/presentation/widgets/camp_grid.dart (1)

93-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Icon-only tiles lack accessible labels.

Locked/built/empty tile states are conveyed only via bare Icon widgets with no semantic label, so screen reader users can't distinguish tile status or building type.

♿ Suggested fix
           child: Center(
             child: isLocked
-                ? Icon(Icons.lock_outline, size: 14, color: kynos.tertiaryLabel)
+                ? Semantics(
+                    label: 'Locked tile',
+                    child: Icon(Icons.lock_outline, size: 14, color: kynos.tertiaryLabel),
+                  )
                 : building != null
-                    ? Icon(
-                        _buildingIcon(building!.type),
-                        size: 18,
-                        color: kynos.label,
-                      )
-                    : Icon(
-                        Icons.terrain_outlined,
-                        size: 16,
-                        color: kynos.secondaryLabel,
-                      ),
+                    ? Semantics(
+                        label: building!.type.label,
+                        child: Icon(_buildingIcon(building!.type), size: 18, color: kynos.label),
+                      )
+                    : Semantics(
+                        label: 'Empty tile',
+                        child: Icon(Icons.terrain_outlined, size: 16, color: kynos.secondaryLabel),
+                      ),
           ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/character/presentation/widgets/camp_grid.dart` around lines 93 -
107, The icon-only tile states in camp_grid.dart are not accessible because the
locked, building, and empty cases use bare Icon widgets without any semantic
description. Update the widget branch in the tile builder to wrap each state in
appropriate accessibility semantics or provide labels for the Icon(s) so screen
readers can distinguish locked, building type, and empty terrain tiles; use the
existing state checks around isLocked, building, and _buildingIcon to attach the
correct label.
lib/features/character/presentation/widgets/summit_progress_card.dart (1)

15-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Direct DateTime.now() call reduces testability.

Computing isSunday inline with DateTime.now() makes this widget harder to test deterministically (can't inject a fixed date) and ties the "summit push" day to device-local time rather than a shared clock/week-boundary source used elsewhere in the domain.

♻️ Suggested fix — accept as a parameter
 class SummitProgressCard extends StatelessWidget {
-  const SummitProgressCard({super.key, required this.camp});
+  const SummitProgressCard({super.key, required this.camp, DateTime? now})
+      : _now = now;

   final CampState camp;
+  final DateTime? _now;

   `@override`
   Widget build(BuildContext context) {
     final kynos = context.kynosTheme;
-    final isSunday = DateTime.now().weekday == DateTime.sunday;
+    final isSunday = (_now ?? DateTime.now()).weekday == DateTime.sunday;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/character/presentation/widgets/summit_progress_card.dart` around
lines 15 - 18, The SummitProgressCard widget computes isSunday directly inside
build with DateTime.now(), which makes the summit-day logic hard to test and
tied to device-local time. Move this date decision out of build by injecting the
current date or a clock-derived value into SummitProgressCard, then use that
injected value to determine isSunday so tests can supply a fixed date and the
shared time source can control the boundary.
test/infrastructure/gamification/character_persistence_repository_test.dart (1)

14-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for legacy adventure-key cleanup.

Round-trip, empty, and corrupt-JSON cases are covered, but the new legacy-migration behavior (removing kynos_adventure_session_v1 on load) isn't tested.

✅ Suggested additional test
test('removes legacy adventure session data on load', () async {
  SharedPreferences.setMockInitialValues({
    'kynos_adventure_session_v1': '{"some":"legacy"}',
  });

  await repo.loadCampState();

  final prefs = await SharedPreferences.getInstance();
  expect(prefs.getString('kynos_adventure_session_v1'), isNull);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/infrastructure/gamification/character_persistence_repository_test.dart`
around lines 14 - 49, Add a test in CharacterPersistenceRepository camp state
coverage for the legacy migration path: when loadCampState() runs and
SharedPreferences contains kynos_adventure_session_v1, the legacy key should be
removed afterward. Extend the existing repository tests alongside the round-trip
and corrupt-JSON cases, using CharacterPersistenceRepository and
SharedPreferences to verify the cleanup behavior on load.
lib/domain/entities/gamification/camp_building.dart (1)

42-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add value equality to PlacedBuilding.

No ==/hashCode override means two structurally identical instances (e.g. after JSON round-trip) are not equal, which can break Riverpod state-change detection and any test that compares PlacedBuilding values directly. Since domain entities cannot depend on freezed (per the zero-dependency rule for lib/domain/), implement equality manually.

♻️ Proposed manual equality
   PlacedBuilding copyWith({
     CampBuildingType? type,
     int? level,
     int? row,
     int? col,
   }) =>
       PlacedBuilding(
         type: type ?? this.type,
         level: level ?? this.level,
         row: row ?? this.row,
         col: col ?? this.col,
       );
+
+  `@override`
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is PlacedBuilding &&
+          type == other.type &&
+          level == other.level &&
+          row == other.row &&
+          col == other.col;
+
+  `@override`
+  int get hashCode => Object.hash(type, level, row, col);

As per coding guidelines, "All data models must use @freezed for immutability, copyWith, equality, and hashCode," which requires equality support even where @freezed itself can't be used.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/entities/gamification/camp_building.dart` around lines 42 - 87,
`PlacedBuilding` currently has no value equality, so identical instances are
treated as different objects. Update the `PlacedBuilding` class to manually
override `==` and `hashCode` using its fields (`type`, `level`, `row`, `col`),
keeping behavior consistent with `copyWith`, `toJson`, and `fromJson`. Make sure
equality is structural so JSON round-trips and direct comparisons work correctly
in state management and tests.

Source: Coding guidelines

lib/domain/entities/gamification/camp_resources.dart (1)

2-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same missing value-equality gap as PlacedBuilding.

CampResources also lacks ==/hashCode, which affects equality-based comparisons of camp state downstream (e.g. Riverpod rebuild checks, test assertions).

♻️ Proposed manual equality
   CampResources withSpent({
     int momentum = 0,
     int fuel = 0,
     int focus = 0,
     int spirit = 0,
   }) =>
       copyWith(
         spentMomentum: spentMomentum + momentum,
         spentFuel: spentFuel + fuel,
         spentFocus: spentFocus + focus,
         spentSpirit: spentSpirit + spirit,
       );
+
+  `@override`
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is CampResources &&
+          totalMomentum == other.totalMomentum &&
+          totalFuel == other.totalFuel &&
+          totalFocus == other.totalFocus &&
+          totalSpirit == other.totalSpirit &&
+          spentMomentum == other.spentMomentum &&
+          spentFuel == other.spentFuel &&
+          spentFocus == other.spentFocus &&
+          spentSpirit == other.spentSpirit &&
+          restMultiplier == other.restMultiplier;
+
+  `@override`
+  int get hashCode => Object.hash(
+        totalMomentum,
+        totalFuel,
+        totalFocus,
+        totalSpirit,
+        spentMomentum,
+        spentFuel,
+        spentFocus,
+        spentSpirit,
+        restMultiplier,
+      );

As per coding guidelines, "All data models must use @freezed for immutability, copyWith, equality, and hashCode."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/entities/gamification/camp_resources.dart` around lines 2 - 74,
CampResources is missing value-based equality, so instances with the same
resource values still compare by identity and can break state comparisons and
tests. Update the CampResources model to use the project’s standard immutable
data-model approach with `@freezed`, or otherwise add proper == and hashCode
support consistent with copyWith/withSpent so equality checks on totalMomentum,
totalFuel, totalFocus, totalSpirit, spentMomentum, spentFuel, spentFocus,
spentSpirit, and restMultiplier work correctly.

Source: Coding guidelines

lib/domain/entities/gamification/camp_state.dart (1)

98-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate hardcoded weeklyGoal value.

100 is repeated in CampState.initial() and forCurrentWeek(). Extract to a shared constant (ideally from gamification_constants.dart, which this PR stack introduces) so both paths stay in sync if the weekly target is tuned later.

♻️ Proposed fix
+  static const int defaultWeeklyGoal = 100;
+
   final int gridSize;
     return CampState(
       gridSize: size,
       tiles: tiles,
       buildings: const [],
       weeklyAltitude: 0,
-      weeklyGoal: 100,
+      weeklyGoal: defaultWeeklyGoal,
       weekStart: weekStart,
     return copyWith(
       weekStart: start,
       weeklyAltitude: 0,
-      weeklyGoal: 100,
+      weeklyGoal: defaultWeeklyGoal,
     );

Also applies to: 119-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/entities/gamification/camp_state.dart` around lines 98 - 111, The
`CampState.initial()` and `CampState.forCurrentWeek()` builders both hardcode
the same `weeklyGoal` value, so extract that value into a shared constant and
use it in both places. Prefer referencing the new shared constant from
`gamification_constants.dart` so `CampState` stays aligned if the weekly target
changes later.
lib/domain/usecases/gamification/expand_camp_tile_usecase.dart (1)

62-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Magic number for altitude gain.

weeklyAltitude + 2 hardcodes the tile-expand altitude reward inline, while the Momentum cost uses GamificationConstants.momentumPerTileExpand. Consider adding a matching constant (e.g. altitudeGainPerTileExpand) for consistency and easier balance tuning.

♻️ Proposed fix
     return ExpandCampTileResult(
       camp: camp.copyWith(
         tiles: updatedTiles,
         spentMomentum:
             camp.spentMomentum + GamificationConstants.momentumPerTileExpand,
-        weeklyAltitude: camp.weeklyAltitude + 2,
+        weeklyAltitude:
+            camp.weeklyAltitude + GamificationConstants.altitudeGainPerTileExpand,
       ),
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/expand_camp_tile_usecase.dart` around lines
62 - 69, The tile-expand altitude reward in ExpandCampTileUsecase is hardcoded
with a magic number, unlike the momentum cost which uses
GamificationConstants.momentumPerTileExpand. Replace the inline weeklyAltitude
increment in ExpandCampTileResult/camp.copyWith with a dedicated
GamificationConstants value such as altitudeGainPerTileExpand, and use that
constant consistently in the expansion logic so the reward is easy to tune.
lib/domain/usecases/gamification/build_camp_structure_usecase.dart (1)

63-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Magic number for max building level.

existing.level < 3 hardcodes the level cap; consider a GamificationConstants.maxBuildingLevel to keep this in sync with CampBuildingType.upgradeFuelCost/summitContribution, which presumably also assume a 3-level cap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/build_camp_structure_usecase.dart` around
lines 63 - 78, The max building level is hardcoded in the build flow, so update
the `BuildCampStructureUseCase` upgrade branch to use a shared constant instead
of `existing.level < 3`. Introduce or reuse
`GamificationConstants.maxBuildingLevel` and make sure the `failure` message
path still triggers when the cap is reached; also verify any related
`CampBuildingType.upgradeFuelCost` and `summitContribution` logic stays aligned
with the same cap.
lib/domain/usecases/gamification/rest_camp_usecase.dart (1)

52-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Magic number for altitude gain.

weeklyAltitude + 3 mirrors the same inline-constant concern raised for ExpandCampTileUseCase; consider centralizing all altitude-gain values in GamificationConstants.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/rest_camp_usecase.dart` around lines 52 -
60, The rest camp update uses an inline altitude increment, which should be
centralized instead of hardcoded. Update RestCampUsecase’s camp.copyWith call to
use a dedicated altitude-gain constant from GamificationConstants rather than
adding 3 directly, and align this with the same centralized pattern used for
other altitude adjustments like in ExpandCampTileUseCase. Ensure the new
constant is reused wherever weeklyAltitude gains are applied so the value stays
consistent across the gamification flow.
lib/shared/providers/camp_providers.dart (2)

213-231: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Silent failure swallowing in _awardXp.

When loadCharacter/saveCharacter fail, the method just returns — the expedition still completes and persists, but the XP/stat reward is silently lost with no log trace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/shared/providers/camp_providers.dart` around lines 213 - 231, The
_awardXp method is swallowing load/save failures, so XP/stat rewards can
disappear without any trace. Update _awardXp in camp_providers.dart to log or
otherwise surface errors from characterRepositoryProvider.loadCharacter() and
repo.saveCharacter() before returning, using the existing _awardXp,
loadCharacter, and saveCharacter flow as the hook points. Keep the early
returns, but add clear failure reporting so reward loss is observable when the
expedition completes.

167-167: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Magic number 3 for spirit cost duplicated.

The expedition spirit cost is hard-coded in two places. Consider extracting a named constant (e.g., GamificationConstants.expeditionSpiritCost) to avoid drift if the cost is tuned later.

Also applies to: 199-199

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/shared/providers/camp_providers.dart` at line 167, The expedition spirit
cost is duplicated as a hard-coded magic number in camp provider logic, which
can drift if the value changes. Extract the shared cost into a named constant
such as GamificationConstants.expeditionSpiritCost, then update the checks in
the relevant camp provider methods (including the current canSpendSpirit guard
and the other matching use) to reference that constant instead of literal 3.
lib/domain/usecases/gamification/generate_camp_quests_usecase.dart (2)

10-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unused character parameter.

character is a required parameter of call() but is never referenced anywhere in the method body or in _momentumQuest/_fuelQuest/_focusQuest. Either wire it into quest personalization (e.g., class-specific stat rewards/titles) or drop the parameter to avoid a misleading API surface.

Also applies to: 52-104

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/generate_camp_quests_usecase.dart` around
lines 10 - 14, The required character parameter in
GenerateCampQuestsUsecase.call is currently unused, so either thread
RunnerCharacter through the quest-building flow or remove it from the API.
Update call, _momentumQuest, _fuelQuest, and _focusQuest consistently so the
character is actually referenced for personalization, or simplify the signature
and all call sites if it is not needed.

60-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Only one momentum-quest branch is unit tested.

_momentumQuest alternates between the steps objective and the exercise-minutes objective based on now.day.isEven. The provided test (camp_game_usecase_test.dart) only exercises referenceTime: DateTime(2026, 7, 6) (day 6, even → steps branch); the exercise-minutes branch (odd day) has no coverage in the supplied tests.

As per coding guidelines, "Every use-case in domain/usecases/ must have a corresponding unit test."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/generate_camp_quests_usecase.dart` around
lines 60 - 104, The _momentumQuest branch in GenerateCampQuestsUsecase is only
covered for the even-day steps path, so add a unit test that uses an odd
`referenceTime` to exercise the exercise-minutes path. Update the existing
`camp_game_usecase_test.dart` coverage to call the same use case and verify the
returned `Quest` from `_momentumQuest` has `QuestObjectiveKind.exerciseMinutes`,
the expected title/objective text, and the correct target chosen from
`GamificationConstants.questExerciseMinNormal` or `questExerciseMinEasy` based
on readiness.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/domain/entities/gamification/expedition_event.dart`:
- Around line 29-46: The stat-delta parsing in ExpeditionEvent.fromJson is too
permissive in the wrong place and unsafe in another: entry.value is cast
directly to num, and unknown stat names are currently mapped to
CharacterStatId.endurance. Update the parsing so malformed or missing values are
handled safely like the other fields, and do not substitute unrecognized keys
with a real stat; instead skip invalid entries or otherwise avoid merging them
into an existing CharacterStatId. Focus the fix inside ExpeditionEvent.fromJson
where rawDeltas, deltas, and CharacterStatId.values.firstWhere are used.

In `@lib/domain/usecases/gamification/advance_weekly_summit_usecase.dart`:
- Around line 4-31: Add a dedicated unit test suite for
AdvanceWeeklySummitUseCase, since this use case currently has no corresponding
coverage. Create tests for the call method that verify the Sunday bonus path,
bonusAltitude accumulation, and the weeklyGoal resync behavior when
CampState.weeklyGoal differs from GamificationConstants.weeklySummitGoal. Use
the AdvanceWeeklySummitUseCase class and its call entrypoint to locate the
logic, and assert the updated CampState values returned by
forCurrentWeek/copyWith.

In `@lib/domain/usecases/gamification/build_camp_structure_usecase.dart`:
- Around line 5-15: BuildCampStructureResult is using a raw String for failure,
which breaks the domain error contract. Update the BuildCampStructureResult
model in build_camp_structure_usecase.dart to store a Failure from
core/errors/failures.dart instead of String, and adjust isSuccess to reflect
that contract. Make sure any code that creates or consumes
BuildCampStructureResult (including the related BuildCampStructureUsecase flow)
now passes and handles the Failure sealed class consistently, matching the
pattern used in ExpandCampTileResult.

In `@lib/domain/usecases/gamification/expand_camp_tile_usecase.dart`:
- Around line 5-15: The ExpandCampTileResult type is using a raw String for
failure instead of the domain Failure contract. Update ExpandCampTileResult to
carry a Failure? from core/errors/failures.dart, and adjust isSuccess to reflect
the presence of a Failure rather than a string. Make sure the related
ExpandCampTile use case and any callers that inspect failure are updated to work
with the Failure sealed class instead of doing string-based handling.

In `@lib/domain/usecases/gamification/resolve_expedition_usecase.dart`:
- Around line 45-54: The stat delta updates in resolve_expedition_usecase.dart
are overwriting when the same CharacterStatId is hit by multiple branches.
Update the logic around statDeltas in the expedition use case so each bonus
accumulates instead of assigning directly, especially for the endurance,
willpower, and character.stats.weakest paths. Preserve the intended totals by
merging increments for the same key rather than replacing prior values.

In `@lib/domain/usecases/gamification/rest_camp_usecase.dart`:
- Around line 4-14: The RestCampResult model is carrying failures as a raw
String instead of using the domain Failure hierarchy. Update RestCampResult to
depend on the Failure sealed class from core/errors/failures.dart, and change
its failure field and any related success checks in RestCampResult to work with
Failure rather than String so this use case follows the same error pattern as
the other camp use cases.

In `@lib/features/character/presentation/widgets/camp_game_panel.dart`:
- Around line 126-144: The async callbacks in `_onTileTap` use the outer
`context` after an async gap, which can fail if the widget is unmounted. Add a
`context.mounted` guard before calling `Navigator.of(context).pop()` and before
any later use of `context` in the `onExpand` and `onBuild` handlers around
`CampBuildSheet.show` and the `campSessionProvider` notifier calls. Keep the
navigation and build/expand flow the same, but only proceed when the widget is
still mounted.

In `@lib/shared/providers/daily_quests_provider.dart`:
- Around line 45-55: The persistence result from repo.saveQuests in
DailyQuestsProvider is being ignored, so save failures are silently dropped.
Update the quest generation flow in the provider method that reads
generateCampQuestsUseCaseProvider to check the return value of
repo.saveQuests(quests) and surface or throw on failure, matching the previous
behavior instead of always returning quests as if they were saved.

---

Outside diff comments:
In `@test/features/character/camp_game_panel_test.dart`:
- Around line 1-35: The test coverage is for CampResourcesBar, not
CampGamePanel, so add a widget test that targets CampGamePanel itself. Build the
panel under a ProviderScope and override the relevant providers such as
campSessionProvider, healthSummaryProvider, and recentRunsProvider with
fakes/mocks so it does not hit real repositories. Verify the panel’s key UI and
flow behavior, including tile selection and the build sheet interaction, using
the CampGamePanel widget rather than the CampResourcesBar widget.

---

Nitpick comments:
In `@lib/domain/entities/gamification/camp_building.dart`:
- Around line 42-87: `PlacedBuilding` currently has no value equality, so
identical instances are treated as different objects. Update the
`PlacedBuilding` class to manually override `==` and `hashCode` using its fields
(`type`, `level`, `row`, `col`), keeping behavior consistent with `copyWith`,
`toJson`, and `fromJson`. Make sure equality is structural so JSON round-trips
and direct comparisons work correctly in state management and tests.

In `@lib/domain/entities/gamification/camp_resources.dart`:
- Around line 2-74: CampResources is missing value-based equality, so instances
with the same resource values still compare by identity and can break state
comparisons and tests. Update the CampResources model to use the project’s
standard immutable data-model approach with `@freezed`, or otherwise add proper ==
and hashCode support consistent with copyWith/withSpent so equality checks on
totalMomentum, totalFuel, totalFocus, totalSpirit, spentMomentum, spentFuel,
spentFocus, spentSpirit, and restMultiplier work correctly.

In `@lib/domain/entities/gamification/camp_state.dart`:
- Around line 98-111: The `CampState.initial()` and `CampState.forCurrentWeek()`
builders both hardcode the same `weeklyGoal` value, so extract that value into a
shared constant and use it in both places. Prefer referencing the new shared
constant from `gamification_constants.dart` so `CampState` stays aligned if the
weekly target changes later.

In `@lib/domain/usecases/gamification/build_camp_structure_usecase.dart`:
- Around line 63-78: The max building level is hardcoded in the build flow, so
update the `BuildCampStructureUseCase` upgrade branch to use a shared constant
instead of `existing.level < 3`. Introduce or reuse
`GamificationConstants.maxBuildingLevel` and make sure the `failure` message
path still triggers when the cap is reached; also verify any related
`CampBuildingType.upgradeFuelCost` and `summitContribution` logic stays aligned
with the same cap.

In `@lib/domain/usecases/gamification/expand_camp_tile_usecase.dart`:
- Around line 62-69: The tile-expand altitude reward in ExpandCampTileUsecase is
hardcoded with a magic number, unlike the momentum cost which uses
GamificationConstants.momentumPerTileExpand. Replace the inline weeklyAltitude
increment in ExpandCampTileResult/camp.copyWith with a dedicated
GamificationConstants value such as altitudeGainPerTileExpand, and use that
constant consistently in the expansion logic so the reward is easy to tune.

In `@lib/domain/usecases/gamification/generate_camp_quests_usecase.dart`:
- Around line 10-14: The required character parameter in
GenerateCampQuestsUsecase.call is currently unused, so either thread
RunnerCharacter through the quest-building flow or remove it from the API.
Update call, _momentumQuest, _fuelQuest, and _focusQuest consistently so the
character is actually referenced for personalization, or simplify the signature
and all call sites if it is not needed.
- Around line 60-104: The _momentumQuest branch in GenerateCampQuestsUsecase is
only covered for the even-day steps path, so add a unit test that uses an odd
`referenceTime` to exercise the exercise-minutes path. Update the existing
`camp_game_usecase_test.dart` coverage to call the same use case and verify the
returned `Quest` from `_momentumQuest` has `QuestObjectiveKind.exerciseMinutes`,
the expected title/objective text, and the correct target chosen from
`GamificationConstants.questExerciseMinNormal` or `questExerciseMinEasy` based
on readiness.

In `@lib/domain/usecases/gamification/rest_camp_usecase.dart`:
- Around line 52-60: The rest camp update uses an inline altitude increment,
which should be centralized instead of hardcoded. Update RestCampUsecase’s
camp.copyWith call to use a dedicated altitude-gain constant from
GamificationConstants rather than adding 3 directly, and align this with the
same centralized pattern used for other altitude adjustments like in
ExpandCampTileUseCase. Ensure the new constant is reused wherever weeklyAltitude
gains are applied so the value stays consistent across the gamification flow.

In `@lib/features/character/presentation/widgets/camp_grid.dart`:
- Around line 93-107: The icon-only tile states in camp_grid.dart are not
accessible because the locked, building, and empty cases use bare Icon widgets
without any semantic description. Update the widget branch in the tile builder
to wrap each state in appropriate accessibility semantics or provide labels for
the Icon(s) so screen readers can distinguish locked, building type, and empty
terrain tiles; use the existing state checks around isLocked, building, and
_buildingIcon to attach the correct label.

In `@lib/features/character/presentation/widgets/summit_progress_card.dart`:
- Around line 15-18: The SummitProgressCard widget computes isSunday directly
inside build with DateTime.now(), which makes the summit-day logic hard to test
and tied to device-local time. Move this date decision out of build by injecting
the current date or a clock-derived value into SummitProgressCard, then use that
injected value to determine isSunday so tests can supply a fixed date and the
shared time source can control the boundary.

In `@lib/shared/providers/camp_providers.dart`:
- Around line 213-231: The _awardXp method is swallowing load/save failures, so
XP/stat rewards can disappear without any trace. Update _awardXp in
camp_providers.dart to log or otherwise surface errors from
characterRepositoryProvider.loadCharacter() and repo.saveCharacter() before
returning, using the existing _awardXp, loadCharacter, and saveCharacter flow as
the hook points. Keep the early returns, but add clear failure reporting so
reward loss is observable when the expedition completes.
- Line 167: The expedition spirit cost is duplicated as a hard-coded magic
number in camp provider logic, which can drift if the value changes. Extract the
shared cost into a named constant such as
GamificationConstants.expeditionSpiritCost, then update the checks in the
relevant camp provider methods (including the current canSpendSpirit guard and
the other matching use) to reference that constant instead of literal 3.

In `@test/infrastructure/gamification/character_persistence_repository_test.dart`:
- Around line 14-49: Add a test in CharacterPersistenceRepository camp state
coverage for the legacy migration path: when loadCampState() runs and
SharedPreferences contains kynos_adventure_session_v1, the legacy key should be
removed afterward. Extend the existing repository tests alongside the round-trip
and corrupt-JSON cases, using CharacterPersistenceRepository and
SharedPreferences to verify the cleanup behavior on load.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 78422db0-28c2-4f22-9883-b95c42e649bb

📥 Commits

Reviewing files that changed from the base of the PR and between 52a845b and 2badae6.

📒 Files selected for processing (59)
  • CODEMAP.md
  • lib/core/constants/gamification_constants.dart
  • lib/domain/entities/gamification/activity_resources.dart
  • lib/domain/entities/gamification/adventure_session.dart
  • lib/domain/entities/gamification/camp_building.dart
  • lib/domain/entities/gamification/camp_resources.dart
  • lib/domain/entities/gamification/camp_state.dart
  • lib/domain/entities/gamification/camp_tile.dart
  • lib/domain/entities/gamification/encounter_state.dart
  • lib/domain/entities/gamification/expedition_event.dart
  • lib/domain/entities/gamification/quest.dart
  • lib/domain/entities/gamification/trail_node.dart
  • lib/domain/repositories/character_repository.dart
  • lib/domain/usecases/gamification/advance_weekly_summit_usecase.dart
  • lib/domain/usecases/gamification/build_camp_structure_usecase.dart
  • lib/domain/usecases/gamification/compute_activity_resources_usecase.dart
  • lib/domain/usecases/gamification/compute_camp_resources_usecase.dart
  • lib/domain/usecases/gamification/evaluate_quest_progress_usecase.dart
  • lib/domain/usecases/gamification/expand_camp_tile_usecase.dart
  • lib/domain/usecases/gamification/generate_camp_quests_usecase.dart
  • lib/domain/usecases/gamification/generate_daily_quests_usecase.dart
  • lib/domain/usecases/gamification/generate_daily_trail_usecase.dart
  • lib/domain/usecases/gamification/resolve_encounter_turn_usecase.dart
  • lib/domain/usecases/gamification/resolve_expedition_usecase.dart
  • lib/domain/usecases/gamification/rest_camp_usecase.dart
  • lib/features/character/presentation/pages/character_page.dart
  • lib/features/character/presentation/widgets/activity_resources_bar.dart
  • lib/features/character/presentation/widgets/camp_build_sheet.dart
  • lib/features/character/presentation/widgets/camp_game_panel.dart
  • lib/features/character/presentation/widgets/camp_grid.dart
  • lib/features/character/presentation/widgets/camp_resources_bar.dart
  • lib/features/character/presentation/widgets/encounter_panel.dart
  • lib/features/character/presentation/widgets/expedition_card.dart
  • lib/features/character/presentation/widgets/summit_progress_card.dart
  • lib/features/character/presentation/widgets/trail_map.dart
  • lib/features/character/presentation/widgets/trail_run_game_panel.dart
  • lib/features/character/providers/adventure_provider.dart
  • lib/features/character/providers/adventure_provider.g.dart
  • lib/features/dashboard/presentation/pages/dashboard_page.dart
  • lib/features/dashboard/presentation/widgets/character_glance_card.dart
  • lib/features/dashboard/presentation/widgets/daily_quest_teaser.dart
  • lib/infrastructure/gamification/character_persistence_repository.dart
  • lib/shared/providers/camp_providers.dart
  • lib/shared/providers/camp_providers.g.dart
  • lib/shared/providers/daily_quests_provider.dart
  • lib/shared/providers/gamification_providers.dart
  • test/domain/entities/gamification/gamification_entity_json_test.dart
  • test/domain/usecases/gamification/camp_actions_usecase_test.dart
  • test/domain/usecases/gamification/camp_game_usecase_test.dart
  • test/domain/usecases/gamification/compute_activity_resources_usecase_test.dart
  • test/domain/usecases/gamification/compute_camp_resources_usecase_test.dart
  • test/domain/usecases/gamification/evaluate_quest_progress_usecase_test.dart
  • test/domain/usecases/gamification/generate_daily_quests_usecase_test.dart
  • test/domain/usecases/gamification/generate_daily_trail_usecase_test.dart
  • test/domain/usecases/gamification/resolve_encounter_turn_usecase_test.dart
  • test/features/character/camp_game_panel_test.dart
  • test/features/character/trail_map_test.dart
  • test/features/character/trail_run_game_panel_test.dart
  • test/infrastructure/gamification/character_persistence_repository_test.dart
💤 Files with no reviewable changes (20)
  • test/domain/usecases/gamification/generate_daily_trail_usecase_test.dart
  • lib/features/character/presentation/widgets/trail_map.dart
  • test/domain/usecases/gamification/compute_activity_resources_usecase_test.dart
  • test/domain/usecases/gamification/generate_daily_quests_usecase_test.dart
  • lib/domain/entities/gamification/adventure_session.dart
  • lib/domain/usecases/gamification/generate_daily_trail_usecase.dart
  • lib/domain/usecases/gamification/compute_activity_resources_usecase.dart
  • lib/domain/usecases/gamification/generate_daily_quests_usecase.dart
  • lib/features/character/presentation/widgets/trail_run_game_panel.dart
  • lib/domain/entities/gamification/encounter_state.dart
  • test/features/character/trail_map_test.dart
  • lib/features/character/presentation/widgets/activity_resources_bar.dart
  • lib/features/character/presentation/widgets/encounter_panel.dart
  • test/features/character/trail_run_game_panel_test.dart
  • test/domain/usecases/gamification/resolve_encounter_turn_usecase_test.dart
  • lib/domain/entities/gamification/trail_node.dart
  • lib/domain/entities/gamification/activity_resources.dart
  • lib/features/character/providers/adventure_provider.g.dart
  • lib/domain/usecases/gamification/resolve_encounter_turn_usecase.dart
  • lib/features/character/providers/adventure_provider.dart

Comment on lines +29 to +46
factory ExpeditionEvent.fromJson(Map<String, dynamic> json) {
final rawDeltas = json['stat_deltas'] as Map<String, dynamic>? ?? {};
final deltas = <CharacterStatId, int>{};
for (final entry in rawDeltas.entries) {
final id = CharacterStatId.values.firstWhere(
(s) => s.name == entry.key,
orElse: () => CharacterStatId.endurance,
);
deltas[id] = (entry.value as num).toInt();
}
return ExpeditionEvent(
title: json['title'] as String? ?? 'Expedition',
narrative: json['narrative'] as String? ?? '',
xpReward: (json['xp_reward'] as num?)?.toInt() ?? 0,
statDeltas: deltas,
summitBonus: (json['summit_bonus'] as num?)?.toInt() ?? 0,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Unsafe cast and silent stat misattribution in fromJson.

Two issues in the stat-delta parsing:

  • Line 37 casts entry.value as num without a null-safe ?, unlike every other field here — a malformed/legacy persisted value will throw instead of degrading gracefully.
  • Lines 33-36 fall back an unrecognized stat name to endurance, which can silently merge/overwrite an unrelated stat's delta rather than just dropping the bad entry.
🛡️ Proposed fix
     final rawDeltas = json['stat_deltas'] as Map<String, dynamic>? ?? {};
     final deltas = <CharacterStatId, int>{};
     for (final entry in rawDeltas.entries) {
-      final id = CharacterStatId.values.firstWhere(
-        (s) => s.name == entry.key,
-        orElse: () => CharacterStatId.endurance,
-      );
-      deltas[id] = (entry.value as num).toInt();
+      final id = CharacterStatId.values
+          .where((s) => s.name == entry.key)
+          .firstOrNull;
+      if (id == null) continue;
+      deltas[id] = (entry.value as num?)?.toInt() ?? 0;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/entities/gamification/expedition_event.dart` around lines 29 - 46,
The stat-delta parsing in ExpeditionEvent.fromJson is too permissive in the
wrong place and unsafe in another: entry.value is cast directly to num, and
unknown stat names are currently mapped to CharacterStatId.endurance. Update the
parsing so malformed or missing values are handled safely like the other fields,
and do not substitute unrecognized keys with a real stat; instead skip invalid
entries or otherwise avoid merging them into an existing CharacterStatId. Focus
the fix inside ExpeditionEvent.fromJson where rawDeltas, deltas, and
CharacterStatId.values.firstWhere are used.

Comment on lines +4 to +31
class AdvanceWeeklySummitUseCase {
const AdvanceWeeklySummitUseCase();

CampState call({
required CampState camp,
required DateTime reference,
int bonusAltitude = 0,
}) {
var updated = camp.forCurrentWeek(reference);
final isSunday = reference.weekday == DateTime.sunday;
final sundayBonus =
isSunday ? GamificationConstants.sundaySummitBonus : 0;

if (bonusAltitude > 0 || sundayBonus > 0) {
updated = updated.copyWith(
weeklyAltitude: updated.weeklyAltitude + bonusAltitude + sundayBonus,
);
}

if (updated.weeklyGoal != GamificationConstants.weeklySummitGoal) {
updated = updated.copyWith(
weeklyGoal: GamificationConstants.weeklySummitGoal,
);
}

return updated;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Missing dedicated unit test for AdvanceWeeklySummitUseCase.

The provided test files (camp_actions_usecase_test.dart, compute_camp_resources_usecase_test.dart) don't cover this use case. As per coding guidelines, "Every use-case in domain/usecases/ must have a corresponding unit test."

Want me to draft a test covering the Sunday bonus, bonusAltitude accumulation, and weeklyGoal resync branches?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/advance_weekly_summit_usecase.dart` around
lines 4 - 31, Add a dedicated unit test suite for AdvanceWeeklySummitUseCase,
since this use case currently has no corresponding coverage. Create tests for
the call method that verify the Sunday bonus path, bonusAltitude accumulation,
and the weeklyGoal resync behavior when CampState.weeklyGoal differs from
GamificationConstants.weeklySummitGoal. Use the AdvanceWeeklySummitUseCase class
and its call entrypoint to locate the logic, and assert the updated CampState
values returned by forCurrentWeek/copyWith.

Source: Coding guidelines

Comment on lines +5 to +15
class BuildCampStructureResult {
const BuildCampStructureResult({
required this.camp,
this.failure,
});

final CampState camp;
final String? failure;

bool get isSuccess => failure == null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Failure represented as raw String instead of the Failure sealed class.

Same concern as in ExpandCampTileResult: this violates the domain error contract.

As per coding guidelines, "Domain errors must extend the Failure sealed class in core/errors/failures.dart."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/build_camp_structure_usecase.dart` around
lines 5 - 15, BuildCampStructureResult is using a raw String for failure, which
breaks the domain error contract. Update the BuildCampStructureResult model in
build_camp_structure_usecase.dart to store a Failure from
core/errors/failures.dart instead of String, and adjust isSuccess to reflect
that contract. Make sure any code that creates or consumes
BuildCampStructureResult (including the related BuildCampStructureUsecase flow)
now passes and handles the Failure sealed class consistently, matching the
pattern used in ExpandCampTileResult.

Source: Coding guidelines

Comment on lines +5 to +15
class ExpandCampTileResult {
const ExpandCampTileResult({
required this.camp,
this.failure,
});

final CampState camp;
final String? failure;

bool get isSuccess => failure == null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Failure represented as raw String instead of the Failure sealed class.

Coding guidelines require domain errors to extend Failure. This result type instead exposes a plain String? failure, which diverges from the established error-handling contract and pushes ad-hoc string matching/display into the UI layer.

As per coding guidelines, "Domain errors must extend the Failure sealed class in core/errors/failures.dart."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/expand_camp_tile_usecase.dart` around lines
5 - 15, The ExpandCampTileResult type is using a raw String for failure instead
of the domain Failure contract. Update ExpandCampTileResult to carry a Failure?
from core/errors/failures.dart, and adjust isSuccess to reflect the presence of
a Failure rather than a string. Make sure the related ExpandCampTile use case
and any callers that inspect failure are updated to work with the Failure sealed
class instead of doing string-based handling.

Source: Coding guidelines

Comment on lines +45 to +54
final statDeltas = <CharacterStatId, int>{};
if (distanceKm >= 5) {
statDeltas[CharacterStatId.endurance] = 2;
}
if (durationMin >= 30) {
statDeltas[CharacterStatId.willpower] = 1;
}
if (distanceKm >= 3) {
statDeltas[character.stats.weakest] = 1;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stat deltas overwrite instead of accumulate.

Since distanceKm >= 5 also satisfies distanceKm >= 3, both the endurance branch and the "weakest stat" branch can fire for the same run. If character.stats.weakest happens to be endurance (or willpower, similarly overlapping with the duration branch), the later assignment on line 53 silently overwrites the earlier one, reducing the intended reward (e.g., endurance +2 becomes +1).

🐛 Proposed fix to accumulate stat deltas
     final statDeltas = <CharacterStatId, int>{};
+    void addStat(CharacterStatId id, int amount) {
+      statDeltas.update(id, (v) => v + amount, ifAbsent: () => amount);
+    }
     if (distanceKm >= 5) {
-      statDeltas[CharacterStatId.endurance] = 2;
+      addStat(CharacterStatId.endurance, 2);
     }
     if (durationMin >= 30) {
-      statDeltas[CharacterStatId.willpower] = 1;
+      addStat(CharacterStatId.willpower, 1);
     }
     if (distanceKm >= 3) {
-      statDeltas[character.stats.weakest] = 1;
+      addStat(character.stats.weakest, 1);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final statDeltas = <CharacterStatId, int>{};
if (distanceKm >= 5) {
statDeltas[CharacterStatId.endurance] = 2;
}
if (durationMin >= 30) {
statDeltas[CharacterStatId.willpower] = 1;
}
if (distanceKm >= 3) {
statDeltas[character.stats.weakest] = 1;
}
final statDeltas = <CharacterStatId, int>{};
void addStat(CharacterStatId id, int amount) {
statDeltas.update(id, (v) => v + amount, ifAbsent: () => amount);
}
if (distanceKm >= 5) {
addStat(CharacterStatId.endurance, 2);
}
if (durationMin >= 30) {
addStat(CharacterStatId.willpower, 1);
}
if (distanceKm >= 3) {
addStat(character.stats.weakest, 1);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/resolve_expedition_usecase.dart` around
lines 45 - 54, The stat delta updates in resolve_expedition_usecase.dart are
overwriting when the same CharacterStatId is hit by multiple branches. Update
the logic around statDeltas in the expedition use case so each bonus accumulates
instead of assigning directly, especially for the endurance, willpower, and
character.stats.weakest paths. Preserve the intended totals by merging
increments for the same key rather than replacing prior values.

Comment on lines +4 to +14
class RestCampResult {
const RestCampResult({
required this.camp,
this.failure,
});

final CampState camp;
final String? failure;

bool get isSuccess => failure == null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Failure represented as raw String instead of the Failure sealed class.

Same concern as the other camp use cases.

As per coding guidelines, "Domain errors must extend the Failure sealed class in core/errors/failures.dart."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/domain/usecases/gamification/rest_camp_usecase.dart` around lines 4 - 14,
The RestCampResult model is carrying failures as a raw String instead of using
the domain Failure hierarchy. Update RestCampResult to depend on the Failure
sealed class from core/errors/failures.dart, and change its failure field and
any related success checks in RestCampResult to work with Failure rather than
String so this use case follows the same error pattern as the other camp use
cases.

Source: Coding guidelines

Comment on lines +126 to +144
await CampBuildSheet.show(
context,
camp: viewState.camp,
row: row,
col: col,
availableFuel: viewState.resources.availableFuel,
availableMomentum: viewState.resources.availableMomentum,
onExpand: () async {
Navigator.of(context).pop();
await ref.read(campSessionProvider.notifier).expandTile(row, col);
},
onBuild: (type) async {
Navigator.of(context).pop();
await ref
.read(campSessionProvider.notifier)
.buildStructure(row: row, col: col, type: type);
},
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing context.mounted guard across async gaps.

_onTileTap is async and the onExpand/onBuild callbacks use the outer context (via Navigator.of(context).pop()) after awaiting CampBuildSheet.show/notifier calls. If the widget is unmounted in the meantime (e.g., user navigates away while the sheet is open), this can throw. As per coding guidelines, Do not use BuildContext across async gaps without a mounted guard.

🛡️ Suggested fix
       onExpand: () async {
+        if (!context.mounted) return;
         Navigator.of(context).pop();
         await ref.read(campSessionProvider.notifier).expandTile(row, col);
       },
       onBuild: (type) async {
+        if (!context.mounted) return;
         Navigator.of(context).pop();
         await ref
             .read(campSessionProvider.notifier)
             .buildStructure(row: row, col: col, type: type);
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await CampBuildSheet.show(
context,
camp: viewState.camp,
row: row,
col: col,
availableFuel: viewState.resources.availableFuel,
availableMomentum: viewState.resources.availableMomentum,
onExpand: () async {
Navigator.of(context).pop();
await ref.read(campSessionProvider.notifier).expandTile(row, col);
},
onBuild: (type) async {
Navigator.of(context).pop();
await ref
.read(campSessionProvider.notifier)
.buildStructure(row: row, col: col, type: type);
},
);
}
await CampBuildSheet.show(
context,
camp: viewState.camp,
row: row,
col: col,
availableFuel: viewState.resources.availableFuel,
availableMomentum: viewState.resources.availableMomentum,
onExpand: () async {
if (!context.mounted) return;
Navigator.of(context).pop();
await ref.read(campSessionProvider.notifier).expandTile(row, col);
},
onBuild: (type) async {
if (!context.mounted) return;
Navigator.of(context).pop();
await ref
.read(campSessionProvider.notifier)
.buildStructure(row: row, col: col, type: type);
},
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/character/presentation/widgets/camp_game_panel.dart` around
lines 126 - 144, The async callbacks in `_onTileTap` use the outer `context`
after an async gap, which can fail if the widget is unmounted. Add a
`context.mounted` guard before calling `Navigator.of(context).pop()` and before
any later use of `context` in the `onExpand` and `onBuild` handlers around
`CampBuildSheet.show` and the `campSessionProvider` notifier calls. Keep the
navigation and build/expand flow the same, but only proceed when the widget is
still mounted.

Source: Coding guidelines

Comment on lines +45 to +55
final useCase = ref.read(generateCampQuestsUseCaseProvider);
final quests = useCase(
character: character,
readinessScore: readiness,
);

if (result.failure != null) {
throw result.failure!;
if (quests.isNotEmpty) {
await repo.saveQuests(quests);
}

if (result.quests.isNotEmpty) {
await repo.saveQuests(result.quests);
}

return result.quests;
return quests;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

repo.saveQuests failure result is discarded.

Unlike the previous flow (which threw on failure), the return value of repo.saveQuests(quests) on line 52 isn't checked. If persistence fails, quests are silently not saved with no error surfaced — the app will just regenerate them next load rather than notifying the caller.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/shared/providers/daily_quests_provider.dart` around lines 45 - 55, The
persistence result from repo.saveQuests in DailyQuestsProvider is being ignored,
so save failures are silently dropped. Update the quest generation flow in the
provider method that reads generateCampQuestsUseCaseProvider to check the return
value of repo.saveQuests(quests) and surface or throw on failure, matching the
previous behavior instead of always returning quests as if they were saved.

…73)

Co-authored-by: Youri Bontekoe <YKDBontekoe@users.noreply.github.com>
@YKDBontekoe
YKDBontekoe merged commit 45a23cb into main Jul 6, 2026
13 checks passed
@YKDBontekoe
YKDBontekoe deleted the cursor/summit-camp-game-redesign-1edb branch July 6, 2026 21:02
github-actions Bot pushed a commit that referenced this pull request Jul 6, 2026
# [1.15.0](v1.14.1...v1.15.0) (2026-07-06)

### Features

* enrich run route viewer with pace charts and splits ([#73](#73)) ([c196f0e](c196f0e))
* replace trail run with summit camp game ([#71](#71)) ([45a23cb](45a23cb))
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.15.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants