ca5788f714
After the player's very first win the engine now writes "First win! Press A to see your achievements." via InfoToastEvent, then flips a persisted Settings.shown_achievement_onboarding flag so the cue never re-fires. Mentions the A hotkey by name so the toast is actionable on its own. The toast path runs after StatsUpdate so games_won has been incremented to 1 when the system reads it; .after(GameMutation) keeps the post-move state visible. Three guards: first win only, flag was false, GameWonEvent fired this tick. Persistence mirrors onboarding_plugin's complete_onboarding pattern: save via save_settings_to with the existing SettingsStoragePath/Option<&PathBuf> graceful-fallback shape. Atomic .tmp+rename writes are unchanged. Settings gains a single bool field with #[serde(default)] so legacy settings.json files deserialize cleanly to false. The field is local-only by design — it's about UI teaching for THIS device, not progression — so SyncPayload and merge logic are untouched. Seven new tests pin the contract: default value is false, field round-trips through save/load, legacy JSON without the field deserializes to false, first win fires the toast and flips the flag, subsequent wins are silent, the fifth win on a synced device is silent (won't fire when games_won has been bumped via sync), and no win event means no toast. Toast duration is the existing animation_plugin QUEUED_TOAST_SECS = 2.5 s — InfoToastEvent is a tuple struct with no duration parameter, so the agent kept the existing event shape rather than expanding it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>