Compare commits

..

4 Commits

Author SHA1 Message Date
64d8c148ce Clean imports 2026-08-07 23:55:42 +03:00
c8172f736f Update blood amount 2026-08-07 23:55:16 +03:00
5ccfc651a1 Add blood decals for clients as well 2026-08-07 23:55:16 +03:00
3ef771d0b6 Add some blood effects 2026-08-07 23:55:16 +03:00
16 changed files with 201 additions and 24 deletions

View File

@ -158,3 +158,9 @@ name = "Fantozzi"
[[people.credits]]
credit = "Fantozzi's Footsteps (Grass/Sand & Stone)"
link = "https://opengameart.org/content/fantozzis-footsteps-grasssand-stone"
[[people]]
name = "TobiasM"
[[people.credits]]
credit = "Blood splat"
link = "https://opengameart.org/content/blood-splat"

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cnahou6bg32pu"
path="res://.godot/imported/blood_splat.png-2bef76897d4f45e457d6e9899a1358ed.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://raw_assets/blood_splat.png"
dest_files=["res://.godot/imported/blood_splat.png-2bef76897d4f45e457d6e9899a1358ed.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,9 @@
[gd_scene format=3 uid="uid://c1sc6yl56mi0s"]
[ext_resource type="Texture2D" uid="uid://cnahou6bg32pu" path="res://raw_assets/blood_splat.png" id="1_1rw23"]
[node name="bullet_hole_decal" type="Node3D" unique_id=18163478]
[node name="decal" type="Decal" parent="." unique_id=1515351623]
size = Vector3(1, 1, 1)
texture_albedo = ExtResource("1_1rw23")

View File

@ -11,6 +11,7 @@
[ext_resource type="AudioStream" uid="uid://dpafq3gxer7qf" path="res://raw_assets/sfx/footsteps_congusbongus/tile/0.ogg" id="8_7a25g"]
[ext_resource type="AudioStream" uid="uid://fn5olcnrb1jn" path="res://raw_assets/sfx/footsteps_congusbongus/water/2.ogg" id="9_k2vl2"]
[ext_resource type="AudioStream" uid="uid://br64dea537kkw" path="res://raw_assets/sfx/footsteps_congusbongus/wood/0.ogg" id="10_x30cj"]
[ext_resource type="PackedScene" uid="uid://c1sc6yl56mi0s" path="res://scenes/player/blood_decal.tscn" id="11_otyku"]
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_1cgct"]
particle_flag_align_y = true
@ -57,6 +58,7 @@ footstep_sounds = Dictionary[String, AudioStream]({
"Wood": ExtResource("10_x30cj")
})
walk_sfx_cd = 0.5
blood_decal = ExtResource("11_otyku")
[node name="splorch" type="GPUParticles3D" parent="." unique_id=986677517]
emitting = false

View File

@ -31,6 +31,7 @@ use crate::{
buffs::{Buff, BuffKind},
local_player::LocalPlayer,
remote_player::RemotePlayer,
shootable::Shootable,
telegrenade::Telegrenade,
weapon::WeaponType,
},
@ -1087,6 +1088,25 @@ impl Game {
}
}
// Handled whenever a player is shot, run on client-side only
pub fn handle_on_shot(
&mut self,
player_id: u16,
source: DamageSource,
damage: i32,
delta: NetVector3,
) {
if let Some(player) = self.find_player_mut(player_id)
&& let Some(character) = &mut player.character
{
if let Ok(mut shootable) = character.clone().into_gd().try_dynify::<dyn Shootable>() {
shootable
.dyn_bind_mut()
.on_shoot_visual(source, damage, delta.into());
}
}
}
// Handle shooting, either from the server of from a client
pub fn handle_shoot(
&mut self,

View File

@ -181,6 +181,8 @@ impl Shootable for ShootableTrigger {
self.signals().on_shot().emit(id, damage, dir);
}
}
fn on_shoot_visual(&mut self, _: DamageSource, _: i32, _: Vector3) {}
}
#[derive(GodotClass)]

