diff --git a/max-effort/Autoloads/event_bus.tscn b/max-effort/Autoloads/event_bus.tscn new file mode 100644 index 0000000..db0c7e8 --- /dev/null +++ b/max-effort/Autoloads/event_bus.tscn @@ -0,0 +1,3 @@ +[gd_scene format=3 uid="uid://bqowx0vi3prqw"] + +[node name="EventBus" type="EventBus"] diff --git a/max-effort/MaxEffort.gdextension b/max-effort/MaxEffort.gdextension index 27a274b..15a0845 100644 --- a/max-effort/MaxEffort.gdextension +++ b/max-effort/MaxEffort.gdextension @@ -12,3 +12,5 @@ macos.debug = "res://../rust/target/debug/libmax_effort_lib.dylib" macos.release = "res://../rust/target/release/libmax_effort_lib.dylib" macos.debug.arm64 = "res://../rust/target/debug/libmax_effort_lib.dylib" macos.release.arm64 = "res://../rust/target/release/libmax_effort_lib.dylib" +web.debug.wasm32 = "res://../rust/target/wasm32-unknown-emscripten/debug/libmax_effort_lib.wasm" +web.release.wasm32 = "res://../rust/target/wasm32-unknown-emscripten/release/libmax_effort_lib.wasm" diff --git a/max-effort/Objects/bench_press.tscn b/max-effort/Objects/bench_press.tscn index 4b32fc7..80e6fe5 100644 --- a/max-effort/Objects/bench_press.tscn +++ b/max-effort/Objects/bench_press.tscn @@ -1,6 +1,5 @@ [gd_scene load_steps=5 format=3 uid="uid://dxq2510ywj1hy"] -[ext_resource type="GameState" uid="uid://2gma8vvisnqo" path="res://Resources/GameState.tres" id="1_tik8c"] [ext_resource type="PackedScene" uid="uid://dn8y3bgovnh4a" path="res://Objects/bench_press_stickman.tscn" id="2_0c1tm"] [ext_resource type="Texture2D" uid="uid://dsovna2tmb4o3" path="res://Sprites/bench.png" id="2_ky8t4"] [ext_resource type="Texture2D" uid="uid://cbgn8aspf7oi0" path="res://Sprites/barbell.png" id="3_ky8t4"] @@ -8,7 +7,6 @@ [node name="BenchPress" type="Node"] [node name="System" type="BenchPressSystem" parent="."] -game_state = ExtResource("1_tik8c") [node name="Bench" type="Sprite2D" parent="."] position = Vector2(11, 1) @@ -25,6 +23,5 @@ texture = ExtResource("3_ky8t4") [node name="LiftSyncController" type="LiftSyncController" parent="." node_paths=PackedStringArray("player_anim", "barbell")] player_anim = NodePath("../BenchPressStickman") barbell = NodePath("../Barbell") -game_state = ExtResource("1_tik8c") animation_name = "default" bar_positions = Array[Vector2]([Vector2(19, -5), Vector2(19, -19), Vector2(21, -36), Vector2(21, -15), Vector2(23, 3), Vector2(22, 11), Vector2(22, -1), Vector2(22, -1)]) diff --git a/max-effort/Objects/deadlift.tscn b/max-effort/Objects/deadlift.tscn index 984418f..7519f08 100644 --- a/max-effort/Objects/deadlift.tscn +++ b/max-effort/Objects/deadlift.tscn @@ -1,6 +1,5 @@ [gd_scene load_steps=3 format=3 uid="uid://dx1k40qfioaas"] -[ext_resource type="GameState" uid="uid://2gma8vvisnqo" path="res://Resources/GameState.tres" id="1_n6ace"] [ext_resource type="Texture2D" uid="uid://cbgn8aspf7oi0" path="res://Sprites/barbell.png" id="2_ltxro"] [node name="Deadlift" type="Node"] @@ -9,7 +8,6 @@ bar_height = 7.0 bar_visual = NodePath("../Barbell") end_pos = Vector2(0, -30) -game_state = ExtResource("1_n6ace") [node name="Barbell" type="Sprite2D" parent="."] scale = Vector2(2, 2) diff --git a/max-effort/Objects/hazard_animated.tscn b/max-effort/Objects/hazard_animated.tscn index 56a9fb1..7306892 100644 --- a/max-effort/Objects/hazard_animated.tscn +++ b/max-effort/Objects/hazard_animated.tscn @@ -1,12 +1,9 @@ -[gd_scene load_steps=2 format=3 uid="uid://bqxc62tofqger"] - -[ext_resource type="GameState" uid="uid://2gma8vvisnqo" path="res://Resources/GameState.tres" id="1_strkh"] +[gd_scene format=3 uid="uid://bqxc62tofqger"] [node name="HazardAnimated" type="HazardController" node_paths=PackedStringArray("anim_sprite", "click_area", "click_shape")] anim_sprite = NodePath("AnimatedSprite2D") click_area = NodePath("Area2D") click_shape = NodePath("Area2D/CollisionShape2D") -game_state = ExtResource("1_strkh") [node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."] diff --git a/max-effort/Resources/GameState.tres b/max-effort/Resources/GameState.tres deleted file mode 100644 index fb88a00..0000000 --- a/max-effort/Resources/GameState.tres +++ /dev/null @@ -1,3 +0,0 @@ -[gd_resource type="GameState" format=3 uid="uid://2gma8vvisnqo"] - -[resource] diff --git a/max-effort/Scenes/main.tscn b/max-effort/Scenes/main.tscn index 22c3439..d73ed23 100644 --- a/max-effort/Scenes/main.tscn +++ b/max-effort/Scenes/main.tscn @@ -1,6 +1,5 @@ [gd_scene load_steps=12 format=3 uid="uid://xtm08af0e82g"] -[ext_resource type="GameState" uid="uid://2gma8vvisnqo" path="res://Resources/GameState.tres" id="1_bo1nx"] [ext_resource type="Shader" uid="uid://dndm4jfifooyk" path="res://Shaders/TunnelVision.gdshader" id="1_jjgbg"] [ext_resource type="DayConfig" uid="uid://d30pwvrr7m72j" path="res://Resources/Day_Day1.tres" id="2_8gbba"] [ext_resource type="SoundBank" uid="uid://b8ouri8tqw8vp" path="res://Resources/SoundBank.tres" id="2_21xkr"] @@ -27,20 +26,16 @@ shader_parameter/vignette_color = Color(0, 0, 0, 1) [node name="Systems" type="Node" parent="."] [node name="PlayerInputSystem" type="PlayerInputSystem" parent="Systems"] -game_state = ExtResource("1_bo1nx") [node name="TunnelSystem" type="TunnelSystem" parent="Systems" node_paths=PackedStringArray("vignette_overlay")] -game_state = ExtResource("1_bo1nx") config = SubResource("TunnelConfig_8gbba") vignette_overlay = NodePath("../../Ui/Vignette") [node name="SoundManager" type="SoundManager" parent="Systems"] bank = ExtResource("2_21xkr") -game_state = ExtResource("1_bo1nx") [node name="GameManager" type="GameManager" parent="Systems" node_paths=PackedStringArray("hazard_system", "minigame_container", "win_screen", "lose_screen")] days = Array[DayConfig]([ExtResource("2_8gbba"), ExtResource("4_344ge")]) -game_state = ExtResource("1_bo1nx") hazard_system = NodePath("../HazardSystem") minigame_container = NodePath("../../GameContainer") win_screen = NodePath("../../Ui/Win") @@ -49,14 +44,12 @@ main_menu_scene = ExtResource("4_6bp64") [node name="CameraShakeSystem" type="CameraShakeSystem" parent="Systems" node_paths=PackedStringArray("camera")] camera = NodePath("../../Camera2D") -game_state = ExtResource("1_bo1nx") min_focus_for_shake = 0.7 [node name="HazardSystem" type="HazardSystem" parent="Systems" node_paths=PackedStringArray("spawn_locations")] possible_hazards = Array[HazardDef]([ExtResource("3_kry3j")]) spawn_locations = [NodePath("../../HazardSpots/Right"), NodePath("../../HazardSpots/Left")] hazard_prefab = ExtResource("4_21xkr") -game_state = ExtResource("1_bo1nx") [node name="GameContainer" type="Node" parent="."] @@ -80,7 +73,6 @@ grow_vertical = 2 mouse_filter = 2 [node name="LiftProgressBar" type="LiftProgressBar" parent="Ui"] -game_state = ExtResource("1_bo1nx") anchors_preset = 12 anchor_top = 1.0 anchor_right = 1.0 diff --git a/max-effort/project.godot b/max-effort/project.godot index d22a3e9..87c5e6a 100644 --- a/max-effort/project.godot +++ b/max-effort/project.godot @@ -15,6 +15,10 @@ run/main_scene="uid://bg4uaukekjbx" config/features=PackedStringArray("4.5", "GL Compatibility") config/icon="res://icon.svg" +[autoload] + +GlobalEventBus="*res://Autoloads/event_bus.tscn" + [input] lift_action={ diff --git a/rust/src/consts.rs b/rust/src/consts.rs new file mode 100644 index 0000000..d8ad44e --- /dev/null +++ b/rust/src/consts.rs @@ -0,0 +1,14 @@ +pub const EVENT_BUS_PATH: &str = "/root/GlobalEventBus"; +pub const LIFT_ACTION: &str = "lift_action"; + +pub const LIFT_EFFORT_APPLIED: &str = "lift_effort_applied"; +pub const FOCUS_RELEASED: &str = "focus_released"; +pub const FOCUS_CHANGED: &str = "focus_changed"; +pub const LIFT_COMPLETED: &str = "lift_completed"; +pub const LIFT_PROGRESS: &str = "lift_progress"; +pub const LIFT_VISUAL_HEIGHT: &str = "lift_visual_height"; +pub const CAMERA_TRAUMA: &str = "camera_trauma"; +pub const HAZARD_SPAWNED: &str = "hazard_spawned"; +pub const HAZARD_RESOLVED: &str = "hazard_resolved"; + +pub const VIGNETTE_INTENSITY_PARAM: &str = "vignette_intensity"; diff --git a/rust/src/core/event_bus.rs b/rust/src/core/event_bus.rs new file mode 100644 index 0000000..964e46e --- /dev/null +++ b/rust/src/core/event_bus.rs @@ -0,0 +1,98 @@ +use crate::data::hazard_def::HazardType; +use godot::prelude::*; + +#[derive(GodotClass)] +#[class(base=Node)] +pub struct EventBus { + base: Base, +} + +#[godot_api] +impl INode for EventBus { + fn init(base: Base) -> Self { + Self { base } + } +} + +#[godot_api] +impl EventBus { + #[signal] + fn lift_effort_applied(strength: f32); + + #[signal] + fn focus_released(); + + #[signal] + fn focus_changed(intensity: f32); + + #[signal] + fn lift_completed(success: bool); + + #[signal] + fn lift_progress(progress: f32); + + #[signal] + fn lift_visual_height(height: f32); + + #[signal] + fn camera_trauma(amount: f32); + + #[signal] + fn hazard_spawned(type_: HazardType); + + #[signal] + fn hazard_resolved(type_: HazardType); + + #[func] + pub fn publish_lift_effort(&mut self, strength: f32) { + self.base_mut() + .emit_signal("lift_effort_applied", &[strength.to_variant()]); + } + + #[func] + pub fn publish_focus_released(&mut self) { + self.base_mut().emit_signal("focus_released", &[]); + } + + #[func] + pub fn publish_focus_changed(&mut self, intensity: f32) { + self.base_mut() + .emit_signal("focus_changed", &[intensity.to_variant()]); + } + + #[func] + pub fn publish_lift_completed(&mut self, success: bool) { + self.base_mut() + .emit_signal("lift_completed", &[success.to_variant()]); + } + + #[func] + pub fn publish_lift_progress(&mut self, progress: f32) { + self.base_mut() + .emit_signal("lift_progress", &[progress.to_variant()]); + } + + #[func] + pub fn publish_lift_visual_height(&mut self, height: f32) { + self.base_mut() + .emit_signal("lift_visual_height", &[height.to_variant()]); + } + + #[func] + pub fn publish_camera_trauma(&mut self, amount: f32) { + self.base_mut() + .emit_signal("camera_trauma", &[amount.to_variant()]); + } + + #[func] + pub fn publish_hazard_spawned(&mut self, type_: HazardType) { + self.base_mut() + .emit_signal("hazard_spawned", &[type_.to_variant()]); + } + + #[func] + pub fn publish_hazard_resolved(&mut self, type_: HazardType) { + self.base_mut() + .emit_signal("hazard_resolved", &[type_.to_variant()]); + } +} diff --git a/rust/src/core/game_state.rs b/rust/src/core/game_state.rs deleted file mode 100644 index d1ae367..0000000 --- a/rust/src/core/game_state.rs +++ /dev/null @@ -1,161 +0,0 @@ -use godot::prelude::*; -use std::sync::RwLock; - -use crate::data::hazard_def::HazardType; - -#[derive(Clone, Debug)] -pub enum GameEvent { - HazardSpawned, - HazardResolved, - TraumaApplied(f32), - LiftCompleted(bool), -} - -#[derive(Default)] -pub struct InnerState { - pub current_focus: f32, // 0.0 to 1.0 - pub lift_effort: f32, // 0.0 to 1.0 - pub is_lifting: bool, - pub active_hazards: Vec, - pub lift_progress: f32, // 0.0 to 1.0 - pub visual_height: f32, - pub is_lift_complete: bool, - pub is_lift_failed: bool, - pub camera_trauma: f32, - pub event_queue: Vec, -} - -#[derive(GodotClass)] -#[class(base=Resource)] -pub struct GameState { - pub inner: RwLock, - - base: Base, -} - -#[godot_api] -impl IResource for GameState { - fn init(base: Base) -> Self { - Self { - inner: RwLock::new(InnerState::default()), - base, - } - } -} - -#[godot_api] -impl GameState { - // --- WRITERS (Updated to push events) --- - - #[func] - pub fn apply_effort(&self, delta: f32) { - let mut guard = self.inner.write().unwrap(); - guard.is_lifting = true; - guard.lift_effort += delta; - } - - #[func] - pub fn release_focus(&self) { - let mut guard = self.inner.write().unwrap(); - guard.is_lifting = false; - } - - #[func] - pub fn set_focus_intensity(&self, intensity: f32) { - let mut guard = self.inner.write().unwrap(); - guard.current_focus = intensity; - } - - #[func] - pub fn set_lift_state(&self, progress: f32, visual_height: f32) { - let mut guard = self.inner.write().unwrap(); - guard.lift_progress = progress; - guard.visual_height = visual_height; - } - - #[func] - pub fn set_complete(&self, complete: bool) { - let mut guard = self.inner.write().unwrap(); - if complete { - guard.is_lift_complete = true; - } else { - guard.is_lift_failed = true; - } - - guard.event_queue.push(GameEvent::LiftCompleted(complete)); - } - - #[func] - pub fn add_trauma(&self, amount: f32) { - let mut guard = self.inner.write().unwrap(); - guard.camera_trauma = (guard.camera_trauma + amount).clamp(0.0, 1.0); - guard.event_queue.push(GameEvent::TraumaApplied(amount)); - } - - #[func] - pub fn add_active_hazard(&self, type_: HazardType) { - let mut guard = self.inner.write().unwrap(); - guard.active_hazards.push(type_); - guard.event_queue.push(GameEvent::HazardSpawned); - } - - #[func] - pub fn remove_active_hazard(&self, type_: HazardType) { - let mut guard = self.inner.write().unwrap(); - if let Some(index) = guard.active_hazards.iter().position(|t| *t == type_) { - guard.active_hazards.remove(index); - guard.event_queue.push(GameEvent::HazardResolved); - } - } - - // --- READERS --- - - #[func] - pub fn consume_effort(&self) -> f32 { - let mut guard = self.inner.write().unwrap(); - let effort = guard.lift_effort; - guard.lift_effort = 0.0; - effort - } - - #[func] - pub fn get_active_hazard_count(&self) -> i32 { - self.inner.read().unwrap().active_hazards.len() as i32 - } - - #[func] - pub fn get_focus(&self) -> f32 { - self.inner.read().unwrap().current_focus - } - - #[func] - pub fn consume_trauma(&self, decay: f32) -> f32 { - let mut guard = self.inner.write().unwrap(); - let t = guard.camera_trauma; - guard.camera_trauma = (t - decay).max(0.0); - t - } - - pub fn pop_events(&self) -> Vec { - let mut guard = self.inner.write().unwrap(); - let events = guard.event_queue.clone(); - guard.event_queue.clear(); - events - } - - pub fn get_lift_progress(&self) -> f32 { - self.inner.read().unwrap().lift_progress - } - - pub fn get_is_lifting(&self) -> bool { - self.inner.read().unwrap().is_lifting - } - - pub fn get_is_lift_complete(&self) -> bool { - self.inner.read().unwrap().is_lift_complete - } - - pub fn get_is_lift_failed(&self) -> bool { - self.inner.read().unwrap().is_lift_failed - } -} diff --git a/rust/src/core/mod.rs b/rust/src/core/mod.rs index 3e51989..690cd92 100644 --- a/rust/src/core/mod.rs +++ b/rust/src/core/mod.rs @@ -1 +1 @@ -pub mod game_state; +pub mod event_bus; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 70a7e7f..0047af8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,5 +1,6 @@ use godot::prelude::*; +mod consts; mod core; mod data; mod systems; diff --git a/rust/src/systems/bench_press_system.rs b/rust/src/systems/bench_press_system.rs index c673a2d..0068c13 100644 --- a/rust/src/systems/bench_press_system.rs +++ b/rust/src/systems/bench_press_system.rs @@ -1,4 +1,4 @@ -use crate::core::game_state::GameState; +use crate::{consts, core::event_bus::EventBus, data::hazard_def::HazardType}; use godot::prelude::*; #[derive(GodotClass)] @@ -11,11 +11,10 @@ pub struct BenchPressSystem { #[export] target_value: f32, - #[export] - game_state: Option>, - current_progress: f32, is_lift_complete: bool, + active_hazard_count: i32, + event_bus: Option>, base: Base, } @@ -27,44 +26,92 @@ impl INode for BenchPressSystem { power_per_click: 5.0, gravity: 2.0, target_value: 100.0, - game_state: None, current_progress: 0.0, + active_hazard_count: 0, is_lift_complete: false, + event_bus: None, base, } } + fn ready(&mut self) { + let mut bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); + + bus.connect( + consts::LIFT_EFFORT_APPLIED, + &self.base().callable("on_lift_effort"), + ); + bus.connect( + consts::HAZARD_SPAWNED, + &self.base().callable("on_hazard_spawned"), + ); + bus.connect( + consts::HAZARD_RESOLVED, + &self.base().callable("on_hazard_resolved"), + ); + bus.connect( + consts::LIFT_COMPLETED, + &self.base().callable("on_lift_completed"), + ); + + self.event_bus = Some(bus); + } + fn process(&mut self, delta: f64) { if self.is_lift_complete { return; } - let Some(state) = &self.game_state else { + let Some(bus) = &self.event_bus else { return; }; let dt = delta as f32; - let state_bind = state.bind(); - let effort = state_bind.consume_effort(); - let hazard_count = state_bind.get_active_hazard_count(); - drop(state_bind); - if self.current_progress > 0.0 { self.current_progress -= self.gravity * dt; self.current_progress = self.current_progress.max(0.0); } - if hazard_count == 0 { - self.current_progress += self.power_per_click * effort; - } - - if self.current_progress >= self.target_value { - self.is_lift_complete = true; - state.bind().set_complete(true); - } - let ratio = self.current_progress / self.target_value; - - state.bind().set_lift_state(ratio, ratio); + bus.clone().bind_mut().publish_lift_progress(ratio); + bus.clone().bind_mut().publish_lift_visual_height(ratio); + } +} + +#[godot_api] +impl BenchPressSystem { + #[func] + fn on_lift_effort(&mut self, delta: f32) { + let Some(bus) = &mut self.event_bus else { + return; + }; + + if self.is_lift_complete || self.active_hazard_count > 0 { + return; + } + + self.current_progress += self.power_per_click * delta; + + if self.current_progress >= self.target_value { + bus.clone().bind_mut().publish_lift_completed(true); + } + } + + #[func] + fn on_lift_completed(&mut self) { + self.is_lift_complete = true; + } + + #[func] + fn on_hazard_spawned(&mut self, _type: HazardType) { + self.active_hazard_count += 1; + } + + #[func] + fn on_hazard_resolved(&mut self, _type: HazardType) { + self.active_hazard_count -= 1; + if self.active_hazard_count < 0 { + self.active_hazard_count = 0; + } } } diff --git a/rust/src/systems/camera_shake_system.rs b/rust/src/systems/camera_shake_system.rs index 806c9c7..74d5f00 100644 --- a/rust/src/systems/camera_shake_system.rs +++ b/rust/src/systems/camera_shake_system.rs @@ -1,4 +1,4 @@ -use crate::core::game_state::GameState; +use crate::{consts, core::event_bus::EventBus}; use godot::{ classes::{Camera2D, RandomNumberGenerator}, prelude::*, @@ -10,9 +10,6 @@ pub struct CameraShakeSystem { #[export] camera: Option>, - #[export] - game_state: Option>, - #[export] decay_rate: f32, #[export] @@ -23,7 +20,9 @@ pub struct CameraShakeSystem { min_focus_for_shake: f32, trauma: f32, + current_focus: f32, rng: Gd, + event_bus: Option>, base: Base, } @@ -33,39 +32,46 @@ impl INode for CameraShakeSystem { fn init(base: Base) -> Self { Self { camera: None, - game_state: None, decay_rate: 0.8, max_offset: 20.0, max_roll: 0.1, trauma: 0.0, min_focus_for_shake: 0.1, + current_focus: 0.0, rng: RandomNumberGenerator::new_gd(), + event_bus: None, base, } } + fn ready(&mut self) { + let mut bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); + + bus.connect( + consts::FOCUS_CHANGED, + &self.base().callable("on_focus_changed"), + ); + bus.connect(consts::CAMERA_TRAUMA, &self.base().callable("add_trauma")); + + self.event_bus = Some(bus); + } + fn process(&mut self, delta: f64) { + let dt = delta as f32; + + if self.current_focus > self.min_focus_for_shake { + self.add_trauma(1.5 * dt); + } + let Some(camera) = &mut self.camera else { godot_error!("CameraShakeSystem: No camera assigned"); return; }; - let Some(state) = &self.game_state else { - godot_error!("CameraShakeSystem: No game state assigned"); - return; - }; - - let dt = delta as f32; - - let focus = state.bind().get_focus(); - - if focus > self.min_focus_for_shake { - state.bind().add_trauma(1.5 * dt); - } - - let new_trauma = state.bind().consume_trauma(self.decay_rate * dt); - self.trauma = new_trauma; if self.trauma > 0.0 { + self.trauma -= self.decay_rate * dt; + self.trauma = self.trauma.max(0.0); + let shake = self.trauma * self.trauma; let offset_x = self.max_offset * shake * self.rng.randf_range(-1.0, 1.0); @@ -84,3 +90,17 @@ impl INode for CameraShakeSystem { } } } + +#[godot_api] +impl CameraShakeSystem { + #[func] + fn on_focus_changed(&mut self, focus: f32) { + self.current_focus = focus; + } + + #[func] + fn add_trauma(&mut self, amount: f32) { + self.trauma += amount; + self.trauma = self.trauma.clamp(0.0, 1.0); + } +} diff --git a/rust/src/systems/deadlift_system.rs b/rust/src/systems/deadlift_system.rs index d401843..53591d6 100644 --- a/rust/src/systems/deadlift_system.rs +++ b/rust/src/systems/deadlift_system.rs @@ -1,4 +1,4 @@ -use crate::core::game_state::GameState; +use crate::{consts, core::event_bus::EventBus}; use godot::prelude::*; #[derive(GodotClass)] @@ -22,12 +22,11 @@ pub struct DeadliftSystem { #[export] end_pos: Vector2, - #[export] - game_state: Option>, - current_bar_height: f32, hold_timer: f32, is_lift_complete: bool, + active_hazard_count: i32, + event_bus: Option>, base: Base, } @@ -46,60 +45,109 @@ impl INode for DeadliftSystem { start_pos: Vector2::ZERO, end_pos: Vector2::ZERO, - game_state: None, - current_bar_height: 0.0, hold_timer: 0.0, + active_hazard_count: 0, is_lift_complete: false, + event_bus: None, base, } } + fn ready(&mut self) { + let mut bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); + + bus.connect( + consts::LIFT_EFFORT_APPLIED, + &self.base().callable("on_lift_effort"), + ); + bus.connect( + consts::HAZARD_SPAWNED, + &self.base().callable("on_hazard_spawned"), + ); + bus.connect( + consts::HAZARD_RESOLVED, + &self.base().callable("on_hazard_resolved"), + ); + bus.connect( + consts::LIFT_COMPLETED, + &self.base().callable("on_lift_completed"), + ); + + self.event_bus = Some(bus); + } + fn process(&mut self, delta: f64) { if self.is_lift_complete { return; } - let Some(state) = &self.game_state else { - return; + let bus = match &mut self.event_bus { + Some(bus) => bus, + None => return, }; - let dt = delta as f32; - let state_bind = state.bind(); - let effort = state_bind.consume_effort(); - let hazard_count = state_bind.get_active_hazard_count(); - drop(state_bind); + let dt = delta as f32; if self.current_bar_height > 0.0 { self.current_bar_height -= self.gravity * dt; self.current_bar_height = self.current_bar_height.max(0.0); } - if hazard_count == 0 { - self.current_bar_height += self.power_per_click * effort; - self.current_bar_height = self.current_bar_height.min(self.bar_height); - } - let visual_ratio = self.current_bar_height / self.bar_height; - if visual_ratio >= self.hold_zone_threshold && hazard_count == 0 { + if visual_ratio >= self.hold_zone_threshold && self.active_hazard_count == 0 { self.hold_timer += dt; - state.bind().add_trauma(0.15 * dt); + bus.clone().bind_mut().publish_camera_trauma(0.15 * dt); } + bus.clone() + .bind_mut() + .publish_lift_progress(self.hold_timer / self.target_value); + bus.clone() + .bind_mut() + .publish_lift_visual_height(visual_ratio); + if let Some(visual) = &mut self.bar_visual { let new_pos = self.start_pos.lerp(self.end_pos, visual_ratio); visual.set_position(new_pos); } - let progress = self.hold_timer / self.target_value; - state.bind().set_lift_state(progress, visual_ratio); - if self.hold_timer >= self.target_value { - self.is_lift_complete = true; - state.bind().add_trauma(1.0); - state.bind().set_complete(true); + bus.clone().bind_mut().publish_camera_trauma(1.0); + bus.clone().bind_mut().publish_lift_completed(true); + } + } +} + +#[godot_api] +impl DeadliftSystem { + #[func] + fn on_lift_effort(&mut self, delta: f32) { + if self.is_lift_complete || self.active_hazard_count > 0 { + return; + } + + self.current_bar_height += self.power_per_click * delta; + self.current_bar_height = self.current_bar_height.min(self.bar_height); + } + + #[func] + fn on_lift_completed(&mut self) { + self.is_lift_complete = true; + } + + #[func] + fn on_hazard_spawned(&mut self, _type: String) { + self.active_hazard_count += 1; + } + + #[func] + fn on_hazard_resolved(&mut self, _type: String) { + self.active_hazard_count -= 1; + if self.active_hazard_count < 0 { + self.active_hazard_count = 0; } } } diff --git a/rust/src/systems/game_manager.rs b/rust/src/systems/game_manager.rs index 755dfaa..fa871df 100644 --- a/rust/src/systems/game_manager.rs +++ b/rust/src/systems/game_manager.rs @@ -1,5 +1,6 @@ use crate::{ - core::game_state::GameState, data::day_config::DayConfig, systems::hazard_system::HazardSystem, + consts, core::event_bus::EventBus, data::day_config::DayConfig, + systems::hazard_system::HazardSystem, }; use godot::{ classes::{Control, Label}, @@ -12,9 +13,6 @@ pub struct GameManager { #[export] days: Array>, - #[export] - game_state: Option>, - #[export] hazard_system: Option>, #[export] @@ -35,7 +33,7 @@ pub struct GameManager { current_day_index: i32, current_mini_game: Option>, - level_ended: bool, + event_bus: Option>, base: Base, } @@ -45,7 +43,6 @@ impl INode for GameManager { fn init(base: Base) -> Self { Self { days: Array::new(), - game_state: None, hazard_system: None, minigame_container: None, win_screen: None, @@ -55,63 +52,36 @@ impl INode for GameManager { main_menu_scene: None, current_day_index: 0, current_mini_game: None, - level_ended: false, + event_bus: None, base, } } fn ready(&mut self) { + let mut bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); + + bus.connect( + consts::LIFT_COMPLETED, + &self.base().callable("handle_lift_result"), + ); + + self.event_bus = Some(bus); + let idx = self.current_day_index; self.base_mut() .call_deferred("start_day", &[idx.to_variant()]); } - - fn process(&mut self, _delta: f64) { - if self.level_ended { - return; - } - - let (won, lost) = if let Some(state) = &self.game_state { - let bind = state.bind(); - (bind.get_is_lift_complete(), bind.get_is_lift_failed()) - } else { - (false, false) - }; - - if won { - self.handle_win(); - self.level_ended = true; - } else if lost { - self.handle_loss(); - self.level_ended = true; - } - } } #[godot_api] impl GameManager { #[func] fn start_day(&mut self, index: i32) { - self.level_ended = false; - - if let Some(state) = &self.game_state { - let bind = state.bind(); - - if let Ok(mut inner) = bind.inner.write() { - inner.is_lift_complete = false; - inner.is_lift_failed = false; - - inner.lift_progress = 0.0; - inner.visual_height = 0.0; - inner.lift_effort = 0.0; - inner.active_hazards.clear(); - } - } - if index >= self.days.len() as i32 { godot_print!("GameManager: All days complete!"); self.cleanup_level(); + if let Some(s) = &mut self.win_screen { s.set_visible(false); } @@ -122,6 +92,7 @@ impl GameManager { if let Some(s) = &mut self.game_complete_screen { s.set_visible(true); } + return; } @@ -139,10 +110,6 @@ impl GameManager { if let Some(mut system) = instance.get_node_or_null("System") { system.set("target_value", &config_bind.target_weight.to_variant()); system.set("gravity", &config_bind.gravity.to_variant()); - - if let Some(state) = &self.game_state { - system.set("game_state", &state.to_variant()); - } } self.current_mini_game = Some(instance); } @@ -186,34 +153,43 @@ impl GameManager { } } + fn next_day(&mut self) { + self.current_day_index += 1; + self.start_day(self.current_day_index); + } + + fn restart_day(&mut self) { + self.base().get_tree().unwrap().reload_current_scene(); + } + + #[func] + fn handle_lift_result(&mut self, success: bool) { + if success { + self.handle_win(); + } else { + self.handle_loss(); + } + } + #[func] pub fn on_next_day_pressed(&mut self) { - self.current_day_index += 1; - let idx = self.current_day_index; - self.base_mut() - .call_deferred("start_day", &[idx.to_variant()]); - godot_print!("GameManager: Starting day {}", idx); + self.next_day(); } #[func] pub fn on_retry_pressed(&mut self) { - godot_print!("GameManager: Retrying day {}", self.current_day_index); - let idx = self.current_day_index; - self.base_mut() - .call_deferred("start_day", &[idx.to_variant()]); + self.restart_day(); } #[func] pub fn on_menu_pressed(&mut self) { if let Some(menu) = &self.main_menu_scene { - godot_print!("GameManager: Returning to main menu"); let menu_ref = menu.clone(); self.base() .get_tree() .unwrap() .change_scene_to_packed(&menu_ref); } else { - godot_error!("GameManager: No main menu scene assigned"); self.base().get_tree().unwrap().quit(); } } diff --git a/rust/src/systems/hazard_controller.rs b/rust/src/systems/hazard_controller.rs index 8109de9..b0666d0 100644 --- a/rust/src/systems/hazard_controller.rs +++ b/rust/src/systems/hazard_controller.rs @@ -7,7 +7,7 @@ use godot::{ prelude::*, }; -use crate::{core::game_state::GameState, data::hazard_def::HazardDef}; +use crate::{consts, core::event_bus::EventBus, data::hazard_def::HazardDef}; #[derive(GodotClass)] #[class(base=Node2D)] @@ -21,13 +21,11 @@ pub struct HazardController { #[export] name_label: Option>, - #[export] - game_state: Option>, - data: Option>, time_active: f32, is_resolved: bool, current_health: i32, + event_bus: Option>, base: Base, } @@ -40,16 +38,20 @@ impl INode2D for HazardController { click_area: None, click_shape: None, name_label: None, - game_state: None, data: None, time_active: 0.0, is_resolved: false, current_health: 1, + event_bus: None, base, } } fn ready(&mut self) { + let bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); + + self.event_bus = Some(bus); + if let Some(mut area) = self.click_area.clone() { let callable = self.base_mut().callable("on_input_event"); area.connect("input_event", &callable); @@ -69,9 +71,10 @@ impl INode2D for HazardController { self.time_active += delta as f32; if self.time_active >= data_bind.time_to_fail { - if let Some(state) = &self.game_state { - state.bind().set_complete(false); + if let Some(bus) = &self.event_bus { + bus.clone().bind_mut().publish_lift_completed(false); } + self.base_mut().queue_free(); } } @@ -186,11 +189,12 @@ impl HazardController { fn resolve(&mut self) { self.is_resolved = true; - if let Some(data) = &self.data { - let hazard_type = data.bind().hazard_type.clone(); - - if let Some(state) = &self.game_state { - state.bind().remove_active_hazard(hazard_type); + if let Some(bus) = &self.event_bus { + if let Some(data) = &self.data { + let data_bind = data.bind(); + bus.clone() + .bind_mut() + .publish_hazard_resolved(data_bind.hazard_type.clone()); } } diff --git a/rust/src/systems/hazard_system.rs b/rust/src/systems/hazard_system.rs index c986562..8654bbe 100644 --- a/rust/src/systems/hazard_system.rs +++ b/rust/src/systems/hazard_system.rs @@ -1,7 +1,9 @@ use godot::{classes::RandomNumberGenerator, prelude::*}; use crate::{ - core::game_state::GameState, data::hazard_def::HazardDef, + consts, + core::event_bus::EventBus, + data::hazard_def::{HazardDef, HazardType}, systems::hazard_controller::HazardController, }; @@ -17,11 +19,10 @@ pub struct HazardSystem { #[export] check_interval: f32, - #[export] - game_state: Option>, - + current_focus: f32, timer: f32, rng: Gd, + event_bus: Option>, base: Base, } @@ -34,13 +35,30 @@ impl INode for HazardSystem { spawn_locations: Array::new(), hazard_prefab: None, check_interval: 1.0, - game_state: None, timer: 0.0, + current_focus: 0.0, rng: RandomNumberGenerator::new_gd(), + event_bus: None, base, } } + fn ready(&mut self) { + let mut bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); + + bus.connect( + consts::FOCUS_CHANGED, + &self.base().callable("on_focus_changed"), + ); + + bus.connect( + consts::HAZARD_RESOLVED, + &self.base().callable("on_hazard_resolved"), + ); + + self.event_bus = Some(bus); + } + fn process(&mut self, delta: f64) { self.timer += delta as f32; if self.timer >= self.check_interval { @@ -67,24 +85,28 @@ impl HazardSystem { self.possible_hazards = hazards; } + #[func] + fn on_focus_changed(&mut self, focus: f32) { + self.current_focus = focus; + } + + #[func] + fn on_hazard_resolved(&mut self, hazard_type: HazardType) { + godot_print!("Hazard resolved: {:?}", hazard_type); + } + fn try_spawn_hazard(&mut self) { - let Some(state) = &self.game_state else { - return; - }; - - let current_focus = state.bind().get_focus(); - - if current_focus < 0.2 { + if self.current_focus < 0.2 { return; } - let spawn_chance = current_focus * 0.5; + let spawn_chance = self.current_focus * 0.5; if self.rng.randf() < spawn_chance { - self.spawn_random_hazard(current_focus); + self.spawn_random_hazard(); } } - fn spawn_random_hazard(&mut self, current_focus: f32) { + fn spawn_random_hazard(&mut self) { if self.possible_hazards.is_empty() || self.spawn_locations.is_empty() || self.hazard_prefab.is_none() @@ -108,7 +130,7 @@ impl HazardSystem { let valid_hazards: Vec> = self .possible_hazards .iter_shared() - .filter(|h| h.bind().min_focus_to_spawn <= current_focus) + .filter(|h| h.bind().min_focus_to_spawn <= self.current_focus) .collect(); if valid_hazards.is_empty() { @@ -131,10 +153,17 @@ impl HazardSystem { hazard_node.bind_mut().initialize(selected_hazard); - if let Some(state) = &self.game_state { - hazard_node.bind_mut().set_game_state(Some(state.clone())); - state.bind().add_active_hazard(hazard_type); + if let Some(bus) = &self.event_bus { + bus.clone() + .bind_mut() + .publish_hazard_spawned(hazard_type.clone()); } + + godot_print!( + "Spawned hazard {:?} at location {:?}", + hazard_type, + target_loc.get_name() + ); } else { godot_print!("Error: Hazard Prefab root is not a HazardController!"); } diff --git a/rust/src/systems/player_input_system.rs b/rust/src/systems/player_input_system.rs index fd7ac0d..293b27d 100644 --- a/rust/src/systems/player_input_system.rs +++ b/rust/src/systems/player_input_system.rs @@ -1,14 +1,11 @@ use godot::{classes::Input, prelude::*}; -use crate::core::game_state::GameState; - -const LIFT_ACTION: &str = "lift_action"; +use crate::{consts, core::event_bus::EventBus}; #[derive(GodotClass)] #[class(base=Node)] pub struct PlayerInputSystem { - #[export] - game_state: Option>, + event_bus: Option>, base: Base, } @@ -16,25 +13,31 @@ pub struct PlayerInputSystem { impl INode for PlayerInputSystem { fn init(base: Base) -> Self { Self { - game_state: None, + event_bus: None, base, } } + fn ready(&mut self) { + self.event_bus = Some(self.base().get_node_as::(consts::EVENT_BUS_PATH)); + } + fn process(&mut self, delta: f64) { - let Some(state) = &self.game_state else { + let Some(event_bus) = &self.event_bus else { return; }; - let state_bind = state.bind(); let input = Input::singleton(); - if input.is_action_pressed(LIFT_ACTION) { - state_bind.apply_effort(delta as f32); + if input.is_action_pressed(consts::LIFT_ACTION) { + event_bus + .clone() + .bind_mut() + .publish_lift_effort(delta as f32); } - if input.is_action_just_released(LIFT_ACTION) { - state_bind.release_focus(); + if input.is_action_just_released(consts::LIFT_ACTION) { + event_bus.clone().bind_mut().publish_focus_released(); } } } diff --git a/rust/src/systems/sound_manager.rs b/rust/src/systems/sound_manager.rs index b9beaa5..8fb8a27 100644 --- a/rust/src/systems/sound_manager.rs +++ b/rust/src/systems/sound_manager.rs @@ -1,19 +1,18 @@ -use crate::{ - core::game_state::{GameEvent, GameState}, - data::sound_bank::SoundBank, -}; +use crate::{consts, core::event_bus::EventBus, data::sound_bank::SoundBank}; use godot::{ - classes::{AudioStream, AudioStreamPlayer}, + classes::{AudioServer, AudioStream, AudioStreamPlayer}, prelude::*, }; +const MIN_DB: f32 = -80.0; +const MAX_DB: f32 = 5.0; +const LOW_DB: f32 = -30.0; + #[derive(GodotClass)] #[class(base=Node)] pub struct SoundManager { #[export] bank: Option>, - #[export] - game_state: Option>, #[export] pool_size: i32, @@ -23,191 +22,252 @@ pub struct SoundManager { heartbeat_player: Option>, music_player: Option>, - was_lifting: bool, + is_lifting: bool, + current_lift_progress: f32, + master_bus_idx: i32, + music_bus_idx: i32, + sfx_bus_idx: i32, + + event_bus: Option>, base: Base, } #[godot_api] impl INode for SoundManager { fn init(base: Base) -> Self { + let audio_server = AudioServer::singleton(); + Self { bank: None, - game_state: None, pool_size: 8, sfx_pool: Vec::new(), strain_player: None, heartbeat_player: None, music_player: None, - was_lifting: false, + current_lift_progress: 0.0, + is_lifting: false, + master_bus_idx: audio_server.get_bus_index("Master"), + music_bus_idx: audio_server.get_bus_index("Music"), + sfx_bus_idx: audio_server.get_bus_index("Sfx"), + event_bus: None, base, } } fn ready(&mut self) { - self.initialize_pool(); + self.initialize_audio_players(); - // FIX: Clone bank to release borrow on self - if let Some(bank) = self.bank.clone() { - if let Some(music) = &bank.bind().game_music { - self.play_music(music.clone()); - } - } - } + let mut bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); - fn process(&mut self, _delta: f64) { - let Some(state) = self.game_state.clone() else { - return; + let mut connect = |signal: &str, method: &str| { + bus.connect(signal, &self.base().callable(method)); }; - let events = state.bind().pop_events(); + connect(consts::LIFT_EFFORT_APPLIED, "handle_lift_effort"); + connect(consts::FOCUS_RELEASED, "handle_focus_release"); + connect(consts::FOCUS_CHANGED, "on_focus_changed"); + connect(consts::LIFT_COMPLETED, "handle_lift_complete"); + connect(consts::LIFT_PROGRESS, "on_lift_progress"); + connect(consts::CAMERA_TRAUMA, "on_camera_trauma"); + connect(consts::HAZARD_SPAWNED, "on_hazard_spawned"); + connect(consts::HAZARD_RESOLVED, "on_hazard_resolved"); - for event in events { - match event { - GameEvent::HazardSpawned => self.play_bank_sfx(|b| b.hazard_spawn.clone()), - GameEvent::HazardResolved => self.play_bank_sfx(|b| b.hazard_resolve.clone()), - GameEvent::LiftCompleted(success) => { - self.stop_strain(); - if success { - self.play_bank_sfx(|b| b.win_stinger.clone()); - } else { - self.play_bank_sfx(|b| b.fail_stinger.clone()); - } - } - GameEvent::TraumaApplied(amount) => { - if amount > 0.5 { - self.play_bank_sfx(|b| b.camera_trauma.clone()); - } - } - } + self.event_bus = Some(bus); + + if let Some(music) = self.get_stream(|b| b.game_music.clone()) { + self.play_music(music); } - - let is_lifting = state.bind().get_is_lifting(); - let progress = state.bind().get_lift_progress(); - let focus = state.bind().get_focus(); - - if is_lifting != self.was_lifting { - if is_lifting { - self.play_strain_loop(); - } else { - self.stop_strain(); - self.play_bank_sfx(|b| b.effort_exhale.clone()); - } - self.was_lifting = is_lifting; - } - - if is_lifting { - self.update_strain_pitch(progress); - } - - self.update_heartbeat(focus); } } +#[godot_api] impl SoundManager { - fn initialize_pool(&mut self) { + fn initialize_audio_players(&mut self) { for _ in 0..self.pool_size { - let mut p = AudioStreamPlayer::new_alloc(); - p.set_bus("Sfx"); - self.base_mut().add_child(&p.clone().upcast::()); + let p = self.create_player("Sfx"); self.sfx_pool.push(p); } - let mut strain = AudioStreamPlayer::new_alloc(); - strain.set_bus("Sfx"); - self.base_mut().add_child(&strain.clone().upcast::()); - self.strain_player = Some(strain); + self.strain_player = Some(self.create_player("Sfx")); + self.heartbeat_player = Some(self.create_player("Sfx")); + self.music_player = Some(self.create_player("Music")); - let mut hb = AudioStreamPlayer::new_alloc(); - hb.set_bus("Sfx"); - self.base_mut().add_child(&hb.clone().upcast::()); - self.heartbeat_player = Some(hb); + if let Some(clip) = self.get_stream(|b| b.heartbeat_loop.clone()) { + if let Some(hb) = &mut self.heartbeat_player { + hb.set_stream(&clip); + hb.set_volume_db(MIN_DB); + hb.play(); + } + } + } - let mut mus = AudioStreamPlayer::new_alloc(); - mus.set_bus("Music"); - self.base_mut().add_child(&mus.clone().upcast::()); - self.music_player = Some(mus); + fn create_player(&mut self, bus: &str) -> Gd { + let mut p = AudioStreamPlayer::new_alloc(); + p.set_bus(bus); + self.base_mut().add_child(&p.clone().upcast::()); + p + } - if let Some(bank) = self.bank.clone() { - let bank_bind = bank.bind(); - if let Some(clip) = &bank_bind.heartbeat_loop { - if let Some(hb_player) = &mut self.heartbeat_player { - hb_player.set_stream(&clip.clone()); - hb_player.set_volume_db(-80.0); - hb_player.play(); + fn get_stream(&self, selector: F) -> Option> + where + F: FnOnce(&SoundBank) -> Option>, + { + self.bank.as_ref().and_then(|b| selector(&b.bind())) + } + + fn play_bank_sfx(&mut self, selector: F, pitch: f32) + where + F: FnOnce(&SoundBank) -> Option>, + { + if let Some(clip) = self.get_stream(selector) { + self.play_sfx(clip, pitch); + } + } + + fn play_sfx(&mut self, clip: Gd, pitch: f32) { + let mut chosen_index = None; + + for (i, p) in self.sfx_pool.iter_mut().enumerate() { + if !p.is_playing() { + chosen_index = Some(i); + break; + } + } + + if chosen_index.is_none() { + let mut max_pos = -1.0; + let mut best_idx = 0; + for (i, p) in self.sfx_pool.iter_mut().enumerate() { + let pos = p.get_playback_position(); + if pos > max_pos { + max_pos = pos; + best_idx = i; } } + chosen_index = Some(best_idx); + } + + if let Some(idx) = chosen_index { + if let Some(p) = self.sfx_pool.get_mut(idx) { + p.set_stream(&clip); + p.set_pitch_scale(pitch); + p.play(); + } } } fn play_music(&mut self, clip: Gd) { if let Some(p) = &mut self.music_player { - p.set_stream(&clip); - p.play(); - } - } - - fn play_bank_sfx(&mut self, selector: F) - where - F: FnOnce(&SoundBank) -> Option>, - { - let clip = self.bank.as_ref().and_then(|b| selector(&b.bind())); - - if let Some(c) = clip { - self.play_sfx(c); - } - } - - fn play_sfx(&mut self, clip: Gd) { - for p in &mut self.sfx_pool { - if !p.is_playing() { + if p.get_stream() != Some(clip.clone()) { p.set_stream(&clip); - p.set_pitch_scale(1.0); p.play(); - return; - } - } - if let Some(p) = self.sfx_pool.first_mut() { - p.set_stream(&clip); - p.play(); - } - } - - fn play_strain_loop(&mut self) { - if let Some(bank) = self.bank.clone() { - if let Some(clip) = &bank.bind().strain_loop { - if let Some(p) = &mut self.strain_player { - p.set_stream(&clip.clone()); - p.play(); - } } } } - fn stop_strain(&mut self) { - if let Some(p) = &mut self.strain_player { - p.stop(); - } - } + fn update_strain_loop(&mut self) { + let strain_clip = if !self.is_lifting { + self.get_stream(|b| b.strain_loop.clone()) + } else { + None + }; - fn update_strain_pitch(&mut self, progress: f32) { if let Some(p) = &mut self.strain_player { - let pitch = 1.0 + (progress * 0.3); + if let Some(clip) = strain_clip { + p.set_stream(&clip); + p.play(); + } + + let pitch = 1.0 + (self.current_lift_progress * 0.3); p.set_pitch_scale(pitch); } } - fn update_heartbeat(&mut self, focus: f32) { - if let Some(p) = &mut self.heartbeat_player { - if focus < 0.1 { - p.set_volume_db(-80.0); - } else { - let t = (focus - 0.1) / 0.9; - let vol = -30.0 + (t * (5.0 - (-30.0))); - let pitch = 1.0 + (t * 0.4); - p.set_volume_db(vol); - p.set_pitch_scale(pitch); + pub fn toggle_master_mute(&mut self, mute: bool) { + AudioServer::singleton().set_bus_mute(self.master_bus_idx, mute); + } + + pub fn toggle_music_mute(&mut self, mute: bool) { + AudioServer::singleton().set_bus_mute(self.music_bus_idx, mute); + } + + #[func] + fn handle_lift_effort(&mut self, _: f32) { + if !self.is_lifting { + self.update_strain_loop(); + self.is_lifting = true; + } else { + self.update_strain_loop(); + } + } + + #[func] + fn handle_focus_release(&mut self) { + if self.is_lifting { + self.is_lifting = false; + + if let Some(p) = &mut self.strain_player { + p.stop(); } + + self.play_bank_sfx(|b| b.effort_exhale.clone(), 1.0); + } + } + + #[func] + fn handle_lift_complete(&mut self, success: bool) { + if let Some(p) = &mut self.strain_player { + p.stop(); + } + + let selector = if success { + |b: &SoundBank| b.win_stinger.clone() + } else { + |b: &SoundBank| b.fail_stinger.clone() + }; + + self.play_bank_sfx(selector, 1.0); + } + + #[func] + fn on_camera_trauma(&mut self, amount: f32) { + if amount > 0.5 { + self.play_bank_sfx(|b| b.camera_trauma.clone(), 1.0); + } + } + + #[func] + fn on_hazard_resolved(&mut self, _type: String) { + self.play_bank_sfx(|b| b.hazard_resolve.clone(), 1.0); + } + + #[func] + fn on_hazard_spawned(&mut self, _type: String) { + self.play_bank_sfx(|b| b.hazard_spawn.clone(), 1.0); + } + + #[func] + fn on_lift_progress(&mut self, progress: f32) { + self.current_lift_progress = progress; + } + + #[func] + fn on_focus_changed(&mut self, focus: f32) { + let Some(p) = &mut self.heartbeat_player else { + return; + }; + + if focus < 0.1 { + p.set_volume_db(MIN_DB); + } else { + let t = (focus - 0.1) / 0.9; + + let vol = LOW_DB + (t * (MAX_DB - LOW_DB)); + let pitch = 1.0 + (t * 0.4); + + p.set_volume_db(vol); + p.set_pitch_scale(pitch); } } } diff --git a/rust/src/systems/tunnel_system.rs b/rust/src/systems/tunnel_system.rs index cbf97e0..c29a44b 100644 --- a/rust/src/systems/tunnel_system.rs +++ b/rust/src/systems/tunnel_system.rs @@ -3,18 +3,19 @@ use godot::{ prelude::*, }; -use crate::{core::game_state::GameState, data::tunnel_config::TunnelConfig}; +use crate::{consts, core::event_bus::EventBus, data::tunnel_config::TunnelConfig}; #[derive(GodotClass)] #[class(base=Node)] pub struct TunnelSystem { - #[export] - game_state: Option>, #[export] config: Option>, #[export] vignette_overlay: Option>, + current_focus: f32, + is_efforting: bool, + event_bus: Option>, base: Base, } @@ -22,33 +23,51 @@ pub struct TunnelSystem { impl INode for TunnelSystem { fn init(base: Base) -> Self { Self { - game_state: None, config: None, vignette_overlay: None, + current_focus: 0.0, + is_efforting: false, + event_bus: None, base, } } + fn ready(&mut self) { + let mut bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); + + bus.connect( + consts::LIFT_EFFORT_APPLIED, + &self.base().callable("on_lift_effort"), + ); + bus.connect( + consts::FOCUS_RELEASED, + &self.base().callable("on_focus_release"), + ); + + self.event_bus = Some(bus); + } + fn process(&mut self, delta: f64) { - let Some(state) = &self.game_state else { + let Some(bus) = &mut self.event_bus else { return; }; + let Some(config) = &self.config else { return; }; - let state_bind = state.bind(); - let is_lifting = state_bind.get_is_lifting(); - let current_focus = state_bind.get_focus(); - - drop(state_bind); - let config_bind = config.bind(); let dt = delta as f32; - let mut new_focus = current_focus; + if self.is_efforting { + self.current_focus += config_bind.vision_narrow_rate * dt; + } else { + self.current_focus -= config_bind.vision_recover_rate * dt; + } - if is_lifting { + let mut new_focus = self.current_focus; + + if self.is_efforting { new_focus += config_bind.vision_narrow_rate * dt; } else { new_focus -= config_bind.vision_recover_rate * dt; @@ -72,11 +91,27 @@ impl INode for TunnelSystem { .and_then(|m| Some(m.try_cast::())) { if let Ok(mut mat) = material { - mat.set_shader_parameter("vignette_intensity", &visual_value.to_variant()); + mat.set_shader_parameter( + consts::VIGNETTE_INTENSITY_PARAM, + &visual_value.to_variant(), + ); } } } - state.bind().set_focus_intensity(new_focus); + bus.bind_mut().publish_focus_changed(new_focus); + } +} + +#[godot_api] +impl TunnelSystem { + #[func] + fn on_lift_effort(&mut self, _strength: f32) { + self.is_efforting = true; + } + + #[func] + fn on_focus_release(&mut self) { + self.is_efforting = false; } } diff --git a/rust/src/ui/lift_progress_bar.rs b/rust/src/ui/lift_progress_bar.rs index 081a57b..1f84517 100644 --- a/rust/src/ui/lift_progress_bar.rs +++ b/rust/src/ui/lift_progress_bar.rs @@ -1,4 +1,4 @@ -use crate::core::game_state::GameState; +use crate::{consts, core::event_bus::EventBus}; use godot::{ classes::{IProgressBar, ProgressBar}, prelude::*, @@ -7,9 +7,7 @@ use godot::{ #[derive(GodotClass)] #[class(base=ProgressBar)] pub struct LiftProgressBar { - #[export] - game_state: Option>, - + event_bus: Option>, base: Base, } @@ -17,21 +15,31 @@ pub struct LiftProgressBar { impl IProgressBar for LiftProgressBar { fn init(base: Base) -> Self { Self { - game_state: None, + event_bus: None, base, } } fn ready(&mut self) { + let mut bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); + + bus.connect( + consts::LIFT_PROGRESS, + &self.base().callable("on_lift_progress"), + ); + + self.event_bus = Some(bus); + self.base_mut().set_min(0.0); self.base_mut().set_max(1.0); self.base_mut().set_value(0.0); } +} - fn process(&mut self, _delta: f64) { - if let Some(state) = &self.game_state { - let progress = state.bind().get_lift_progress(); - self.base_mut().set_value(progress as f64); - } +#[godot_api] +impl LiftProgressBar { + #[func] + pub fn on_lift_progress(&mut self, progress: f32) { + self.base_mut().set_value(progress as f64); } } diff --git a/rust/src/visuals/lift_sync_controller.rs b/rust/src/visuals/lift_sync_controller.rs index f2cb613..5f325a5 100644 --- a/rust/src/visuals/lift_sync_controller.rs +++ b/rust/src/visuals/lift_sync_controller.rs @@ -1,4 +1,4 @@ -use crate::core::game_state::GameState; +use crate::{consts, core::event_bus::EventBus}; use godot::{ classes::{AnimatedSprite2D, Sprite2D}, prelude::*, @@ -11,8 +11,6 @@ pub struct LiftSyncController { player_anim: Option>, #[export] barbell: Option>, - #[export] - game_state: Option>, #[export] animation_name: GString, @@ -24,7 +22,9 @@ pub struct LiftSyncController { rep_speed: f32, current_rep_progress: f32, + is_lifting: bool, + event_bus: Option>, base: Base, } @@ -34,49 +34,58 @@ impl INode for LiftSyncController { Self { player_anim: None, barbell: None, - game_state: None, animation_name: "lift".into(), bar_positions: Array::new(), simulate_reps: true, rep_speed: 2.0, current_rep_progress: 0.0, + is_lifting: false, + event_bus: None, base, } } + fn ready(&mut self) { + let mut bus = self.base().get_node_as::(consts::EVENT_BUS_PATH); + bus.connect( + consts::LIFT_VISUAL_HEIGHT, + &self.base().callable("handle_direct_sync"), + ); + bus.connect( + consts::LIFT_EFFORT_APPLIED, + &self.base().callable("handle_lift_effort"), + ); + bus.connect( + consts::FOCUS_RELEASED, + &self.base().callable("on_focus_released"), + ); + + self.event_bus = Some(bus); + } + fn process(&mut self, delta: f64) { - let Some(state) = &self.game_state else { - godot_error!("LiftSyncController: No game state assigned"); + if !self.simulate_reps { return; - }; - - let state_bind = state.bind(); - let is_lifting = state_bind.get_is_lifting(); - - let visual_height = state_bind.inner.read().unwrap().visual_height; - drop(state_bind); + } let dt = delta as f32; - if self.simulate_reps { - if is_lifting { - self.current_rep_progress += self.rep_speed * dt; - } else { - if self.current_rep_progress > 0.0 { - self.current_rep_progress -= self.rep_speed * dt; - self.current_rep_progress = self.current_rep_progress.max(0.0); - } - } - - let ping_pong_val = - godot::global::pingpong(self.current_rep_progress as f64, 1.0) as f32; - self.update_visuals(ping_pong_val); + if self.is_lifting { + self.current_rep_progress += self.rep_speed * dt; } else { - self.update_visuals(visual_height); + if self.current_rep_progress > 0.0 { + self.current_rep_progress -= self.rep_speed * dt; + self.current_rep_progress = self.current_rep_progress.max(0.0); + } } + + let ping_pong_value = godot::global::pingpong(self.current_rep_progress as f64, 1.0) as f32; + self.update_visuals(ping_pong_value); + self.is_lifting = false; } } +#[godot_api] impl LiftSyncController { fn update_visuals(&mut self, normalized_height: f32) { if self.player_anim.is_none() || self.barbell.is_none() || self.bar_positions.is_empty() { @@ -104,4 +113,21 @@ impl LiftSyncController { } } } + + #[func] + fn handle_direct_sync(&mut self, height: f32) { + if !self.simulate_reps { + self.update_visuals(height); + } + } + + #[func] + fn handle_lift_effort(&mut self, _delta: f32) { + self.is_lifting = true; + } + + #[func] + fn on_focus_released(&mut self) { + self.is_lifting = false; + } }