feat(engine,core): add Time Attack mode + unlocks panel
- Core: GameMode::TimeAttack variant (no scoring/undo changes — session marker only) - Engine: TimeAttackPlugin with TimeAttackResource, TimeAttackEndedEvent, T hotkey (gated to level >= 5), auto-deal on win, summary toast - Engine: Stats overlay (S) gains an Unlocks subsection (card backs / backgrounds, sorted/deduped) and a live Time Attack panel while active Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+15
-7
@@ -1,8 +1,8 @@
|
||||
# Solitaire Quest — Session Handoff
|
||||
|
||||
> Last updated: 2026-04-24
|
||||
> Last updated: 2026-04-25
|
||||
> Branch: `master` — pushed to https://git.aleshym.co/funman300/Rusty_Solitare.git
|
||||
> Test count: **214 passing** (83 core + 54 data + 77 engine), `cargo clippy --workspace -- -D warnings` clean
|
||||
> Test count: **222 passing** (83 core + 54 data + 85 engine), `cargo clippy --workspace -- -D warnings` clean
|
||||
|
||||
---
|
||||
|
||||
@@ -140,19 +140,27 @@ All sub-phases (3A–3F) done. Plugins: `GamePlugin`, `TablePlugin`, `CardPlugin
|
||||
- `ChallengePlugin` advances the cursor on Challenge-mode wins, persists, fires `ChallengeAdvancedEvent`. **X** key starts a Challenge-mode game with the current seed.
|
||||
- Both **Z** (Zen) and **X** (Challenge) are gated to `level >= CHALLENGE_UNLOCK_LEVEL` (5).
|
||||
|
||||
### Phase 6 (part 4c) — Time Attack + Unlock UI ✅ COMPLETE
|
||||
|
||||
- `GameMode::TimeAttack` variant added to core (no scoring/undo changes — just a session marker).
|
||||
- `TimeAttackPlugin` (engine) — `TimeAttackResource { active, remaining_secs, wins }` (session-only, not persisted), `TimeAttackEndedEvent { wins }`. **T** starts a session (gated to level ≥ 5) and deals a TimeAttack-mode game; the timer (`TIME_ATTACK_DURATION_SECS = 600.0`) decrements each frame; wins during the active session bump the counter and auto-deal a fresh game.
|
||||
- `AnimationPlugin` surfaces `TimeAttackEndedEvent` as a 5-second summary toast.
|
||||
- `StatsPlugin` overlay (**S**) appends an "Unlocks" subsection (card backs / backgrounds, sorted/deduped, "None" when empty) and a live "Time Attack" panel showing remaining minutes/seconds + wins while a session is active.
|
||||
- Helper `format_id_list` factored out + tested.
|
||||
|
||||
## What Is Next
|
||||
|
||||
### Phase 6 (part 4c) — Time Attack + Unlock UI
|
||||
### Phase 7 — Audio + Polish
|
||||
|
||||
- **Time Attack mode**: 10-minute countdown, auto-deal a fresh game on win, score = total wins; on timer expiry show summary. Likely needs a `TimeAttackResource { remaining: f32, wins: u32 }` and a system that decrements `remaining` and ends the session.
|
||||
- **Card-back / background unlock UI** for `unlocked_card_backs` / `unlocked_backgrounds`. Achievement rewards already populate these vecs via the persisted `AchievementRecord.reward_granted` flag — UI just needs to surface what's available.
|
||||
- Audio (`kira`): card deal/flip/place/invalid SFX, win fanfare, ambient loop. Volume sliders in a Settings overlay.
|
||||
- Onboarding: first-run hint overlay (rules summary + key list).
|
||||
- Pause menu (Esc currently logs a placeholder).
|
||||
- Optional: ChallengeAdvancedEvent → toast in `AnimationPlugin`.
|
||||
|
||||
### Phases 7–8 (in order after Phase 6 part 4c)
|
||||
### Phase 8 — Sync
|
||||
|
||||
| Phase | Scope |
|
||||
|---|---|
|
||||
| Phase 7 | Audio (`kira`), polish, hints, onboarding, pause menu |
|
||||
| Phase 8A–C | Local storage + `SyncProvider` + self-hosted Axum server + client |
|
||||
| Phase 8D | GPGS stub fully wired into settings UI |
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use bevy::prelude::*;
|
||||
use solitaire_engine::{
|
||||
AchievementPlugin, AnimationPlugin, CardPlugin, ChallengePlugin, DailyChallengePlugin,
|
||||
GamePlugin, InputPlugin, ProgressPlugin, StatsPlugin, TablePlugin, WeeklyGoalsPlugin,
|
||||
GamePlugin, InputPlugin, ProgressPlugin, StatsPlugin, TablePlugin, TimeAttackPlugin,
|
||||
WeeklyGoalsPlugin,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@@ -27,5 +28,6 @@ fn main() {
|
||||
.add_plugins(DailyChallengePlugin)
|
||||
.add_plugins(WeeklyGoalsPlugin)
|
||||
.add_plugins(ChallengePlugin)
|
||||
.add_plugins(TimeAttackPlugin)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -22,12 +22,16 @@ pub enum DrawMode {
|
||||
/// - `Zen`: scoring suppressed (stays at 0); undo allowed; intended for relaxed play.
|
||||
/// - `Challenge`: standard scoring, **undo disabled** (returns
|
||||
/// `MoveError::RuleViolation`).
|
||||
/// - `TimeAttack`: standard scoring + undo; the engine wraps a 10-minute
|
||||
/// countdown around the session and auto-deals a fresh game on every win
|
||||
/// (see `solitaire_engine::TimeAttackPlugin`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum GameMode {
|
||||
#[default]
|
||||
Classic,
|
||||
Zen,
|
||||
Challenge,
|
||||
TimeAttack,
|
||||
}
|
||||
|
||||
/// Snapshot of game state used for undo.
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::events::{AchievementUnlockedEvent, GameWonEvent};
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::layout::LayoutResource;
|
||||
use crate::progress_plugin::LevelUpEvent;
|
||||
use crate::time_attack_plugin::TimeAttackEndedEvent;
|
||||
use crate::weekly_goals_plugin::WeeklyGoalCompletedEvent;
|
||||
|
||||
/// Duration of a card slide (move) animation in seconds.
|
||||
@@ -22,6 +23,7 @@ const ACHIEVEMENT_TOAST_SECS: f32 = 3.0;
|
||||
const LEVELUP_TOAST_SECS: f32 = 3.0;
|
||||
const DAILY_TOAST_SECS: f32 = 3.0;
|
||||
const WEEKLY_TOAST_SECS: f32 = 3.0;
|
||||
const TIME_ATTACK_TOAST_SECS: f32 = 5.0;
|
||||
const CASCADE_STAGGER: f32 = 0.05;
|
||||
const CASCADE_DURATION: f32 = 0.5;
|
||||
|
||||
@@ -59,6 +61,7 @@ impl Plugin for AnimationPlugin {
|
||||
.add_event::<LevelUpEvent>()
|
||||
.add_event::<DailyChallengeCompletedEvent>()
|
||||
.add_event::<WeeklyGoalCompletedEvent>()
|
||||
.add_event::<TimeAttackEndedEvent>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
@@ -68,6 +71,7 @@ impl Plugin for AnimationPlugin {
|
||||
handle_levelup_toast,
|
||||
handle_daily_toast,
|
||||
handle_weekly_toast,
|
||||
handle_time_attack_toast,
|
||||
tick_toasts,
|
||||
)
|
||||
.after(GameMutation),
|
||||
@@ -181,6 +185,19 @@ fn handle_weekly_toast(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_time_attack_toast(
|
||||
mut commands: Commands,
|
||||
mut events: EventReader<TimeAttackEndedEvent>,
|
||||
) {
|
||||
for ev in events.read() {
|
||||
spawn_toast(
|
||||
&mut commands,
|
||||
format!("Time Attack: {} win{}", ev.wins, if ev.wins == 1 { "" } else { "s" }),
|
||||
TIME_ATTACK_TOAST_SECS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_toasts(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod progress_plugin;
|
||||
pub mod resources;
|
||||
pub mod stats_plugin;
|
||||
pub mod table_plugin;
|
||||
pub mod time_attack_plugin;
|
||||
pub mod weekly_goals_plugin;
|
||||
|
||||
pub use achievement_plugin::{AchievementPlugin, AchievementsResource};
|
||||
@@ -36,3 +37,6 @@ pub use layout::{compute_layout, Layout, LayoutResource};
|
||||
pub use resources::{DragState, GameStateResource, SyncStatus, SyncStatusResource};
|
||||
pub use stats_plugin::{StatsPlugin, StatsResource, StatsScreen, StatsUpdate};
|
||||
pub use table_plugin::{PileMarker, TableBackground, TablePlugin};
|
||||
pub use time_attack_plugin::{
|
||||
TimeAttackEndedEvent, TimeAttackPlugin, TimeAttackResource, TIME_ATTACK_DURATION_SECS,
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::events::{GameWonEvent, NewGameRequestEvent};
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::progress_plugin::ProgressResource;
|
||||
use crate::resources::GameStateResource;
|
||||
use crate::time_attack_plugin::TimeAttackResource;
|
||||
|
||||
/// Bevy resource wrapping the current stats.
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
@@ -127,6 +128,7 @@ fn toggle_stats_screen(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
stats: Res<StatsResource>,
|
||||
progress: Option<Res<ProgressResource>>,
|
||||
time_attack: Option<Res<TimeAttackResource>>,
|
||||
screens: Query<Entity, With<StatsScreen>>,
|
||||
) {
|
||||
if !keys.just_pressed(KeyCode::KeyS) {
|
||||
@@ -135,7 +137,12 @@ fn toggle_stats_screen(
|
||||
if let Ok(entity) = screens.get_single() {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
} else {
|
||||
spawn_stats_screen(&mut commands, &stats.0, progress.as_deref().map(|p| &p.0));
|
||||
spawn_stats_screen(
|
||||
&mut commands,
|
||||
&stats.0,
|
||||
progress.as_deref().map(|p| &p.0),
|
||||
time_attack.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +150,7 @@ fn spawn_stats_screen(
|
||||
commands: &mut Commands,
|
||||
stats: &StatsSnapshot,
|
||||
progress: Option<&PlayerProgress>,
|
||||
time_attack: Option<&TimeAttackResource>,
|
||||
) {
|
||||
let win_rate = stats
|
||||
.win_rate()
|
||||
@@ -194,6 +202,27 @@ fn spawn_stats_screen(
|
||||
goal.description, progress_value, goal.target
|
||||
));
|
||||
}
|
||||
lines.push(String::new());
|
||||
lines.push("-- Unlocks --".to_string());
|
||||
lines.push(format!(
|
||||
" Card Backs: {}",
|
||||
format_id_list(&p.unlocked_card_backs)
|
||||
));
|
||||
lines.push(format!(
|
||||
" Backgrounds: {}",
|
||||
format_id_list(&p.unlocked_backgrounds)
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(ta) = time_attack {
|
||||
if ta.active {
|
||||
let mins = (ta.remaining_secs / 60.0).floor() as u64;
|
||||
let secs = (ta.remaining_secs % 60.0).floor() as u64;
|
||||
lines.push(String::new());
|
||||
lines.push("=== Time Attack ===".to_string());
|
||||
lines.push(format!("Remaining: {mins}m {secs:02}s"));
|
||||
lines.push(format!("Wins: {}", ta.wins));
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(String::new());
|
||||
@@ -237,6 +266,22 @@ fn format_duration(secs: u64) -> String {
|
||||
format!("{m}m {s:02}s")
|
||||
}
|
||||
|
||||
/// Renders a sorted, comma-separated list of unlock indexes for the overlay.
|
||||
/// Empty list shows as "None".
|
||||
fn format_id_list(ids: &[usize]) -> String {
|
||||
if ids.is_empty() {
|
||||
return "None".to_string();
|
||||
}
|
||||
let mut sorted: Vec<usize> = ids.to_vec();
|
||||
sorted.sort_unstable();
|
||||
sorted.dedup();
|
||||
sorted
|
||||
.iter()
|
||||
.map(|i| format!("#{i}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -369,4 +414,14 @@ mod tests {
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_id_list_renders_empty_as_none() {
|
||||
assert_eq!(format_id_list(&[]), "None");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_id_list_sorts_dedups_and_prefixes() {
|
||||
assert_eq!(format_id_list(&[3, 1, 1, 2]), "#1, #2, #3");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
//! Time Attack mode runtime: 10-minute countdown wrapped around back-to-back
|
||||
//! `GameMode::TimeAttack` games. Pressing **T** starts a session (gated by
|
||||
//! level ≥ `CHALLENGE_UNLOCK_LEVEL`); each win during the session bumps the
|
||||
//! counter and auto-deals a fresh game. When the timer expires the session
|
||||
//! ends and `TimeAttackEndedEvent` fires.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use solitaire_core::game_state::GameMode;
|
||||
|
||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||
use crate::events::{GameWonEvent, NewGameRequestEvent};
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::progress_plugin::ProgressResource;
|
||||
use crate::resources::GameStateResource;
|
||||
|
||||
/// Length of a Time Attack session in real-world seconds (10 minutes).
|
||||
pub const TIME_ATTACK_DURATION_SECS: f32 = 600.0;
|
||||
|
||||
/// Session state for an in-progress Time Attack run. Not persisted.
|
||||
#[derive(Resource, Debug, Clone, Default)]
|
||||
pub struct TimeAttackResource {
|
||||
pub active: bool,
|
||||
pub remaining_secs: f32,
|
||||
pub wins: u32,
|
||||
}
|
||||
|
||||
/// Fired when the Time Attack timer expires. The summary toast in
|
||||
/// `AnimationPlugin` consumes this; UI/stats consumers can also subscribe.
|
||||
#[derive(Event, Debug, Clone, Copy)]
|
||||
pub struct TimeAttackEndedEvent {
|
||||
pub wins: u32,
|
||||
}
|
||||
|
||||
pub struct TimeAttackPlugin;
|
||||
|
||||
impl Plugin for TimeAttackPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<TimeAttackResource>()
|
||||
.add_event::<TimeAttackEndedEvent>()
|
||||
.add_event::<GameWonEvent>()
|
||||
.add_event::<NewGameRequestEvent>()
|
||||
.add_systems(
|
||||
Update,
|
||||
handle_start_time_attack_request.before(GameMutation),
|
||||
)
|
||||
.add_systems(Update, advance_time_attack)
|
||||
.add_systems(Update, auto_deal_on_time_attack_win.after(GameMutation));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_start_time_attack_request(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
progress: Res<ProgressResource>,
|
||||
mut session: ResMut<TimeAttackResource>,
|
||||
mut new_game: EventWriter<NewGameRequestEvent>,
|
||||
) {
|
||||
if !keys.just_pressed(KeyCode::KeyT) {
|
||||
return;
|
||||
}
|
||||
if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
|
||||
info!(
|
||||
"Time Attack locked — reach level {} (currently {}).",
|
||||
CHALLENGE_UNLOCK_LEVEL, progress.0.level
|
||||
);
|
||||
return;
|
||||
}
|
||||
*session = TimeAttackResource {
|
||||
active: true,
|
||||
remaining_secs: TIME_ATTACK_DURATION_SECS,
|
||||
wins: 0,
|
||||
};
|
||||
new_game.send(NewGameRequestEvent {
|
||||
seed: None,
|
||||
mode: Some(GameMode::TimeAttack),
|
||||
});
|
||||
}
|
||||
|
||||
fn advance_time_attack(
|
||||
time: Res<Time>,
|
||||
mut session: ResMut<TimeAttackResource>,
|
||||
mut ended: EventWriter<TimeAttackEndedEvent>,
|
||||
) {
|
||||
if !session.active {
|
||||
return;
|
||||
}
|
||||
session.remaining_secs -= time.delta_secs();
|
||||
if session.remaining_secs <= 0.0 {
|
||||
let wins = session.wins;
|
||||
session.active = false;
|
||||
session.remaining_secs = 0.0;
|
||||
ended.send(TimeAttackEndedEvent { wins });
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_deal_on_time_attack_win(
|
||||
mut wins: EventReader<GameWonEvent>,
|
||||
game: Res<GameStateResource>,
|
||||
mut session: ResMut<TimeAttackResource>,
|
||||
mut new_game: EventWriter<NewGameRequestEvent>,
|
||||
) {
|
||||
for _ in wins.read() {
|
||||
if !session.active || game.0.mode != GameMode::TimeAttack {
|
||||
continue;
|
||||
}
|
||||
session.wins = session.wins.saturating_add(1);
|
||||
new_game.send(NewGameRequestEvent {
|
||||
seed: None,
|
||||
mode: Some(GameMode::TimeAttack),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::game_plugin::GamePlugin;
|
||||
use crate::progress_plugin::ProgressPlugin;
|
||||
use crate::table_plugin::TablePlugin;
|
||||
use solitaire_core::game_state::{DrawMode, GameState};
|
||||
|
||||
fn headless_app() -> App {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins)
|
||||
.add_plugins(GamePlugin)
|
||||
.add_plugins(TablePlugin)
|
||||
.add_plugins(ProgressPlugin::headless())
|
||||
.add_plugins(TimeAttackPlugin);
|
||||
app.init_resource::<ButtonInput<KeyCode>>();
|
||||
app.update();
|
||||
app
|
||||
}
|
||||
|
||||
fn press_t(app: &mut App) {
|
||||
let mut input = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
|
||||
input.release(KeyCode::KeyT);
|
||||
input.clear();
|
||||
input.press(KeyCode::KeyT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_t_below_unlock_level_is_ignored() {
|
||||
let mut app = headless_app();
|
||||
press_t(&mut app);
|
||||
app.update();
|
||||
|
||||
let session = app.world().resource::<TimeAttackResource>();
|
||||
assert!(!session.active);
|
||||
|
||||
let events = app.world().resource::<Events<NewGameRequestEvent>>();
|
||||
let mut cursor = events.get_cursor();
|
||||
assert!(cursor.read(events).next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_t_at_unlock_level_starts_session_and_deals_time_attack_game() {
|
||||
let mut app = headless_app();
|
||||
app.world_mut().resource_mut::<ProgressResource>().0.level = CHALLENGE_UNLOCK_LEVEL;
|
||||
|
||||
press_t(&mut app);
|
||||
app.update();
|
||||
|
||||
let session = app.world().resource::<TimeAttackResource>().clone();
|
||||
assert!(session.active);
|
||||
assert_eq!(session.wins, 0);
|
||||
assert!((session.remaining_secs - TIME_ATTACK_DURATION_SECS).abs() < 1.0);
|
||||
|
||||
let events = app.world().resource::<Events<NewGameRequestEvent>>();
|
||||
let mut cursor = events.get_cursor();
|
||||
let fired: Vec<_> = cursor.read(events).copied().collect();
|
||||
assert_eq!(fired.len(), 1);
|
||||
assert_eq!(fired[0].mode, Some(GameMode::TimeAttack));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timer_expiry_fires_ended_event_and_clears_active() {
|
||||
let mut app = headless_app();
|
||||
// Manually start a near-expired session.
|
||||
*app.world_mut().resource_mut::<TimeAttackResource>() = TimeAttackResource {
|
||||
active: true,
|
||||
remaining_secs: 0.001,
|
||||
wins: 5,
|
||||
};
|
||||
app.update();
|
||||
// First update advances time slightly; force the timer past zero.
|
||||
*app.world_mut().resource_mut::<TimeAttackResource>() = TimeAttackResource {
|
||||
active: true,
|
||||
remaining_secs: -1.0,
|
||||
wins: 5,
|
||||
};
|
||||
app.update();
|
||||
|
||||
let session = app.world().resource::<TimeAttackResource>();
|
||||
assert!(!session.active);
|
||||
assert_eq!(session.remaining_secs, 0.0);
|
||||
|
||||
let events = app.world().resource::<Events<TimeAttackEndedEvent>>();
|
||||
let mut cursor = events.get_cursor();
|
||||
let fired: Vec<_> = cursor.read(events).copied().collect();
|
||||
assert_eq!(fired.len(), 1);
|
||||
assert_eq!(fired[0].wins, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn win_during_session_increments_wins_and_auto_deals() {
|
||||
let mut app = headless_app();
|
||||
// Start a session manually.
|
||||
*app.world_mut().resource_mut::<TimeAttackResource>() = TimeAttackResource {
|
||||
active: true,
|
||||
remaining_secs: 100.0,
|
||||
wins: 0,
|
||||
};
|
||||
// The current game must be in TimeAttack mode for auto-deal to fire.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack);
|
||||
|
||||
app.world_mut().send_event(GameWonEvent {
|
||||
score: 500,
|
||||
time_seconds: 60,
|
||||
});
|
||||
app.update();
|
||||
|
||||
let session = app.world().resource::<TimeAttackResource>();
|
||||
assert_eq!(session.wins, 1);
|
||||
|
||||
let events = app.world().resource::<Events<NewGameRequestEvent>>();
|
||||
let mut cursor = events.get_cursor();
|
||||
let fired: Vec<_> = cursor.read(events).copied().collect();
|
||||
assert_eq!(fired.len(), 1);
|
||||
assert_eq!(fired[0].mode, Some(GameMode::TimeAttack));
|
||||
assert!(fired[0].seed.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn win_when_session_inactive_does_not_increment() {
|
||||
let mut app = headless_app();
|
||||
// Default session is inactive. Game is TimeAttack mode — still no count.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack);
|
||||
|
||||
app.world_mut().send_event(GameWonEvent {
|
||||
score: 500,
|
||||
time_seconds: 60,
|
||||
});
|
||||
app.update();
|
||||
|
||||
let session = app.world().resource::<TimeAttackResource>();
|
||||
assert_eq!(session.wins, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_win_during_session_does_not_increment() {
|
||||
let mut app = headless_app();
|
||||
*app.world_mut().resource_mut::<TimeAttackResource>() = TimeAttackResource {
|
||||
active: true,
|
||||
remaining_secs: 100.0,
|
||||
wins: 0,
|
||||
};
|
||||
// GameStateResource defaults to Classic mode.
|
||||
app.world_mut().send_event(GameWonEvent {
|
||||
score: 500,
|
||||
time_seconds: 60,
|
||||
});
|
||||
app.update();
|
||||
|
||||
let session = app.world().resource::<TimeAttackResource>();
|
||||
assert_eq!(session.wins, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user