feat: implement procedural humanoid animations, modular weapon system, and dummy entity base classes
This commit is contained in:
@@ -36,6 +36,8 @@ var movement_speed: float = 0.0
|
|||||||
var _anim_time: float = 0.0
|
var _anim_time: float = 0.0
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
rotation_degrees.y = 180
|
||||||
|
|
||||||
var mat := StandardMaterial3D.new()
|
var mat := StandardMaterial3D.new()
|
||||||
mat.albedo_color = color
|
mat.albedo_color = color
|
||||||
mat.roughness = 0.8
|
mat.roughness = 0.8
|
||||||
@@ -197,7 +199,6 @@ func set_weapon(script_path: String) -> void:
|
|||||||
_set_shadows_recursive(w)
|
_set_shadows_recursive(w)
|
||||||
|
|
||||||
# Force position after the weapon's _build_model() sets it for 1st person
|
# Force position after the weapon's _build_model() sets it for 1st person
|
||||||
# root_pivot is rotated 180 deg around Y, so local +Z is FORWARD and local -X is RIGHT
|
|
||||||
w.position = Vector3(-0.15, 1.0, 0.4)
|
w.position = Vector3(-0.15, 1.0, 0.4)
|
||||||
w.rotation_degrees = Vector3(0, 180, 0)
|
w.rotation_degrees = Vector3(0, 180, 0)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -301,6 +301,15 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
rep_config.add_property(":position")
|
rep_config.add_property(":position")
|
||||||
rep_config.add_property(":rotation")
|
rep_config.add_property(":rotation")
|
||||||
rep_config.add_property("HeadPivot:rotation")
|
rep_config.add_property("HeadPivot:rotation")
|
||||||
|
rep_config.add_property(":synced_movement_state")
|
||||||
|
rep_config.add_property(":synced_movement_speed")
|
||||||
|
rep_config.add_property(":synced_is_crouching")
|
||||||
|
rep_config.add_property(":synced_weapon_path")
|
||||||
|
rep_config.add_property(":synced_loadout_p1")
|
||||||
|
rep_config.add_property(":synced_loadout_p2")
|
||||||
|
rep_config.add_property(":synced_loadout_sp")
|
||||||
|
rep_config.add_property(":synced_loadout_melee")
|
||||||
|
rep_config.add_property(":synced_loadout_ready")
|
||||||
sync.replication_config = rep_config
|
sync.replication_config = rep_config
|
||||||
player.add_child(sync)
|
player.add_child(sync)
|
||||||
|
|
||||||
@@ -395,9 +404,10 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
player.set_process(true)
|
player.set_process(true)
|
||||||
sm.set_physics_process(true)
|
sm.set_physics_process(true)
|
||||||
else:
|
else:
|
||||||
# Disable processing/input for remote players
|
# Disable physics processing/input for remote players
|
||||||
|
# but keep _process enabled so synced state updates the HumanoidModel
|
||||||
player.set_physics_process(false)
|
player.set_physics_process(false)
|
||||||
player.set_process(false)
|
player.set_process(true)
|
||||||
player.set_process_input(false)
|
player.set_process_input(false)
|
||||||
sm.set_physics_process(false)
|
sm.set_physics_process(false)
|
||||||
sm.set_process(false)
|
sm.set_process(false)
|
||||||
@@ -410,6 +420,10 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
wman.set_process_input(false)
|
wman.set_process_input(false)
|
||||||
wman.set_process(false)
|
wman.set_process(false)
|
||||||
wman.set_physics_process(false)
|
wman.set_physics_process(false)
|
||||||
|
wman.visible = false
|
||||||
|
var cv = wman.get("canvas_layer")
|
||||||
|
if cv:
|
||||||
|
cv.visible = false
|
||||||
|
|
||||||
return player
|
return player
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func _ready() -> void:
|
|||||||
|
|
||||||
_update_label()
|
_update_label()
|
||||||
|
|
||||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> void:
|
func take_damage(amount: float, hit_position: Vector3, source: Node = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
||||||
if is_dead:
|
if is_dead:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func _ready() -> void:
|
|||||||
dps_label.modulate = Color(1, 0.8, 0)
|
dps_label.modulate = Color(1, 0.8, 0)
|
||||||
add_child(dps_label)
|
add_child(dps_label)
|
||||||
|
|
||||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> void:
|
func take_damage(amount: float, hit_position: Vector3, source: Node = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
||||||
var now = Time.get_ticks_msec() / 1000.0
|
var now = Time.get_ticks_msec() / 1000.0
|
||||||
damage_history.append({"time": now, "amount": amount})
|
damage_history.append({"time": now, "amount": amount})
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
var current_h_speed = Vector2(velocity.x, velocity.z).length()
|
var current_h_speed = Vector2(velocity.x, velocity.z).length()
|
||||||
visual_node.update_state("ground", current_h_speed)
|
visual_node.update_state("ground", current_h_speed)
|
||||||
|
|
||||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> void:
|
func take_damage(amount: float, hit_position: Vector3, source: Node = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
||||||
if is_dead:
|
if is_dead:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,19 @@ var _hit_marker_tween: Tween
|
|||||||
# Grapple
|
# Grapple
|
||||||
var grapple_rope: MeshInstance3D
|
var grapple_rope: MeshInstance3D
|
||||||
|
|
||||||
|
# ── Multiplayer Sync Variables ─────────────────────────────────────────────
|
||||||
|
# These are written by the authority each frame and replicated to remote peers
|
||||||
|
# via the MultiplayerSynchronizer properties.
|
||||||
|
var synced_movement_state: String = "idle"
|
||||||
|
var synced_movement_speed: float = 0.0
|
||||||
|
var synced_is_crouching: bool = false
|
||||||
|
var synced_weapon_path: String = ""
|
||||||
|
|
||||||
|
var synced_loadout_p1: String = ""
|
||||||
|
var synced_loadout_p2: String = ""
|
||||||
|
var synced_loadout_sp: String = ""
|
||||||
|
var synced_loadout_melee: String = ""
|
||||||
|
var synced_loadout_ready: bool = false
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
var sm := _ensure_machine()
|
var sm := _ensure_machine()
|
||||||
@@ -61,16 +74,30 @@ func _ready() -> void:
|
|||||||
if head_pivot:
|
if head_pivot:
|
||||||
camera = head_pivot.get_node_or_null("Camera3D")
|
camera = head_pivot.get_node_or_null("Camera3D")
|
||||||
|
|
||||||
_damage_layer = CanvasLayer.new()
|
|
||||||
add_child(_damage_layer)
|
|
||||||
|
|
||||||
_setup_audio()
|
_setup_audio()
|
||||||
_setup_hit_marker()
|
|
||||||
_setup_grapple()
|
_setup_grapple()
|
||||||
_setup_hud()
|
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
_damage_layer = CanvasLayer.new()
|
||||||
|
add_child(_damage_layer)
|
||||||
|
_setup_hit_marker()
|
||||||
|
_setup_hud()
|
||||||
|
else:
|
||||||
|
call_deferred("_hide_remote_weapons")
|
||||||
|
|
||||||
set_process(true)
|
set_process(true)
|
||||||
set_physics_process(true)
|
if is_multiplayer_authority():
|
||||||
|
set_physics_process(true)
|
||||||
|
|
||||||
|
func _hide_remote_weapons() -> void:
|
||||||
|
if camera:
|
||||||
|
var wman = camera.get_node_or_null("WeaponManager")
|
||||||
|
if wman:
|
||||||
|
wman.visible = false
|
||||||
|
wman.process_mode = Node.PROCESS_MODE_DISABLED
|
||||||
|
var cv = wman.get("canvas_layer")
|
||||||
|
if cv:
|
||||||
|
cv.visible = false
|
||||||
|
|
||||||
func _setup_audio() -> void:
|
func _setup_audio() -> void:
|
||||||
footstep_player = AudioStreamPlayer.new()
|
footstep_player = AudioStreamPlayer.new()
|
||||||
@@ -219,16 +246,9 @@ func apply_impulse(force: Vector3) -> void:
|
|||||||
global_position.y += 0.1
|
global_position.y += 0.1
|
||||||
|
|
||||||
func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
||||||
if is_dead:
|
|
||||||
if is_instance_valid(ragdoll_instance):
|
|
||||||
ragdoll_instance.apply_initial_velocities(Vector3.ZERO, impulse)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Show damage number for self-damage
|
|
||||||
spawn_damage_number(amount, _hit_pos)
|
|
||||||
|
|
||||||
# Handle damage tracking for assists
|
|
||||||
var attacker_id = 0
|
var attacker_id = 0
|
||||||
|
var weapon_name = "Killed"
|
||||||
|
|
||||||
if _source:
|
if _source:
|
||||||
if _source.has_method("get_multiplayer_authority"):
|
if _source.has_method("get_multiplayer_authority"):
|
||||||
attacker_id = _source.get_multiplayer_authority()
|
attacker_id = _source.get_multiplayer_authority()
|
||||||
@@ -237,6 +257,198 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
|||||||
elif _source is CharacterBody3D:
|
elif _source is CharacterBody3D:
|
||||||
attacker_id = int(str(_source.name))
|
attacker_id = int(str(_source.name))
|
||||||
|
|
||||||
|
if _source.has_method("get_weapon_name"):
|
||||||
|
weapon_name = _source.get_weapon_name()
|
||||||
|
|
||||||
|
# If this is singleplayer offline, just call it directly
|
||||||
|
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||||
|
rpc_take_damage(amount, _hit_pos, attacker_id, weapon_name, impulse)
|
||||||
|
else:
|
||||||
|
# Tell the server to handle the damage and broadcast it to everyone!
|
||||||
|
server_take_damage.rpc_id(1, amount, _hit_pos, attacker_id, weapon_name, impulse)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "unreliable")
|
||||||
|
func server_play_fire_effects(origin: Vector3, target_or_dir: Vector3, weapon_name: String, is_hitscan: bool) -> void:
|
||||||
|
if not multiplayer.is_server(): return
|
||||||
|
rpc_play_fire_effects.rpc(origin, target_or_dir, weapon_name, is_hitscan)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "unreliable")
|
||||||
|
func rpc_play_fire_effects(origin: Vector3, target_or_dir: Vector3, _weapon_name: String, is_hitscan: bool) -> void:
|
||||||
|
# Local player already spawned effects locally with 0 latency
|
||||||
|
if is_multiplayer_authority(): return
|
||||||
|
|
||||||
|
if is_hitscan:
|
||||||
|
# Spawn a standalone tracer directly into the scene
|
||||||
|
_spawn_remote_tracer(origin, target_or_dir)
|
||||||
|
else:
|
||||||
|
# Spawn a visual-only projectile flying from origin along the direction
|
||||||
|
_spawn_remote_projectile(origin, target_or_dir, _weapon_name)
|
||||||
|
|
||||||
|
func _spawn_remote_tracer(origin: Vector3, target: Vector3) -> void:
|
||||||
|
var tracer = Node3D.new()
|
||||||
|
tracer.set_script(load("res://weapons/hitscan_tracer.gd"))
|
||||||
|
|
||||||
|
var t_mesh_inst = MeshInstance3D.new()
|
||||||
|
var t_mesh = CapsuleMesh.new()
|
||||||
|
t_mesh.radius = 0.02
|
||||||
|
t_mesh.height = 1.0
|
||||||
|
var t_mat = StandardMaterial3D.new()
|
||||||
|
t_mat.albedo_color = Color(1.0, 0.8, 0.2)
|
||||||
|
t_mat.emission_enabled = true
|
||||||
|
t_mat.emission = Color(2.0, 1.5, 0.5)
|
||||||
|
t_mesh.material = t_mat
|
||||||
|
|
||||||
|
t_mesh_inst.mesh = t_mesh
|
||||||
|
t_mesh_inst.rotation.x = deg_to_rad(90)
|
||||||
|
t_mesh_inst.position = Vector3(0, 0, -0.5)
|
||||||
|
tracer.add_child(t_mesh_inst)
|
||||||
|
|
||||||
|
tracer.target_position = target
|
||||||
|
|
||||||
|
if origin.distance_squared_to(target) > 0.1:
|
||||||
|
get_tree().current_scene.add_child(tracer)
|
||||||
|
tracer.global_position = origin
|
||||||
|
|
||||||
|
var up_vec = Vector3.UP
|
||||||
|
if abs((target - origin).normalized().dot(up_vec)) > 0.99:
|
||||||
|
up_vec = Vector3.RIGHT
|
||||||
|
tracer.look_at(target, up_vec)
|
||||||
|
|
||||||
|
func _spawn_remote_projectile(origin: Vector3, fire_dir: Vector3, weapon_name: String) -> void:
|
||||||
|
# Spawn a visual-only projectile (no damage, just the mesh flying)
|
||||||
|
var proj = Node3D.new()
|
||||||
|
proj.name = "RemoteProjectile"
|
||||||
|
|
||||||
|
if weapon_name == "Plasma Gun":
|
||||||
|
var mesh_instance = MeshInstance3D.new()
|
||||||
|
var p_mesh = SphereMesh.new()
|
||||||
|
p_mesh.radius = 0.08
|
||||||
|
p_mesh.height = 0.16
|
||||||
|
var mat = StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = Color(0.2, 0.6, 1.0)
|
||||||
|
mat.emission_enabled = true
|
||||||
|
mat.emission = Color(0.2, 0.6, 1.0)
|
||||||
|
mat.emission_energy_multiplier = 5.0
|
||||||
|
p_mesh.material = mat
|
||||||
|
mesh_instance.mesh = p_mesh
|
||||||
|
proj.add_child(mesh_instance)
|
||||||
|
|
||||||
|
var light = OmniLight3D.new()
|
||||||
|
light.light_color = Color(0.2, 0.6, 1.0)
|
||||||
|
light.light_energy = 2.0
|
||||||
|
light.omni_range = 4.0
|
||||||
|
proj.add_child(light)
|
||||||
|
else:
|
||||||
|
# Rocket-shaped mesh (cone + cylinder)
|
||||||
|
var mesh_inst = MeshInstance3D.new()
|
||||||
|
var p_mesh = CylinderMesh.new()
|
||||||
|
p_mesh.top_radius = 0.0
|
||||||
|
p_mesh.bottom_radius = 0.05
|
||||||
|
p_mesh.height = 0.2
|
||||||
|
var mat = StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = Color(0.8, 0.2, 0.2)
|
||||||
|
mat.emission_enabled = true
|
||||||
|
mat.emission = Color(1.5, 0.3, 0.1)
|
||||||
|
p_mesh.material = mat
|
||||||
|
mesh_inst.mesh = p_mesh
|
||||||
|
mesh_inst.rotation.x = deg_to_rad(-90)
|
||||||
|
proj.add_child(mesh_inst)
|
||||||
|
|
||||||
|
# Smoke trail
|
||||||
|
var smoke = GPUParticles3D.new()
|
||||||
|
var smoke_mat = ParticleProcessMaterial.new()
|
||||||
|
smoke_mat.direction = Vector3(0, 0, 1)
|
||||||
|
smoke_mat.spread = 5.0
|
||||||
|
smoke_mat.initial_velocity_min = 0.0
|
||||||
|
smoke_mat.initial_velocity_max = 0.5
|
||||||
|
smoke_mat.gravity = Vector3(0, 0.5, 0)
|
||||||
|
smoke.process_material = smoke_mat
|
||||||
|
smoke.amount = 20
|
||||||
|
smoke.lifetime = 0.5
|
||||||
|
smoke.position = Vector3(0, 0, 0.1)
|
||||||
|
|
||||||
|
var smoke_mesh = SphereMesh.new()
|
||||||
|
smoke_mesh.radius = 0.04
|
||||||
|
var smoke_mesh_mat = StandardMaterial3D.new()
|
||||||
|
smoke_mesh_mat.albedo_color = Color(0.5, 0.5, 0.5, 0.6)
|
||||||
|
smoke_mesh_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
smoke_mesh.material = smoke_mesh_mat
|
||||||
|
smoke.draw_pass_1 = smoke_mesh
|
||||||
|
proj.add_child(smoke)
|
||||||
|
|
||||||
|
# Orient the projectile in the direction of fire
|
||||||
|
var up_vec = Vector3.UP
|
||||||
|
if abs(fire_dir.normalized().dot(up_vec)) > 0.99:
|
||||||
|
up_vec = Vector3.RIGHT
|
||||||
|
|
||||||
|
get_tree().current_scene.add_child(proj)
|
||||||
|
proj.global_position = origin
|
||||||
|
proj.look_at(origin + fire_dir, up_vec)
|
||||||
|
|
||||||
|
# Animate movement using tween at appropriate speed
|
||||||
|
var speed = 35.0 if weapon_name != "Plasma Gun" else 80.0
|
||||||
|
var end_pos = origin + fire_dir.normalized() * (speed * 5.0)
|
||||||
|
var tween = get_tree().create_tween()
|
||||||
|
tween.tween_property(proj, "global_position", end_pos, 5.0)
|
||||||
|
tween.tween_callback(proj.queue_free)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func server_play_explosion(pos: Vector3, radius: float) -> void:
|
||||||
|
if not multiplayer.is_server(): return
|
||||||
|
rpc_play_explosion.rpc(pos, radius)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func rpc_play_explosion(pos: Vector3, radius: float) -> void:
|
||||||
|
if is_multiplayer_authority(): return
|
||||||
|
|
||||||
|
var visual = MeshInstance3D.new()
|
||||||
|
var s_mesh = SphereMesh.new()
|
||||||
|
s_mesh.radius = radius
|
||||||
|
s_mesh.height = radius * 2.0
|
||||||
|
visual.mesh = s_mesh
|
||||||
|
|
||||||
|
var mat = StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = Color(1.0, 0.5, 0.0, 0.6) # Translucent orange
|
||||||
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
mat.emission_enabled = true
|
||||||
|
mat.emission = Color(1.0, 0.3, 0.0)
|
||||||
|
visual.material_override = mat
|
||||||
|
|
||||||
|
visual.position = pos
|
||||||
|
get_tree().current_scene.add_child(visual)
|
||||||
|
|
||||||
|
var tween = visual.create_tween()
|
||||||
|
tween.tween_property(mat, "albedo_color:a", 0.0, 0.3)
|
||||||
|
tween.parallel().tween_property(visual, "scale", Vector3(1.1, 1.1, 1.1), 0.3)
|
||||||
|
tween.tween_callback(visual.queue_free)
|
||||||
|
|
||||||
|
var audio = AudioStreamPlayer3D.new()
|
||||||
|
audio.stream = load("res://assets/sounds/shotgun_fire.wav")
|
||||||
|
audio.bus = "SFX"
|
||||||
|
audio.pitch_scale = 0.7
|
||||||
|
audio.max_distance = 150.0
|
||||||
|
get_tree().current_scene.add_child(audio)
|
||||||
|
audio.global_position = pos
|
||||||
|
audio.play()
|
||||||
|
audio.finished.connect(audio.queue_free)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func server_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_name: String, impulse: Vector3) -> void:
|
||||||
|
if not multiplayer.is_server(): return
|
||||||
|
# Broadcast damage event to all peers so the victim dies on all screens
|
||||||
|
rpc_take_damage.rpc(amount, hit_pos, attacker_id, weapon_name, impulse)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func rpc_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_name: String, impulse: Vector3) -> void:
|
||||||
|
if is_dead:
|
||||||
|
if is_instance_valid(ragdoll_instance):
|
||||||
|
ragdoll_instance.apply_initial_velocities(Vector3.ZERO, impulse)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show damage number for the attacker
|
||||||
|
if not multiplayer.has_multiplayer_peer() or attacker_id == multiplayer.get_unique_id():
|
||||||
|
spawn_damage_number(amount, hit_pos)
|
||||||
|
|
||||||
if attacker_id != 0:
|
if attacker_id != 0:
|
||||||
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
|
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
|
||||||
|
|
||||||
@@ -256,28 +468,18 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
|||||||
health = 0.0
|
health = 0.0
|
||||||
die(impulse)
|
die(impulse)
|
||||||
|
|
||||||
if get_node_or_null("/root/NetworkManager") and multiplayer.has_multiplayer_peer():
|
if is_multiplayer_authority():
|
||||||
var victim_id = multiplayer.get_unique_id() if is_multiplayer_authority() else int(str(name))
|
if get_node_or_null("/root/NetworkManager") and multiplayer.has_multiplayer_peer():
|
||||||
var killer_id = 0
|
var victim_id = multiplayer.get_unique_id()
|
||||||
var weapon_name = "Killed"
|
var killer_id = attacker_id
|
||||||
|
|
||||||
var assist_ids: Array = []
|
|
||||||
var now = Time.get_ticks_msec() / 1000.0
|
|
||||||
for aid in recent_attackers.keys():
|
|
||||||
if aid != killer_id and now - recent_attackers[aid] <= 10.0:
|
|
||||||
assist_ids.append(aid)
|
|
||||||
|
|
||||||
if _source and _source is Node:
|
|
||||||
if _source.has_method("get_multiplayer_authority"):
|
|
||||||
killer_id = _source.get_multiplayer_authority()
|
|
||||||
elif "owner_player" in _source and _source.owner_player:
|
|
||||||
killer_id = int(str(_source.owner_player.name))
|
|
||||||
elif _source is PlayerMovementController:
|
|
||||||
killer_id = int(str(_source.name))
|
|
||||||
|
|
||||||
if _source.has_method("get_weapon_name"):
|
var assist_ids: Array = []
|
||||||
weapon_name = _source.get_weapon_name()
|
var now = Time.get_ticks_msec() / 1000.0
|
||||||
get_node("/root/NetworkManager").register_kill.rpc(victim_id, killer_id, weapon_name, "", assist_ids)
|
for aid in recent_attackers.keys():
|
||||||
|
if aid != killer_id and now - recent_attackers[aid] <= 10.0:
|
||||||
|
assist_ids.append(aid)
|
||||||
|
|
||||||
|
get_node("/root/NetworkManager").register_kill.rpc(victim_id, killer_id, weapon_name, "", assist_ids)
|
||||||
|
|
||||||
func get_weapon_name() -> String:
|
func get_weapon_name() -> String:
|
||||||
if camera:
|
if camera:
|
||||||
@@ -391,6 +593,11 @@ func _physics_process(_delta: float) -> void:
|
|||||||
if humanoid:
|
if humanoid:
|
||||||
var h_speed = Vector2(velocity.x, velocity.z).length()
|
var h_speed = Vector2(velocity.x, velocity.z).length()
|
||||||
humanoid.update_state(sm.current_state, h_speed, sm.input_crouch)
|
humanoid.update_state(sm.current_state, h_speed, sm.input_crouch)
|
||||||
|
|
||||||
|
# Write synced state for remote peers
|
||||||
|
synced_movement_state = sm.current_state
|
||||||
|
synced_movement_speed = Vector2(velocity.x, velocity.z).length()
|
||||||
|
synced_is_crouching = sm.input_crouch
|
||||||
|
|
||||||
|
|
||||||
func _on_movement_event(ev: String, data: Dictionary) -> void:
|
func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||||
@@ -402,7 +609,22 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
|
|||||||
grapple_latch_player.play()
|
grapple_latch_player.play()
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if not is_multiplayer_authority(): return
|
# Remote players: apply synced movement state to their HumanoidModel
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
if synced_loadout_ready and not has_meta("remote_weapons_built"):
|
||||||
|
set_meta("remote_weapons_built", true)
|
||||||
|
var wman = camera.get_node_or_null("WeaponManager")
|
||||||
|
if wman and wman.has_method("_build_remote_loadout"):
|
||||||
|
wman._build_remote_loadout(synced_loadout_p1, synced_loadout_p2, synced_loadout_sp, synced_loadout_melee)
|
||||||
|
|
||||||
|
var humanoid = get_node_or_null("HumanoidModel")
|
||||||
|
if humanoid:
|
||||||
|
humanoid.update_state(synced_movement_state, synced_movement_speed, synced_is_crouching)
|
||||||
|
# Check for weapon changes
|
||||||
|
if synced_weapon_path != "" and synced_weapon_path != humanoid.get_meta("current_weapon_path", ""):
|
||||||
|
humanoid.set_weapon(synced_weapon_path)
|
||||||
|
humanoid.set_meta("current_weapon_path", synced_weapon_path)
|
||||||
|
return
|
||||||
if is_dead:
|
if is_dead:
|
||||||
# Continuously follow the ragdoll torso
|
# Continuously follow the ragdoll torso
|
||||||
if is_instance_valid(ragdoll_instance) and is_instance_valid(camera):
|
if is_instance_valid(ragdoll_instance) and is_instance_valid(camera):
|
||||||
@@ -538,7 +760,8 @@ func _setup_hud() -> void:
|
|||||||
tw.tween_property(respawn_label, "modulate:a", 0.2, 0.8)
|
tw.tween_property(respawn_label, "modulate:a", 0.2, 0.8)
|
||||||
tw.tween_property(respawn_label, "modulate:a", 1.0, 0.8)
|
tw.tween_property(respawn_label, "modulate:a", 1.0, 0.8)
|
||||||
|
|
||||||
_damage_layer.add_child(death_screen)
|
if is_multiplayer_authority():
|
||||||
|
_damage_layer.add_child(death_screen)
|
||||||
|
|
||||||
func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
||||||
if is_dead: return
|
if is_dead: return
|
||||||
@@ -630,8 +853,9 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
|||||||
ragdoll_instance.set_meta("spring_arm", spring_arm)
|
ragdoll_instance.set_meta("spring_arm", spring_arm)
|
||||||
|
|
||||||
# Show death screen
|
# Show death screen
|
||||||
death_screen.visible = true
|
if is_multiplayer_authority():
|
||||||
set_process_input(true)
|
death_screen.visible = true
|
||||||
|
set_process_input(true)
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if not is_multiplayer_authority(): return
|
if not is_multiplayer_authority(): return
|
||||||
@@ -641,5 +865,60 @@ func _input(event: InputEvent) -> void:
|
|||||||
_on_respawn_pressed()
|
_on_respawn_pressed()
|
||||||
|
|
||||||
func _on_respawn_pressed() -> void:
|
func _on_respawn_pressed() -> void:
|
||||||
# Reload scene
|
var spawn_pos = Vector3(randf_range(-5, 5), 5, randf_range(-5, 5))
|
||||||
get_tree().reload_current_scene()
|
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||||
|
rpc_respawn(spawn_pos)
|
||||||
|
else:
|
||||||
|
rpc_respawn.rpc(spawn_pos)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func rpc_respawn(spawn_pos: Vector3) -> void:
|
||||||
|
is_dead = false
|
||||||
|
health = max_health
|
||||||
|
shield = 0.0
|
||||||
|
|
||||||
|
if is_instance_valid(ragdoll_instance):
|
||||||
|
if ragdoll_instance.has_meta("spring_arm"):
|
||||||
|
var sa = ragdoll_instance.get_meta("spring_arm")
|
||||||
|
if is_instance_valid(sa):
|
||||||
|
sa.queue_free()
|
||||||
|
ragdoll_instance.queue_free()
|
||||||
|
|
||||||
|
# Restore camera parent
|
||||||
|
if is_instance_valid(camera) and is_instance_valid(head_pivot):
|
||||||
|
var p = camera.get_parent()
|
||||||
|
if p != head_pivot:
|
||||||
|
p.remove_child(camera)
|
||||||
|
head_pivot.add_child(camera)
|
||||||
|
camera.position = Vector3.ZERO
|
||||||
|
camera.rotation = Vector3.ZERO
|
||||||
|
|
||||||
|
# Re-enable movement state machine
|
||||||
|
if _machine:
|
||||||
|
_machine.process_mode = Node.PROCESS_MODE_INHERIT
|
||||||
|
|
||||||
|
# Re-enable collision
|
||||||
|
var col = get_node_or_null("CollisionShape3D")
|
||||||
|
if col:
|
||||||
|
col.set_deferred("disabled", false)
|
||||||
|
|
||||||
|
var visual_model = get_node_or_null("HumanoidModel")
|
||||||
|
if visual_model:
|
||||||
|
visual_model.visible = true
|
||||||
|
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
if death_screen:
|
||||||
|
death_screen.visible = false
|
||||||
|
position = spawn_pos
|
||||||
|
velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
# Optional: reset weapons
|
||||||
|
var wman = camera.get_node_or_null("WeaponManager") if camera else null
|
||||||
|
if wman:
|
||||||
|
wman.visible = true
|
||||||
|
wman.process_mode = Node.PROCESS_MODE_INHERIT
|
||||||
|
var cv = wman.get("canvas_layer")
|
||||||
|
if cv:
|
||||||
|
cv.visible = true
|
||||||
|
if wman.has_method("_build_loadout"):
|
||||||
|
wman._build_loadout()
|
||||||
|
|||||||
@@ -161,7 +161,12 @@ func _shoot_hitscan() -> void:
|
|||||||
hits_remaining -= 1
|
hits_remaining -= 1
|
||||||
current_damage_mult *= penetration_damage_penalty
|
current_damage_mult *= penetration_damage_penalty
|
||||||
final_target = result.position
|
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
|
# Spawn cosmetic tracer
|
||||||
var tracer = Node3D.new()
|
var tracer = Node3D.new()
|
||||||
tracer.set_script(load("res://weapons/hitscan_tracer.gd"))
|
tracer.set_script(load("res://weapons/hitscan_tracer.gd"))
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ func _shoot_projectile() -> void:
|
|||||||
var fire_dir = (forward + offset_dir).normalized()
|
var fire_dir = (forward + offset_dir).normalized()
|
||||||
|
|
||||||
_spawn_custom_projectile(origin, fire_dir)
|
_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:
|
func _spawn_custom_projectile(_origin: Vector3, _fire_dir: Vector3) -> void:
|
||||||
# Virtual method for custom projectiles (e.g., bouncing, homing)
|
# Virtual method for custom projectiles (e.g., bouncing, homing)
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ func _on_hit(result: Dictionary) -> void:
|
|||||||
if not result.collider.has_method("take_damage"):
|
if not result.collider.has_method("take_damage"):
|
||||||
if result.collider is StaticBody3D or result.collider is CSGShape3D:
|
if result.collider is StaticBody3D or result.collider is CSGShape3D:
|
||||||
ImpactSpawner.spawn(get_tree(), "crater", result.position, result.normal, explosion_radius * 1.5)
|
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)
|
_explode(result.position)
|
||||||
destroy()
|
destroy()
|
||||||
|
|
||||||
@@ -198,10 +200,14 @@ func _explode(pos: Vector3) -> void:
|
|||||||
if col == owner_player and not can_self_damage:
|
if col == owner_player and not can_self_damage:
|
||||||
# Skip self damage, but we still applied the knockback!
|
# Skip self damage, but we still applied the knockback!
|
||||||
pass
|
pass
|
||||||
else:
|
elif not owner_player or owner_player.is_multiplayer_authority():
|
||||||
col.take_damage(final_damage, target_pos, owner_player)
|
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 visual = MeshInstance3D.new()
|
||||||
var s_mesh = SphereMesh.new()
|
var s_mesh = SphereMesh.new()
|
||||||
s_mesh.radius = explosion_radius
|
s_mesh.radius = explosion_radius
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ func _on_hit(result: Dictionary) -> void:
|
|||||||
final_damage = lerpf(damage, min_damage, falloff_factor)
|
final_damage = lerpf(damage, min_damage, falloff_factor)
|
||||||
|
|
||||||
if result.collider.has_method("take_damage"):
|
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:
|
elif result.collider is StaticBody3D or result.collider is CSGShape3D:
|
||||||
var size = 0.4 if impact_type == "plasma" else 0.15
|
var size = 0.4 if impact_type == "plasma" else 0.15
|
||||||
ImpactSpawner.spawn(get_tree(), impact_type, result.position, result.normal, size)
|
ImpactSpawner.spawn(get_tree(), impact_type, result.position, result.normal, size)
|
||||||
|
|||||||
@@ -140,6 +140,9 @@ func _fire() -> void:
|
|||||||
|
|
||||||
var fire_dir = (base_dir + right * x_dir + up * y_dir).normalized()
|
var fire_dir = (base_dir + right * x_dir + up * y_dir).normalized()
|
||||||
_spawn_custom_projectile(proj_origin, fire_dir)
|
_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
|
var _current_aim_dir: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ func _process(_delta: float) -> void:
|
|||||||
vm_camera.fov = camera.fov
|
vm_camera.fov = camera.fov
|
||||||
|
|
||||||
func _build_loadout() -> void:
|
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
|
# Clear existing weapons
|
||||||
for w in weapons.values():
|
for w in weapons.values():
|
||||||
if is_instance_valid(w):
|
if is_instance_valid(w):
|
||||||
@@ -69,6 +78,16 @@ func _build_loadout() -> void:
|
|||||||
|
|
||||||
var l = LoadoutManager.get_active_loadout()
|
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(1, l["primary_1"])
|
||||||
_spawn_weapon(2, l["primary_2"])
|
_spawn_weapon(2, l["primary_2"])
|
||||||
_spawn_weapon(3, l["special"])
|
_spawn_weapon(3, l["special"])
|
||||||
@@ -77,6 +96,20 @@ func _build_loadout() -> void:
|
|||||||
|
|
||||||
_equip_slot(1)
|
_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:
|
func _spawn_weapon(slot: int, weapon_id: String) -> void:
|
||||||
if weapon_id == "" or weapon_id == "none" or not LoadoutManager.weapon_db.has(weapon_id):
|
if weapon_id == "" or weapon_id == "none" or not LoadoutManager.weapon_db.has(weapon_id):
|
||||||
return
|
return
|
||||||
@@ -184,6 +217,9 @@ func _equip_slot(slot: int) -> void:
|
|||||||
var humanoid = player.get_node("HumanoidModel")
|
var humanoid = player.get_node("HumanoidModel")
|
||||||
if humanoid.has_method("set_weapon") and w.has_meta("script_path"):
|
if humanoid.has_method("set_weapon") and w.has_meta("script_path"):
|
||||||
humanoid.set_weapon(w.get_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:
|
func _input(event: InputEvent) -> void:
|
||||||
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||||
|
|||||||
Reference in New Issue
Block a user