feat: implement procedural humanoid animations, modular weapon system, and dummy entity base classes

This commit is contained in:
DottsGit
2026-06-06 19:10:29 -04:00
parent fcdd75afc0
commit ff3e01a9e5
12 changed files with 400 additions and 52 deletions
+5
View File
@@ -161,7 +161,12 @@ func _shoot_hitscan() -> void:
hits_remaining -= 1
current_damage_mult *= penetration_damage_penalty
final_target = result.position
_spawn_tracer(origin, final_target)
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
player.server_play_fire_effects.rpc_id(1, origin, final_target, name, true)
func _spawn_tracer(_origin: Vector3, final_target: Vector3) -> void:
# Spawn cosmetic tracer
var tracer = Node3D.new()
tracer.set_script(load("res://weapons/hitscan_tracer.gd"))
+3
View File
@@ -120,6 +120,9 @@ func _shoot_projectile() -> void:
var fire_dir = (forward + offset_dir).normalized()
_spawn_custom_projectile(origin, fire_dir)
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
player.server_play_fire_effects.rpc_id(1, origin, fire_dir, weapon_name, false)
func _spawn_custom_projectile(_origin: Vector3, _fire_dir: Vector3) -> void:
# Virtual method for custom projectiles (e.g., bouncing, homing)
+9 -3
View File
@@ -137,6 +137,8 @@ func _on_hit(result: Dictionary) -> void:
if not result.collider.has_method("take_damage"):
if result.collider is StaticBody3D or result.collider is CSGShape3D:
ImpactSpawner.spawn(get_tree(), "crater", result.position, result.normal, explosion_radius * 1.5)
elif not owner_player or owner_player.is_multiplayer_authority():
result.collider.take_damage(damage, result.position, owner_player)
_explode(result.position)
destroy()
@@ -198,10 +200,14 @@ func _explode(pos: Vector3) -> void:
if col == owner_player and not can_self_damage:
# Skip self damage, but we still applied the knockback!
pass
else:
col.take_damage(final_damage, target_pos, owner_player)
elif not owner_player or owner_player.is_multiplayer_authority():
col.take_damage(final_damage, target_pos, owner_player, dir * final_knockback)
# Visual Effect
# Broadcast Visual Explosion Effect to other peers
if owner_player and owner_player.has_method("server_play_explosion") and owner_player.multiplayer.has_multiplayer_peer() and owner_player.multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
owner_player.server_play_explosion.rpc_id(1, pos, explosion_radius)
# Local Visual Effect
var visual = MeshInstance3D.new()
var s_mesh = SphereMesh.new()
s_mesh.radius = explosion_radius
+2 -1
View File
@@ -58,7 +58,8 @@ func _on_hit(result: Dictionary) -> void:
final_damage = lerpf(damage, min_damage, falloff_factor)
if result.collider.has_method("take_damage"):
result.collider.take_damage(final_damage, result.position, owner_player)
if not owner_player or owner_player.is_multiplayer_authority():
result.collider.take_damage(final_damage, result.position, owner_player)
elif result.collider is StaticBody3D or result.collider is CSGShape3D:
var size = 0.4 if impact_type == "plasma" else 0.15
ImpactSpawner.spawn(get_tree(), impact_type, result.position, result.normal, size)
+3
View File
@@ -140,6 +140,9 @@ func _fire() -> void:
var fire_dir = (base_dir + right * x_dir + up * y_dir).normalized()
_spawn_custom_projectile(proj_origin, fire_dir)
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
player.server_play_fire_effects.rpc_id(1, proj_origin, fire_dir, weapon_name, false)
var _current_aim_dir: Vector3 = Vector3.ZERO
+36
View File
@@ -61,6 +61,15 @@ func _process(_delta: float) -> void:
vm_camera.fov = camera.fov
func _build_loadout() -> void:
var is_auth = false
if player:
is_auth = player.is_multiplayer_authority()
else:
is_auth = is_multiplayer_authority()
if not is_auth:
return # Let the player_movement_controller's remote _process call _build_remote_loadout
# Clear existing weapons
for w in weapons.values():
if is_instance_valid(w):
@@ -69,6 +78,16 @@ func _build_loadout() -> void:
var l = LoadoutManager.get_active_loadout()
if player:
player.synced_loadout_p1 = l["primary_1"]
player.synced_loadout_p2 = l["primary_2"]
player.synced_loadout_sp = l["special"]
if l.has("melee"):
player.synced_loadout_melee = l["melee"]
else:
player.synced_loadout_melee = ""
player.synced_loadout_ready = true
_spawn_weapon(1, l["primary_1"])
_spawn_weapon(2, l["primary_2"])
_spawn_weapon(3, l["special"])
@@ -77,6 +96,20 @@ func _build_loadout() -> void:
_equip_slot(1)
func _build_remote_loadout(p1: String, p2: String, sp: String, melee: String) -> void:
for w in weapons.values():
if is_instance_valid(w):
w.queue_free()
weapons.clear()
_spawn_weapon(1, p1)
_spawn_weapon(2, p2)
_spawn_weapon(3, sp)
if melee != "":
_spawn_weapon(4, melee)
_equip_slot(1)
func _spawn_weapon(slot: int, weapon_id: String) -> void:
if weapon_id == "" or weapon_id == "none" or not LoadoutManager.weapon_db.has(weapon_id):
return
@@ -184,6 +217,9 @@ func _equip_slot(slot: int) -> void:
var humanoid = player.get_node("HumanoidModel")
if humanoid.has_method("set_weapon") and w.has_meta("script_path"):
humanoid.set_weapon(w.get_meta("script_path"))
# Update the synced variable so remote peers pick it up
if "synced_weapon_path" in player:
player.synced_weapon_path = w.get_meta("script_path")
func _input(event: InputEvent) -> void:
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED: