Merge branch '63-grant-berries-as-focus-reward' into 'dev'

Grant berries as focus reward + Notifications

See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!37
This commit is contained in:
Dominik Bernroider
2025-12-10 14:54:55 +00:00
10 changed files with 250 additions and 3 deletions

View File

@@ -27,5 +27,6 @@
"growth_stages": 6 "growth_stages": 6
} }
], ],
"wonder_event_url": "wss://pomomon.farm/ws" "wonder_event_url": "wss://pomomon.farm/ws",
"berries_per_focus_minute": 1
} }

View File

@@ -11,6 +11,7 @@ pub struct GameConfig {
pub shovel_rate: f32, pub shovel_rate: f32,
pub berry_seeds: Vec<BerrySeedConfig>, pub berry_seeds: Vec<BerrySeedConfig>,
pub wonder_event_url: String, pub wonder_event_url: String,
pub berries_per_focus_minute: u32,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
@@ -54,6 +55,7 @@ impl Default for GameConfig {
}, },
], ],
wonder_event_url: "wss://pomomon.farm/ws".into(), wonder_event_url: "wss://pomomon.farm/ws".into(),
berries_per_focus_minute: 1,
} }
} }
} }

View File

@@ -5,6 +5,7 @@ pub mod grid;
pub mod hud; pub mod hud;
pub mod input; pub mod input;
pub mod inventory; pub mod inventory;
pub mod notification;
pub mod phase; pub mod phase;
pub mod pom; pub mod pom;
pub mod savegame; pub mod savegame;
@@ -19,6 +20,7 @@ pub use grid::GridPlugin;
pub use hud::HudPlugin; pub use hud::HudPlugin;
pub use input::InputPlugin; pub use input::InputPlugin;
pub use inventory::InventoryPlugin; pub use inventory::InventoryPlugin;
pub use notification::NotificationPlugin;
pub use phase::PhasePlugin; pub use phase::PhasePlugin;
pub use pom::PomPlugin; pub use pom::PomPlugin;
pub use savegame::SavegamePlugin; pub use savegame::SavegamePlugin;

View File

@@ -0,0 +1,74 @@
use crate::prelude::*;
#[derive(Resource)]
pub struct Notifications {
items: Vec<(Notification, NotificationLevel)>,
}
impl Default for Notifications {
fn default() -> Self {
Self { items: Vec::new() }
}
}
impl Notifications {
fn new(
&mut self,
level: NotificationLevel,
title: Option<impl Into<String>>,
message: impl Into<String>,
) {
self.items.push((
Notification {
title: title.map(|s| s.into()),
message: message.into(),
},
level,
));
}
pub fn drain(&mut self) -> std::vec::Drain<'_, (Notification, NotificationLevel)> {
self.items.drain(..)
}
pub fn info(&mut self, title: Option<impl Into<String>>, message: impl Into<String>) {
self.new(NotificationLevel::Info, title, message);
}
pub fn warn(&mut self, title: Option<impl Into<String>>, message: impl Into<String>) {
self.new(NotificationLevel::Warning, title, message);
}
pub fn error(&mut self, title: Option<impl Into<String>>, message: impl Into<String>) {
self.new(NotificationLevel::Error, title, message);
}
}
#[derive(Component)]
pub struct NotificationContainer;
#[derive(Component)]
pub struct NotificationLifetime(pub Timer);
#[derive(Component)]
pub struct Notification {
pub title: Option<String>,
pub message: String,
}
#[derive(Component)]
pub enum NotificationLevel {
Info,
Warning,
Error,
}
impl NotificationLevel {
pub fn bg_color(&self) -> Color {
match self {
NotificationLevel::Info => Color::srgba(0.0, 0.0, 0.0, 0.7),
NotificationLevel::Warning => Color::srgba(1.0, 1.0, 0.0, 0.7),
NotificationLevel::Error => Color::srgba(1.0, 0.0, 0.0, 0.7),
}
}
}

View File

@@ -0,0 +1,58 @@
use crate::{
features::notification::ui::{notification_container, spawn_notification},
prelude::*,
};
use components::{NotificationContainer, NotificationLifetime, Notifications};
pub mod components;
pub mod ui;
pub struct NotificationPlugin;
impl Plugin for NotificationPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<Notifications>()
.add_systems(Startup, setup)
.add_systems(Update, handle_notification);
}
}
// Spawns the notification container up
fn setup(mut commands: Commands) {
commands.spawn(notification_container());
}
// Spawns/Despawns UI elements for each item in the `Notifications` Resource
fn handle_notification(
mut commands: Commands,
mut notifications: ResMut<Notifications>,
container_q: Query<Entity, With<NotificationContainer>>,
mut notification_entities: Query<(Entity, &mut NotificationLifetime)>,
time: Res<Time>,
) {
if let Some(container) = container_q.iter().next() {
let mut spawned_entities = Vec::new();
for (content, level) in notifications.drain() {
let entity = spawn_notification(&mut commands, content, level);
commands.entity(container).add_child(entity);
spawned_entities.push(entity);
}
for entity in spawned_entities {
commands
.entity(entity)
.insert(NotificationLifetime(Timer::from_seconds(
5.0,
TimerMode::Once,
)));
}
}
for (entity, mut timer) in notification_entities.iter_mut() {
timer.0.tick(time.delta());
if timer.0.is_finished() {
commands.entity(entity).despawn();
}
}
}

View File

