feat(engine): add daily challenge, level-up toast, and daily_devotee achievement

Phase 6 part 2 (partial):
- daily_seed_for(date) and PlayerProgress::record_daily_completion in
  solitaire_data, with streak logic that increments on consecutive days,
  resets on a skipped day, and is idempotent on same-day re-completions.
- DailyChallengePlugin tracks today's seed, awards +100 XP and updates
  the streak when the player wins a game whose seed matches. Pressing C
  starts a new game with the daily seed.
- LevelUpEvent toast in AnimationPlugin announces level changes.
- AchievementContext gains daily_challenge_streak; daily_devotee
  achievement unlocks at streak >= 7. AchievementPlugin reads
  ProgressResource and runs after ProgressUpdate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-24 19:17:59 -07:00
parent 0cb8b32ec4
commit 622b35a3bf
8 changed files with 376 additions and 9 deletions
+27
View File
@@ -20,6 +20,10 @@ pub struct AchievementContext {
pub lifetime_score: u64,
pub draw_three_wins: u32,
// Progression.
/// Current daily-challenge completion streak (consecutive days).
pub daily_challenge_streak: u32,
// Last-win facts (GameWonEvent + GameState at win time).
pub last_win_score: i32,
pub last_win_time_seconds: u64,
@@ -96,6 +100,9 @@ fn early_bird(c: &AchievementContext) -> bool {
fn speed_and_skill(c: &AchievementContext) -> bool {
c.last_win_time_seconds < 90 && !c.last_win_used_undo
}
fn daily_devotee(c: &AchievementContext) -> bool {
c.daily_challenge_streak >= 7
}
/// All currently-evaluable achievements. Order is stable so persistence files
/// remain readable across versions (new achievements append).
@@ -198,6 +205,13 @@ pub const ALL_ACHIEVEMENTS: &[AchievementDef] = &[
secret: true,
condition: speed_and_skill,
},
AchievementDef {
id: "daily_devotee",
name: "Daily Devotee",
description: "Complete the daily challenge 7 days in a row",
secret: false,
condition: daily_devotee,
},
];
/// Return every `AchievementDef` whose condition is satisfied by `ctx`.
@@ -225,6 +239,7 @@ mod tests {
best_single_score: 0,
lifetime_score: 0,
draw_three_wins: 0,
daily_challenge_streak: 0,
last_win_score: 0,
last_win_time_seconds: u64::MAX,
last_win_used_undo: true,
@@ -310,6 +325,18 @@ mod tests {
assert!(!ids.contains(&"night_owl"));
}
#[test]
fn daily_devotee_requires_7_day_streak() {
let mut c = ctx();
c.daily_challenge_streak = 6;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(!ids.contains(&"daily_devotee"));
c.daily_challenge_streak = 7;
let ids: Vec<&str> = check_achievements(&c).iter().map(|d| d.id).collect();
assert!(ids.contains(&"daily_devotee"));
}
#[test]
fn achievement_by_id_finds_known_and_returns_none_for_unknown() {
assert_eq!(achievement_by_id("first_win").map(|d| d.name), Some("First Win"));