use std::{f32::consts::PI, ops::Neg}; use godot::{ classes::{ Camera3D, CharacterBody3D, CollisionShape3D, GpuParticles3D, ICharacterBody3D, Input, InputEvent, InputEventMouseMotion, Label, input::MouseMode, }, obj::WithBaseField, prelude::*, }; use serde::{Deserialize, Serialize}; use crate::{ game_manager::Game, net::{ network_manager::{ NetworkManager, Package::{self, Jump, Shoot}, PeerKind, }, util::{NetVector3, cast_ray}, }, soldier_mesh::SoldierMesh, ui::cli::CommandLinePanel, weapon::{BallWeapon, Weapon, WeaponType}, }; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct NetTransform { pub location: NetVector3, pub rotation: NetVector3, } pub trait IPlayer { fn get_player_id(&self) -> Option; fn get_y_speed(&self) -> f32; fn set_y_speed(&mut self, speed: f32); fn get_movement_dir(&self) -> Vector3; fn set_movement_dir(&mut self, dir: Vector3); fn get_move_speed(&self) -> f32; fn apply_transform(&mut self, transform: NetTransform); fn apply_rotation(&mut self, rot: Vector3); fn apply_look_up(&mut self, rot: f32); fn get_look_up(&self) -> f32; fn get_transform(&self) -> NetTransform; fn jump(&mut self); fn try_shoot(&mut self, to: Vector3, deal_damage: bool) -> bool; fn take_damage(&mut self, source_player: DamageSource, damage: i32, kill: bool); fn is_dead(&self) -> bool; fn kill(&mut self, source_player: DamageSource); fn dead_for(&self) -> f64; fn swap_weapon(&mut self, weapon: WeaponType); fn get_weapon(&self) -> Option>; fn drop_ball(&mut self, with_velocity: bool) -> bool; fn get_ball_origin(&self) -> Option>; } pub trait PlayerMovement { fn player_update(&mut self, delta: f64); fn drop_ball_common(&mut self, with_velocity: bool) -> bool; fn get_look_dir(&self) -> Vector3; } impl PlayerMovement for T { fn player_update(&mut self, delta: f64) { self.set_y_speed(self.get_y_speed() - 9.8 * delta as f32); if self.base().is_on_floor() && self.get_y_speed() < 0. { self.set_y_speed(0.); } let speed = Vector3::UP * self.get_y_speed() + self .get_movement_dir() .try_normalized() .unwrap_or_default() .rotated(Vector3::UP, self.base().get_rotation().y) * self.get_move_speed(); if self.is_dead() { self.base_mut().set_velocity(Vector3::ZERO); } else { self.base_mut().set_velocity(speed); self.base_mut().move_and_slide(); } } fn drop_ball_common(&mut self, with_velocity: bool) -> bool { if let Some(weapon) = self.get_weapon() && let Ok(_) = weapon.try_cast::() { self.run_deferred(move |s| { if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer && let PeerKind::Server(peer, _) = peer { s.swap_weapon(WeaponType::Raygun); peer.broadcast_reliable(Package::SwapWeapon( s.get_player_id().unwrap_or(0), WeaponType::Raygun, )); if let Some(mut game) = Game::singleton() { if let Some(ball_origin) = &s.get_ball_origin() { let velocity = if with_velocity { s.get_look_dir() * 20. } else { Vector3::ZERO }; game.bind_mut().spawn_ball( Some(NetTransform { location: ball_origin.get_global_position().into(), rotation: ball_origin.get_global_rotation().into(), }), Some(velocity.into()), s.get_player_id(), ); } } } }); true } else { false } } fn get_look_dir(&self) -> Vector3 { if let Some(ball_origin) = self.get_ball_origin() { ball_origin.get_global_basis().col_c().neg() } else { Vector3::ZERO } } } #[derive(GodotClass)] #[class(base=CharacterBody3D, init)] pub struct LocalPlayer { #[export] look_sensitivity: f32, #[export] camera: Option>, #[export] move_speed: f32, #[export] soldier_mesh: Option>, #[export] collider: Option>, #[export] splorch: Option>, #[export] dead_cam_target: Option>, #[export] respawn_label: Option>, #[export] ball_origin: Option>, #[export] health: i32, pub player_id: Option, movement_dir: Vector3, y_speed: f32, look_up: f32, shoot_cd: f64, is_dead: bool, dead_for: f64, killer_id: Option, locked: bool, base: Base, } #[godot_dyn] impl IPlayer for LocalPlayer { fn get_player_id(&self) -> Option { self.player_id } fn get_y_speed(&self) -> f32 { self.y_speed } fn set_y_speed(&mut self, speed: f32) { self.y_speed = speed; } fn get_movement_dir(&self) -> Vector3 { self.movement_dir } fn set_movement_dir(&mut self, _dir: Vector3) {} fn get_move_speed(&self) -> f32 { self.move_speed } fn apply_transform(&mut self, transform: NetTransform) { self.base_mut() .set_global_position(transform.location.into()); self.base_mut() .set_global_rotation(transform.rotation.into()); } fn apply_rotation(&mut self, _rot: Vector3) {} fn get_transform(&self) -> NetTransform { NetTransform { location: self.base().get_global_position().into(), rotation: self.base().get_global_rotation().into(), } } fn jump(&mut self) { self.set_y_speed(5.); if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer { match peer { PeerKind::Client(peer, addr) => peer.send_reliable(&addr, Jump(0)), PeerKind::Server(peer, _) => { peer.broadcast_reliable(Jump(self.player_id.unwrap_or(0))) } } } } fn get_look_up(&self) -> f32 { self.look_up } fn apply_look_up(&mut self, _look: f32) {} fn try_shoot(&mut self, to: Vector3, deal_damage: bool) -> bool { if self.is_dead { return false; } if self.drop_ball(true) { return true; } if self.shoot_cd <= 0. { let rid = self.base().get_rid(); if let Some(mut weapon) = self.get_weapon() { weapon .dyn_bind_mut() .shoot(rid, self.player_id.unwrap_or(0), to, deal_damage); } self.shoot_cd = 0.2; true } else { false } } fn take_damage(&mut self, source: DamageSource, damage: i32, kill: bool) { let (signal, player) = source.into_signal(); self.run_deferred(move |s| s.signals().on_take_damage().emit(signal, player, damage)); self.health -= damage; if kill && self.health <= 0 { self.kill(source); } } fn is_dead(&self) -> bool { self.is_dead } fn dead_for(&self) -> f64 { self.dead_for } fn kill(&mut self, source: DamageSource) { self.drop_ball(false); let (signal, player) = source.into_signal(); self.run_deferred(move |s| s.signals().on_die().emit(signal, player)); if let Some(mut mesh) = self.soldier_mesh.take() { mesh.queue_free(); } if let Some(mut collider) = self.collider.take() { collider.queue_free(); } if let Some(splorch) = &mut self.splorch { splorch.set_emitting(true); } self.is_dead = true; if let DamageSource::Player(player_id) = source { self.killer_id = Some(player_id); } } fn swap_weapon(&mut self, weapon: WeaponType) { if let Some(mesh) = &mut self.soldier_mesh { match weapon { WeaponType::Raygun => mesh.bind_mut().spawn_raygun(), WeaponType::Ball => mesh.bind_mut().spawn_ball(), }; } } fn get_weapon(&self) -> Option> { self.soldier_mesh .as_ref() .and_then(|m| m.bind().get_weapon()) } fn drop_ball(&mut self, with_velocity: bool) -> bool { self.drop_ball_common(with_velocity) } fn get_ball_origin(&self) -> Option> { self.ball_origin.clone() } } #[godot_api] impl ICharacterBody3D for LocalPlayer { fn ready(&mut self) { if !CommandLinePanel::singleton().bind().opened { self.set_lock(true); } CommandLinePanel::singleton() .signals() .on_open() .connect_other(self, |s| s.set_lock(false)); CommandLinePanel::singleton() .signals() .on_close() .connect_other(self, |s| { s.set_lock(true); }); self.swap_weapon(WeaponType::Raygun); if let Some(camera) = &mut self.camera { camera.set_current(true); } } fn process(&mut self, delta: f64) { if self.is_dead { self.dead_for += delta; if let Some(label) = &mut self.respawn_label { let respawn_timer = if let Some(game) = Game::singleton() { game.bind().respawn_timer } else { 0. }; let time_remaining = respawn_timer - self.dead_for; label.set_text(&format!("Respawn in\n{:.2}", time_remaining)); } if let Some(camera) = &self.camera { if camera.is_current() && self.dead_for > 3. { if let Some(game) = Game::singleton() && let Some(killer_id) = self.killer_id { let game = game.bind(); let remote_player = game.find_player(killer_id); if let Some(remote_player) = &remote_player && let Some(remote_character) = remote_player.character.clone() && let Ok(mut remote_character) = remote_character.try_cast::() && let Some(camera) = &mut remote_character.bind_mut().spectator_camera { camera.set_current(true); } } } } } self.shoot_cd -= delta; let input_direction = if self.locked { let input = Input::singleton(); if self.base().is_on_floor() && input.is_action_just_pressed("jump") { self.jump(); } Vector3::RIGHT * input.is_action_pressed("right") as u32 as f32 - Vector3::RIGHT * input.is_action_pressed("left") as u32 as f32 + Vector3::FORWARD * input.is_action_pressed("forward") as u32 as f32 - Vector3::FORWARD * input.is_action_pressed("backward") as u32 as f32 } else { Vector3::ZERO }; self.movement_dir = input_direction; if let Some(mesh) = &mut self.soldier_mesh { mesh.bind_mut().update_y_look(self.look_up); } self.player_update(delta); if self.is_dead && let Some(target) = &self.dead_cam_target && let Some(camera) = &mut self.camera { let camera_prev_pos = camera.get_global_position(); let new_pos = camera_prev_pos.lerp(target.get_global_position(), 0.1); camera.set_global_position(new_pos); } } fn input(&mut self, event: Gd) { if self.locked { if event.is_action_pressed("shoot") { let pos = if let Some(camera) = &self.camera { let forward = camera.get_global_basis().col_c().neg(); match cast_ray( camera.get_global_position(), camera.get_global_position() + forward, self.base().get_world_3d(), Array::from_iter(vec![self.base().get_rid()]), ) { Some(res) => Some(res.position), None => Some(camera.get_global_position() + forward * 100.), } } else { None }; if let Some(pos) = pos { if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer { match peer { PeerKind::Client(peer, addr) => { if self.try_shoot(pos, false) { peer.send_reliable( addr, Shoot(0, pos.into(), self.get_transform(), self.look_up), ); } } PeerKind::Server(peer, _) => { if self.try_shoot(pos, true) { peer.broadcast_reliable(Shoot( 0, pos.into(), self.get_transform(), self.look_up, )); } } } } } } if let Ok(event) = event.try_cast::() { if !self.is_dead() { let x = event.get_relative().x * 0.001 * self.look_sensitivity; let y = event.get_relative().y * 0.001 * self.look_sensitivity; if let Some(camera) = self.camera.as_mut() { let curr_rotation = camera.get_rotation(); let x_rot = (curr_rotation.x - y * self.look_sensitivity).clamp(-PI / 2., PI / 2.); camera.set_rotation(Vector3::RIGHT * x_rot); self.look_up = x_rot; } let y_rot = -x * self.look_sensitivity; self.base_mut().rotate_y(y_rot); } } } } } #[godot_api] impl LocalPlayer { #[signal] pub fn on_die(source: DamageSourceSignal, player: u16); #[signal] pub fn on_take_damage(source: DamageSourceSignal, source_player: u16, damage: i32); fn set_lock(&mut self, locked: bool) { self.locked = locked; if self.locked { Input::singleton().set_mouse_mode(MouseMode::CAPTURED); } else { Input::singleton().set_mouse_mode(MouseMode::VISIBLE); } } } #[derive(GodotClass)] #[class(base=CharacterBody3D, init)] pub struct RemotePlayer { #[export] move_speed: f32, #[export] soldier_mesh: Option>, #[export] collider: Option>, #[export] splorch: Option>, #[export] health: i32, #[export] spectator_camera: Option>, #[export] ball_origin: Option>, pub player_id: Option, movement_dir: Vector3, y_speed: f32, look_up: f32, is_dead: bool, dead_for: f64, shoot_cd: f64, base: Base, } #[godot_dyn] impl IPlayer for RemotePlayer { fn get_y_speed(&self) -> f32 { self.y_speed } fn set_y_speed(&mut self, speed: f32) { self.y_speed = speed; } fn get_movement_dir(&self) -> Vector3 { self.movement_dir } fn set_movement_dir(&mut self, dir: Vector3) { self.movement_dir = dir.into(); } fn get_move_speed(&self) -> f32 { self.move_speed } fn get_player_id(&self) -> Option { self.player_id } fn apply_transform(&mut self, transform: NetTransform) { self.base_mut() .set_global_position(transform.location.into()); self.base_mut() .set_global_rotation(transform.rotation.into()); } fn apply_rotation(&mut self, rot: Vector3) { self.base_mut().set_global_rotation(rot); } fn get_transform(&self) -> NetTransform { NetTransform { location: self.base().get_global_position().into(), rotation: self.base().get_global_rotation().into(), } } fn jump(&mut self) { self.set_y_speed(5.); } fn get_look_up(&self) -> f32 { self.look_up } fn apply_look_up(&mut self, look: f32) { self.look_up = look; } fn try_shoot(&mut self, to: Vector3, deal_damage: bool) -> bool { if self.is_dead { return false; } if self.drop_ball(true) { return true; } if self.shoot_cd <= 0. { let rid = self.base().get_rid(); if let Some(mesh) = &mut self.soldier_mesh && let Some(mut weapon) = mesh.bind().get_weapon() { weapon .dyn_bind_mut() .shoot(rid, self.player_id.unwrap_or(0), to, deal_damage); } self.shoot_cd = 0.2; true } else { false } } fn take_damage(&mut self, source: DamageSource, damage: i32, kill: bool) { let (signal, player) = source.into_signal(); self.run_deferred(move |s| s.signals().on_take_damage().emit(signal, player, damage)); self.health -= damage; if kill && self.health <= 0 { self.kill(source); } } fn is_dead(&self) -> bool { self.is_dead } fn dead_for(&self) -> f64 { self.dead_for } fn kill(&mut self, source: DamageSource) { self.drop_ball(false); let (signal, player) = source.into_signal(); self.run_deferred(move |s| s.signals().on_die().emit(signal, player)); if let Some(mut mesh) = self.soldier_mesh.take() { mesh.queue_free(); } if let Some(mut collider) = self.collider.take() { collider.queue_free(); } if let Some(splorch) = &mut self.splorch { splorch.set_emitting(true); } self.is_dead = true; } fn swap_weapon(&mut self, weapon: WeaponType) { if let Some(mesh) = &mut self.soldier_mesh { match weapon { WeaponType::Raygun => mesh.bind_mut().spawn_raygun(), WeaponType::Ball => mesh.bind_mut().spawn_ball(), }; } } fn get_weapon(&self) -> Option> { self.soldier_mesh .as_ref() .and_then(|m| m.bind().get_weapon()) } fn drop_ball(&mut self, with_velocity: bool) -> bool { self.drop_ball_common(with_velocity) } fn get_ball_origin(&self) -> Option> { self.ball_origin.clone() } } #[godot_api] impl ICharacterBody3D for RemotePlayer { fn ready(&mut self) { self.swap_weapon(WeaponType::Raygun); } fn process(&mut self, delta: f64) { if self.is_dead { self.dead_for += delta; } self.player_update(delta); self.shoot_cd -= delta; if let Some(mesh) = &mut self.soldier_mesh { mesh.bind_mut() .update_movement(Vector2::new(self.movement_dir.x, -self.movement_dir.z)); mesh.bind_mut().update_y_look(self.look_up); } if let Some(ball_origin) = &mut self.ball_origin { ball_origin.set_rotation(Vector3::RIGHT * self.look_up); } } } #[godot_api] impl RemotePlayer { #[signal] pub fn on_die(source: DamageSourceSignal, player: u16); #[signal] pub fn on_take_damage(source: DamageSourceSignal, source_player: u16, damage: i32); } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DamageSource { Player(u16), Killbox, } impl DamageSource { pub fn into_signal(&self) -> (DamageSourceSignal, u16) { match self { DamageSource::Player(id) => (DamageSourceSignal::Player, *id), DamageSource::Killbox => (DamageSourceSignal::Killbox, 0), } } pub fn from_signal(signal: DamageSourceSignal, player: u16) -> DamageSource { match signal { DamageSourceSignal::Player => DamageSource::Player(player), DamageSourceSignal::Killbox => DamageSource::Killbox, } } } #[derive(Debug, GodotConvert, Clone)] #[godot(via = u8)] pub enum DamageSourceSignal { Player, Killbox, }