From 56e4875a7b95ee48ab0ea84a1b9f7f71aa3ed80d Mon Sep 17 00:00:00 2001 From: Sofia Date: Thu, 6 Aug 2026 16:31:41 +0300 Subject: [PATCH] Add necessary boilerplate for triggers --- godot/maps/two_towers/two_towers.tscn | 13 +++ rust/src/game/mod.rs | 44 ++++++++ rust/src/map/mod.rs | 13 ++- rust/src/map/triggers.rs | 141 ++++++++++++++++++++++++++ rust/src/net/network_manager.rs | 2 + rust/src/net/util.rs | 1 + rust/src/player/mod.rs | 2 +- rust/src/replay.rs | 52 ++++++++++ 8 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 rust/src/map/triggers.rs diff --git a/godot/maps/two_towers/two_towers.tscn b/godot/maps/two_towers/two_towers.tscn index 04de6a3..26e2cf7 100644 --- a/godot/maps/two_towers/two_towers.tscn +++ b/godot/maps/two_towers/two_towers.tscn @@ -3,6 +3,10 @@ [ext_resource type="PackedScene" uid="uid://dpqqe7hjktit8" path="res://maps/two_towers/two_towers_static.scn" id="2_l5vmt"] [ext_resource type="Script" uid="uid://diepa0rcedh5f" path="res://maps/meta/map_rebuilder.gd" id="4_y3aqj"] +[sub_resource type="BoxMesh" id="BoxMesh_1cgct"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_1cgct"] + [node name="two_towers" type="Map" unique_id=590524284 node_paths=PackedStringArray("spawners", "ball_spawner")] spawners = [NodePath("two_towers_static/func_godot_map/entity_11_info_spawn_point"), NodePath("two_towers_static/func_godot_map/entity_5_info_spawn_point")] ball_spawner = NodePath("two_towers_static/func_godot_map/entity_1_info_spawn_point") @@ -11,3 +15,12 @@ ball_spawner = NodePath("two_towers_static/func_godot_map/entity_1_info_spawn_po script = ExtResource("4_y3aqj") [node name="two_towers_static" parent="." unique_id=86902040 instance=ExtResource("2_l5vmt")] + +[node name="shootable_trigger" type="ShootableTrigger" parent="." unique_id=306909670] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 42.184772, 10.981467, 7.8725615) + +[node name="mesh_instance_3d" type="MeshInstance3D" parent="shootable_trigger" unique_id=174590790] +mesh = SubResource("BoxMesh_1cgct") + +[node name="collision_shape_3d" type="CollisionShape3D" parent="shootable_trigger" unique_id=298869370] +shape = SubResource("BoxShape3D_1cgct") diff --git a/rust/src/game/mod.rs b/rust/src/game/mod.rs index 0f7d429..2d85bf5 100644 --- a/rust/src/game/mod.rs +++ b/rust/src/game/mod.rs @@ -1350,6 +1350,50 @@ impl Game { }); } + pub fn on_trigger_activate(&mut self, id: u8, player_id: u16) { + self.run_deferred(move |s| { + if let Some(map) = &mut s.current_map + && let Some(triggerable) = map.bind_mut().triggerables.get_mut(&id) + { + triggerable.bind_mut().activate_visual(player_id, 0.); + } + + if let Some(recorder) = &mut s.replay_recorder { + recorder + .bind_mut() + .record_event(ReplayEvent::TriggerActivate(id, player_id)); + } + + if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer + && let PeerKind::Server(peer, _) = peer + { + peer.broadcast_reliable(Package::TriggerActivate(id, player_id)); + } + }); + } + + pub fn on_trigger_deactivate(&mut self, id: u8) { + self.run_deferred(move |s| { + if let Some(map) = &mut s.current_map + && let Some(triggerable) = map.bind_mut().triggerables.get_mut(&id) + { + triggerable.bind_mut().deactivate_visual(); + } + + if let Some(recorder) = &mut s.replay_recorder { + recorder + .bind_mut() + .record_event(ReplayEvent::TriggerDeactivate(id)); + } + + if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer + && let PeerKind::Server(peer, _) = peer + { + peer.broadcast_reliable(Package::TriggerDeactivate(id)); + } + }); + } + pub fn spawn_telegrenade( &mut self, player_id: u16, diff --git a/rust/src/map/mod.rs b/rust/src/map/mod.rs index 95f88a7..62c0421 100644 --- a/rust/src/map/mod.rs +++ b/rust/src/map/mod.rs @@ -4,7 +4,7 @@ use godot::{classes::CharacterBody3D, prelude::*}; use crate::{ game::{Game, GameManager}, - map::pickup::Pickup, + map::{pickup::Pickup, triggers::Triggerable}, net::util::NetVector3, player::{ IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer, @@ -18,6 +18,7 @@ pub mod killbox; pub mod map_resource; pub mod map_static; pub mod pickup; +pub mod triggers; #[derive(GodotClass)] #[class(base=Node3D, init)] @@ -30,6 +31,9 @@ pub struct Map { pickup_counter: u8, pub pickups: HashMap>, + triggerable_counter: u8, + pub triggerables: HashMap>, + base: Base, } @@ -209,6 +213,13 @@ impl Map { id } + pub fn register_triggerable(&mut self, triggerable: Gd) -> u8 { + let id = self.triggerable_counter; + self.triggerable_counter += 1; + self.triggerables.insert(id, triggerable); + id + } + pub fn spawn_telegrenade( &mut self, location: NetVector3, diff --git a/rust/src/map/triggers.rs b/rust/src/map/triggers.rs new file mode 100644 index 0000000..ea027e2 --- /dev/null +++ b/rust/src/map/triggers.rs @@ -0,0 +1,141 @@ +use godot::{ + classes::{Area3D, IArea3D}, + prelude::*, +}; + +use crate::{ + game::Game, + map::Map, + net::network_manager::NetworkManager, + player::{DamageSource, shootable::Shootable}, + replay::StandaloneReplayManager, +}; + +#[derive(GodotClass)] +#[class(base=Node3D, init)] +pub struct Triggerable { + triggerable_id: Option, + /// Contains the activator player's ID if active. + active: Option, + pub active_for: f64, + + base: Base, +} + +#[godot_api] +impl INode3D for Triggerable { + fn ready(&mut self) { + if let Some(game) = &Game::singleton() { + game.signals() + .on_map_loaded() + .connect_other(self, Self::register_triggerable); + } else { + StandaloneReplayManager::singleton() + .signals() + .on_map_loaded() + .connect_other(self, Self::register_triggerable); + } + } + + fn process(&mut self, delta: f64) { + if self.active.is_some() { + self.active_for += delta; + } else { + self.active_for = 0.; + } + } +} + +#[godot_api] +impl Triggerable { + /// Called whenever this triggerable's visuals should be immediately + /// activated. + #[func(virtual)] + pub fn activate_visual(&mut self, player_id: u16, time_since_activation: f64) {} + /// Called whenever this triggerable's visuals should be immediately + /// deactivated. + #[func(virtual)] + pub fn deactivate_visual(&mut self) {} + + /// Called whenever this triggerable is activated on the server-side. + /// Returns a bool indicating if this triggerable should be immediately + /// deactivated. Defaults to true. + #[func(virtual)] + pub fn on_activate(&mut self) -> bool { + true + } + /// Called whenever this triggerable is deactivated on the server-side. + #[func(virtual)] + pub fn on_deactivate(&mut self) {} + + /// Activates this trigger. Active triggerables can not be re-activated + /// without being deactivated first. Only executes on the server-side. + #[func] + pub fn activate(&mut self, player_id: u16) { + if NetworkManager::singleton().bind().is_host() && !self.active.is_some() { + self.active = Some(player_id); + if let Some(game) = &mut Game::singleton() { + game.bind_mut() + .on_trigger_activate(self.triggerable_id.unwrap_or(0), player_id); + } + if self.on_activate() { + self.deactivate(); + } + } + } + + /// Deactivates this trigger. Only executes on the server-side. + #[func] + pub fn deactivate(&mut self) { + if NetworkManager::singleton().bind().is_host() { + self.active = None; + if let Some(game) = &mut Game::singleton() { + game.bind_mut() + .on_trigger_deactivate(self.triggerable_id.unwrap_or(0)); + } + self.on_deactivate(); + } + } + + #[func] + pub fn is_active(&self) -> bool { + self.active.is_some() + } + + #[func] + pub fn active_for(&self) -> f64 { + self.active_for + } + + pub fn get_active(&self) -> Option { + self.active + } + + fn register_triggerable(&mut self, mut map: Gd) { + self.run_deferred(move |s| { + let id = map.bind_mut().register_triggerable(s.to_gd()); + s.triggerable_id = Some(id); + }); + } +} + +#[derive(GodotClass)] +#[class(base=Area3D, init)] +pub struct ShootableTrigger { + base: Base, +} + +#[godot_api] +impl ShootableTrigger { + #[signal] + pub fn on_shot(player: u16, damage: i32, dir: Vector3); +} + +#[godot_dyn] +impl Shootable for ShootableTrigger { + fn on_shoot(&mut self, source: DamageSource, damage: i32, dir: Vector3) { + if let DamageSource::Player(id) = source { + self.signals().on_shot().emit(id, damage, dir); + } + } +} diff --git a/rust/src/net/network_manager.rs b/rust/src/net/network_manager.rs index a40ef70..d5a8543 100644 --- a/rust/src/net/network_manager.rs +++ b/rust/src/net/network_manager.rs @@ -362,6 +362,8 @@ pub enum Package { Goal(u16, Vec), Announcement(u16, Announcement), Pickup(u8, u16), + TriggerActivate(u8, u16), + TriggerDeactivate(u8), ThrowTelegrenade(u16, NetVector3), TelegrenadeTeleport(u16, NetVector3), TelegrenadeActivate, diff --git a/rust/src/net/util.rs b/rust/src/net/util.rs index c05a3e6..6d80695 100644 --- a/rust/src/net/util.rs +++ b/rust/src/net/util.rs @@ -39,6 +39,7 @@ pub fn cast_ray( ray.set_hit_back_faces(false); ray.set_hit_from_inside(false); ray.set_collide_with_bodies(true); + ray.set_collide_with_areas(true); ray.set_exclude(&exclude); if let Some(world) = world && let Some(mut space) = world.get_direct_space_state() diff --git a/rust/src/player/mod.rs b/rust/src/player/mod.rs index 973e7d6..b2ee527 100644 --- a/rust/src/player/mod.rs +++ b/rust/src/player/mod.rs @@ -368,7 +368,7 @@ impl DamageSource { } #[derive(Debug, GodotConvert, Clone)] -#[godot(via = u8)] +#[godot(via = GString)] pub enum DamageSourceSignal { Player, Killbox, diff --git a/rust/src/replay.rs b/rust/src/replay.rs index c97bfeb..5373a65 100644 --- a/rust/src/replay.rs +++ b/rust/src/replay.rs @@ -265,6 +265,8 @@ pub struct ReplayPlayback { pub spectated_player_id: u16, pub fast_forwarding: bool, + /// True for one frame after fast-forwarding is done + pub was_fast_forwarding: bool, pub finished: bool, @@ -353,6 +355,20 @@ impl INode for ReplayPlayback { } } } + + if self.was_fast_forwarding { + for (id, triggerable) in &mut map.bind_mut().triggerables { + if let Some(state) = frame.triggerables.get(id) { + if let Some(player_id) = state.active { + triggerable + .bind_mut() + .activate_visual(player_id, state.active_for); + } else { + triggerable.bind_mut().deactivate_visual(); + } + } + } + } } for event in &frame.events { @@ -411,6 +427,20 @@ impl INode for ReplayPlayback { pickup.dyn_bind_mut().on_pickup(); } } + ReplayEvent::TriggerActivate(id, player_id) => { + if let Some(map) = &mut self.current_map + && let Some(triggerable) = map.bind_mut().triggerables.get_mut(id) + { + triggerable.bind_mut().activate_visual(*player_id, 0.); + } + } + ReplayEvent::TriggerDeactivate(id) => { + if let Some(map) = &mut self.current_map + && let Some(triggerable) = map.bind_mut().triggerables.get_mut(id) + { + triggerable.bind_mut().deactivate_visual(); + } + } ReplayEvent::TelegrenadeSpawn(player_id, location, velocity) => { if let Some(prev_grenade) = self.telegrenades.get_mut(player_id) { prev_grenade.queue_free(); @@ -463,6 +493,7 @@ impl INode for ReplayPlayback { self.signals().on_data_changed().emit(); } + self.was_fast_forwarding = self.fast_forwarding; self.fast_forwarding = false; if !self.finished && self.current_frame_idx >= self.replay.frames.len() { @@ -660,6 +691,8 @@ impl ReplayRecorder { let mut pickups_visible = HashMap::new(); + let mut triggerables = HashMap::new(); + if let Some(game) = Game::singleton() { for player in &game.bind().players { if let Some(state) = player.get_state(player.clone()) { @@ -682,6 +715,15 @@ impl ReplayRecorder { for (id, pickup) in &map.bind().pickups { pickups_visible.insert(*id, pickup.dyn_bind().is_active()); } + for (id, triggerable) in &map.bind().triggerables { + triggerables.insert( + *id, + ReplayTriggerableState { + active: triggerable.bind().get_active(), + active_for: triggerable.bind().active_for(), + }, + ); + } } goals = game.bind().goals.clone(); @@ -693,6 +735,7 @@ impl ReplayRecorder { time: self.time_elapsed, goals, pickups_visible, + triggerables, player_data, ball_data, events: Vec::new(), @@ -807,6 +850,7 @@ pub struct ReplayFrame { player_data: HashMap, ball_data: ReplayBall, pickups_visible: HashMap, + triggerables: HashMap, events: Vec, goals: HashMap, } @@ -820,6 +864,8 @@ pub enum ReplayEvent { Punch(u16, NetVector3), TakeDamage(u16, DamageSource, i32), Pickup(PickupKind, u8, u16), + TriggerActivate(u8, u16), + TriggerDeactivate(u8), TelegrenadeSpawn(u16, NetVector3, NetVector3), TelegrenadeDespawn(u16), } @@ -873,3 +919,9 @@ pub struct ReplayBallData { transform: NetTransform, velocity: NetVector3, } + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ReplayTriggerableState { + active: Option, + active_for: f64, +}