Merge branch '41-wonder-event-handling' into 'dev'

Wonder Event Handling

See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!32
This commit is contained in:
Dominik Bernroider
2025-12-09 15:20:14 +00:00
2 changed files with 230 additions and 4 deletions

View File

@@ -1,6 +1,8 @@
use self::components::{MaxFieldSize, WonderEventMessage};
use crate::features::phase::components::{CurrentPhase, Phase, TimerSettings};
use crate::prelude::*;
use std::sync::Mutex;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread;
use tungstenite::{Message as WsMessage, connect};
use url::Url;
@@ -15,8 +17,24 @@ pub struct RequestWonderEvent {
pub max_field_size: MaxFieldSize,
}
#[derive(Resource)]
pub struct WonderEventSender(pub Sender<WonderEventMessage>);
#[derive(Resource)]
pub struct WonderEventReceiver(pub Mutex<Receiver<WonderEventMessage>>);
#[derive(Component)]
struct FloatingText {
timer: Timer,
velocity: Vec3,
}
impl Plugin for WonderEventPlugin {
fn build(&self, app: &mut App) {
let (tx, rx) = channel();
app.insert_resource(WonderEventSender(tx));
app.insert_resource(WonderEventReceiver(Mutex::new(rx)));
app.init_resource::<WonderEventState>();
app.add_message::<RequestWonderEvent>();
app.add_systems(
@@ -24,6 +42,8 @@ impl Plugin for WonderEventPlugin {
(
check_halfway_focus.run_if(in_state(AppState::GameScreen)),
handle_wonder_event_trigger,
handle_wonder_event_response.run_if(in_state(AppState::GameScreen)),
animate_floating_text,
),
);
}
@@ -75,10 +95,14 @@ fn check_halfway_focus(
}
}
fn handle_wonder_event_trigger(mut events: MessageReader<RequestWonderEvent>) {
fn handle_wonder_event_trigger(
mut events: MessageReader<RequestWonderEvent>,
sender: Res<WonderEventSender>,
) {
for event in events.read() {
let url_str = event.url_str.clone();
let max_field_size = event.max_field_size.clone();
let tx = sender.0.clone();
thread::spawn(move || {
println!("WonderEvent: Connecting to {}", url_str);
@@ -96,6 +120,25 @@ fn handle_wonder_event_trigger(mut events: MessageReader<RequestWonderEvent>) {
eprintln!("WonderEvent: Error sending message: {}", e);
} else {
println!("WonderEvent: Request sent successfully");
// Read response
if let Ok(msg) = socket.read() {
if let Ok(text) = msg.into_text() {
println!("WonderEvent: Received response: {}", text);
if let Ok(response) =
serde_json::from_str::<WonderEventMessage>(&text)
{
if let Err(e) = tx.send(response) {
eprintln!(
"WonderEvent: Failed to send response to main thread: {}",
e
);
}
} else {
eprintln!("WonderEvent: Failed to parse response JSON");
}
}
}
}
} else {
eprintln!("WonderEvent: Error serializing request");
@@ -115,3 +158,98 @@ fn handle_wonder_event_trigger(mut events: MessageReader<RequestWonderEvent>) {
});
}
}
fn handle_wonder_event_response(
receiver: Res<WonderEventReceiver>,
grid: Res<Grid>,
mut tile_query: Query<&mut TileState>,
game_config: Res<GameConfig>,
mut commands: Commands,
) {
if let Ok(rx) = receiver.0.try_lock() {
while let Ok(msg) = rx.try_recv() {
match msg {
WonderEventMessage::WonderGranted { position } => {
println!("WonderEvent: GRANTED at ({}, {})", position.x, position.y);
// Update Tile State
if let Ok(_) = grid.map_tile_state(
(position.x, position.y),
|state| {
if let TileState::Occupied {
seed,
watered,
growth_stage,
withered,
dry_counter,
} = state
{
// Progress growth stage
let mut new_stage = *growth_stage;
if let Some(config) = seed.get_seed_config(&game_config) {
if new_stage < config.growth_stages {
new_stage += 1;
}
}
TileState::Occupied {
seed: seed.clone(),
watered: *watered,
growth_stage: new_stage,
withered: *withered,
dry_counter: *dry_counter,
}
} else {
state.clone()
}
},
tile_query.reborrow(),
) {
let world_pos = grid_to_world_coords(
position.x,
position.y,
None,
grid.width,
grid.height,
);
commands.spawn((
Text2d::new("Wunder!"),
TextFont {
font_size: 20.0,
..Default::default()
},
TextColor(Color::srgb(1.0, 0.8, 0.2)),
Transform::from_translation(world_pos + Vec3::new(0.0, 20.0, 10.0)),
FloatingText {
timer: Timer::from_seconds(1.5, TimerMode::Once),
velocity: Vec3::new(0.0, 20.0, 0.0),
},
));
}
}
WonderEventMessage::RequestError { error } => {
println!("WonderEvent: REQUEST ERROR: {}", error);
}
WonderEventMessage::NoWonder => {
println!("WonderEvent: NO WONDER");
}
_ => {}
}
}
}
}
fn animate_floating_text(
mut commands: Commands,
time: Res<Time>,
mut query: Query<(Entity, &mut FloatingText, &mut Transform)>,
) {
for (entity, mut float, mut transform) in query.iter_mut() {
float.timer.tick(time.delta());
transform.translation += float.velocity * time.delta_secs();
if float.timer.is_finished() {
commands.entity(entity).despawn();
}
}
}

View File

@@ -1,9 +1,13 @@
use bevy::prelude::*;
use pomomon_garden::features::config::components::GameConfig;
use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig};
use pomomon_garden::features::core::states::AppState;
use pomomon_garden::features::grid::components::Grid;
use pomomon_garden::features::grid::components::{Grid, Tile, TileState};
use pomomon_garden::features::inventory::components::ItemType;
use pomomon_garden::features::phase::components::{CurrentPhase, Phase, TimerSettings};
use pomomon_garden::features::wonderevent::{RequestWonderEvent, WonderEventPlugin};
use pomomon_garden::features::wonderevent::components::{Position, WonderEventMessage};
use pomomon_garden::features::wonderevent::{
RequestWonderEvent, WonderEventPlugin, WonderEventSender,
};
#[test]
fn test_wonder_event_triggers() {
@@ -83,3 +87,87 @@ fn verify_event_count(app: &mut App, expected: usize) {
expected, count
);
}
#[test]
fn test_wonder_response_handling() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_plugins(bevy::state::app::StatesPlugin);
app.add_plugins(WonderEventPlugin);
app.insert_resource(GameConfig {
berry_seeds: vec![BerrySeedConfig {
name: "TestSeed".to_string(),
cost: 1,
grants: 1,
slice: "Seed1".to_string(),
growth_stages: 5,
}],
..Default::default()
});
app.insert_resource(CurrentPhase(Phase::Focus { duration: 60.0 }));
// Setup Grid with an occupied tile
let mut tiles = Vec::new();
let column = vec![
app.world_mut()
.spawn((
Tile { x: 0, y: 0 },
TileState::Occupied {
seed: ItemType::BerrySeed {
name: "TestSeed".to_string(),
},
watered: true,
growth_stage: 1,
withered: false,
dry_counter: 0,
},
))
.id(),
];
tiles.push(column);
app.insert_resource(Grid {
width: 1,
height: 1,
tiles,
});
app.insert_resource(TimerSettings::default());
// Init State
app.init_state::<AppState>();
let mut next_state = app.world_mut().resource_mut::<NextState<AppState>>();
next_state.set(AppState::GameScreen);
// Run update to enter state
app.update();
// Send WonderEventMessage directly to the channel
let sender = app.world().resource::<WonderEventSender>().0.clone();
sender
.send(WonderEventMessage::WonderGranted {
position: Position { x: 0, y: 0 },
})
.expect("Failed to send WonderEventMessage");
// Run update to process message
app.update();
// Verify Tile State (growth should increase from 1 to 2)
let grid = app.world().resource::<Grid>();
let tile_entity = grid.get_tile((0, 0)).unwrap();
let tile_state = app.world().get::<TileState>(tile_entity).unwrap();
if let TileState::Occupied { growth_stage, .. } = tile_state {
assert_eq!(*growth_stage, 2, "Growth stage should increase to 2");
} else {
panic!("Tile state should be Occupied");
}
// Verify Floating Text Spawned
let mut text_query = app.world_mut().query::<&Text2d>();
assert_eq!(
text_query.iter(app.world()).count(),
1,
"Should spawn one floating text"
);
}