@@ -0,0 +1,60 @@
use super::components::*;
use crate::prelude::*;
pub fn notification_container() -> impl Bundle {
(
NotificationContainer,
Node {
position_type: PositionType::Absolute,
top: px(0),
left: px(0),
padding: UiRect::all(px(5)),
flex_direction: FlexDirection::Column,
row_gap: px(5),
..default()
},
)
}
pub fn spawn_notification(
commands: &mut Commands,
content: Notification,
level: NotificationLevel,
) -> Entity {
let entity = commands
.spawn((
Node {
flex_direction: FlexDirection::Column,
row_gap: px(5),
padding: UiRect::all(px(10)),
margin: UiRect::all(px(2)),
width: px(400),
..default()
},
BackgroundColor(level.bg_color()),
BorderRadius::all(px(4)),
))
.with_children(|p| {
if let Some(title) = content.title {
p.spawn((
Text::new(format!("{}\n", title)),
TextFont {
font_size: 20.0,
..default()
},
TextColor(Color::WHITE),
));
}
p.spawn((
Text::new(content.message),
TextFont {
font_size: 16.0,
..default()
},
TextColor(Color::WHITE),
));
})
.id();
entity
}

View File

@@ -22,7 +22,13 @@ impl Plugin for PhasePlugin {
app.add_systems(OnEnter(AppState::GameScreen), load_rules); app.add_systems(OnEnter(AppState::GameScreen), load_rules);
app.add_systems( app.add_systems(
Update, Update,
(tick_timer, handle_pause, handle_continue).run_if(in_state(AppState::GameScreen)), (
tick_timer,
handle_pause,
handle_continue,
grant_focus_rewards,
)
.run_if(in_state(AppState::GameScreen)),
); );
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -87,7 +93,7 @@ fn tick_timer(
completed_phase: Box::new(completed), completed_phase: Box::new(completed),
}; };
println!("phase ended"); println!("Phase ended");
savegame_messages.write(SavegameDumpMessage); savegame_messages.write(SavegameDumpMessage);
} }
} }
@@ -95,6 +101,46 @@ fn tick_timer(
} }
} }
// Rewards the player at the end of a focus phase with `berries_per_focus_minute` * `focus_duration`
fn grant_focus_rewards(
mut messages: MessageReader<PhaseTimerFinishedMessage>,
config: Res<GameConfig>,
mut inventory: ResMut<Inventory>,
mut commands: Commands,
mut items_query: Query<&mut ItemStack>,
mut session_tracker: ResMut<SessionTracker>,
game_config: Res<GameConfig>,
mut notifications: ResMut<Notifications>,
timer_settings: Res<TimerSettings>,
) {
for message in messages.read() {
if matches!(message.phase, Phase::Focus { .. }) {
let berries = config.berries_per_focus_minute
* (timer_settings.focus_duration as f32 / 60.0).floor() as u32;
inventory.update_item_stack(
&mut commands,
&mut items_query,
ItemType::Berry,
berries as i32,
);
session_tracker.total_berries_earned += berries;
let berries_name = match berries {
1 => ItemType::Berry.singular(&game_config),
_ => ItemType::Berry.plural(&game_config),
};
notifications.info(
Some("Fokus Belohnung"),
format!(
"Du hast {} {} als Belohnung für das Abschließen einer Fokus-Phase erhalten!",
berries, berries_name
),
);
}
}
}
fn handle_pause( fn handle_pause(
mut messages: MessageReader<PhaseTimerPauseMessage>, mut messages: MessageReader<PhaseTimerPauseMessage>,
mut phase_res: ResMut<CurrentPhase>, mut phase_res: ResMut<CurrentPhase>,

View File

@@ -165,6 +165,7 @@ fn handle_wonder_event_response(
mut tile_query: Query<&mut TileState>, mut tile_query: Query<&mut TileState>,
game_config: Res<GameConfig>, game_config: Res<GameConfig>,
mut commands: Commands, mut commands: Commands,
mut notifications: ResMut<Notifications>,
) { ) {
if let Ok(rx) = receiver.0.try_lock() { if let Ok(rx) = receiver.0.try_lock() {
while let Ok(msg) = rx.try_recv() { while let Ok(msg) = rx.try_recv() {
@@ -191,6 +192,7 @@ fn handle_wonder_event_response(
new_stage += 1; new_stage += 1;
} }
} }
notifications.info(Some("Wunder Ereignis"), "Es ist ein Wunder passiert! Eine deiner Pflanzen ist auf magischer Weise gewachsen.");
TileState::Occupied { TileState::Occupied {
seed: seed.clone(), seed: seed.clone(),

View File

@@ -35,6 +35,7 @@ fn main() {
features::InventoryPlugin, features::InventoryPlugin,
features::ShopPlugin, features::ShopPlugin,
features::WonderEventPlugin, features::WonderEventPlugin,
features::NotificationPlugin,
)) ))
.insert_resource(config) .insert_resource(config)
.add_systems(Startup, overwrite_default_font) .add_systems(Startup, overwrite_default_font)

View File

@@ -8,6 +8,7 @@ pub use crate::features::{
utils::{grid_to_world_coords, world_to_grid_coords}, utils::{grid_to_world_coords, world_to_grid_coords},
}, },
inventory::components::{Inventory, ItemStack, ItemType}, inventory::components::{Inventory, ItemStack, ItemType},
notification::components::Notifications,
phase::components::{CurrentPhase, Phase}, phase::components::{CurrentPhase, Phase},
pom::{ pom::{
components::{GridPosition, MovingState, Pom}, components::{GridPosition, MovingState, Pom},