Feat/10 adding multiplayer fundamentals #14

Merged
Dotts merged 16 commits from feat/10-adding-multiplayer-fundamentals into main 2026-06-06 19:31:25 -07:00
12 changed files with 400 additions and 52 deletions
Showing only changes of commit ff3e01a9e5 - Show all commits
+2 -1
View File
@@ -36,6 +36,8 @@ var movement_speed: float = 0.0
var _anim_time: float = 0.0
func _ready() -> void:
rotation_degrees.y = 180
var mat := StandardMaterial3D.new()
mat.albedo_color = color
mat.roughness = 0.8
@@ -197,7 +199,6 @@ func set_weapon(script_path: String) -> void:
_set_shadows_recursive(w)
# 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.rotation_degrees = Vector3(0, 180, 0)
)
+16 -2
View File
@@ -301,6 +301,15 @@ func _spawn_player(pid: int) -> CharacterBody3D:
rep_config.add_property(":position")
rep_config.add_property(":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
player.add_child(sync)
@@ -395,9 +404,10 @@ func _spawn_player(pid: int) -> CharacterBody3D:
player.set_process(true)
sm.set_physics_process(true)
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_process(false)
player.set_process(true)
player.set_process_input(false)
sm.set_physics_process(false)
sm.set_process(false)
@@ -410,6 +420,10 @@ func _spawn_player(pid: int) -> CharacterBody3D:
wman.set_process_input(false)
wman.set_process(false)
wman.set_physics_process(false)
wman.visible = false
var cv = wman.get("canvas_layer")
if cv:
cv.visible = false
return player
+1 -1
View File
@@ -39,7 +39,7 @@ func _ready() -> void:
_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:
return
+1 -1
View File
@@ -33,7 +33,7 @@ func _ready() -> void:
dps_label.modulate = Color(1, 0.8, 0)
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
damage_history.append({"time": now, "amount": amount})
+1 -1
View File
@@ -82,7 +82,7 @@ func _physics_process(delta: float) -> void:
var current_h_speed = Vector2(velocity.x, velocity.z).length()
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:
return
+321 -42
View File
@@ -48,6 +48,19 @@ var _hit_marker_tween: Tween
# Grapple
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:
var sm := _ensure_machine()
@@ -61,16 +74,30 @@ func _ready() -> void:
if head_pivot:
camera = head_pivot.get_node_or_null("Camera3D")
_damage_layer = CanvasLayer.new()
add_child(_damage_layer)
_setup_audio()
_setup_hit_marker()
_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_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:
footstep_player = AudioStreamPlayer.new()
@@ -219,16 +246,9 @@ func apply_impulse(force: Vector3) -> void:
global_position.y += 0.1
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 weapon_name = "Killed"
if _source:
if _source.has_method("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:
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:
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
die(impulse)
if get_node_or_null("/root/NetworkManager") and multiplayer.has_multiplayer_peer():
var victim_id = multiplayer.get_unique_id() if is_multiplayer_authority() else int(str(name))
var killer_id = 0
var weapon_name = "Killed"
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 is_multiplayer_authority():
if get_node_or_null("/root/NetworkManager") and multiplayer.has_multiplayer_peer():
var victim_id = multiplayer.get_unique_id()
var killer_id = attacker_id
if _source.has_method("get_weapon_name"):
weapon_name = _source.get_weapon_name()
get_node("/root/NetworkManager").register_kill.rpc(victim_id, killer_id, weapon_name, "", assist_ids)
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)
get_node("/root/NetworkManager").register_kill.rpc(victim_id, killer_id, weapon_name, "", assist_ids)
func get_weapon_name() -> String:
if camera:
@@ -391,6 +593,11 @@ func _physics_process(_delta: float) -> void:
if humanoid:
var h_speed = Vector2(velocity.x, velocity.z).length()
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:
@@ -402,7 +609,22 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
grapple_latch_player.play()
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:
# Continuously follow the ragdoll torso
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", 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:
if is_dead: return
@@ -630,8 +853,9 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
ragdoll_instance.set_meta("spring_arm", spring_arm)
# Show death screen
death_screen.visible = true
set_process_input(true)
if is_multiplayer_authority():
death_screen.visible = true
set_process_input(true)
func _input(event: InputEvent) -> void:
if not is_multiplayer_authority(): return
@@ -641,5 +865,60 @@ func _input(event: InputEvent) -> void:
_on_respawn_pressed()
func _on_respawn_pressed() -> void:
# Reload scene
get_tree().reload_current_scene()
var spawn_pos = Vector3(randf_range(-5, 5), 5, randf_range(-5, 5))
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()
+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: