Make shotgun functional

This commit is contained in:
Sofia 2026-07-24 19:35:53 +03:00
parent f1ee0f0ddb
commit 678268642b
12 changed files with 175 additions and 39 deletions

View File

@ -4,7 +4,7 @@
[ext_resource type="PackedScene" uid="uid://b8nwd3323kwlc" path="res://mesh/shotgun.tscn" id="3_0xpw2"] [ext_resource type="PackedScene" uid="uid://b8nwd3323kwlc" path="res://mesh/shotgun.tscn" id="3_0xpw2"]
[ext_resource type="AudioStream" uid="uid://bq553wxrelpdr" path="res://raw_assets/sfx/LaserWithRecoveryEditedCompressed.mp3" id="3_8oj30"] [ext_resource type="AudioStream" uid="uid://bq553wxrelpdr" path="res://raw_assets/sfx/LaserWithRecoveryEditedCompressed.mp3" id="3_8oj30"]
[node name="shotgun" type="Raygun" unique_id=573018429 node_paths=PackedStringArray("sound_emitter", "bullet_source")] [node name="shotgun" type="Shotgun" unique_id=786130999 node_paths=PackedStringArray("sound_emitter", "bullet_source")]
sound_emitter = NodePath("sound_emitter") sound_emitter = NodePath("sound_emitter")
bullet_prefab = ExtResource("1_0xpw2") bullet_prefab = ExtResource("1_0xpw2")
bullet_source = NodePath("bullet_source") bullet_source = NodePath("bullet_source")

View File

@ -8,7 +8,6 @@ use crate::{
Announcement::{self, FirstBlood}, Announcement::{self, FirstBlood},
Announcer, Announcer,
}, },
ball::Ball,
chat::{Chat, ChatMessage}, chat::{Chat, ChatMessage},
map::Map, map::Map,
map_resource::MapResource, map_resource::MapResource,
@ -20,7 +19,10 @@ use crate::{
}, },
util::NetVector3, util::NetVector3,
}, },
player::{DamageSource, IPlayer, LocalPlayer, NetTransform, RemotePlayer, weapon::WeaponType}, player::{
DamageSource, IPlayer, LocalPlayer, NetTransform, RemotePlayer, ball::Ball,
weapon::WeaponType,
},
team_resource::TeamResource, team_resource::TeamResource,
}; };

View File

@ -3,7 +3,7 @@ use godot::{
prelude::*, prelude::*,
}; };
use crate::{ball::Ball, game_manager::Game, net::network_manager::NetworkManager}; use crate::{game_manager::Game, net::network_manager::NetworkManager, player::ball::Ball};
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=Area3D, init)] #[class(base=Area3D, init)]

View File

@ -4,10 +4,9 @@ use godot::{
}; };
use crate::{ use crate::{
ball::Ball,
game_manager::Game, game_manager::Game,
net::network_manager::NetworkManager, net::network_manager::NetworkManager,
player::{DamageSource, IPlayer}, player::{DamageSource, IPlayer, ball::Ball},
}; };
#[derive(GodotClass)] #[derive(GodotClass)]

View File

@ -1,7 +1,6 @@
use godot::prelude::*; use godot::prelude::*;
pub mod announcer; pub mod announcer;
pub mod ball;
pub mod chat; pub mod chat;
pub mod cli_parser; pub mod cli_parser;
pub mod game_manager; pub mod game_manager;

View File

@ -1,10 +1,9 @@
use godot::{classes::CharacterBody3D, prelude::*}; use godot::{classes::CharacterBody3D, prelude::*};
use crate::{ use crate::{
ball::Ball,
game_manager::Game, game_manager::Game,
net::util::NetVector3, net::util::NetVector3,
player::{IPlayer, LocalPlayer, NetTransform, RemotePlayer}, player::{IPlayer, LocalPlayer, NetTransform, RemotePlayer, ball::Ball},
}; };
#[derive(GodotClass)] #[derive(GodotClass)]

View File

