Merge branch '30-pause-phase' into 'dev'
Resolve "Pause Phase" See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!8
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
pub mod phase;
|
||||
pub mod pom;
|
||||
pub mod tile;
|
||||
pub mod ui;
|
||||
|
||||
79
src/components/phase.rs
Normal file
79
src/components/phase.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Phase {
|
||||
Break { duration: f32 },
|
||||
Focus { duration: f32 },
|
||||
Paused { previous_phase: Box<Phase> },
|
||||
Finished { completed_phase: Box<Phase> },
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
10
src/components/ui.rs
Normal file
10
src/components/ui.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct UiStatusRootContainer;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct UiPhaseText;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct UiTimerText;
|
||||
@@ -31,6 +31,8 @@ fn main() {
|
||||
plugins::GridPlugin,
|
||||
plugins::PomPlugin,
|
||||
plugins::InputPlugin,
|
||||
plugins::PhasePlugin,
|
||||
plugins::StatusPlugin,
|
||||
))
|
||||
.insert_resource(config)
|
||||
.run();
|
||||
|
||||
7
src/messages/interact.rs
Normal file
7
src/messages/interact.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct InteractStartMessage {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
3
src/messages/mod.rs
Normal file
3
src/messages/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod interact;
|
||||
pub mod r#move;
|
||||
pub mod phase;
|
||||
@@ -10,9 +10,3 @@ pub struct MoveMessage {
|
||||
pub struct InvalidMoveMessage {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct InteractStartMessage {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
13
src/messages/phase.rs
Normal file
13
src/messages/phase.rs
Normal file
@@ -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;
|
||||
@@ -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::<PhaseTimerPauseMessage>();
|
||||
app.add_systems(
|
||||
Update,
|
||||
phase_timer_pause.run_if(in_state(AppState::GameScreen)),
|
||||
);
|
||||
|
||||
app.add_message::<NextPhaseMessage>();
|
||||
app.add_systems(Update, next_phase.run_if(in_state(AppState::GameScreen)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +44,13 @@ fn move_click(
|
||||
window: Single<&Window, With<PrimaryWindow>>,
|
||||
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||
config: Res<GameConfig>,
|
||||
phase: Res<CurrentPhase>,
|
||||
) {
|
||||
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<PrimaryWindow>>,
|
||||
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||
config: Res<GameConfig>,
|
||||
phase: Res<CurrentPhase>,
|
||||
// for debug
|
||||
grid: ResMut<Grid>,
|
||||
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<PhaseTimerPauseMessage>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
) {
|
||||
if keys.just_pressed(KeyCode::Space) {
|
||||
pause_messages.write(PhaseTimerPauseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
fn next_phase(mut messages: MessageWriter<NextPhaseMessage>, keys: Res<ButtonInput<KeyCode>>) {
|
||||
if keys.just_pressed(KeyCode::Enter) {
|
||||
messages.write(NextPhaseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
153
src/plugins/phase.rs
Normal file
153
src/plugins/phase.rs
Normal file
@@ -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::<TimerSettings>();
|
||||
app.init_resource::<SessionTracker>();
|
||||
app.insert_resource(CurrentPhase(Phase::Focus {
|
||||
duration: 25.0 * 60.0,
|
||||
}));
|
||||
|
||||
app.add_message::<PhaseTimerFinishedMessage>();
|
||||
|
||||
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<CurrentPhase>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
) {
|
||||
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<CurrentPhase>, settings: Res<TimerSettings>) {
|
||||
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<Time>,
|
||||
mut phase_res: ResMut<CurrentPhase>,
|
||||
mut finish_writer: MessageWriter<PhaseTimerFinishedMessage>,
|
||||
) {
|
||||
let delta = time.delta_secs();
|
||||
let phase = &mut phase_res.0;
|
||||
|
||||
match phase {
|
||||
Phase::Focus { duration } | Phase::Break { duration } => {
|
||||
*duration -= delta;
|
||||
|
||||
if *duration <= 0.0 {
|
||||
finish_writer.write(PhaseTimerFinishedMessage {
|
||||
phase: phase.clone(),
|
||||
});
|
||||
let completed = phase.clone();
|
||||
*phase = Phase::Finished {
|
||||
completed_phase: Box::new(completed),
|
||||
};
|
||||
|
||||
println!("phase ended");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_pause(
|
||||
mut messages: MessageReader<PhaseTimerPauseMessage>,
|
||||
mut phase_res: ResMut<CurrentPhase>,
|
||||
) {
|
||||
for _ in messages.read() {
|
||||
let phase = &mut phase_res.0;
|
||||
|
||||
match phase {
|
||||
Phase::Focus { .. } | Phase::Break { .. } => {
|
||||
let current_state = phase.clone();
|
||||
*phase = Phase::Paused {
|
||||
previous_phase: Box::new(current_state),
|
||||
};
|
||||
println!("Phase paused");
|
||||
}
|
||||
Phase::Paused { previous_phase } => {
|
||||
*phase = *previous_phase.clone();
|
||||
println!("Phase resumed");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_continue(
|
||||
mut messages: MessageReader<NextPhaseMessage>,
|
||||
mut phase_res: ResMut<CurrentPhase>,
|
||||
mut session_tracker: ResMut<SessionTracker>,
|
||||
settings: Res<TimerSettings>,
|
||||
) {
|
||||
for _ in messages.read() {
|
||||
let phase = &mut phase_res.0;
|
||||
|
||||
if let Phase::Finished { completed_phase } = phase {
|
||||
match **completed_phase {
|
||||
Phase::Focus { .. } => {
|
||||
session_tracker.completed_focus_phases += 1;
|
||||
|
||||
// TODO: add berry grant logic
|
||||
|
||||
let is_long_break = session_tracker.completed_focus_phases > 0
|
||||
&& session_tracker.completed_focus_phases % settings.long_break_interval
|
||||
== 0;
|
||||
|
||||
if is_long_break {
|
||||
*phase = Phase::Break {
|
||||
duration: settings.long_break_duration,
|
||||
};
|
||||
} else {
|
||||
*phase = Phase::Break {
|
||||
duration: settings.short_break_duration,
|
||||
};
|
||||
}
|
||||
}
|
||||
Phase::Break { .. } => {
|
||||
*phase = Phase::Focus {
|
||||
duration: 25.0 * 60.0,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::components::pom::{GridPosition, MovingState, PathQueue, Pom};
|
||||
use crate::components::tile::{Grid, TileState};
|
||||
use crate::config::GameConfig;
|
||||
use crate::messages::{InvalidMoveMessage, MoveMessage};
|
||||
use crate::messages::r#move::{InvalidMoveMessage, MoveMessage};
|
||||
use crate::plugins::grid::{TILE_SIZE, grid_to_world_coords};
|
||||
use crate::states::*;
|
||||
use crate::utils::pathfinding::find_path;
|
||||
|
||||
74
src/plugins/status.rs
Normal file
74
src/plugins/status.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::{
|
||||
components::{phase::CurrentPhase, ui::*},
|
||||
states::AppState,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub struct StatusPlugin;
|
||||
|
||||
impl Plugin for StatusPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(OnEnter(AppState::GameScreen), setup);
|
||||
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
||||
app.add_systems(Update, update_status.run_if(in_state(AppState::GameScreen)));
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
UiStatusRootContainer,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
bottom: px(0),
|
||||
left: px(0),
|
||||
|
||||
width: percent(100),
|
||||
height: px(50),
|
||||
|
||||
justify_content: JustifyContent::SpaceAround,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Row,
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||
children![
|
||||
(
|
||||
UiPhaseText,
|
||||
Text::new("..."),
|
||||
TextFont::from_font_size(16.0),
|
||||
TextColor(Color::WHITE)
|
||||
),
|
||||
(
|
||||
UiTimerText,
|
||||
Text::new("--:--"),
|
||||
TextFont::from_font_size(16.0),
|
||||
TextColor(Color::WHITE)
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
fn update_status(
|
||||
phase_res: Res<CurrentPhase>,
|
||||
mut phase_query: Query<&mut Text, (With<UiPhaseText>, Without<UiTimerText>)>,
|
||||
mut timer_query: Query<&mut Text, (With<UiTimerText>, Without<UiPhaseText>)>,
|
||||
) {
|
||||
if !phase_res.is_changed() {
|
||||
return;
|
||||
}
|
||||
let current_phase = &phase_res.0;
|
||||
|
||||
if let Ok(mut phase_text) = phase_query.single_mut() {
|
||||
phase_text.0 = current_phase.display_name().to_string();
|
||||
}
|
||||
|
||||
if let Ok(mut timer_text) = timer_query.single_mut() {
|
||||
timer_text.0 = current_phase.format_duration();
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup(mut commands: Commands, query: Query<Entity, With<UiStatusRootContainer>>) {
|
||||
for entity in query.iter() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user