diff --git a/godot/assets/HealthPickup.fbx b/godot/assets/HealthPickup.fbx new file mode 100644 index 0000000..b418a73 Binary files /dev/null and b/godot/assets/HealthPickup.fbx differ diff --git a/godot/assets/HealthPickup.fbx.import b/godot/assets/HealthPickup.fbx.import new file mode 100644 index 0000000..fafd288 --- /dev/null +++ b/godot/assets/HealthPickup.fbx.import @@ -0,0 +1,46 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dgav57iyh6nxg" +path="res://.godot/imported/HealthPickup.fbx-608a017b8743e952888462d93dec636c.scn" + +[deps] + +source_file="res://assets/HealthPickup.fbx" +dest_files=["res://.godot/imported/HealthPickup.fbx-608a017b8743e952888462d93dec636c.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=true +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +fbx/importer=0 +fbx/allow_geometry_helper_nodes=false +fbx/embedded_image_handling=1 +fbx/naming_version=2 diff --git a/godot/maps/default_map/map0.tscn b/godot/maps/default_map/map0.tscn index 2ae7f2b..258b087 100644 --- a/godot/maps/default_map/map0.tscn +++ b/godot/maps/default_map/map0.tscn @@ -1,11 +1,7 @@ [gd_scene format=3 uid="uid://7iooamjot1tg"] -[ext_resource type="PackedScene" uid="uid://ct70bpbca8one" path="res://scenes/player/local_player.tscn" id="1_yjvui"] -[ext_resource type="PackedScene" uid="uid://kfxc0migw80o" path="res://scenes/player/remote_player.tscn" id="2_kcncr"] -[ext_resource type="PackedScene" uid="uid://hmh2lrrlo2wy" path="res://scenes/player/replay_player.tscn" id="3_8af6u"] -[ext_resource type="PackedScene" uid="uid://dfnbc8sbufa4j" path="res://scenes/misc/ball.tscn" id="3_e0nj3"] +[ext_resource type="PackedScene" uid="uid://dov2eh5u4ypnx" path="res://scenes/misc/health_pickup.tscn" id="2_yffox"] [ext_resource type="PackedScene" uid="uid://c1r8nk58cci3a" path="res://scenes/misc/goal.tscn" id="4_w7til"] -[ext_resource type="PackedScene" uid="uid://dc17w5n6y1an" path="res://scenes/misc/replay_ball.tscn" id="5_oqkce"] [sub_resource type="ImageTexture" id="ImageTexture_3vyb7"] @@ -15,32 +11,20 @@ albedo_texture = SubResource("ImageTexture_3vyb7") [sub_resource type="PlaneMesh" id="PlaneMesh_1cgct"] size = Vector2(100, 100) -[sub_resource type="BoxShape3D" id="BoxShape3D_1cgct"] -size = Vector3(100, 0.01, 100) - [sub_resource type="BoxShape3D" id="BoxShape3D_3g557"] size = Vector3(1000, 1, 1000) +[sub_resource type="BoxShape3D" id="BoxShape3D_1cgct"] +size = Vector3(100, 0.01, 100) + [node name="map" type="Map" unique_id=914577955 node_paths=PackedStringArray("spawners", "ball_spawner")] spawners = [NodePath("team1_spawner"), NodePath("team2_spawner")] ball_spawner = NodePath("ball_spawner") -local_player = ExtResource("1_yjvui") -remote_player = ExtResource("2_kcncr") -replay_player = ExtResource("3_8af6u") -ball = ExtResource("3_e0nj3") -replay_ball = ExtResource("5_oqkce") [node name="terrain" type="MeshInstance3D" parent="." unique_id=1474127252] material_override = SubResource("StandardMaterial3D_u8vuu") mesh = SubResource("PlaneMesh_1cgct") -[node name="static_body_3d" type="StaticBody3D" parent="terrain" unique_id=203785193] -collision_layer = 7 -collision_mask = 7 - -[node name="collision_shape_3d" type="CollisionShape3D" parent="terrain/static_body_3d" unique_id=1466526153] -shape = SubResource("BoxShape3D_1cgct") - [node name="directional_light_3d" type="DirectionalLight3D" parent="." unique_id=793524416] transform = Transform3D(1, 0, 0, 0, 0.7274598, 0.6861503, 0, -0.6861503, 0.7274598, -5.1267624, 3.3997262, 3.715582) @@ -67,3 +51,13 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12.468555, 0.0050001144, 4.4 [node name="goal2" parent="." unique_id=1997661586 instance=ExtResource("4_w7til")] team = 1 transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8.417626, 0.0050001144, -3.8839006) + +[node name="static_body_3d" type="StaticBody3D" parent="." unique_id=203785193] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.526536, 0, 10.695821) +collision_layer = 7 +collision_mask = 7 + +[node name="collision_shape_3d" type="CollisionShape3D" parent="static_body_3d" unique_id=1466526153] +shape = SubResource("BoxShape3D_1cgct") + +[node name="health_pickup" parent="static_body_3d/collision_shape_3d" unique_id=1860192333 instance=ExtResource("2_yffox")] diff --git a/godot/mesh/health_pickup.tscn b/godot/mesh/health_pickup.tscn new file mode 100644 index 0000000..e3c9b57 --- /dev/null +++ b/godot/mesh/health_pickup.tscn @@ -0,0 +1,14 @@ +[gd_scene format=3 uid="uid://d0hq2ctndreej"] + +[ext_resource type="PackedScene" uid="uid://dgav57iyh6nxg" path="res://assets/HealthPickup.fbx" id="1_r21p4"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_tue51"] +albedo_color = Color(0, 1, 0, 1) +emission_enabled = true +emission = Color(0, 1, 0, 1) +emission_energy_multiplier = 5.0 + +[node name="HealthPickup" unique_id=663926357 instance=ExtResource("1_r21p4")] + +[node name="Cube" parent="." index="0" unique_id=659090742] +surface_material_override/0 = SubResource("StandardMaterial3D_tue51") diff --git a/godot/raw_assets/Soldier/HealthPickup.blend b/godot/raw_assets/Soldier/HealthPickup.blend new file mode 100644 index 0000000..d8aaf8c Binary files /dev/null and b/godot/raw_assets/Soldier/HealthPickup.blend differ diff --git a/godot/raw_assets/Soldier/HealthPickup.blend1 b/godot/raw_assets/Soldier/HealthPickup.blend1 new file mode 100644 index 0000000..24d291f Binary files /dev/null and b/godot/raw_assets/Soldier/HealthPickup.blend1 differ diff --git a/godot/scenes/misc/health_pickup.tscn b/godot/scenes/misc/health_pickup.tscn new file mode 100644 index 0000000..780d7d3 --- /dev/null +++ b/godot/scenes/misc/health_pickup.tscn @@ -0,0 +1,19 @@ +[gd_scene format=3 uid="uid://dov2eh5u4ypnx"] + +[ext_resource type="PackedScene" uid="uid://d0hq2ctndreej" path="res://mesh/health_pickup.tscn" id="1_1mios"] + +[sub_resource type="SphereShape3D" id="SphereShape3D_1cgct"] +radius = 0.8577998 + +[node name="health_pickup" type="HealthPickup" unique_id=1860192333 node_paths=PackedStringArray("pickup_area")] +pickup_area = NodePath("area_3d") + +[node name="area_3d" type="Area3D" parent="." unique_id=168425054] +transform = Transform3D(0.5306476, 0, -0.8475926, 0, 1, 0, 0.8475926, 0, 0.5306476, 0, 1.2881019, 0) +collision_layer = 0 +collision_mask = 3 + +[node name="HealthPickup" parent="area_3d" unique_id=663926357 instance=ExtResource("1_1mios")] + +[node name="collision_shape_3d" type="CollisionShape3D" parent="area_3d" unique_id=1572818215] +shape = SubResource("SphereShape3D_1cgct") diff --git a/rust/src/game_manager.rs b/rust/src/game_manager.rs index 9bcd253..3a3d6f7 100644 --- a/rust/src/game_manager.rs +++ b/rust/src/game_manager.rs @@ -19,6 +19,7 @@ use crate::{ }, util::NetVector3, }, + pickup::{Pickup, PickupCommon, PickupKind}, player::{ DamageSource, IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer, weapon::WeaponType, @@ -366,6 +367,8 @@ impl Game { pub fn on_game_finished(); #[signal] pub fn on_player_killed(player: u16); + #[signal] + pub fn on_map_loaded(map: Gd); pub fn singleton() -> Option> { if GameManager::singleton().get_children().len() > 0 { @@ -693,7 +696,7 @@ impl Game { } pub fn on_map_loaded(&mut self, map: Gd) { - self.current_map = Some(map); + self.current_map = Some(map.clone()); if let Some(peer) = &mut NetworkManager::singleton().bind_mut().peer { if let PeerKind::Client(peer, to) = peer { peer.send_reliable(to, Package::MapLoaded); @@ -706,6 +709,7 @@ impl Game { }); } } + self.signals().on_map_loaded().emit(&map); } pub fn spawn_ball( diff --git a/rust/src/lib.rs b/rust/src/lib.rs index b3c8a24..c96d962 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -11,6 +11,7 @@ pub mod killbox; pub mod map; pub mod map_resource; pub mod net; +pub mod pickup; pub mod player; pub mod popup_queue; pub mod replay; diff --git a/rust/src/map.rs b/rust/src/map.rs index ceac27a..76a0b1e 100644 --- a/rust/src/map.rs +++ b/rust/src/map.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; + use godot::{classes::CharacterBody3D, prelude::*}; use crate::{ game_manager::{Game, GameManager}, net::util::NetVector3, + pickup::Pickup, player::{ IPlayer, NetTransform, ball::Ball, local_player::LocalPlayer, remote_player::RemotePlayer, }, @@ -17,6 +20,9 @@ pub struct Map { #[export] pub ball_spawner: Option>, + pickup_counter: u8, + pub pickups: HashMap>, + base: Base, } @@ -188,4 +194,13 @@ impl Map { None } } + + pub fn register_pickup(&mut self, pickup: DynGd) -> u8 { + godot_print!("Registered {:?}", pickup); + + let id = self.pickup_counter; + self.pickup_counter += 1; + self.pickups.insert(id, pickup); + id + } } diff --git a/rust/src/net/network_manager.rs b/rust/src/net/network_manager.rs index d4f2f7c..74897b7 100644 --- a/rust/src/net/network_manager.rs +++ b/rust/src/net/network_manager.rs @@ -13,6 +13,7 @@ use crate::{ game_manager::{Game, GameManager, GameOption, GameOptionValue, NetPlayer}, game_settings::GameSettingsManager, net::{net_stats::Stats, util::NetVector3}, + pickup::PickupKind, player::{DamageSource, NetTransform, weapon::WeaponType}, popup_queue::{Popup, PopupQueue}, ui::cli::{CliColor, CommandLinePanel}, diff --git a/rust/src/pickup.rs b/rust/src/pickup.rs new file mode 100644 index 0000000..b1dcbb0 --- /dev/null +++ b/rust/src/pickup.rs @@ -0,0 +1,101 @@ +use godot::{classes::Area3D, prelude::*}; +use serde::{Deserialize, Serialize}; + +use crate::{ + game_manager::Game, + map::Map, + net::network_manager::NetworkManager, + player::{IPlayer, local_player::LocalPlayer}, + replay::StandaloneReplayManager, +}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum PickupKind { + Health, +} + +#[derive(GodotClass)] +#[class(base=Node3D, init, tool)] +pub struct HealthPickup { + #[export] + pub pickup_area: Option>, + + time_elapsed: f64, + pickup_id: Option, + + base: Base, +} + +#[godot_api] +impl INode3D for HealthPickup { + fn ready(&mut self) { + if let Some(area) = self.pickup_area.clone() { + area.signals() + .body_entered() + .connect_other(self, Self::on_collision); + } + + if let Some(game) = &Game::singleton() { + game.signals() + .on_map_loaded() + .connect_other(self, Self::register_pickup); + } else { + StandaloneReplayManager::singleton() + .signals() + .on_map_loaded() + .connect_other(self, Self::register_pickup); + } + } + + fn process(&mut self, delta: f64) { + self.time_elapsed += delta; + self.update_common(self.time_elapsed); + } +} + +impl HealthPickup { + pub fn register_pickup(&mut self, mut map: Gd) { + self.run_deferred(move |s| { + let id = map + .bind_mut() + .register_pickup(s.to_gd().into_dyn().upcast()); + s.pickup_id = Some(id); + }); + } +} + +#[godot_dyn] +impl Pickup for HealthPickup { + fn area(&mut self) -> &mut Option> { + &mut self.pickup_area + } + + fn kind(&self) -> PickupKind { + PickupKind::Health + } +} + +pub trait Pickup { + fn area(&mut self) -> &mut Option>; + fn kind(&self) -> PickupKind; +} + +pub trait PickupCommon { + fn update_common(&mut self, time_elapsed: f64); + fn on_collision(&mut self, collider: Gd); +} + +impl PickupCommon for T { + fn update_common(&mut self, time_elapsed: f64) { + if let Some(area) = self.area() { + area.set_position(Vector3::UP * 1.5 + Vector3::UP * (time_elapsed.sin() / 4.) as f32); + area.set_rotation(Vector3::UP * time_elapsed as f32); + } + } + + fn on_collision(&mut self, collider: Gd) { + if !NetworkManager::singleton().bind().is_host() { + return; + } + } +} diff --git a/rust/src/player/local_player.rs b/rust/src/player/local_player.rs index d5e986f..212ec41 100644 --- a/rust/src/player/local_player.rs +++ b/rust/src/player/local_player.rs @@ -21,6 +21,7 @@ use crate::{ }, util::cast_ray, }, + pickup::PickupKind, player::{ DamageSource, DamageSourceSignal, IPlayer, NetTransform, PlayerCommon, effects::PlayerEffects, @@ -298,6 +299,21 @@ impl IPlayer for LocalPlayer { fn get_health(&self) -> i32 { self.health } + + fn heal(&mut self, amount: i32) { + self.run_deferred(move |s| { + let delta = (s.health + amount).min(200) - s.health; + s.run_deferred(move |s| s.signals().on_heal().emit(delta)); + s.health += delta; + }); + } + + fn on_pickup(&mut self, kind: PickupKind) { + godot_print!("picked up"); + match kind { + PickupKind::Health => self.heal(100), + } + } } #[godot_api] @@ -592,6 +608,8 @@ impl LocalPlayer { killbox_kind: KillboxKind, damage: i32, ); + #[signal] + pub fn on_heal(amount: i32); fn set_lock(&mut self, locked: bool) { self.locked = locked; diff --git a/rust/src/player/mod.rs b/rust/src/player/mod.rs index 95c2bb0..f20b7a5 100644 --- a/rust/src/player/mod.rs +++ b/rust/src/player/mod.rs @@ -10,6 +10,7 @@ use crate::{ network_manager::NetworkManager, util::{NetVector3, shotgun_ray}, }, + pickup::PickupKind, player::{ shootable::Shootable, weapon::{BallWeapon, Weapon, WeaponType}, @@ -58,6 +59,8 @@ pub trait IPlayer { fn get_camera_origin(&self) -> Option>; fn is_replay_mode(&self) -> bool; fn get_health(&self) -> i32; + fn heal(&mut self, amount: i32); + fn on_pickup(&mut self, kind: PickupKind); } pub trait PlayerCommon { diff --git a/rust/src/player/remote_player.rs b/rust/src/player/remote_player.rs index 79ba337..2d423c4 100644 --- a/rust/src/player/remote_player.rs +++ b/rust/src/player/remote_player.rs @@ -10,6 +10,7 @@ use godot::{ use crate::{ game_manager::Game, killbox::KillboxKind, + pickup::PickupKind, player::{ DamageSource, DamageSourceSignal, IPlayer, NetTransform, PlayerCommon, effects::PlayerEffects, @@ -234,6 +235,20 @@ impl IPlayer for RemotePlayer { fn get_health(&self) -> i32 { self.health } + + fn heal(&mut self, amount: i32) { + self.run_deferred(move |s| { + let delta = (s.health + amount).min(200) - s.health; + s.run_deferred(move |s| s.signals().on_heal().emit(delta)); + s.health += delta; + }); + } + + fn on_pickup(&mut self, kind: PickupKind) { + match kind { + PickupKind::Health => self.heal(100), + } + } } #[godot_api] @@ -292,6 +307,8 @@ impl RemotePlayer { killbox_kind: KillboxKind, damage: i32, ); + #[signal] + pub fn on_heal(amount: i32); } #[godot_dyn] diff --git a/rust/src/replay.rs b/rust/src/replay.rs index 4bbf342..ad05c27 100644 --- a/rust/src/replay.rs +++ b/rust/src/replay.rs @@ -43,7 +43,11 @@ pub struct StandaloneReplayManager { base: Base, } +#[godot_api] impl StandaloneReplayManager { + #[signal] + pub fn on_map_loaded(map: Gd); + pub fn singleton() -> Gd { get_autoload_by_name::(STANDALONE_REPLAY_GLOBAL) } @@ -85,7 +89,7 @@ impl StandaloneReplayManager { .connect_other(self, Self::on_done); playback.bind_mut().replay = replay; - playback.bind_mut().current_map = Some(map); + playback.bind_mut().current_map = Some(map.clone()); playback.bind_mut().current_time = 0.; playback.bind_mut().fast_forwarding = false; playback.bind_mut().spectated_player_id = 0; @@ -109,6 +113,7 @@ impl StandaloneReplayManager { } } } + self.signals().on_map_loaded().emit(&map); } pub fn on_done(&mut self) {