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:
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user