@ -30,8 +30,8 @@ impl From<NetVector3> for Vector3 {
pub fn cast_ray( pub fn cast_ray(
from: Vector3, from: Vector3,
to: Vector3, to: Vector3,
world: Option<Gd<World3D>>, world: &Option<Gd<World3D>>,
exclude: Array<Rid>, exclude: &Array<Rid>,
) -> Option<RaycastResult> { ) -> Option<RaycastResult> {
let ray = PhysicsRayQueryParameters3D::create(from, to); let ray = PhysicsRayQueryParameters3D::create(from, to);
if let Some(mut ray) = ray { if let Some(mut ray) = ray {
@ -62,6 +62,39 @@ pub fn cast_ray(
} }
} }
pub fn shotgun_ray(
from: Vector3,
to: Vector3,
up: Vector3,
world: &Option<Gd<World3D>>,
exclude: &Array<Rid>,
density: u8,
angle: f32,
distance: f32,
) -> Vec<RaycastResult> {
let mut results = Vec::new();
let original_dir = (to - from).normalized();
let step = angle / density as f32;
let up = up.normalized();
let right = original_dir.cross(up).normalized();
for y in 0..density {
let y_rot = -(angle / 2.) + step * y as f32;
for x in 0..density {
let x_rot = -(angle / 2.) + step * x as f32;
let dir = original_dir.rotated(up, x_rot).rotated(right, y_rot);
let to = from + dir * distance;
if let Some(res) = cast_ray(from, to, world, exclude) {
results.push(res);
}
}
}
results
}
pub struct RaycastResult { pub struct RaycastResult {
pub position: Vector3, pub position: Vector3,
pub collider: Gd<Object>, pub collider: Gd<Object>,

View File

@ -5,7 +5,7 @@ use godot::{
use crate::{ use crate::{
net::{network_manager::BallSync, util::NetVector3}, net::{network_manager::BallSync, util::NetVector3},
player::{IPlayer, NetTransform}, player::{DamageSource, IPlayer, NetTransform, shootable::Shootable},
}; };
#[derive(GodotClass)] #[derive(GodotClass)]
@ -80,3 +80,12 @@ impl Ball {
self.base_mut().set_linear_velocity(sync.velocity.into()); self.base_mut().set_linear_velocity(sync.velocity.into());
} }
} }
#[godot_dyn]
impl Shootable for Ball {
fn on_shoot(&mut self, _: DamageSource, _: i32, dir: Vector3) {
let prev_velocity = self.base().get_linear_velocity();
self.base_mut()
.set_linear_velocity(prev_velocity + dir * 10.);
}
}

View File

@ -23,14 +23,17 @@ use crate::{
}, },
player::{ player::{
effects::PlayerEffects, effects::PlayerEffects,
shootable::Shootable,
soldier_mesh::SoldierMesh, soldier_mesh::SoldierMesh,
weapon::{BallWeapon, Raygun, Weapon, WeaponType}, weapon::{BallWeapon, Raygun, Shotgun, Weapon, WeaponType},
}, },
ui::{chatbox::ChatBox, cli::CommandLinePanel, settings::SettingsPanel}, ui::{chatbox::ChatBox, cli::CommandLinePanel, settings::SettingsPanel},
}; };
pub mod ball;
pub mod effects; pub mod effects;
pub mod player_hand_remover; pub mod player_hand_remover;
pub mod shootable;
pub mod soldier_mesh; pub mod soldier_mesh;
pub mod weapon; pub mod weapon;
@ -307,6 +310,7 @@ impl IPlayer for LocalPlayer {
self.player_id.unwrap_or(0), self.player_id.unwrap_or(0),
camera_origin.get_global_position(), camera_origin.get_global_position(),
to, to,
camera_origin.get_global_basis().col_b(),
deal_damage, deal_damage,
) )
} else { } else {
@ -372,7 +376,7 @@ impl IPlayer for LocalPlayer {
} }
WeaponType::Shotgun => { WeaponType::Shotgun => {
if let Some(raygun) = &mesh.bind().shotgun { if let Some(raygun) = &mesh.bind().shotgun {
self.spawn_weapon(raygun.instantiate_as::<Raygun>().into_dyn().upcast()); self.spawn_weapon(raygun.instantiate_as::<Shotgun>().into_dyn().upcast());
} }
} }
WeaponType::Ball => { WeaponType::Ball => {
@ -580,8 +584,8 @@ impl ICharacterBody3D for LocalPlayer {
match cast_ray( match cast_ray(
camera.get_global_position(), camera.get_global_position(),
camera.get_global_position() + forward, camera.get_global_position() + forward,
self.base().get_world_3d(), &self.base().get_world_3d(),
Array::from_iter(vec![self.base().get_rid()]), &Array::from_iter(vec![self.base().get_rid()]),
) { ) {
Some(res) => Some(res.position), Some(res) => Some(res.position),
None => Some(camera.get_global_position() + forward * 100.), None => Some(camera.get_global_position() + forward * 100.),
@ -736,6 +740,13 @@ impl LocalPlayer {
} }
} }
#[godot_dyn]
impl Shootable for LocalPlayer {
fn on_shoot(&mut self, source: DamageSource, damage: i32, _: Vector3) {
self.take_damage(source, damage, true);
}
}
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=CharacterBody3D, init)] #[class(base=CharacterBody3D, init)]
pub struct RemotePlayer { pub struct RemotePlayer {
@ -842,6 +853,7 @@ impl IPlayer for RemotePlayer {
rid, rid,
self.player_id.unwrap_or(0), self.player_id.unwrap_or(0),
camera_origin.get_global_position(), camera_origin.get_global_position(),
camera_origin.get_basis().col_b(),
to, to,
deal_damage, deal_damage,
) )
@ -978,6 +990,13 @@ impl RemotePlayer {
pub fn on_take_damage(source: DamageSourceSignal, source_player: u16, damage: i32); pub fn on_take_damage(source: DamageSourceSignal, source_player: u16, damage: i32);
} }
#[godot_dyn]
impl Shootable for RemotePlayer {
fn on_shoot(&mut self, source: DamageSource, damage: i32, _: Vector3) {
self.take_damage(source, damage, true);
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum DamageSource { pub enum DamageSource {
Player(u16), Player(u16),

View File

@ -0,0 +1,6 @@
use crate::player::DamageSource;
use godot::prelude::*;
pub trait Shootable {
fn on_shoot(&mut self, source: DamageSource, damage: i32, dir: Vector3);
}

View File

@ -5,7 +5,7 @@ use godot::{
use crate::player::{ use crate::player::{
player_hand_remover::PlayerHandRemover, player_hand_remover::PlayerHandRemover,
weapon::{BallWeapon, Raygun, Weapon}, weapon::{BallWeapon, Raygun, Shotgun, Weapon},
}; };
#[derive(GodotClass)] #[derive(GodotClass)]
@ -62,7 +62,7 @@ impl SoldierMesh {
pub fn spawn_shotgun(&mut self) -> Option<DynGd<Node3D, dyn Weapon>> { pub fn spawn_shotgun(&mut self) -> Option<DynGd<Node3D, dyn Weapon>> {
if let Some(shotgun) = &self.shotgun { if let Some(shotgun) = &self.shotgun {
let node = shotgun.instantiate_as::<Raygun>(); let node = shotgun.instantiate_as::<Shotgun>();
self.spawn_weapon(node.into_dyn().upcast()) self.spawn_weapon(node.into_dyn().upcast())
} else { } else {
None None

View File

@ -1,11 +1,12 @@
use std::{collections::HashSet, f32::consts::PI};
use godot::{classes::AudioStreamPlayer3D, prelude::*}; use godot::{classes::AudioStreamPlayer3D, prelude::*};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
ball::Ball,
game_manager::{Game, GameOption}, game_manager::{Game, GameOption},
net::util::cast_ray, net::util::{cast_ray, shotgun_ray},
player::{DamageSource, IPlayer}, player::{DamageSource, IPlayer, ball::Ball, shootable::Shootable},
}; };
pub trait Weapon { pub trait Weapon {
@ -15,6 +16,7 @@ pub trait Weapon {
player_id: u16, player_id: u16,
from: Vector3, from: Vector3,
to: Vector3, to: Vector3,
up: Vector3,
deal_damage: bool, deal_damage: bool,
) -> bool; ) -> bool;
fn get_type(&self) -> WeaponType; fn get_type(&self) -> WeaponType;
@ -60,6 +62,7 @@ impl Weapon for Raygun {
player_id: u16, player_id: u16,
from: Vector3, from: Vector3,
to: Vector3, to: Vector3,
_: Vector3,
deal_damage: bool, deal_damage: bool,
) -> bool { ) -> bool {
if self.cooldown_remaining > 0. { if self.cooldown_remaining > 0. {
@ -74,8 +77,8 @@ impl Weapon for Raygun {
let target = cast_ray( let target = cast_ray(
from, from,
to, to,
self.base().get_world_3d(), &self.base().get_world_3d(),
Array::from_iter(vec![player]), &Array::from_iter(vec![player]),
); );
let dir = (to - source.get_global_position()).normalized(); let dir = (to - source.get_global_position()).normalized();
@ -100,23 +103,15 @@ impl Weapon for Raygun {
} }
if deal_damage && let Some(target) = target { if deal_damage && let Some(target) = target {
if let Ok(mut player) = target.collider.clone().try_dynify::<dyn IPlayer>() { if let Ok(mut shootable) = target.collider.clone().try_dynify::<dyn Shootable>() {
godot_print!("{}", target.collider);
self.run_deferred(move |_| { self.run_deferred(move |_| {
if let Some(game) = Game::singleton() shootable.dyn_bind_mut().on_shoot(
&& game.bind().opts.is_true(GameOption::FriendlyFire)
{
player.dyn_bind_mut().take_damage(
DamageSource::Player(player_id), DamageSource::Player(player_id),
100, 100,
true, dir.normalized(),
); );
}
}); });
} else if let Ok(mut ball) = target.collider.try_cast::<Ball>() {
let prev_velocity = ball.bind().base().get_linear_velocity();
ball.bind_mut()
.base_mut()
.set_linear_velocity(prev_velocity + dir * 10.);
} }
} }
} }
@ -131,6 +126,81 @@ impl Weapon for Raygun {
} }
} }
#[derive(GodotClass)]
#[class(base=Node3D, init)]
pub struct Shotgun {
#[export]
sound_emitter: Option<Gd<AudioStreamPlayer3D>>,
#[export]
bullet_prefab: Option<Gd<PackedScene>>,
#[export]
bullet_source: Option<Gd<Node3D>>,
#[export]
cooldown: f64,
cooldown_remaining: f64,
base: Base<Node3D>,
}
#[godot_dyn]
impl Weapon for Shotgun {
fn shoot(
&mut self,
player: Rid,
player_id: u16,
from: Vector3,
to: Vector3,
up: Vector3,
deal_damage: bool,
) -> bool {
// TODO do effects
let mut colliders = HashSet::new();
for result in shotgun_ray(
from,
to,
up,
&self.base().get_world_3d(),
&Array::from_iter(vec![player]),
10,
PI / 2.,
10.,
) {
colliders.insert(result.collider);
}
if deal_damage {
for collider in colliders {
let collider_pos = if let Ok(node) = collider.clone().try_cast::<Node3D>() {
node.get_global_position()
} else {
Vector3::ZERO
};
let delta = collider_pos - from;
if let Ok(mut shootable) = collider.clone().try_dynify::<dyn Shootable>() {
godot_print!("{} ({})", collider, delta.length());
self.run_deferred(move |_| {
shootable.dyn_bind_mut().on_shoot(
DamageSource::Player(player_id),
(500. / delta.length()) as i32,
delta.normalized(),
);
});
}
}
}
true
}
fn get_type(&self) -> WeaponType {
WeaponType::Shotgun
}
}
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=Node3D, init)] #[class(base=Node3D, init)]
pub struct Beam { pub struct Beam {
@ -183,7 +253,7 @@ pub struct BallWeapon {
#[godot_dyn] #[godot_dyn]
impl Weapon for BallWeapon { impl Weapon for BallWeapon {
fn shoot(&mut self, _: Rid, _: u16, _: Vector3, _: Vector3, _: bool) -> bool { fn shoot(&mut self, _: Rid, _: u16, _: Vector3, _: Vector3, _: Vector3, _: bool) -> bool {
true true
} }