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:
@@ -41,6 +41,11 @@ pub trait SyncProvider: Send + Sync {
|
||||
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError> {
|
||||
Ok(None)
|
||||
}
|
||||
/// Opt the authenticated player into the leaderboard with the given
|
||||
/// display name. No-op for backends that don't support leaderboards.
|
||||
async fn opt_in_leaderboard(&self, _display_name: &str) -> Result<(), SyncError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Blanket impl so `Box<dyn SyncProvider + Send + Sync>` (returned by
|
||||
@@ -68,6 +73,9 @@ impl SyncProvider for Box<dyn SyncProvider + Send + Sync> {
|
||||
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError> {
|
||||
(**self).fetch_daily_challenge().await
|
||||
}
|
||||
async fn opt_in_leaderboard(&self, display_name: &str) -> Result<(), SyncError> {
|
||||
(**self).opt_in_leaderboard(display_name).await
|
||||
}
|
||||
}
|
||||
|
||||
pub mod stats;
|
||||
|
||||
@@ -225,6 +225,42 @@ impl SyncProvider for SolitaireServerClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn opt_in_leaderboard(&self, display_name: &str) -> Result<(), SyncError> {
|
||||
let token = self.access_token()?;
|
||||
let url = format!("{}/api/leaderboard/opt-in", self.base_url);
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.post(&url)
|
||||
.bearer_auth(&token)
|
||||
.json(&serde_json::json!({ "display_name": display_name }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| SyncError::Network(e.to_string()))?;
|
||||
|
||||
if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
self.refresh_token().await?;
|
||||
let new_token = self.access_token()?;
|
||||
let resp = self
|
||||
.client
|
||||
.post(&url)
|
||||
.bearer_auth(new_token)
|
||||
.json(&serde_json::json!({ "display_name": display_name }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| SyncError::Network(e.to_string()))?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(SyncError::Auth(format!("opt-in failed: {}", resp.status())));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(SyncError::Auth(format!("opt-in failed: {}", resp.status())));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, SyncError> {
|
||||
let token = self.access_token()?;
|
||||
let url = format!("{}/api/leaderboard", self.base_url);
|
||||
|
||||
Reference in New Issue
Block a user