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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
74
src/features/notification/components.rs
Normal file
74
src/features/notification/components.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/features/notification/mod.rs
Normal file
58
src/features/notification/mod.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/features/notification/ui.rs
Normal file
60
src/features/notification/ui.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
Reference in New Issue
Block a user