Refactor systems to use EventBus for communication
- Removed direct dependencies on GameState in multiple systems, replacing them with EventBus for event-driven architecture. - Updated CameraShakeSystem to handle focus changes and trauma through EventBus. - Modified DeadliftSystem to manage lift efforts and hazards using EventBus. - Refactored GameManager to handle lift results and day transitions via EventBus. - Adjusted HazardController and HazardSystem to publish and respond to hazard events through EventBus. - Enhanced PlayerInputSystem to publish lift efforts and focus releases using EventBus. - Updated SoundManager to handle various audio events through EventBus, improving sound management. - Refined TunnelSystem to manage focus changes and lift efforts using EventBus. - Adjusted LiftProgressBar to update progress based on events from EventBus. - Enhanced LiftSyncController to synchronize animations based on lift efforts and focus events via EventBus.
This commit is contained in:
3
max-effort/Autoloads/event_bus.tscn
Normal file
3
max-effort/Autoloads/event_bus.tscn
Normal file
@@ -0,0 +1,3 @@
|
||||
[gd_scene format=3 uid="uid://bqowx0vi3prqw"]
|
||||
|
||||
[node name="EventBus" type="EventBus"]
|
||||
@@ -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"
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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="."]
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[gd_resource type="GameState" format=3 uid="uid://2gma8vvisnqo"]
|
||||
|
||||
[resource]
|
||||
@@ -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
|
||||
|
||||
@@ -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={
|
||||
|
||||
14
rust/src/consts.rs
Normal file
14
rust/src/consts.rs
Normal file
@@ -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";
|
||||
98
rust/src/core/event_bus.rs
Normal file
98
rust/src/core/event_bus.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use crate::data::hazard_def::HazardType;
|
||||
use godot::prelude::*;
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=Node)]
|
||||
pub struct EventBus {
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl INode for EventBus {
|
||||
fn init(base: Base<Node>) -> 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()]);
|
||||
}
|
||||
}
|
||||
@@ -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<HazardType>,
|
||||
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<GameEvent>,
|
||||
}
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=Resource)]
|
||||
pub struct GameState {
|
||||
pub inner: RwLock<InnerState>,
|
||||
|
||||
base: Base<Resource>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IResource for GameState {
|
||||
fn init(base: Base<Resource>) -> 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<GameEvent> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
pub mod game_state;
|
||||
pub mod event_bus;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use godot::prelude::*;
|
||||
|
||||
mod consts;
|
||||
mod core;
|
||||
mod data;
|
||||
mod systems;
|
||||
|
||||
@@ -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<Gd<GameState>>,
|
||||
|
||||
current_progress: f32,
|
||||
is_lift_complete: bool,
|
||||
active_hazard_count: i32,
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
@@ -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::<EventBus>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Gd<Camera2D>>,
|
||||
|
||||
#[export]
|
||||
game_state: Option<Gd<GameState>>,
|
||||
|
||||
#[export]
|
||||
decay_rate: f32,
|
||||
#[export]
|
||||
@@ -23,7 +20,9 @@ pub struct CameraShakeSystem {
|
||||
min_focus_for_shake: f32,
|
||||
|
||||
trauma: f32,
|
||||
current_focus: f32,
|
||||
rng: Gd<RandomNumberGenerator>,
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
@@ -33,39 +32,46 @@ impl INode for CameraShakeSystem {
|
||||
fn init(base: Base<Node>) -> 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::<EventBus>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Gd<GameState>>,
|
||||
|
||||
current_bar_height: f32,
|
||||
hold_timer: f32,
|
||||
is_lift_complete: bool,
|
||||
active_hazard_count: i32,
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
@@ -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::<EventBus>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Gd<DayConfig>>,
|
||||
|
||||
#[export]
|
||||
game_state: Option<Gd<GameState>>,
|
||||
|
||||
#[export]
|
||||
hazard_system: Option<Gd<HazardSystem>>,
|
||||
#[export]
|
||||
@@ -35,7 +33,7 @@ pub struct GameManager {
|
||||
current_day_index: i32,
|
||||
current_mini_game: Option<Gd<Node>>,
|
||||
|
||||
level_ended: bool,
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
@@ -45,7 +43,6 @@ impl INode for GameManager {
|
||||
fn init(base: Base<Node>) -> 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::<EventBus>(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Gd<Label>>,
|
||||
|
||||
#[export]
|
||||
game_state: Option<Gd<GameState>>,
|
||||
|
||||
data: Option<Gd<HazardDef>>,
|
||||
time_active: f32,
|
||||
is_resolved: bool,
|
||||
current_health: i32,
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
|
||||
base: Base<Node2D>,
|
||||
}
|
||||
@@ -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::<EventBus>(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(bus) = &self.event_bus {
|
||||
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);
|
||||
let data_bind = data.bind();
|
||||
bus.clone()
|
||||
.bind_mut()
|
||||
.publish_hazard_resolved(data_bind.hazard_type.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Gd<GameState>>,
|
||||
|
||||
current_focus: f32,
|
||||
timer: f32,
|
||||
rng: Gd<RandomNumberGenerator>,
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
|
||||
base: Base<Node>,
|
||||
}
|
||||
@@ -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::<EventBus>(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<Gd<HazardDef>> = 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!");
|
||||
}
|
||||
|
||||
@@ -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<Gd<GameState>>,
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
@@ -16,25 +13,31 @@ pub struct PlayerInputSystem {
|
||||
impl INode for PlayerInputSystem {
|
||||
fn init(base: Base<Node>) -> Self {
|
||||
Self {
|
||||
game_state: None,
|
||||
event_bus: None,
|
||||
base,
|
||||
}
|
||||
}
|
||||
|
||||
fn ready(&mut self) {
|
||||
self.event_bus = Some(self.base().get_node_as::<EventBus>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Gd<SoundBank>>,
|
||||
#[export]
|
||||
game_state: Option<Gd<GameState>>,
|
||||
|
||||
#[export]
|
||||
pool_size: i32,
|
||||
@@ -23,191 +22,252 @@ pub struct SoundManager {
|
||||
heartbeat_player: Option<Gd<AudioStreamPlayer>>,
|
||||
music_player: Option<Gd<AudioStreamPlayer>>,
|
||||
|
||||
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<Gd<EventBus>>,
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl INode for SoundManager {
|
||||
fn init(base: Base<Node>) -> 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::<EventBus>(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);
|
||||
|
||||
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());
|
||||
if let Some(music) = self.get_stream(|b| b.game_music.clone()) {
|
||||
self.play_music(music);
|
||||
}
|
||||
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::<Node>());
|
||||
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::<Node>());
|
||||
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::<Node>());
|
||||
self.heartbeat_player = Some(hb);
|
||||
|
||||
let mut mus = AudioStreamPlayer::new_alloc();
|
||||
mus.set_bus("Music");
|
||||
self.base_mut().add_child(&mus.clone().upcast::<Node>());
|
||||
self.music_player = Some(mus);
|
||||
|
||||
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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_player(&mut self, bus: &str) -> Gd<AudioStreamPlayer> {
|
||||
let mut p = AudioStreamPlayer::new_alloc();
|
||||
p.set_bus(bus);
|
||||
self.base_mut().add_child(&p.clone().upcast::<Node>());
|
||||
p
|
||||
}
|
||||
|
||||
fn get_stream<F>(&self, selector: F) -> Option<Gd<AudioStream>>
|
||||
where
|
||||
F: FnOnce(&SoundBank) -> Option<Gd<AudioStream>>,
|
||||
{
|
||||
self.bank.as_ref().and_then(|b| selector(&b.bind()))
|
||||
}
|
||||
|
||||
fn play_bank_sfx<F>(&mut self, selector: F, pitch: f32)
|
||||
where
|
||||
F: FnOnce(&SoundBank) -> Option<Gd<AudioStream>>,
|
||||
{
|
||||
if let Some(clip) = self.get_stream(selector) {
|
||||
self.play_sfx(clip, pitch);
|
||||
}
|
||||
}
|
||||
|
||||
fn play_sfx(&mut self, clip: Gd<AudioStream>, 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<AudioStream>) {
|
||||
if let Some(p) = &mut self.music_player {
|
||||
if p.get_stream() != Some(clip.clone()) {
|
||||
p.set_stream(&clip);
|
||||
p.play();
|
||||
}
|
||||
}
|
||||
|
||||
fn play_bank_sfx<F>(&mut self, selector: F)
|
||||
where
|
||||
F: FnOnce(&SoundBank) -> Option<Gd<AudioStream>>,
|
||||
{
|
||||
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<AudioStream>) {
|
||||
for p in &mut self.sfx_pool {
|
||||
if !p.is_playing() {
|
||||
p.set_stream(&clip);
|
||||
p.set_pitch_scale(1.0);
|
||||
p.play();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(p) = self.sfx_pool.first_mut() {
|
||||
fn update_strain_loop(&mut self) {
|
||||
let strain_clip = if !self.is_lifting {
|
||||
self.get_stream(|b| b.strain_loop.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(p) = &mut self.strain_player {
|
||||
if let Some(clip) = strain_clip {
|
||||
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_pitch(&mut self, progress: f32) {
|
||||
if let Some(p) = &mut self.strain_player {
|
||||
let pitch = 1.0 + (progress * 0.3);
|
||||
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 {
|
||||
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(-80.0);
|
||||
p.set_volume_db(MIN_DB);
|
||||
} else {
|
||||
let t = (focus - 0.1) / 0.9;
|
||||
let vol = -30.0 + (t * (5.0 - (-30.0)));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Gd<GameState>>,
|
||||
#[export]
|
||||
config: Option<Gd<TunnelConfig>>,
|
||||
#[export]
|
||||
vignette_overlay: Option<Gd<ColorRect>>,
|
||||
|
||||
current_focus: f32,
|
||||
is_efforting: bool,
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
@@ -22,33 +23,51 @@ pub struct TunnelSystem {
|
||||
impl INode for TunnelSystem {
|
||||
fn init(base: Base<Node>) -> 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::<EventBus>(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::<ShaderMaterial>()))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Gd<GameState>>,
|
||||
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
base: Base<ProgressBar>,
|
||||
}
|
||||
|
||||
@@ -17,21 +15,31 @@ pub struct LiftProgressBar {
|
||||
impl IProgressBar for LiftProgressBar {
|
||||
fn init(base: Base<ProgressBar>) -> Self {
|
||||
Self {
|
||||
game_state: None,
|
||||
event_bus: None,
|
||||
base,
|
||||
}
|
||||
}
|
||||
|
||||
fn ready(&mut self) {
|
||||
let mut bus = self.base().get_node_as::<EventBus>(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();
|
||||
#[godot_api]
|
||||
impl LiftProgressBar {
|
||||
#[func]
|
||||
pub fn on_lift_progress(&mut self, progress: f32) {
|
||||
self.base_mut().set_value(progress as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Gd<AnimatedSprite2D>>,
|
||||
#[export]
|
||||
barbell: Option<Gd<Sprite2D>>,
|
||||
#[export]
|
||||
game_state: Option<Gd<GameState>>,
|
||||
|
||||
#[export]
|
||||
animation_name: GString,
|
||||
@@ -24,7 +22,9 @@ pub struct LiftSyncController {
|
||||
rep_speed: f32,
|
||||
|
||||
current_rep_progress: f32,
|
||||
is_lifting: bool,
|
||||
|
||||
event_bus: Option<Gd<EventBus>>,
|
||||
base: Base<Node>,
|
||||
}
|
||||
|
||||
@@ -34,32 +34,43 @@ 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::<EventBus>(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 {
|
||||
if self.is_lifting {
|
||||
self.current_rep_progress += self.rep_speed * dt;
|
||||
} else {
|
||||
if self.current_rep_progress > 0.0 {
|
||||
@@ -68,15 +79,13 @@ impl INode for LiftSyncController {
|
||||
}
|
||||
}
|
||||
|
||||
let ping_pong_val =
|
||||
godot::global::pingpong(self.current_rep_progress as f64, 1.0) as f32;
|
||||
self.update_visuals(ping_pong_val);
|
||||
} else {
|
||||
self.update_visuals(visual_height);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user