feat(engine): wire AnimSpeed to animation, new achievements, leaderboard opt-in, daily goal display
- AnimSpeed setting now drives card slide duration (Normal=0.15s, Fast=0.07s, Instant=snap); EffectiveSlideDuration resource updated on SettingsChangedEvent; AnimSpeed row added to Settings panel - GameState.recycle_count tracks waste recycles; perfectionist/comeback/zen_winner achievements added with full unit tests - SyncProvider gains opt_in_leaderboard(); SolitaireServerClient implements POST /api/leaderboard/opt-in; Opt In button added to leaderboard panel - DailyChallengeResource stores goal_description/target_score/max_time_secs from server; pressing C shows goal description as toast (DailyGoalAnnouncementEvent) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,10 @@ pub struct GameState {
|
||||
/// Number of times `undo()` has been successfully invoked this game.
|
||||
/// Used by achievement conditions like `no_undo`.
|
||||
pub undo_count: u32,
|
||||
/// Number of times the waste pile has been recycled back to stock this game.
|
||||
/// Used by the `comeback` achievement condition.
|
||||
#[serde(default)]
|
||||
pub recycle_count: u32,
|
||||
undo_stack: VecDeque<StateSnapshot>,
|
||||
}
|
||||
|
||||
@@ -116,6 +120,7 @@ impl GameState {
|
||||
is_won: false,
|
||||
is_auto_completable: false,
|
||||
undo_count: 0,
|
||||
recycle_count: 0,
|
||||
undo_stack: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
@@ -167,6 +172,7 @@ impl GameState {
|
||||
card.face_up = false;
|
||||
stock.cards.push(card);
|
||||
}
|
||||
self.recycle_count = self.recycle_count.saturating_add(1);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -481,6 +487,24 @@ mod tests {
|
||||
assert!(g.piles[&PileType::Waste].cards.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recycle_count_increments_on_each_waste_recycle() {
|
||||
let mut g = new_game();
|
||||
assert_eq!(g.recycle_count, 0);
|
||||
// Drain entire stock to waste.
|
||||
while !g.piles[&PileType::Stock].cards.is_empty() {
|
||||
g.draw().unwrap();
|
||||
}
|
||||
g.draw().unwrap(); // first recycle
|
||||
assert_eq!(g.recycle_count, 1);
|
||||
// Drain again and recycle a second time.
|
||||
while !g.piles[&PileType::Stock].cards.is_empty() {
|
||||
g.draw().unwrap();
|
||||
}
|
||||
g.draw().unwrap(); // second recycle
|
||||
assert_eq!(g.recycle_count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_from_empty_stock_and_waste_returns_error() {
|
||||
// The only stop condition for draw() is: both stock AND waste are
|
||||
|
||||
Reference in New Issue
Block a user