From 96658406838736fcd2fe9ddb82543e7fd1e74ac1 Mon Sep 17 00:00:00 2001 From: demenik Date: Mon, 24 Nov 2025 17:54:51 +0100 Subject: [PATCH] feat: Add phases (#30, #31) --- src/components/mod.rs | 2 + src/components/phase.rs | 79 +++++++++++++ src/components/ui.rs | 10 ++ src/main.rs | 2 + src/messages/interact.rs | 7 ++ src/messages/mod.rs | 3 + src/{messages.rs => messages/move.rs} | 6 - src/messages/phase.rs | 13 +++ src/plugins/input.rs | 43 +++++++- src/plugins/mod.rs | 4 + src/plugins/phase.rs | 153 ++++++++++++++++++++++++++ src/plugins/pom.rs | 2 +- src/plugins/status.rs | 74 +++++++++++++ 13 files changed, 390 insertions(+), 8 deletions(-) create mode 100644 src/components/phase.rs create mode 100644 src/components/ui.rs create mode 100644 src/messages/interact.rs create mode 100644 src/messages/mod.rs rename src/{messages.rs => messages/move.rs} (67%) create mode 100644 src/messages/phase.rs create mode 100644 src/plugins/phase.rs create mode 100644 src/plugins/status.rs diff --git a/src/components/mod.rs b/src/components/mod.rs index 9cc5313..1f63dcf 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,2 +1,4 @@ +pub mod phase; pub mod pom; pub mod tile; +pub mod ui; diff --git a/src/components/phase.rs b/src/components/phase.rs new file mode 100644 index 0000000..1b15849 --- /dev/null +++ b/src/components/phase.rs @@ -0,0 +1,79 @@ +use bevy::prelude::*; + +#[derive(Debug, Clone, PartialEq)] +pub enum Phase { + Break { duration: f32 }, + Focus { duration: f32 }, + Paused { previous_phase: Box }, + Finished { completed_phase: Box }, +} + +fn format_time(seconds: f32) -> String { + let seconds = seconds.max(0.0) as u32; + + if seconds >= 3600 { + let hours = seconds / 3600; + let minutes = (seconds % 3600) / 60; + let secs = seconds % 60; + format!("{:02}:{:02}:{:02}", hours, minutes, secs) + } else { + let minutes = seconds / 60; + let secs = seconds % 60; + format!("{:02}:{:02}", minutes, secs) + } +} + +impl Phase { + pub fn ui_color(&self) -> Color { + match self { + Phase::Focus { .. } => Color::srgb(0.9, 0.3, 0.3), + Phase::Break { .. } => Color::srgb(0.3, 0.8, 0.5), + Phase::Paused { .. } => Color::srgb(0.5, 0.5, 0.5), + Phase::Finished { .. } => Color::srgb(0.9, 0.8, 0.2), + } + } + + pub fn display_name(&self) -> &str { + match self { + Phase::Focus { .. } => "Fokus", + Phase::Break { .. } => "Pause", + Phase::Paused { .. } => "PAUSIERT [Leertaste]", + Phase::Finished { .. } => "ABGELAUFEN [Enter]", + } + } + + pub fn format_duration(&self) -> String { + match self { + Phase::Focus { duration } | Phase::Break { duration } => format_time(*duration), + Phase::Paused { previous_phase } => previous_phase.format_duration(), + Phase::Finished { .. } => "00:00".to_string(), + } + } +} + +#[derive(Resource, Debug)] +pub struct CurrentPhase(pub Phase); + +#[derive(Resource, Debug)] +pub struct TimerSettings { + pub focus_duration: f32, + pub short_break_duration: f32, + pub long_break_duration: f32, + pub long_break_interval: u32, +} + +impl Default for TimerSettings { + fn default() -> Self { + Self { + focus_duration: 25.0 * 60.0, + short_break_duration: 5.0 * 60.0, + long_break_duration: 15.0 * 60.0, + long_break_interval: 4, + } + } +} + +#[derive(Resource, Debug, Default)] +pub struct SessionTracker { + pub completed_focus_phases: u32, +} diff --git a/src/components/ui.rs b/src/components/ui.rs new file mode 100644 index 0000000..4ef0a3f --- /dev/null +++ b/src/components/ui.rs @@ -0,0 +1,10 @@ +use bevy::prelude::*; + +#[derive(Component)] +pub struct UiStatusRootContainer; + +#[derive(Component)] +pub struct UiPhaseText; + +#[derive(Component)] +pub struct UiTimerText; diff --git a/src/main.rs b/src/main.rs index c045bd4..9404c2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,8 @@ fn main() { plugins::GridPlugin, plugins::PomPlugin, plugins::InputPlugin, + plugins::PhasePlugin, + plugins::StatusPlugin, )) .insert_resource(config) .run(); diff --git a/src/messages/interact.rs b/src/messages/interact.rs new file mode 100644 index 0000000..edd2a8d --- /dev/null +++ b/src/messages/interact.rs @@ -0,0 +1,7 @@ +use bevy::prelude::*; + +#[derive(Message)] +pub struct InteractStartMessage { + pub x: u32, + pub y: u32, +} diff --git a/src/messages/mod.rs b/src/messages/mod.rs new file mode 100644 index 0000000..0e44e6d --- /dev/null +++ b/src/messages/mod.rs @@ -0,0 +1,3 @@ +pub mod interact; +pub mod r#move; +pub mod phase; diff --git a/src/messages.rs b/src/messages/move.rs similarity index 67% rename from src/messages.rs rename to src/messages/move.rs index bd71c95..9567306 100644 --- a/src/messages.rs +++ b/src/messages/move.rs @@ -10,9 +10,3 @@ pub struct MoveMessage { pub struct InvalidMoveMessage { pub message: String, } - -#[derive(Message)] -pub struct InteractStartMessage { - pub x: u32, - pub y: u32, -} diff --git a/src/messages/phase.rs b/src/messages/phase.rs new file mode 100644 index 0000000..3ecb181 --- /dev/null +++ b/src/messages/phase.rs @@ -0,0 +1,13 @@ +use crate::components::phase::Phase; +use bevy::prelude::*; + +#[derive(Message)] +pub struct PhaseTimerFinishedMessage { + pub phase: Phase, +} + +#[derive(Message)] +pub struct PhaseTimerPauseMessage; + +#[derive(Message)] +pub struct NextPhaseMessage; diff --git a/src/plugins/input.rs b/src/plugins/input.rs index 1a9f77b..e6c4af6 100644 --- a/src/plugins/input.rs +++ b/src/plugins/input.rs @@ -2,9 +2,14 @@ use bevy::input::mouse::MouseButton; use bevy::prelude::*; use bevy::window::PrimaryWindow; +use crate::components::phase::{CurrentPhase, Phase}; use crate::components::tile::{Grid, TileState}; use crate::config::GameConfig; -use crate::messages::{InteractStartMessage, InvalidMoveMessage, MoveMessage}; +use crate::messages::phase::{NextPhaseMessage, PhaseTimerPauseMessage}; +use crate::messages::{ + interact::InteractStartMessage, + r#move::{InvalidMoveMessage, MoveMessage}, +}; use crate::plugins::grid::world_to_grid_coords; use crate::states::AppState; @@ -21,6 +26,15 @@ impl Plugin for InputPlugin { Update, interact_click.run_if(in_state(AppState::GameScreen)), ); + + app.add_message::(); + app.add_systems( + Update, + phase_timer_pause.run_if(in_state(AppState::GameScreen)), + ); + + app.add_message::(); + app.add_systems(Update, next_phase.run_if(in_state(AppState::GameScreen))); } } @@ -30,7 +44,13 @@ fn move_click( window: Single<&Window, With>, camera: Single<(&Camera, &GlobalTransform), With>, config: Res, + phase: Res, ) { + match phase.0 { + Phase::Focus { .. } => return, + _ => {} + } + if mouse_btn.just_pressed(MouseButton::Right) { let (cam, cam_transform) = *camera; @@ -53,10 +73,16 @@ fn interact_click( window: Single<&Window, With>, camera: Single<(&Camera, &GlobalTransform), With>, config: Res, + phase: Res, // for debug grid: ResMut, tile_query: Query<&mut TileState>, ) { + match phase.0 { + Phase::Focus { .. } => return, + _ => {} + } + if mouse_btn.just_pressed(MouseButton::Left) { let (cam, cam_transform) = *camera; @@ -85,3 +111,18 @@ fn interact_click( } } } + +fn phase_timer_pause( + mut pause_messages: MessageWriter, + keys: Res>, +) { + if keys.just_pressed(KeyCode::Space) { + pause_messages.write(PhaseTimerPauseMessage); + } +} + +fn next_phase(mut messages: MessageWriter, keys: Res>) { + if keys.just_pressed(KeyCode::Enter) { + messages.write(NextPhaseMessage); + } +} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 314256f..7eab8fd 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -2,12 +2,16 @@ pub mod core; pub mod game_screen; pub mod grid; pub mod input; +pub mod phase; pub mod pom; pub mod start_screen; +pub mod status; pub use core::CorePlugin; pub use game_screen::GameScreenPlugin; pub use grid::GridPlugin; pub use input::InputPlugin; +pub use phase::PhasePlugin; pub use pom::PomPlugin; pub use start_screen::StartScreenPlugin; +pub use status::StatusPlugin; diff --git a/src/plugins/phase.rs b/src/plugins/phase.rs new file mode 100644 index 0000000..2747b20 --- /dev/null +++ b/src/plugins/phase.rs @@ -0,0 +1,153 @@ +use crate::{components::phase::*, messages::phase::*, states::AppState}; +use bevy::prelude::*; + +pub struct PhasePlugin; + +impl Plugin for PhasePlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + app.init_resource::(); + app.insert_resource(CurrentPhase(Phase::Focus { + duration: 25.0 * 60.0, + })); + + app.add_message::(); + + app.add_systems(OnEnter(AppState::GameScreen), load_rules); + app.add_systems( + Update, + (tick_timer, handle_pause, handle_continue).run_if(in_state(AppState::GameScreen)), + ); + + #[cfg(debug_assertions)] + app.add_systems(Update, debug_short_phase_duration); + } +} + +#[cfg(debug_assertions)] +fn debug_short_phase_duration( + mut phase_res: ResMut, + keys: Res>, +) { + if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) + && keys.just_pressed(KeyCode::Enter) + { + let phase = &mut phase_res.0; + match phase { + Phase::Focus { duration } | Phase::Break { duration } => { + *duration = 3.0; + println!("Debug: Phase duration set to 3 seconds!"); + } + _ => {} + } + } +} + +fn load_rules(mut phase_res: ResMut, settings: Res) { + let phase = &mut phase_res.0; + + let new_phase = match phase { + Phase::Focus { .. } => Some(Phase::Focus { + duration: settings.focus_duration, + }), + Phase::Break { .. } => Some(Phase::Break { duration: 0.0 }), + _ => None, + }; + + if let Some(p) = new_phase { + *phase = p; + } +} + +fn tick_timer( + time: Res