use std::collections::HashSet; use godot::{ classes::{Area3D, IArea3D, StaticBody3D}, prelude::*, }; use crate::{ game::Game, map::Map, net::network_manager::NetworkManager, player::{ DamageSource, local_player::LocalPlayer, remote_player::RemotePlayer, 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, active_for: f64, visual_active: Option, visual_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 physics_process(&mut self, delta: f64) { if self.active.is_some() { self.active_for += delta; } else { self.active_for = 0.; } if self.visual_active.is_some() { self.visual_active_for += delta; } else { self.visual_active_for = 0.; } } } #[godot_api] impl Triggerable { /// Called whenever this triggerable's visuals should be immediately /// activated. #[func(virtual)] pub fn on_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 on_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) {} pub fn activate_visual(&mut self, player_id: u16, time_since_activation: f64) { self.visual_active = Some(player_id); self.visual_active_for = time_since_activation; self.on_activate_visual(player_id, time_since_activation); } pub fn deactivate_visual(&mut self) { self.visual_active = None; self.visual_active_for = 0.; self.on_deactivate_visual(); } /// 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(); } } /// Returns true if this triggerable is active currently on the real running /// version of the game. #[func] pub fn is_server_active(&self) -> bool { self.active.is_some() } /// Returns the amount of time this triggerable has been active on the real /// running version of the game. #[func] pub fn server_active_for(&self) -> f64 { self.active_for } /// Returns true if this triggerable should be visibly active, whether it's /// happening in a replay or the real game. #[func] pub fn is_active(&self) -> bool { self.visual_active.is_some() } /// Returns the amount of time this triggerable has been visually active, /// whether it's happening in replay or in the real game. #[func] pub fn active_for(&self) -> f64 { self.visual_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=StaticBody3D, 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); } } } #[derive(GodotClass)] #[class(base=Area3D, init)] pub struct ProximityTrigger { colliders_inside: HashSet>, base: Base, } #[godot_api] impl IArea3D for ProximityTrigger { fn ready(&mut self) { let base = self.base_mut().clone(); base.signals() .body_entered() .connect_other(self, Self::on_enter); base.signals() .body_exited() .connect_other(self, Self::on_leave); } } #[godot_api] impl ProximityTrigger { #[signal] pub fn on_trigger(player: u16); #[signal] pub fn on_all_left(); fn on_enter(&mut self, node: Gd) { if NetworkManager::singleton().bind().is_host() { let was_empty = self.colliders_inside.is_empty(); let player_id = if let Ok(local_player) = node.clone().try_cast::() { self.colliders_inside.insert(node); Some(local_player.bind().player_id.unwrap_or(0)) } else if let Ok(remote_player) = node.clone().try_cast::() { if !remote_player.bind().replay_mode { self.colliders_inside.insert(node); } Some(remote_player.bind().player_id.unwrap_or(0)) } else { None }; if was_empty && let Some(player_id) = player_id { self.signals().on_trigger().emit(player_id); } } } fn on_leave(&mut self, node: Gd) { self.colliders_inside.remove(&node); if self.colliders_inside.is_empty() { self.signals().on_all_left().emit(); } } }