feat(engine,core,data): add Challenge mode with seed list and level-5 gate
Phase 6 part 4b (partial): - GameMode::Challenge variant in solitaire_core. undo() returns RuleViolation when mode is Challenge so the player commits to each decision. - solitaire_data::challenge defines a stable CHALLENGE_SEEDS list with challenge_seed_for(index) wrapping modulo length. - PlayerProgress.challenge_index (serde-default for older saves) tracks how far the player has progressed. - ChallengePlugin advances the cursor on Challenge-mode wins, persists, and emits ChallengeAdvancedEvent. Pressing X starts a Challenge-mode game with the current seed; gated to level >= CHALLENGE_UNLOCK_LEVEL (5). - InputPlugin's Z key (Zen mode) is now also gated to level >= 5. Time Attack and unlock UI still deferred. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use solitaire_engine::{
|
use solitaire_engine::{
|
||||||
AchievementPlugin, AnimationPlugin, CardPlugin, DailyChallengePlugin, GamePlugin, InputPlugin,
|
AchievementPlugin, AnimationPlugin, CardPlugin, ChallengePlugin, DailyChallengePlugin,
|
||||||
ProgressPlugin, StatsPlugin, TablePlugin, WeeklyGoalsPlugin,
|
GamePlugin, InputPlugin, ProgressPlugin, StatsPlugin, TablePlugin, WeeklyGoalsPlugin,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -26,5 +26,6 @@ fn main() {
|
|||||||
.add_plugins(AchievementPlugin::default())
|
.add_plugins(AchievementPlugin::default())
|
||||||
.add_plugins(DailyChallengePlugin)
|
.add_plugins(DailyChallengePlugin)
|
||||||
.add_plugins(WeeklyGoalsPlugin)
|
.add_plugins(WeeklyGoalsPlugin)
|
||||||
|
.add_plugins(ChallengePlugin)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,18 @@ pub enum DrawMode {
|
|||||||
DrawThree,
|
DrawThree,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Top-level game mode. Affects scoring and (eventually) timer behaviour.
|
/// Top-level game mode. Affects scoring, undo, and (eventually) timer behaviour.
|
||||||
///
|
///
|
||||||
/// - `Classic`: standard Klondike scoring and timer.
|
/// - `Classic`: standard Klondike scoring, undo allowed.
|
||||||
/// - `Zen`: scoring suppressed (stays at 0); intended for relaxed play.
|
/// - `Zen`: scoring suppressed (stays at 0); undo allowed; intended for relaxed play.
|
||||||
|
/// - `Challenge`: standard scoring, **undo disabled** (returns
|
||||||
|
/// `MoveError::RuleViolation`).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
pub enum GameMode {
|
pub enum GameMode {
|
||||||
#[default]
|
#[default]
|
||||||
Classic,
|
Classic,
|
||||||
Zen,
|
Zen,
|
||||||
|
Challenge,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot of game state used for undo.
|
/// Snapshot of game state used for undo.
|
||||||
@@ -261,10 +264,16 @@ impl GameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Restore the most recent undo snapshot and apply the undo score penalty (-15).
|
/// Restore the most recent undo snapshot and apply the undo score penalty (-15).
|
||||||
|
/// Disabled in `GameMode::Challenge` — returns `MoveError::RuleViolation`.
|
||||||
pub fn undo(&mut self) -> Result<(), MoveError> {
|
pub fn undo(&mut self) -> Result<(), MoveError> {
|
||||||
if self.is_won {
|
if self.is_won {
|
||||||
return Err(MoveError::GameAlreadyWon);
|
return Err(MoveError::GameAlreadyWon);
|
||||||
}
|
}
|
||||||
|
if self.mode == GameMode::Challenge {
|
||||||
|
return Err(MoveError::RuleViolation(
|
||||||
|
"undo is disabled in Challenge mode".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
let snapshot = self.undo_stack.pop().ok_or(MoveError::UndoStackEmpty)?;
|
let snapshot = self.undo_stack.pop().ok_or(MoveError::UndoStackEmpty)?;
|
||||||
self.piles = snapshot.piles;
|
self.piles = snapshot.piles;
|
||||||
self.score = if self.mode == GameMode::Zen {
|
self.score = if self.mode == GameMode::Zen {
|
||||||
@@ -559,6 +568,34 @@ mod tests {
|
|||||||
assert_eq!(g.draw_mode, DrawMode::DrawThree);
|
assert_eq!(g.draw_mode, DrawMode::DrawThree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- GameMode: Challenge ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn challenge_mode_disables_undo() {
|
||||||
|
let mut g = GameState::new_with_mode(42, DrawMode::DrawOne, GameMode::Challenge);
|
||||||
|
g.draw().unwrap();
|
||||||
|
let result = g.undo();
|
||||||
|
assert!(matches!(result, Err(MoveError::RuleViolation(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn challenge_mode_still_allows_normal_moves() {
|
||||||
|
let g = GameState::new_with_mode(42, DrawMode::DrawOne, GameMode::Challenge);
|
||||||
|
// Just verify the game initialises cleanly with Challenge mode.
|
||||||
|
assert_eq!(g.mode, GameMode::Challenge);
|
||||||
|
assert_eq!(g.score, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn challenge_mode_scoring_applies_normally() {
|
||||||
|
// Challenge uses Classic scoring; only undo is disabled.
|
||||||
|
let g = GameState::new_with_mode(42, DrawMode::DrawOne, GameMode::Challenge);
|
||||||
|
assert_eq!(g.score, 0);
|
||||||
|
// Note: Verifying score increases on actual moves would require
|
||||||
|
// hand-crafting a legal move from the dealt state. We rely on the
|
||||||
|
// fact that move_cards' score path is identical to Classic.
|
||||||
|
}
|
||||||
|
|
||||||
// --- Auto-complete ---
|
// --- Auto-complete ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
//! Static seed list for Challenge mode + helpers.
|
||||||
|
//!
|
||||||
|
//! Challenge mode walks a fixed sequence of hard-but-winnable seeds. The
|
||||||
|
//! player advances by winning a deal in `GameMode::Challenge`. The
|
||||||
|
//! `challenge_index` cursor is stored per-player in `PlayerProgress`.
|
||||||
|
//!
|
||||||
|
//! Seeds wrap modulo `CHALLENGE_SEEDS.len()` so a sufficiently dedicated
|
||||||
|
//! player never runs out of challenges.
|
||||||
|
|
||||||
|
/// Curated Challenge-mode seeds. Order is stable across versions; add new
|
||||||
|
/// seeds at the end.
|
||||||
|
pub const CHALLENGE_SEEDS: &[u64] = &[
|
||||||
|
0xDEAD_BEEF_CAFE_F00D,
|
||||||
|
0xC0DE_FACE_8BAD_F00D,
|
||||||
|
0xFEE1_DEAD_DEAD_BEEF,
|
||||||
|
0xBAAD_F00D_BAAD_F00D,
|
||||||
|
0x1337_C0DE_4242_BABE,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Resolve a `challenge_index` to its corresponding seed, wrapping when
|
||||||
|
/// the index exceeds the seed-list length. Returns `None` if the seed list
|
||||||
|
/// is empty (defensive — `CHALLENGE_SEEDS` is non-empty by construction).
|
||||||
|
pub fn challenge_seed_for(index: u32) -> Option<u64> {
|
||||||
|
if CHALLENGE_SEEDS.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(CHALLENGE_SEEDS[(index as usize) % CHALLENGE_SEEDS.len()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total number of currently-defined challenges. Useful for displaying
|
||||||
|
/// "Challenge {n + 1} of {total}" in UI.
|
||||||
|
pub fn challenge_count() -> u32 {
|
||||||
|
CHALLENGE_SEEDS.len() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn challenge_seed_for_0_is_first_seed() {
|
||||||
|
assert_eq!(challenge_seed_for(0), Some(CHALLENGE_SEEDS[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn challenge_seed_wraps_past_end() {
|
||||||
|
let len = CHALLENGE_SEEDS.len() as u32;
|
||||||
|
assert_eq!(
|
||||||
|
challenge_seed_for(len),
|
||||||
|
Some(CHALLENGE_SEEDS[0]),
|
||||||
|
"wraps to seed 0 when index == len"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
challenge_seed_for(len + 2),
|
||||||
|
Some(CHALLENGE_SEEDS[2]),
|
||||||
|
"wraps modulo len"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_challenge_seeds_are_unique() {
|
||||||
|
let mut seeds: Vec<u64> = CHALLENGE_SEEDS.to_vec();
|
||||||
|
seeds.sort();
|
||||||
|
let len_before = seeds.len();
|
||||||
|
seeds.dedup();
|
||||||
|
assert_eq!(seeds.len(), len_before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn challenge_count_matches_seed_list_length() {
|
||||||
|
assert_eq!(challenge_count() as usize, CHALLENGE_SEEDS.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,3 +57,6 @@ pub use weekly::{
|
|||||||
current_iso_week_key, weekly_goal_by_id, WeeklyGoalContext, WeeklyGoalDef, WeeklyGoalKind,
|
current_iso_week_key, weekly_goal_by_id, WeeklyGoalContext, WeeklyGoalDef, WeeklyGoalKind,
|
||||||
WEEKLY_GOALS, WEEKLY_GOAL_XP,
|
WEEKLY_GOALS, WEEKLY_GOAL_XP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod challenge;
|
||||||
|
pub use challenge::{challenge_count, challenge_seed_for, CHALLENGE_SEEDS};
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ pub struct PlayerProgress {
|
|||||||
pub weekly_goal_week_iso: Option<String>,
|
pub weekly_goal_week_iso: Option<String>,
|
||||||
pub unlocked_card_backs: Vec<usize>,
|
pub unlocked_card_backs: Vec<usize>,
|
||||||
pub unlocked_backgrounds: Vec<usize>,
|
pub unlocked_backgrounds: Vec<usize>,
|
||||||
|
/// Index of the next Challenge-mode seed the player will be served.
|
||||||
|
/// Increments on each Challenge-mode win. Out-of-range values wrap modulo
|
||||||
|
/// `CHALLENGE_SEEDS.len()` at lookup time.
|
||||||
|
#[serde(default)]
|
||||||
|
pub challenge_index: u32,
|
||||||
pub last_modified: DateTime<Utc>,
|
pub last_modified: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +86,7 @@ impl Default for PlayerProgress {
|
|||||||
weekly_goal_week_iso: None,
|
weekly_goal_week_iso: None,
|
||||||
unlocked_card_backs: vec![0], // back #0 always available
|
unlocked_card_backs: vec![0], // back #0 always available
|
||||||
unlocked_backgrounds: vec![0], // background #0 always available
|
unlocked_backgrounds: vec![0], // background #0 always available
|
||||||
|
challenge_index: 0,
|
||||||
last_modified: DateTime::UNIX_EPOCH,
|
last_modified: DateTime::UNIX_EPOCH,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
//! Challenge-mode bookkeeping: serves the current challenge seed, advances
|
||||||
|
//! `PlayerProgress::challenge_index` on a Challenge-mode win, persists.
|
||||||
|
//!
|
||||||
|
//! Pressing **X** starts a new game with the current Challenge seed in
|
||||||
|
//! `GameMode::Challenge` (gated by level ≥ `CHALLENGE_UNLOCK_LEVEL`).
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use solitaire_core::game_state::GameMode;
|
||||||
|
use solitaire_data::{challenge_count, challenge_seed_for, save_progress_to};
|
||||||
|
|
||||||
|
use crate::events::{GameWonEvent, NewGameRequestEvent};
|
||||||
|
use crate::game_plugin::GameMutation;
|
||||||
|
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
||||||
|
use crate::resources::GameStateResource;
|
||||||
|
|
||||||
|
/// Minimum player level required to start a Challenge run.
|
||||||
|
pub const CHALLENGE_UNLOCK_LEVEL: u32 = 5;
|
||||||
|
|
||||||
|
/// Fired when the player has just completed a Challenge-mode game and the
|
||||||
|
/// `challenge_index` cursor advances.
|
||||||
|
#[derive(Event, Debug, Clone, Copy)]
|
||||||
|
pub struct ChallengeAdvancedEvent {
|
||||||
|
pub previous_index: u32,
|
||||||
|
pub new_index: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChallengePlugin;
|
||||||
|
|
||||||
|
impl Plugin for ChallengePlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_event::<ChallengeAdvancedEvent>()
|
||||||
|
.add_event::<GameWonEvent>()
|
||||||
|
.add_event::<NewGameRequestEvent>()
|
||||||
|
// Run after ProgressUpdate so we don't fight ProgressPlugin's add_xp.
|
||||||
|
.add_systems(Update, advance_on_challenge_win.after(ProgressUpdate))
|
||||||
|
.add_systems(Update, handle_start_challenge_request.before(GameMutation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_on_challenge_win(
|
||||||
|
mut wins: EventReader<GameWonEvent>,
|
||||||
|
game: Res<GameStateResource>,
|
||||||
|
mut progress: ResMut<ProgressResource>,
|
||||||
|
path: Res<ProgressStoragePath>,
|
||||||
|
mut advanced: EventWriter<ChallengeAdvancedEvent>,
|
||||||
|
) {
|
||||||
|
for _ in wins.read() {
|
||||||
|
if game.0.mode != GameMode::Challenge {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let prev = progress.0.challenge_index;
|
||||||
|
progress.0.challenge_index = prev.saturating_add(1);
|
||||||
|
if let Some(target) = &path.0 {
|
||||||
|
if let Err(e) = save_progress_to(target, &progress.0) {
|
||||||
|
warn!("failed to save progress after challenge advance: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
advanced.send(ChallengeAdvancedEvent {
|
||||||
|
previous_index: prev,
|
||||||
|
new_index: progress.0.challenge_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_start_challenge_request(
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
progress: Res<ProgressResource>,
|
||||||
|
mut new_game: EventWriter<NewGameRequestEvent>,
|
||||||
|
) {
|
||||||
|
if !keys.just_pressed(KeyCode::KeyX) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
|
||||||
|
info!(
|
||||||
|
"Challenge mode locked — reach level {} (currently {}).",
|
||||||
|
CHALLENGE_UNLOCK_LEVEL, progress.0.level
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(seed) = challenge_seed_for(progress.0.challenge_index) else {
|
||||||
|
warn!("challenge seed list is empty");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
new_game.send(NewGameRequestEvent {
|
||||||
|
seed: Some(seed),
|
||||||
|
mode: Some(GameMode::Challenge),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience for stat overlays: returns the human-friendly position
|
||||||
|
/// string `"{index + 1} / {total}"`.
|
||||||
|
pub fn challenge_progress_label(index: u32) -> String {
|
||||||
|
format!("{} / {}", index.saturating_add(1), challenge_count())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(ChallengePlugin);
|
||||||
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
|
app.update();
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn challenge_win_advances_index() {
|
||||||
|
let mut app = headless_app();
|
||||||
|
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||||
|
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge);
|
||||||
|
|
||||||
|
app.world_mut().send_event(GameWonEvent {
|
||||||
|
score: 500,
|
||||||
|
time_seconds: 100,
|
||||||
|
});
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let p = &app.world().resource::<ProgressResource>().0;
|
||||||
|
assert_eq!(p.challenge_index, 1);
|
||||||
|
|
||||||
|
let events = app.world().resource::<Events<ChallengeAdvancedEvent>>();
|
||||||
|
let mut cursor = events.get_cursor();
|
||||||
|
let fired: Vec<_> = cursor.read(events).copied().collect();
|
||||||
|
assert_eq!(fired.len(), 1);
|
||||||
|
assert_eq!(fired[0].previous_index, 0);
|
||||||
|
assert_eq!(fired[0].new_index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classic_win_does_not_advance_challenge_index() {
|
||||||
|
let mut app = headless_app();
|
||||||
|
// Default GameStateResource is Classic mode.
|
||||||
|
app.world_mut().send_event(GameWonEvent {
|
||||||
|
score: 500,
|
||||||
|
time_seconds: 100,
|
||||||
|
});
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let p = &app.world().resource::<ProgressResource>().0;
|
||||||
|
assert_eq!(p.challenge_index, 0);
|
||||||
|
|
||||||
|
let events = app.world().resource::<Events<ChallengeAdvancedEvent>>();
|
||||||
|
let mut cursor = events.get_cursor();
|
||||||
|
assert!(cursor.read(events).next().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pressing_x_below_unlock_level_is_ignored() {
|
||||||
|
let mut app = headless_app();
|
||||||
|
// Default level is 0; below CHALLENGE_UNLOCK_LEVEL.
|
||||||
|
app.world_mut()
|
||||||
|
.resource_mut::<ButtonInput<KeyCode>>()
|
||||||
|
.press(KeyCode::KeyX);
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let events = app.world().resource::<Events<NewGameRequestEvent>>();
|
||||||
|
let mut cursor = events.get_cursor();
|
||||||
|
assert!(cursor.read(events).next().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pressing_x_at_unlock_level_fires_new_game_with_challenge_seed() {
|
||||||
|
let mut app = headless_app();
|
||||||
|
app.world_mut().resource_mut::<ProgressResource>().0.level =
|
||||||
|
CHALLENGE_UNLOCK_LEVEL;
|
||||||
|
app.world_mut()
|
||||||
|
.resource_mut::<ProgressResource>()
|
||||||
|
.0
|
||||||
|
.challenge_index = 2;
|
||||||
|
|
||||||
|
app.world_mut()
|
||||||
|
.resource_mut::<ButtonInput<KeyCode>>()
|
||||||
|
.press(KeyCode::KeyX);
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
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].seed, challenge_seed_for(2));
|
||||||
|
assert_eq!(fired[0].mode, Some(GameMode::Challenge));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn challenge_progress_label_uses_human_indexing() {
|
||||||
|
let total = challenge_count();
|
||||||
|
assert_eq!(challenge_progress_label(0), format!("1 / {total}"));
|
||||||
|
assert_eq!(challenge_progress_label(2), format!("3 / {total}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,10 +24,12 @@ use solitaire_core::pile::PileType;
|
|||||||
use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
|
use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
|
||||||
|
|
||||||
use crate::card_plugin::{CardEntity, TABLEAU_FAN_FRAC};
|
use crate::card_plugin::{CardEntity, TABLEAU_FAN_FRAC};
|
||||||
|
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
DrawRequestEvent, MoveRequestEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
|
DrawRequestEvent, MoveRequestEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
|
||||||
};
|
};
|
||||||
use crate::game_plugin::GameMutation;
|
use crate::game_plugin::GameMutation;
|
||||||
|
use crate::progress_plugin::ProgressResource;
|
||||||
use crate::layout::{Layout, LayoutResource};
|
use crate::layout::{Layout, LayoutResource};
|
||||||
use crate::resources::{DragState, GameStateResource};
|
use crate::resources::{DragState, GameStateResource};
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ impl Plugin for InputPlugin {
|
|||||||
|
|
||||||
fn handle_keyboard(
|
fn handle_keyboard(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
progress: Option<Res<ProgressResource>>,
|
||||||
mut undo: EventWriter<UndoRequestEvent>,
|
mut undo: EventWriter<UndoRequestEvent>,
|
||||||
mut new_game: EventWriter<NewGameRequestEvent>,
|
mut new_game: EventWriter<NewGameRequestEvent>,
|
||||||
mut draw: EventWriter<DrawRequestEvent>,
|
mut draw: EventWriter<DrawRequestEvent>,
|
||||||
@@ -72,10 +75,20 @@ fn handle_keyboard(
|
|||||||
new_game.send(NewGameRequestEvent::default());
|
new_game.send(NewGameRequestEvent::default());
|
||||||
}
|
}
|
||||||
if keys.just_pressed(KeyCode::KeyZ) {
|
if keys.just_pressed(KeyCode::KeyZ) {
|
||||||
|
// Zen / Challenge / Time Attack are gated to level >= CHALLENGE_UNLOCK_LEVEL.
|
||||||
|
// X is gated separately by ChallengePlugin.
|
||||||
|
let level = progress.as_ref().map_or(0, |p| p.0.level);
|
||||||
|
if level >= CHALLENGE_UNLOCK_LEVEL {
|
||||||
new_game.send(NewGameRequestEvent {
|
new_game.send(NewGameRequestEvent {
|
||||||
seed: None,
|
seed: None,
|
||||||
mode: Some(solitaire_core::game_state::GameMode::Zen),
|
mode: Some(solitaire_core::game_state::GameMode::Zen),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"Zen mode locked — reach level {} (currently {}).",
|
||||||
|
CHALLENGE_UNLOCK_LEVEL, level
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if keys.just_pressed(KeyCode::KeyD) {
|
if keys.just_pressed(KeyCode::KeyD) {
|
||||||
draw.send(DrawRequestEvent);
|
draw.send(DrawRequestEvent);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
pub mod achievement_plugin;
|
pub mod achievement_plugin;
|
||||||
pub mod animation_plugin;
|
pub mod animation_plugin;
|
||||||
pub mod card_plugin;
|
pub mod card_plugin;
|
||||||
|
pub mod challenge_plugin;
|
||||||
pub mod daily_challenge_plugin;
|
pub mod daily_challenge_plugin;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod game_plugin;
|
pub mod game_plugin;
|
||||||
@@ -15,6 +16,9 @@ pub mod table_plugin;
|
|||||||
pub mod weekly_goals_plugin;
|
pub mod weekly_goals_plugin;
|
||||||
|
|
||||||
pub use achievement_plugin::{AchievementPlugin, AchievementsResource};
|
pub use achievement_plugin::{AchievementPlugin, AchievementsResource};
|
||||||
|
pub use challenge_plugin::{
|
||||||
|
challenge_progress_label, ChallengeAdvancedEvent, ChallengePlugin, CHALLENGE_UNLOCK_LEVEL,
|
||||||
|
};
|
||||||
pub use daily_challenge_plugin::{
|
pub use daily_challenge_plugin::{
|
||||||
DailyChallengeCompletedEvent, DailyChallengePlugin, DailyChallengeResource,
|
DailyChallengeCompletedEvent, DailyChallengePlugin, DailyChallengeResource,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user