View File

@ -357,6 +357,7 @@ pub enum Package {
Jump(u16),
Shoot(u16, NetVector3, NetTransform, f32),
Punch(u16, NetVector3, NetTransform, f32),
OnShot(u16, DamageSource, i32, NetVector3),
TakeDamage(DamageSource, u16, i32),
Kill(DamageSource, u16),
SpawnBall(NetTransform, NetVector3, Option<u16>),

View File

@ -262,6 +262,12 @@ impl NetworkManager {
game.bind_mut().on_trigger_deactivate(id);
}
}
Package::OnShot(player_id, source, damage, delta) => {
if let Some(game) = &mut Game::singleton() {
game.bind_mut()
.handle_on_shot(player_id, source, damage, delta);
}
}
_ => {}
},
},

View File

@ -1,7 +1,4 @@
use godot::{
classes::{PhysicsRayQueryParameters3D, World3D},
prelude::*,
};
use godot::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]

View File

@ -94,10 +94,12 @@ impl Ball {
#[godot_dyn]
impl Shootable for Ball {
fn on_shoot(&mut self, _: DamageSource, damage: i32, dir: Vector3) {
fn on_shoot(&mut self, _: DamageSource, damage: i32, delta: Vector3) {
let prev_velocity = self.base().get_linear_velocity();
let force = damage as f32 / 10.;
self.base_mut()
.set_linear_velocity(prev_velocity + dir * force);
.set_linear_velocity(prev_velocity + delta.normalized() * force);
}
fn on_shoot_visual(&mut self, _: DamageSource, _: i32, _: Vector3) {}
}

View File

@ -1,10 +1,12 @@
use std::collections::HashMap;
use std::f32::consts::PI;
use godot::{
classes::{AudioStream, AudioStreamPlayer3D, GpuParticles3D},
classes::{AudioStream, AudioStreamPlayer3D, GpuParticles3D, RandomNumberGenerator},
prelude::*,
};
use crate::{game::Game, util::shotgun_ray};
#[derive(Debug, Clone, Copy, GodotConvert, Export, Var, Default, PartialEq, PartialOrd, Eq)]
#[godot(via = GString)]
pub enum FootstepKind {
@ -36,6 +38,10 @@ pub struct PlayerEffects {
footstep_sounds: Dictionary<FootstepKind, Option<Gd<AudioStream>>>,
#[export]
walk_sfx_cd: f64,
#[export]
blood_decal: Option<Gd<PackedScene>>,
rng: Gd<RandomNumberGenerator>,
walk_sfx_cd_remaining: f64,
@ -93,4 +99,44 @@ impl PlayerEffects {
sfx.set_stream(&sound);
}
}
pub fn shoot_blood(&mut self, delta: Vector3, force: f32) {
self.run_deferred(move |s| {
if let Some(mut game) = Game::singleton()
&& let Some(map) = &mut game.bind_mut().current_map
&& let Some(prefab) = &s.blood_decal
{
let results = shotgun_ray(
s.base().get_global_position(),
s.base().get_global_position() + delta,
s.base().get_basis().col_c(),
&s.base().get_world_3d(),
&Array::new(),
10,
// Calculate the particular spread of blood
(((PI / 4.) * force) / delta.length()).min(PI / 2.),
10. + delta.length() / 3.,
None,
);
let mut decal_results = Vec::new();
for _ in 0..(force * force * 3.) as usize {
if let Some(result) = results
.get(s.rng.randi_range(0, results.len() as i32 - 1) as usize)
.cloned()
{
decal_results.push(result);
}
}
for result in decal_results {
let mut decal = prefab.instantiate_as::<Node3D>();
map.add_child(&decal);
decal.set_global_position(result.position);
let quat = Quaternion::from_rotation_arc(Vector3::UP, result.normal);
decal.set_global_rotation(quat.get_euler());
}
}
});
}
}

View File

@ -939,7 +939,28 @@ impl LocalPlayer {
#[godot_dyn]
impl Shootable for LocalPlayer {
fn on_shoot(&mut self, source: DamageSource, damage: i32, _: Vector3) {
fn on_shoot(&mut self, source: DamageSource, damage: i32, delta: Vector3) {
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer
&& let PeerKind::Server(peer, _) = peer
{
peer.broadcast_reliable(Package::OnShot(
self.player_id.unwrap_or(0),
source,
damage,
delta.into(),
));
self.on_shoot_visual(source, damage, delta);
}
self.take_damage(source, damage, true);
}
fn on_shoot_visual(&mut self, source: DamageSource, damage: i32, delta: Vector3) {
self.run_deferred(move |s| {
if s.can_take_damage(&source)
&& let Some(fx) = &mut s.effects
{
fx.bind_mut().shoot_blood(delta, damage as f32 / 100.);
}
});
}
}

View File

@ -10,6 +10,7 @@ use godot::{
use crate::{
game::{Game, opts::GameOption},
map::{killbox::KillboxKind, pickup::PickupKind},
net::network_manager::{NetworkManager, Package, PeerKind},
player::{
DamageSource, DamageSourceSignal, IPlayer, NetTransform, PlayerCommon,
buffs::{Buff, Buffs},
@ -501,7 +502,28 @@ impl RemotePlayer {
#[godot_dyn]
impl Shootable for RemotePlayer {
fn on_shoot(&mut self, source: DamageSource, damage: i32, _: Vector3) {
fn on_shoot(&mut self, source: DamageSource, damage: i32, delta: Vector3) {
if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer
&& let PeerKind::Server(peer, _) = peer
{
peer.broadcast_reliable(Package::OnShot(
self.player_id.unwrap_or(0),
source,
damage,
delta.into(),
));
self.on_shoot_visual(source, damage, delta);
}
self.take_damage(source, damage, true);
}
fn on_shoot_visual(&mut self, source: DamageSource, damage: i32, delta: Vector3) {
self.run_deferred(move |s| {
if s.can_take_damage(&source)
&& let Some(fx) = &mut s.effects
{
fx.bind_mut().shoot_blood(delta, damage as f32 / 100.);
}
});
}
}

View File

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

View File

@ -1,4 +1,4 @@
use std::{collections::HashSet, f32::consts::PI};
use std::{collections::HashMap, f32::consts::PI};
use godot::{
classes::{AudioStreamPlayer3D, GpuParticles3D, RandomNumberGenerator},
@ -104,11 +104,13 @@ impl Weapon for Raygun {
});
}
let dir = (to - source.get_global_position()).normalized();
let beam_dir = (to - source.get_global_position()).normalized();
let beam_end = target
.as_ref()
.map(|r| r.position)
.unwrap_or(source.get_global_position() + dir * 100.);
.unwrap_or(source.get_global_position() + beam_dir * 100.);
let delta = beam_end - source.get_global_position();
godot_print!("{}", delta);
if let Some(prefab) = &self.bullet_prefab
&& play_fx
@ -138,7 +140,7 @@ impl Weapon for Raygun {
shootable.dyn_bind_mut().on_shoot(
DamageSource::Player(player_id),
100,
dir.normalized(),
delta,
);
});
}
@ -222,7 +224,7 @@ impl Weapon for Shotgun {
vfx.set_emitting(true);
}
let mut colliders = HashSet::new();
let mut colliders: HashMap<Gd<Object>, Vec<Vector3>> = HashMap::new();
let results = shotgun_ray(
from,
to,
@ -253,17 +255,17 @@ impl Weapon for Shotgun {
}
for result in &results {
colliders.insert(result.collider.clone());
if let Some(positions) = colliders.get_mut(&result.collider) {
positions.push(result.position)
} else {
colliders.insert(result.collider.clone(), vec![result.position]);
}
}
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;
for (collider, positions) in colliders {
let position = positions.iter().sum::<Vector3>() / positions.len() as f32;
let delta = position - from;
if let Ok(mut shootable) = collider.clone().try_dynify::<dyn Shootable>() {
self.run_deferred(move |_| {