skullball/rust/src/map/triggers.rs
2026-08-06 21:45:52 +03:00

242 lines
6.9 KiB
Rust

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<u8>,
/// Contains the activator player's ID if active.
active: Option<u16>,
active_for: f64,
visual_active: Option<u16>,
visual_active_for: f64,
base: Base<Node3D>,
}
#[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<u16> {
self.active
}
fn register_triggerable(&mut self, mut map: Gd<Map>) {
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<StaticBody3D>,
}
#[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<Gd<Node3D>>,
base: Base<Area3D>,
}
#[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<Node3D>) {
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::<LocalPlayer>() {
self.colliders_inside.insert(node);
Some(local_player.bind().player_id.unwrap_or(0))
} else if let Ok(remote_player) = node.clone().try_cast::<RemotePlayer>() {
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<Node3D>) {
self.colliders_inside.remove(&node);
if self.colliders_inside.is_empty() {
self.signals().on_all_left().emit();
}
}
}