feat(engine): add AchievementPlugin with persistent unlock tracking

On GameWonEvent, build an AchievementContext from StatsResource + GameState
+ wall-clock hour, evaluate ALL_ACHIEVEMENTS, flip newly-satisfied records
to unlocked, persist atomically, and emit AchievementUnlockedEvent for
each new unlock. AnimationPlugin's toast resolves the event's ID to the
achievement's display name via achievement_plugin::display_name_for.

Introduces StatsUpdate system set so AchievementPlugin can reliably run
after StatsResource reflects the win. AchievementPlugin::headless() used
in tests to avoid touching ~/.local/share/solitaire_quest/achievements.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-24 12:53:31 -07:00
parent 4589c52368
commit 1f6994a084
5 changed files with 256 additions and 6 deletions
+2 -1
View File
@@ -5,6 +5,7 @@
use bevy::prelude::*;
use crate::achievement_plugin::display_name_for;
use crate::card_plugin::CardEntity;
use crate::events::{AchievementUnlockedEvent, GameWonEvent};
use crate::game_plugin::GameMutation;
@@ -126,7 +127,7 @@ fn handle_achievement_toast(
for ev in events.read() {
spawn_toast(
&mut commands,
format!("Achievement: {}", ev.0),
format!("Achievement: {}", display_name_for(&ev.0)),
ACHIEVEMENT_TOAST_SECS,
);
}