Merge pull request 'Feat/10 adding multiplayer fundamentals' (#14) from feat/10-adding-multiplayer-fundamentals into main
Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
@@ -51,3 +51,6 @@ user_settings/
|
|||||||
*LF*
|
*LF*
|
||||||
tags
|
tags
|
||||||
*.log
|
*.log
|
||||||
|
Papaya-Shooter.pck
|
||||||
|
Papaya-Shooter.exe
|
||||||
|
Papaya-Shooter.console.exe
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
[gd_scene load_steps=2 format=3 uid="uid://player_base"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://movement/player_movement_controller.gd" id="1"]
|
|
||||||
|
|
||||||
[node name="PlayerBase" type="CharacterBody3D"]
|
|
||||||
|
|
||||||
[node name="PlayerMovementController" type="Node" parent="."]
|
|
||||||
script = ExtResource("1")
|
|
||||||
|
|
||||||
[node name="MovementStateMachine" type="Node" parent="PlayerMovementController"]
|
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="PlayerMovementController"]
|
|
||||||
|
|
||||||
[node name="CapsuleShape3D" type="CollisionShape3D" parent="."]
|
|
||||||
shape = CapsuleShape3D.new()
|
|
||||||
shape.radius = 0.4
|
|
||||||
shape.height = 1.8
|
|
||||||
@@ -9,18 +9,18 @@ func build_ragdoll(color: Color) -> void:
|
|||||||
mat.roughness = 0.8
|
mat.roughness = 0.8
|
||||||
|
|
||||||
# Create bodies
|
# Create bodies
|
||||||
torso_body = _create_body(Vector3(0.4, 0.7, 0.25), mat, Vector3(0, 1.15, 0), 20.0)
|
torso_body = _create_body(Vector3(0.4, 0.7, 0.25), mat, Vector3(0, 1.15, 0), 20.0, "Torso")
|
||||||
var head_body = _create_body(Vector3(0.25, 0.25, 0.25), mat, Vector3(0, 1.65, 0), 5.0)
|
var head_body = _create_body(Vector3(0.25, 0.25, 0.25), mat, Vector3(0, 1.65, 0), 5.0, "Head")
|
||||||
|
|
||||||
var upper_arm_l_body = _create_body(Vector3(0.12, 0.35, 0.12), mat, Vector3(-0.28, 1.275, 0), 3.0)
|
var upper_arm_l_body = _create_body(Vector3(0.12, 0.35, 0.12), mat, Vector3(-0.28, 1.275, 0), 3.0, "UpperArmL")
|
||||||
var lower_arm_l_body = _create_body(Vector3(0.1, 0.35, 0.1), mat, Vector3(-0.28, 0.925, 0), 2.0)
|
var lower_arm_l_body = _create_body(Vector3(0.1, 0.35, 0.1), mat, Vector3(-0.28, 0.925, 0), 2.0, "LowerArmL")
|
||||||
var upper_arm_r_body = _create_body(Vector3(0.12, 0.35, 0.12), mat, Vector3(0.28, 1.275, 0), 3.0)
|
var upper_arm_r_body = _create_body(Vector3(0.12, 0.35, 0.12), mat, Vector3(0.28, 1.275, 0), 3.0, "UpperArmR")
|
||||||
var lower_arm_r_body = _create_body(Vector3(0.1, 0.35, 0.1), mat, Vector3(0.28, 0.925, 0), 2.0)
|
var lower_arm_r_body = _create_body(Vector3(0.1, 0.35, 0.1), mat, Vector3(0.28, 0.925, 0), 2.0, "LowerArmR")
|
||||||
|
|
||||||
var thigh_l_body = _create_body(Vector3(0.15, 0.45, 0.15), mat, Vector3(-0.12, 0.625, 0), 5.0)
|
var thigh_l_body = _create_body(Vector3(0.15, 0.45, 0.15), mat, Vector3(-0.12, 0.625, 0), 5.0, "ThighL")
|
||||||
var calf_l_body = _create_body(Vector3(0.13, 0.45, 0.13), mat, Vector3(-0.12, 0.175, 0), 3.0)
|
var calf_l_body = _create_body(Vector3(0.13, 0.45, 0.13), mat, Vector3(-0.12, 0.175, 0), 3.0, "CalfL")
|
||||||
var thigh_r_body = _create_body(Vector3(0.15, 0.45, 0.15), mat, Vector3(0.12, 0.625, 0), 5.0)
|
var thigh_r_body = _create_body(Vector3(0.15, 0.45, 0.15), mat, Vector3(0.12, 0.625, 0), 5.0, "ThighR")
|
||||||
var calf_r_body = _create_body(Vector3(0.13, 0.45, 0.13), mat, Vector3(0.12, 0.175, 0), 3.0)
|
var calf_r_body = _create_body(Vector3(0.13, 0.45, 0.13), mat, Vector3(0.12, 0.175, 0), 3.0, "CalfR")
|
||||||
|
|
||||||
add_child(torso_body)
|
add_child(torso_body)
|
||||||
add_child(head_body)
|
add_child(head_body)
|
||||||
@@ -33,6 +33,21 @@ func build_ragdoll(color: Color) -> void:
|
|||||||
add_child(thigh_r_body)
|
add_child(thigh_r_body)
|
||||||
add_child(calf_r_body)
|
add_child(calf_r_body)
|
||||||
|
|
||||||
|
var is_server = not multiplayer.has_multiplayer_peer() or multiplayer.is_server()
|
||||||
|
var rep = SceneReplicationConfig.new()
|
||||||
|
for child in get_children():
|
||||||
|
if child is RigidBody3D:
|
||||||
|
rep.add_property(NodePath(str(child.name) + ":position"))
|
||||||
|
rep.add_property(NodePath(str(child.name) + ":rotation"))
|
||||||
|
if not is_server:
|
||||||
|
child.freeze = true
|
||||||
|
child.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
||||||
|
|
||||||
|
var sync = MultiplayerSynchronizer.new()
|
||||||
|
sync.name = "RagdollSync"
|
||||||
|
sync.replication_config = rep
|
||||||
|
add_child(sync)
|
||||||
|
|
||||||
# Need to wait a frame for paths to be valid before creating joints
|
# Need to wait a frame for paths to be valid before creating joints
|
||||||
call_deferred("_setup_joints", head_body,
|
call_deferred("_setup_joints", head_body,
|
||||||
upper_arm_l_body, lower_arm_l_body,
|
upper_arm_l_body, lower_arm_l_body,
|
||||||
@@ -76,8 +91,9 @@ func apply_initial_velocities(linear_vel: Vector3, impulse: Vector3) -> void:
|
|||||||
if is_instance_valid(torso_body):
|
if is_instance_valid(torso_body):
|
||||||
torso_body.apply_central_impulse(impulse * 2.0)
|
torso_body.apply_central_impulse(impulse * 2.0)
|
||||||
|
|
||||||
func _create_body(size: Vector3, mat: Material, pos: Vector3, mass: float) -> RigidBody3D:
|
func _create_body(size: Vector3, mat: Material, pos: Vector3, mass: float, bname: String) -> RigidBody3D:
|
||||||
var body = RigidBody3D.new()
|
var body = RigidBody3D.new()
|
||||||
|
body.name = bname
|
||||||
body.position = pos
|
body.position = pos
|
||||||
body.mass = mass
|
body.mass = mass
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,13 @@ func _build_geometry() -> void:
|
|||||||
fill.light_energy = 0.3
|
fill.light_energy = 0.3
|
||||||
|
|
||||||
_build_dust2_layout()
|
_build_dust2_layout()
|
||||||
_build_player()
|
|
||||||
|
|
||||||
# Override player spawn point for Dust 2
|
func _spawn_player(pid: int) -> CharacterBody3D:
|
||||||
if _player:
|
var player = super._spawn_player(pid)
|
||||||
_player.global_position = Vector3(0, 1, 50)
|
player.position = Vector3(randf_range(-2, 2), 1, 50 + randf_range(-2, 2))
|
||||||
_player.look_at(Vector3(0, 1, 0), Vector3.UP)
|
# Looking at (0,1,0) from (0,1,50) is facing straight down the -Z axis, which is rotation = 0,0,0
|
||||||
|
player.rotation = Vector3.ZERO
|
||||||
|
return player
|
||||||
|
|
||||||
func _build_dust2_layout() -> void:
|
func _build_dust2_layout() -> void:
|
||||||
# Materials
|
# Materials
|
||||||
@@ -208,6 +209,11 @@ func _build_dust2_layout() -> void:
|
|||||||
add_child(combat_area)
|
add_child(combat_area)
|
||||||
|
|
||||||
func _build_dust2_dummies() -> void:
|
func _build_dust2_dummies() -> void:
|
||||||
|
# Do not spawn dummies in real multiplayer matches
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm and nm.multiplayer.multiplayer_peer is ENetMultiplayerPeer:
|
||||||
|
return
|
||||||
|
|
||||||
var spawn_dummy = func(pos: Vector3, rot_y: float):
|
var spawn_dummy = func(pos: Vector3, rot_y: float):
|
||||||
var dummy = load("res://entities/killable_dummy.gd").new()
|
var dummy = load("res://entities/killable_dummy.gd").new()
|
||||||
dummy.position = pos
|
dummy.position = pos
|
||||||
|
|||||||
+98
-22
@@ -25,6 +25,31 @@ func _ready() -> void:
|
|||||||
_build_geometry()
|
_build_geometry()
|
||||||
_build_hud()
|
_build_hud()
|
||||||
|
|
||||||
|
# Multiplayer Spawning
|
||||||
|
var spawner = MultiplayerSpawner.new()
|
||||||
|
spawner.name = "PlayerSpawner"
|
||||||
|
spawner.spawn_path = "." # Spawn directly as children of the level
|
||||||
|
spawner.spawn_function = _spawn_player
|
||||||
|
add_child(spawner)
|
||||||
|
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm and nm.multiplayer.is_server():
|
||||||
|
# Spawn existing players
|
||||||
|
for pid in nm.player_stats.keys():
|
||||||
|
spawner.spawn(pid)
|
||||||
|
|
||||||
|
# Spawn late joiners
|
||||||
|
nm.player_connected.connect(func(pid):
|
||||||
|
spawner.spawn(pid)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Despawn on disconnect
|
||||||
|
nm.player_disconnected.connect(func(pid):
|
||||||
|
var p = get_node_or_null(str(pid))
|
||||||
|
if p:
|
||||||
|
p.queue_free()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _build_materials() -> void:
|
func _build_materials() -> void:
|
||||||
pass
|
pass
|
||||||
@@ -40,7 +65,6 @@ func _build_geometry() -> void:
|
|||||||
_build_speed_corridor()
|
_build_speed_corridor()
|
||||||
_build_target_dummy()
|
_build_target_dummy()
|
||||||
_build_lighting()
|
_build_lighting()
|
||||||
_build_player()
|
|
||||||
|
|
||||||
|
|
||||||
# ── Utility ───────────────────────────────────────────────────────────────────
|
# ── Utility ───────────────────────────────────────────────────────────────────
|
||||||
@@ -261,12 +285,48 @@ func _build_lighting() -> void:
|
|||||||
|
|
||||||
# ── Player ────────────────────────────────────────────────────────────────────
|
# ── Player ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _build_player() -> void:
|
func _spawn_player(pid: int) -> CharacterBody3D:
|
||||||
var player := CharacterBody3D.new()
|
var player := CharacterBody3D.new()
|
||||||
player.name = "Player"
|
player.name = str(pid)
|
||||||
player.position = Vector3(0, 1.0, 0)
|
player.set_multiplayer_authority(pid)
|
||||||
add_child(player)
|
|
||||||
_player = player
|
# Use a slightly random spawn position to avoid exact overlapping
|
||||||
|
player.position = Vector3(randf_range(-2, 2), 2.0, randf_range(-2, 2))
|
||||||
|
|
||||||
|
# Server Synchronizer (Host is the ground truth)
|
||||||
|
var server_sync = MultiplayerSynchronizer.new()
|
||||||
|
server_sync.name = "ServerSynchronizer"
|
||||||
|
server_sync.set_multiplayer_authority(1) # Host always controls these
|
||||||
|
var server_rep_config = SceneReplicationConfig.new()
|
||||||
|
server_rep_config.add_property(":position")
|
||||||
|
server_rep_config.add_property(":synced_movement_state")
|
||||||
|
server_rep_config.add_property(":synced_movement_speed")
|
||||||
|
server_rep_config.add_property(":synced_is_crouching")
|
||||||
|
server_rep_config.add_property(":health")
|
||||||
|
server_rep_config.add_property(":shield")
|
||||||
|
server_rep_config.add_property(":is_dead")
|
||||||
|
server_rep_config.add_property(":synced_grapple_point")
|
||||||
|
server_rep_config.add_property(":synced_is_grapple_shooting")
|
||||||
|
server_sync.replication_config = server_rep_config
|
||||||
|
player.add_child(server_sync)
|
||||||
|
|
||||||
|
# Client Synchronizer (Client dictates their aim and loadout setup)
|
||||||
|
var client_sync = MultiplayerSynchronizer.new()
|
||||||
|
client_sync.name = "MultiplayerSynchronizer" # Keep original name for compatibility if needed elsewhere
|
||||||
|
client_sync.set_multiplayer_authority(pid)
|
||||||
|
var client_rep_config = SceneReplicationConfig.new()
|
||||||
|
client_rep_config.add_property(":rotation")
|
||||||
|
client_rep_config.add_property("HeadPivot:rotation")
|
||||||
|
client_rep_config.add_property(":synced_weapon_path")
|
||||||
|
client_rep_config.add_property(":synced_loadout_p1")
|
||||||
|
client_rep_config.add_property(":synced_loadout_p2")
|
||||||
|
client_rep_config.add_property(":synced_loadout_sp")
|
||||||
|
client_rep_config.add_property(":synced_loadout_melee")
|
||||||
|
client_rep_config.add_property(":synced_loadout_ready")
|
||||||
|
client_rep_config.add_property(":synced_is_targeting")
|
||||||
|
client_rep_config.add_property(":synced_homing_target_pos")
|
||||||
|
client_sync.replication_config = client_rep_config
|
||||||
|
player.add_child(client_sync)
|
||||||
|
|
||||||
# Collision shape
|
# Collision shape
|
||||||
var col_shape := CollisionShape3D.new()
|
var col_shape := CollisionShape3D.new()
|
||||||
@@ -282,11 +342,11 @@ func _build_player() -> void:
|
|||||||
if mover_script:
|
if mover_script:
|
||||||
player.set_script(mover_script)
|
player.set_script(mover_script)
|
||||||
|
|
||||||
# Humanoid Model for Player (Shadows only so it doesn't clip with camera)
|
# Humanoid Model for Player (Shadows only locally, visible to others)
|
||||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||||
humanoid.name = "HumanoidModel"
|
humanoid.name = "HumanoidModel"
|
||||||
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
||||||
humanoid.shadows_only = true
|
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
||||||
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
||||||
player.add_child(humanoid)
|
player.add_child(humanoid)
|
||||||
|
|
||||||
@@ -324,7 +384,7 @@ func _build_player() -> void:
|
|||||||
|
|
||||||
var camera := Camera3D.new()
|
var camera := Camera3D.new()
|
||||||
camera.name = "Camera3D"
|
camera.name = "Camera3D"
|
||||||
camera.current = true
|
camera.current = (pid == multiplayer.get_unique_id())
|
||||||
camera.fov = 90.0
|
camera.fov = 90.0
|
||||||
head_pivot.add_child(camera)
|
head_pivot.add_child(camera)
|
||||||
|
|
||||||
@@ -344,27 +404,43 @@ func _build_player() -> void:
|
|||||||
# Store original capsule height
|
# Store original capsule height
|
||||||
sm.original_capsule_height = capsule.height
|
sm.original_capsule_height = capsule.height
|
||||||
|
|
||||||
# ── Bootstrap: set_script() doesn't trigger _ready() ─────────────────
|
# ── Dependencies ────────────────────────────────────────────────────────
|
||||||
# Set references BEFORE calling _ready() since sm._ready() will switch_to("ground")
|
# Set references before the node enters the tree. Godot will call _ready()
|
||||||
# which needs machine.player to already be set.
|
# automatically when MultiplayerSpawner adds the node to the scene.
|
||||||
sm.player = player
|
sm.player = player
|
||||||
sm.params = player.params
|
sm.params = player.params
|
||||||
head_pivot.params = player.params
|
head_pivot.params = player.params
|
||||||
|
|
||||||
# Initialize in dependency order: machine (enters initial state) → player → camera
|
# Authority configuration
|
||||||
if sm.has_method("_ready"):
|
var is_local = (pid == multiplayer.get_unique_id())
|
||||||
sm._ready()
|
if is_local:
|
||||||
if player.has_method("_ready"):
|
_player = player
|
||||||
player._ready()
|
|
||||||
if head_pivot.has_method("_ready"):
|
|
||||||
head_pivot._ready()
|
|
||||||
|
|
||||||
# Ensure processing is enabled
|
|
||||||
player.set_physics_process(true)
|
player.set_physics_process(true)
|
||||||
player.set_process(true)
|
player.set_process(true)
|
||||||
sm.set_physics_process(true)
|
sm.set_physics_process(true)
|
||||||
|
else:
|
||||||
|
# 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(true)
|
||||||
|
player.set_process_input(false)
|
||||||
|
sm.set_physics_process(false)
|
||||||
|
sm.set_process(false)
|
||||||
|
sm.set_process_input(false)
|
||||||
|
head_pivot.set_process_input(false)
|
||||||
|
head_pivot.set_process(false)
|
||||||
|
head_pivot.set_physics_process(false)
|
||||||
|
var wman = camera.get_node_or_null("WeaponManager")
|
||||||
|
if wman:
|
||||||
|
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
|
||||||
|
|
||||||
_player = player
|
return player
|
||||||
|
|
||||||
|
|
||||||
# ── HUD ───────────────────────────────────────────────────────────────────────
|
# ── HUD ───────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
+82
-43
@@ -1,13 +1,14 @@
|
|||||||
extends Area3D
|
extends Area3D
|
||||||
class_name CombatArea
|
class_name CombatArea
|
||||||
|
|
||||||
var time_left: float = 5.0
|
var out_of_bounds_players: Dictionary = {} # body -> { time_left: 5.0 }
|
||||||
var time_safe: float = 0.0
|
var recovery_players: Dictionary = {} # body -> { time_safe: 0.0, time_left: float }
|
||||||
|
|
||||||
var _player_out_of_bounds: Node3D = null
|
|
||||||
var _ui_layer: CanvasLayer = null
|
var _ui_layer: CanvasLayer = null
|
||||||
var _ui_label: Label = null
|
var _ui_label: Label = null
|
||||||
var _ui_overlay: ColorRect = null
|
var _ui_overlay: ColorRect = null
|
||||||
|
var _is_local_player_out: bool = false
|
||||||
|
var _local_player_body: Node3D = null
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
@@ -17,50 +18,85 @@ func _ready() -> void:
|
|||||||
set_process(false)
|
set_process(false)
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if _player_out_of_bounds:
|
# Process players who are actively out of bounds
|
||||||
# If the player dies while out of bounds, stop the timer and remove the UI
|
var to_kill: Array = []
|
||||||
if "is_dead" in _player_out_of_bounds and _player_out_of_bounds.is_dead:
|
for body in out_of_bounds_players.keys():
|
||||||
_player_out_of_bounds = null
|
if "is_dead" in body and body.is_dead:
|
||||||
|
out_of_bounds_players.erase(body)
|
||||||
|
if body == _local_player_body:
|
||||||
_remove_ui()
|
_remove_ui()
|
||||||
return
|
continue
|
||||||
|
|
||||||
# Player is outside the combat area!
|
var data = out_of_bounds_players[body]
|
||||||
time_left -= delta
|
data.time_left -= delta
|
||||||
if time_left <= 0.0:
|
if data.time_left <= 0.0:
|
||||||
time_left = 0.0
|
data.time_left = 0.0
|
||||||
_kill_player()
|
to_kill.append(body)
|
||||||
|
|
||||||
# Update UI
|
if body == _local_player_body and _is_local_player_out and _ui_label and _ui_overlay:
|
||||||
if _ui_label and _ui_overlay:
|
_ui_label.text = "WARNING: RETURN TO COMBAT AREA\n%.1f" % data.time_left
|
||||||
_ui_label.text = "WARNING: RETURN TO COMBAT AREA\n%.1f" % time_left
|
_ui_overlay.color = Color(1.0, 0.0, 0.0, 0.8 * (1.0 - (data.time_left / 5.0)))
|
||||||
_ui_overlay.color = Color(1.0, 0.0, 0.0, 0.8 * (1.0 - (time_left / 5.0)))
|
|
||||||
else:
|
for body in to_kill:
|
||||||
# Player is inside the combat area, but might have a damaged timer
|
_kill_player(body)
|
||||||
if time_left < 5.0:
|
|
||||||
time_safe += delta
|
# Process players who returned but are recovering
|
||||||
if time_safe >= 10.0:
|
var recovered: Array = []
|
||||||
time_left = 5.0
|
for body in recovery_players.keys():
|
||||||
time_safe = 0.0
|
if "is_dead" in body and body.is_dead:
|
||||||
set_process(false) # Fully recovered, sleep
|
recovered.append(body)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var data = recovery_players[body]
|
||||||
|
data.time_safe += delta
|
||||||
|
if data.time_safe >= 10.0:
|
||||||
|
recovered.append(body)
|
||||||
|
|
||||||
|
for body in recovered:
|
||||||
|
recovery_players.erase(body)
|
||||||
|
|
||||||
|
if out_of_bounds_players.is_empty() and recovery_players.is_empty():
|
||||||
|
set_process(false)
|
||||||
|
|
||||||
func _on_body_exited(body: Node3D) -> void:
|
func _on_body_exited(body: Node3D) -> void:
|
||||||
if body.name == "Player":
|
if body is CharacterBody3D and body.has_method("take_damage") and str(body.name).is_valid_int():
|
||||||
# Don't trigger if the player is already dead or the scene is tearing down
|
|
||||||
if "is_dead" in body and body.is_dead:
|
if "is_dead" in body and body.is_dead:
|
||||||
return
|
return
|
||||||
if body.is_queued_for_deletion() or not is_inside_tree():
|
if body.is_queued_for_deletion() or not is_inside_tree():
|
||||||
return
|
return
|
||||||
|
|
||||||
_player_out_of_bounds = body
|
var start_time = 5.0
|
||||||
time_safe = 0.0 # Reset recovery timer
|
if recovery_players.has(body):
|
||||||
|
start_time = recovery_players[body].time_left
|
||||||
|
recovery_players.erase(body)
|
||||||
|
|
||||||
|
out_of_bounds_players[body] = { "time_left": start_time }
|
||||||
|
|
||||||
|
var is_local = false
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
if body.get_multiplayer_authority() == multiplayer.get_unique_id():
|
||||||
|
is_local = true
|
||||||
|
|
||||||
|
if is_local:
|
||||||
|
_is_local_player_out = true
|
||||||
|
_local_player_body = body
|
||||||
_create_ui()
|
_create_ui()
|
||||||
|
|
||||||
set_process(true)
|
set_process(true)
|
||||||
|
|
||||||
func _on_body_entered(body: Node3D) -> void:
|
func _on_body_entered(body: Node3D) -> void:
|
||||||
if body == _player_out_of_bounds:
|
if out_of_bounds_players.has(body):
|
||||||
_player_out_of_bounds = null
|
var time_left_val = out_of_bounds_players[body].time_left
|
||||||
|
out_of_bounds_players.erase(body)
|
||||||
|
|
||||||
|
if time_left_val < 5.0:
|
||||||
|
recovery_players[body] = { "time_left": time_left_val, "time_safe": 0.0 }
|
||||||
|
set_process(true)
|
||||||
|
|
||||||
|
if body == _local_player_body and _is_local_player_out:
|
||||||
_remove_ui()
|
_remove_ui()
|
||||||
# set_process(true) stays on to tick the time_safe recovery
|
_is_local_player_out = false
|
||||||
|
_local_player_body = null
|
||||||
|
|
||||||
func _create_ui() -> void:
|
func _create_ui() -> void:
|
||||||
if _ui_layer: return
|
if _ui_layer: return
|
||||||
@@ -71,6 +107,7 @@ func _create_ui() -> void:
|
|||||||
_ui_overlay = ColorRect.new()
|
_ui_overlay = ColorRect.new()
|
||||||
_ui_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
_ui_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
_ui_overlay.color = Color(1.0, 0.0, 0.0, 0.0)
|
_ui_overlay.color = Color(1.0, 0.0, 0.0, 0.0)
|
||||||
|
_ui_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
_ui_layer.add_child(_ui_overlay)
|
_ui_layer.add_child(_ui_overlay)
|
||||||
|
|
||||||
_ui_label = Label.new()
|
_ui_label = Label.new()
|
||||||
@@ -82,7 +119,10 @@ func _create_ui() -> void:
|
|||||||
_ui_label.add_theme_color_override("font_color", Color(1, 0.2, 0.2))
|
_ui_label.add_theme_color_override("font_color", Color(1, 0.2, 0.2))
|
||||||
_ui_label.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
_ui_label.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||||
_ui_label.add_theme_constant_override("outline_size", 8)
|
_ui_label.add_theme_constant_override("outline_size", 8)
|
||||||
_ui_label.text = "WARNING: RETURN TO COMBAT AREA\n%.1f" % time_left
|
var display_time = 5.0
|
||||||
|
if out_of_bounds_players.has(_local_player_body):
|
||||||
|
display_time = out_of_bounds_players[_local_player_body].time_left
|
||||||
|
_ui_label.text = "WARNING: RETURN TO COMBAT AREA\n%.1f" % display_time
|
||||||
|
|
||||||
_ui_layer.add_child(_ui_label)
|
_ui_layer.add_child(_ui_label)
|
||||||
|
|
||||||
@@ -96,15 +136,14 @@ func _remove_ui() -> void:
|
|||||||
_ui_label = null
|
_ui_label = null
|
||||||
_ui_overlay = null
|
_ui_overlay = null
|
||||||
|
|
||||||
func _kill_player() -> void:
|
func _kill_player(body: Node3D) -> void:
|
||||||
if _player_out_of_bounds:
|
if body:
|
||||||
# PlayerMovementController's take_damage requires (amount, source)
|
if multiplayer.is_server():
|
||||||
if _player_out_of_bounds.has_method("take_damage"):
|
if body.has_method("take_damage"):
|
||||||
_player_out_of_bounds.take_damage(9999, Vector3.ZERO)
|
body.take_damage(9999, Vector3.ZERO, null, Vector3.ZERO)
|
||||||
elif "health" in _player_out_of_bounds:
|
|
||||||
_player_out_of_bounds.health = 0
|
|
||||||
if "is_dead" in _player_out_of_bounds:
|
|
||||||
_player_out_of_bounds.is_dead = true
|
|
||||||
|
|
||||||
_player_out_of_bounds = null
|
out_of_bounds_players.erase(body)
|
||||||
|
if body == _local_player_body and _is_local_player_out:
|
||||||
_remove_ui()
|
_remove_ui()
|
||||||
|
_is_local_player_out = false
|
||||||
|
_local_player_body = null
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ var max_shield: float = 100.0
|
|||||||
var shield: float = 100.0
|
var shield: float = 100.0
|
||||||
|
|
||||||
var is_dead: bool = false
|
var is_dead: bool = false
|
||||||
|
var time_since_last_damage: float = 0.0
|
||||||
var ragdoll_instance: Node3D = null
|
var ragdoll_instance: Node3D = null
|
||||||
var pending_impulse: Vector3 = Vector3.ZERO
|
var pending_impulse: Vector3 = Vector3.ZERO
|
||||||
|
var recent_attackers: Dictionary = {}
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("enemies")
|
add_to_group("enemies")
|
||||||
@@ -38,9 +40,33 @@ func _ready() -> void:
|
|||||||
|
|
||||||
_update_label()
|
_update_label()
|
||||||
|
|
||||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> void:
|
func _process(delta: float) -> void:
|
||||||
if is_dead:
|
if is_dead:
|
||||||
return
|
return
|
||||||
|
time_since_last_damage += delta
|
||||||
|
if time_since_last_damage >= 5.0 and shield < max_shield:
|
||||||
|
shield += 20.0 * delta
|
||||||
|
if shield > max_shield:
|
||||||
|
shield = max_shield
|
||||||
|
_update_label()
|
||||||
|
|
||||||
|
func take_damage(amount: float, hit_position: Vector3, source: Node = null, _impulse: Vector3 = Vector3.ZERO) -> void:
|
||||||
|
if is_dead:
|
||||||
|
return
|
||||||
|
|
||||||
|
var attacker_id = 0
|
||||||
|
if source:
|
||||||
|
if source.has_method("get_multiplayer_authority"):
|
||||||
|
attacker_id = source.get_multiplayer_authority()
|
||||||
|
elif "owner_player" in source and source.owner_player:
|
||||||
|
attacker_id = int(str(source.owner_player.name))
|
||||||
|
elif source is CharacterBody3D:
|
||||||
|
attacker_id = int(str(source.name))
|
||||||
|
|
||||||
|
if attacker_id != 0:
|
||||||
|
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
|
||||||
|
|
||||||
|
time_since_last_damage = 0.0
|
||||||
|
|
||||||
var dmg_taken = amount
|
var dmg_taken = amount
|
||||||
if shield > 0:
|
if shield > 0:
|
||||||
@@ -61,6 +87,29 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
|
|||||||
|
|
||||||
if health <= 0:
|
if health <= 0:
|
||||||
_die(hit_position, source, dmg_taken)
|
_die(hit_position, source, dmg_taken)
|
||||||
|
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm and multiplayer.has_multiplayer_peer():
|
||||||
|
var killer_id = 0
|
||||||
|
var weapon_name = "Killed"
|
||||||
|
if source:
|
||||||
|
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 CharacterBody3D:
|
||||||
|
killer_id = int(str(source.name))
|
||||||
|
|
||||||
|
if source.has_method("get_weapon_name"):
|
||||||
|
weapon_name = source.get_weapon_name()
|
||||||
|
|
||||||
|
var assist_ids: Array = []
|
||||||
|
var now = Time.get_ticks_msec() / 1000.0
|
||||||
|
for aid in recent_attackers.keys():
|
||||||
|
if now - recent_attackers[aid] <= 10.0:
|
||||||
|
assist_ids.append(aid)
|
||||||
|
|
||||||
|
nm.register_kill.rpc(-1, killer_id, weapon_name, "Target Dummy", assist_ids)
|
||||||
else:
|
else:
|
||||||
_wiggle()
|
_wiggle()
|
||||||
|
|
||||||
@@ -105,14 +154,13 @@ func _die(_hit_position: Vector3, source: Node, damage_amount: float) -> void:
|
|||||||
linear_vel = hit_dir * (damage_amount * 0.02)
|
linear_vel = hit_dir * (damage_amount * 0.02)
|
||||||
impulse = hit_dir * (damage_amount * 0.5)
|
impulse = hit_dir * (damage_amount * 0.5)
|
||||||
|
|
||||||
get_tree().create_timer(0.01).timeout.connect(func():
|
get_tree().create_timer(0.01).timeout.connect(_apply_ragdoll_velocity.bind(ragdoll_instance, linear_vel, impulse))
|
||||||
if is_instance_valid(ragdoll_instance):
|
|
||||||
ragdoll_instance.apply_initial_velocities(linear_vel, impulse)
|
|
||||||
)
|
|
||||||
|
|
||||||
get_tree().create_timer(3.0).timeout.connect(func():
|
get_tree().create_timer(3.0).timeout.connect(_respawn)
|
||||||
_respawn()
|
|
||||||
)
|
func _apply_ragdoll_velocity(ragdoll: Node3D, l_vel: Vector3, imp: Vector3) -> void:
|
||||||
|
if is_instance_valid(ragdoll):
|
||||||
|
ragdoll.apply_initial_velocities(l_vel, imp)
|
||||||
|
|
||||||
func _respawn() -> void:
|
func _respawn() -> void:
|
||||||
if is_instance_valid(ragdoll_instance):
|
if is_instance_valid(ragdoll_instance):
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ var max_shield: float = 100.0
|
|||||||
var shield: float = 100.0
|
var shield: float = 100.0
|
||||||
|
|
||||||
var is_dead: bool = false
|
var is_dead: bool = false
|
||||||
|
var time_since_last_damage: float = 0.0
|
||||||
|
var recent_attackers: Dictionary = {} # attacker_id: timestamp
|
||||||
var ragdoll_instance: Node3D = null
|
var ragdoll_instance: Node3D = null
|
||||||
var pending_impulse: Vector3 = Vector3.ZERO
|
var pending_impulse: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
@@ -50,6 +52,14 @@ func _physics_process(delta: float) -> void:
|
|||||||
if is_dead:
|
if is_dead:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Shield recharge logic
|
||||||
|
time_since_last_damage += delta
|
||||||
|
if time_since_last_damage >= 5.0 and shield < max_shield:
|
||||||
|
shield += 20.0 * delta
|
||||||
|
if shield > max_shield:
|
||||||
|
shield = max_shield
|
||||||
|
_update_label()
|
||||||
|
|
||||||
# Basic gravity
|
# Basic gravity
|
||||||
if not is_on_floor():
|
if not is_on_floor():
|
||||||
velocity.y -= 9.8 * delta
|
velocity.y -= 9.8 * delta
|
||||||
@@ -81,10 +91,24 @@ 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
|
||||||
|
|
||||||
|
var attacker_id = 0
|
||||||
|
if source:
|
||||||
|
if source.has_method("get_multiplayer_authority"):
|
||||||
|
attacker_id = source.get_multiplayer_authority()
|
||||||
|
elif "owner_player" in source and source.owner_player:
|
||||||
|
attacker_id = int(str(source.owner_player.name))
|
||||||
|
elif source is CharacterBody3D:
|
||||||
|
attacker_id = int(str(source.name))
|
||||||
|
|
||||||
|
if attacker_id != 0:
|
||||||
|
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
|
||||||
|
|
||||||
|
time_since_last_damage = 0.0
|
||||||
|
|
||||||
var dmg_taken = amount
|
var dmg_taken = amount
|
||||||
if shield > 0:
|
if shield > 0:
|
||||||
if shield >= amount:
|
if shield >= amount:
|
||||||
@@ -96,6 +120,8 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
|
|||||||
|
|
||||||
if amount > 0:
|
if amount > 0:
|
||||||
health -= amount
|
health -= amount
|
||||||
|
if health <= 0:
|
||||||
|
health = 0
|
||||||
|
|
||||||
_update_label()
|
_update_label()
|
||||||
|
|
||||||
@@ -104,6 +130,29 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
|
|||||||
|
|
||||||
if health <= 0:
|
if health <= 0:
|
||||||
_die(hit_position, source, dmg_taken)
|
_die(hit_position, source, dmg_taken)
|
||||||
|
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm and multiplayer.has_multiplayer_peer():
|
||||||
|
var killer_id = 0
|
||||||
|
var weapon_name = "Killed"
|
||||||
|
if source:
|
||||||
|
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 CharacterBody3D:
|
||||||
|
killer_id = int(str(source.name))
|
||||||
|
|
||||||
|
if source.has_method("get_weapon_name"):
|
||||||
|
weapon_name = source.get_weapon_name()
|
||||||
|
|
||||||
|
var assist_ids: Array = []
|
||||||
|
var now = Time.get_ticks_msec() / 1000.0
|
||||||
|
for aid in recent_attackers.keys():
|
||||||
|
if now - recent_attackers[aid] <= 10.0:
|
||||||
|
assist_ids.append(aid)
|
||||||
|
|
||||||
|
nm.register_kill.rpc(-1, killer_id, weapon_name, "Walking Dummy", assist_ids)
|
||||||
else:
|
else:
|
||||||
_wiggle()
|
_wiggle()
|
||||||
|
|
||||||
@@ -149,15 +198,13 @@ func _die(_hit_position: Vector3, source: Node, damage_amount: float) -> void:
|
|||||||
linear_vel = hit_dir * (damage_amount * 0.02)
|
linear_vel = hit_dir * (damage_amount * 0.02)
|
||||||
impulse = hit_dir * (damage_amount * 0.5)
|
impulse = hit_dir * (damage_amount * 0.5)
|
||||||
|
|
||||||
get_tree().create_timer(0.01).timeout.connect(func():
|
get_tree().create_timer(0.01).timeout.connect(_apply_ragdoll_velocity.bind(ragdoll_instance, velocity + linear_vel, impulse))
|
||||||
if is_instance_valid(ragdoll_instance):
|
|
||||||
# The ragdoll also inherits the current walking velocity natively
|
|
||||||
ragdoll_instance.apply_initial_velocities(velocity + linear_vel, impulse)
|
|
||||||
)
|
|
||||||
|
|
||||||
get_tree().create_timer(3.0).timeout.connect(func():
|
get_tree().create_timer(3.0).timeout.connect(_respawn)
|
||||||
_respawn()
|
|
||||||
)
|
func _apply_ragdoll_velocity(ragdoll: Node3D, l_vel: Vector3, imp: Vector3) -> void:
|
||||||
|
if is_instance_valid(ragdoll):
|
||||||
|
ragdoll.apply_initial_velocities(l_vel, imp)
|
||||||
|
|
||||||
func _respawn() -> void:
|
func _respawn() -> void:
|
||||||
if is_instance_valid(ragdoll_instance):
|
if is_instance_valid(ragdoll_instance):
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
[preset.0]
|
||||||
|
|
||||||
|
name="Windows Desktop"
|
||||||
|
platform="Windows Desktop"
|
||||||
|
runnable=true
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="./Papaya-Shooter.exe"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.0.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
debug/export_console_wrapper=2
|
||||||
|
binary_format/embed_pck=false
|
||||||
|
texture_format/s3tc_bptc=true
|
||||||
|
texture_format/etc2_astc=false
|
||||||
|
shader_baker/enabled=false
|
||||||
|
binary_format/architecture="x86_64"
|
||||||
|
codesign/enable=false
|
||||||
|
codesign/timestamp=true
|
||||||
|
codesign/timestamp_server_url=""
|
||||||
|
codesign/digest_algorithm=1
|
||||||
|
codesign/description=""
|
||||||
|
codesign/custom_options=PackedStringArray()
|
||||||
|
application/modify_resources=true
|
||||||
|
application/icon="uid://cxxfrs5ov5vay"
|
||||||
|
application/console_wrapper_icon=""
|
||||||
|
application/icon_interpolation=4
|
||||||
|
application/file_version=""
|
||||||
|
application/product_version=""
|
||||||
|
application/company_name=""
|
||||||
|
application/product_name=""
|
||||||
|
application/file_description=""
|
||||||
|
application/copyright=""
|
||||||
|
application/trademarks=""
|
||||||
|
application/export_angle=0
|
||||||
|
application/export_d3d12=0
|
||||||
|
application/d3d12_agility_sdk_multiarch=true
|
||||||
|
ssh_remote_deploy/enabled=false
|
||||||
|
ssh_remote_deploy/host="user@host_ip"
|
||||||
|
ssh_remote_deploy/port="22"
|
||||||
|
ssh_remote_deploy/extra_args_ssh=""
|
||||||
|
ssh_remote_deploy/extra_args_scp=""
|
||||||
|
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
|
||||||
|
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||||
|
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
|
||||||
|
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
|
||||||
|
Start-ScheduledTask -TaskName godot_remote_debug
|
||||||
|
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
|
||||||
|
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
|
||||||
|
Remove-Item -Recurse -Force '{temp_dir}'"
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
signal player_connected(id: int)
|
||||||
|
signal player_disconnected(id: int)
|
||||||
|
signal connection_failed
|
||||||
|
signal connection_succeeded
|
||||||
|
signal match_state_updated
|
||||||
|
signal stats_updated
|
||||||
|
signal killfeed_event(victim: String, killer: String, weapon: String, v_color: String, k_color: String)
|
||||||
|
|
||||||
|
const DEFAULT_PORT = 31415
|
||||||
|
|
||||||
|
var connected_players: Array[int] = []
|
||||||
|
|
||||||
|
# Dict of ID -> { "username": String, "kills": int, "deaths": int, "assists": int, "ping": int }
|
||||||
|
var player_stats: Dictionary = {}
|
||||||
|
|
||||||
|
var match_active: bool = false
|
||||||
|
var match_time_remaining: float = 0.0
|
||||||
|
var current_gamemode: String = "Deathmatch"
|
||||||
|
var current_scene_path: String = ""
|
||||||
|
|
||||||
|
var _ping_timer: Timer
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||||
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||||
|
multiplayer.connected_to_server.connect(_on_connected_to_server)
|
||||||
|
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||||
|
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||||
|
|
||||||
|
_ping_timer = Timer.new()
|
||||||
|
_ping_timer.wait_time = 1.0
|
||||||
|
_ping_timer.autostart = false
|
||||||
|
_ping_timer.timeout.connect(_on_ping_timer)
|
||||||
|
add_child(_ping_timer)
|
||||||
|
|
||||||
|
func host_game(port: int = DEFAULT_PORT) -> Error:
|
||||||
|
var peer = ENetMultiplayerPeer.new()
|
||||||
|
var err = peer.create_server(port)
|
||||||
|
if err == OK:
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
connected_players.clear()
|
||||||
|
connected_players.append(1) # Host is 1
|
||||||
|
_init_player_stats(1, SettingsManager.username)
|
||||||
|
_ping_timer.start()
|
||||||
|
print("Hosting on port %s" % port)
|
||||||
|
return err
|
||||||
|
|
||||||
|
func join_game(ip: String, port: int = DEFAULT_PORT) -> Error:
|
||||||
|
var peer = ENetMultiplayerPeer.new()
|
||||||
|
var err = peer.create_client(ip, port)
|
||||||
|
if err == OK:
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
print("Connecting to %s:%s" % [ip, port])
|
||||||
|
return err
|
||||||
|
|
||||||
|
func disconnect_game() -> void:
|
||||||
|
multiplayer.multiplayer_peer = null
|
||||||
|
connected_players.clear()
|
||||||
|
player_stats.clear()
|
||||||
|
match_active = false
|
||||||
|
_ping_timer.stop()
|
||||||
|
|
||||||
|
func start_singleplayer_match(gamemode: String = "Deathmatch") -> void:
|
||||||
|
# Ensure peer is offline if not already connected
|
||||||
|
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||||
|
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||||
|
|
||||||
|
connected_players.clear()
|
||||||
|
connected_players.append(1)
|
||||||
|
_init_player_stats(1, SettingsManager.username)
|
||||||
|
|
||||||
|
match_active = true
|
||||||
|
match_time_remaining = 300.0
|
||||||
|
current_gamemode = gamemode
|
||||||
|
rpc("sync_match_state", match_active, match_time_remaining, current_gamemode)
|
||||||
|
|
||||||
|
func _init_player_stats(id: int, username: String) -> void:
|
||||||
|
# Keep existing color if player re-joins, or generate new
|
||||||
|
var p_color = Color(randf_range(0.2, 1.0), randf_range(0.2, 1.0), randf_range(0.2, 1.0)).to_html(false)
|
||||||
|
|
||||||
|
player_stats[id] = {
|
||||||
|
"username": username,
|
||||||
|
"kills": 0,
|
||||||
|
"deaths": 0,
|
||||||
|
"assists": 0,
|
||||||
|
"ping": 0,
|
||||||
|
"color": p_color
|
||||||
|
}
|
||||||
|
stats_updated.emit()
|
||||||
|
|
||||||
|
func _on_peer_connected(id: int) -> void:
|
||||||
|
print("Player connected: ", id)
|
||||||
|
if not connected_players.has(id):
|
||||||
|
connected_players.append(id)
|
||||||
|
|
||||||
|
# If server, we wait for the client to register themselves via RPC
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _on_peer_disconnected(id: int) -> void:
|
||||||
|
print("Player disconnected: ", id)
|
||||||
|
connected_players.erase(id)
|
||||||
|
if player_stats.has(id):
|
||||||
|
player_stats.erase(id)
|
||||||
|
player_disconnected.emit(id)
|
||||||
|
if multiplayer.is_server():
|
||||||
|
rpc("sync_player_stats", player_stats)
|
||||||
|
|
||||||
|
func _on_connected_to_server() -> void:
|
||||||
|
print("Successfully connected to server")
|
||||||
|
connection_succeeded.emit()
|
||||||
|
# Tell the server our username
|
||||||
|
rpc_id(1, "register_player", SettingsManager.username)
|
||||||
|
|
||||||
|
func _on_connection_failed() -> void:
|
||||||
|
print("Failed to connect to server")
|
||||||
|
multiplayer.multiplayer_peer = null
|
||||||
|
connection_failed.emit()
|
||||||
|
|
||||||
|
func _on_server_disconnected() -> void:
|
||||||
|
print("Server disconnected")
|
||||||
|
multiplayer.multiplayer_peer = null
|
||||||
|
connected_players.clear()
|
||||||
|
player_stats.clear()
|
||||||
|
match_active = false
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "reliable")
|
||||||
|
func register_player(username: String) -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var sender_id = multiplayer.get_remote_sender_id()
|
||||||
|
_init_player_stats(sender_id, username)
|
||||||
|
player_connected.emit(sender_id)
|
||||||
|
|
||||||
|
# Sync everyone's stats
|
||||||
|
rpc("sync_player_stats", player_stats)
|
||||||
|
|
||||||
|
# If a match is active, force the late-joiner to load the level
|
||||||
|
if match_active:
|
||||||
|
rpc_id(sender_id, "rpc_load_level", current_scene_path)
|
||||||
|
rpc_id(sender_id, "sync_match_state", match_active, match_time_remaining, current_gamemode)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func update_username(new_name: String) -> void:
|
||||||
|
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||||
|
var sender_id = multiplayer.get_remote_sender_id()
|
||||||
|
if sender_id == 0:
|
||||||
|
sender_id = 1
|
||||||
|
if player_stats.has(sender_id):
|
||||||
|
player_stats[sender_id]["username"] = new_name
|
||||||
|
stats_updated.emit()
|
||||||
|
if multiplayer.has_multiplayer_peer() and not multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||||
|
rpc("sync_player_stats", player_stats)
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func sync_player_stats(stats: Dictionary) -> void:
|
||||||
|
player_stats = stats
|
||||||
|
stats_updated.emit()
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func sync_match_state(active: bool, time_rem: float, mode: String) -> void:
|
||||||
|
match_active = active
|
||||||
|
match_time_remaining = time_rem
|
||||||
|
current_gamemode = mode
|
||||||
|
match_state_updated.emit()
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func rpc_load_level(scene_path: String) -> void:
|
||||||
|
print("Loading level: ", scene_path)
|
||||||
|
current_scene_path = scene_path
|
||||||
|
if multiplayer.is_server():
|
||||||
|
match_active = true
|
||||||
|
match_time_remaining = 300.0 # 5 minutes
|
||||||
|
rpc("sync_match_state", match_active, match_time_remaining, current_gamemode)
|
||||||
|
get_tree().change_scene_to_file(scene_path)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if match_active:
|
||||||
|
if match_time_remaining > 0:
|
||||||
|
match_time_remaining -= delta
|
||||||
|
if match_time_remaining <= 0:
|
||||||
|
match_time_remaining = 0
|
||||||
|
match_active = false
|
||||||
|
# We emit this so HUDs can update their timers smoothly
|
||||||
|
match_state_updated.emit()
|
||||||
|
|
||||||
|
# --- PING SYSTEM ---
|
||||||
|
func _on_ping_timer() -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var time = Time.get_ticks_msec()
|
||||||
|
for pid in connected_players:
|
||||||
|
if pid != 1:
|
||||||
|
rpc_id(pid, "receive_ping_request", time)
|
||||||
|
rpc("sync_player_stats", player_stats)
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "unreliable")
|
||||||
|
func receive_ping_request(server_time: int) -> void:
|
||||||
|
rpc_id(1, "receive_ping_response", server_time)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_remote", "unreliable")
|
||||||
|
func receive_ping_response(server_time: int) -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var sender_id = multiplayer.get_remote_sender_id()
|
||||||
|
var rtt = Time.get_ticks_msec() - server_time
|
||||||
|
if player_stats.has(sender_id):
|
||||||
|
player_stats[sender_id]["ping"] = rtt
|
||||||
|
|
||||||
|
# --- GAMEPLAY EVENTS ---
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func register_kill(victim_id: int, killer_id: int, weapon_name: String, custom_victim_name: String = "", assist_ids: Array = []) -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
if killer_id != 0 and killer_id != victim_id:
|
||||||
|
if player_stats.has(killer_id):
|
||||||
|
player_stats[killer_id]["kills"] += 1
|
||||||
|
|
||||||
|
if victim_id != -1:
|
||||||
|
if player_stats.has(victim_id):
|
||||||
|
player_stats[victim_id]["deaths"] += 1
|
||||||
|
|
||||||
|
for aid in assist_ids:
|
||||||
|
if player_stats.has(aid) and aid != killer_id and aid != victim_id:
|
||||||
|
player_stats[aid]["assists"] += 1
|
||||||
|
|
||||||
|
var v_name = player_stats[victim_id]["username"] if player_stats.has(victim_id) else (custom_victim_name if custom_victim_name != "" else "Unknown")
|
||||||
|
var k_name = player_stats[killer_id]["username"] if player_stats.has(killer_id) else "Unknown"
|
||||||
|
|
||||||
|
var v_color = player_stats[victim_id].get("color", "cccccc") if player_stats.has(victim_id) else "cccccc"
|
||||||
|
var k_color = player_stats[killer_id].get("color", "cccccc") if player_stats.has(killer_id) else "cccccc"
|
||||||
|
|
||||||
|
# Auto-fallback for singleplayer testing where player_stats might not be initialized
|
||||||
|
if not player_stats.has(killer_id) and killer_id == 1:
|
||||||
|
k_name = SettingsManager.username
|
||||||
|
k_color = "55aaff" # Default blueish for uninitialized local player
|
||||||
|
|
||||||
|
if victim_id == -1 and "Dummy" in custom_victim_name:
|
||||||
|
v_color = "ff5555" # Red for dummies
|
||||||
|
|
||||||
|
if killer_id == 0 or killer_id == victim_id:
|
||||||
|
k_name = "" # Suicide or world death
|
||||||
|
|
||||||
|
broadcast_killfeed.rpc(v_name, k_name, weapon_name, v_color, k_color)
|
||||||
|
sync_player_stats.rpc(player_stats)
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func broadcast_killfeed(victim: String, killer: String, weapon: String, v_color: String = "cccccc", k_color: String = "cccccc") -> void:
|
||||||
|
killfeed_event.emit(victim, killer, weapon, v_color, k_color)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bcq67g48h8hka
|
||||||
@@ -26,6 +26,7 @@ var default_bindings = {
|
|||||||
|
|
||||||
var current_bindings = {}
|
var current_bindings = {}
|
||||||
var show_debug_ui: bool = false
|
var show_debug_ui: bool = false
|
||||||
|
var username: String = "Player"
|
||||||
var mouse_sensitivity: float = 0.002
|
var mouse_sensitivity: float = 0.002
|
||||||
var ads_sensitivity: float = 0.001
|
var ads_sensitivity: float = 0.001
|
||||||
|
|
||||||
@@ -131,6 +132,7 @@ func _save_settings() -> void:
|
|||||||
var save_data = {
|
var save_data = {
|
||||||
"bindings": current_bindings,
|
"bindings": current_bindings,
|
||||||
"show_debug_ui": show_debug_ui,
|
"show_debug_ui": show_debug_ui,
|
||||||
|
"username": username,
|
||||||
"mouse_sensitivity": mouse_sensitivity,
|
"mouse_sensitivity": mouse_sensitivity,
|
||||||
"ads_sensitivity": ads_sensitivity,
|
"ads_sensitivity": ads_sensitivity,
|
||||||
"display_mode": display_mode,
|
"display_mode": display_mode,
|
||||||
@@ -165,12 +167,10 @@ func _load_settings() -> void:
|
|||||||
for action in loaded_bindings:
|
for action in loaded_bindings:
|
||||||
if current_bindings.has(action):
|
if current_bindings.has(action):
|
||||||
current_bindings[action] = loaded_bindings[action]
|
current_bindings[action] = loaded_bindings[action]
|
||||||
if data.has("show_debug_ui"):
|
if data.has("username"): username = data["username"]
|
||||||
show_debug_ui = data["show_debug_ui"]
|
if data.has("show_debug_ui"): show_debug_ui = data["show_debug_ui"]
|
||||||
if data.has("mouse_sensitivity"):
|
if data.has("mouse_sensitivity"): mouse_sensitivity = data["mouse_sensitivity"]
|
||||||
mouse_sensitivity = data["mouse_sensitivity"]
|
if data.has("ads_sensitivity"): ads_sensitivity = data["ads_sensitivity"]
|
||||||
if data.has("ads_sensitivity"):
|
|
||||||
ads_sensitivity = data["ads_sensitivity"]
|
|
||||||
if data.has("display_mode"): display_mode = data["display_mode"]
|
if data.has("display_mode"): display_mode = data["display_mode"]
|
||||||
if data.has("resolution_index"): resolution_index = data["resolution_index"]
|
if data.has("resolution_index"): resolution_index = data["resolution_index"]
|
||||||
if data.has("fps_cap"): fps_cap = data["fps_cap"]
|
if data.has("fps_cap"): fps_cap = data["fps_cap"]
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
|
if player and not player.multiplayer.is_server(): return
|
||||||
|
|
||||||
# Update coyote time
|
# Update coyote time
|
||||||
if on_ground:
|
if on_ground:
|
||||||
coyote_timer = params.coyote_time
|
coyote_timer = params.coyote_time
|
||||||
@@ -92,6 +94,9 @@ func _physics_process(delta: float) -> void:
|
|||||||
_try_start_grapple()
|
_try_start_grapple()
|
||||||
|
|
||||||
if is_grapple_shooting:
|
if is_grapple_shooting:
|
||||||
|
if not input_grapple:
|
||||||
|
is_grapple_shooting = false
|
||||||
|
else:
|
||||||
grapple_shoot_time += delta
|
grapple_shoot_time += delta
|
||||||
if grapple_shoot_time >= grapple_travel_time:
|
if grapple_shoot_time >= grapple_travel_time:
|
||||||
is_grapple_shooting = false
|
is_grapple_shooting = false
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ var health: float = 100.0
|
|||||||
var max_shield: float = 100.0
|
var max_shield: float = 100.0
|
||||||
var shield: float = 100.0
|
var shield: float = 100.0
|
||||||
var time_since_last_damage: float = 0.0
|
var time_since_last_damage: float = 0.0
|
||||||
|
var death_count: int = 0
|
||||||
var is_dead: bool = false
|
var is_dead: bool = false
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
@@ -26,10 +27,10 @@ var shield_label: Label
|
|||||||
var death_screen: Control
|
var death_screen: Control
|
||||||
|
|
||||||
var ragdoll_instance: Node3D
|
var ragdoll_instance: Node3D
|
||||||
|
|
||||||
# Audio
|
# Audio
|
||||||
var footstep_player: AudioStreamPlayer
|
var footstep_player: AudioStreamPlayer
|
||||||
var dash_player: AudioStreamPlayer
|
var dash_player: AudioStreamPlayer
|
||||||
|
var recent_attackers: Dictionary = {} # attacker_id: timestamp
|
||||||
var hit_player: AudioStreamPlayer
|
var hit_player: AudioStreamPlayer
|
||||||
var slide_player: AudioStreamPlayer
|
var slide_player: AudioStreamPlayer
|
||||||
var wallrun_player: AudioStreamPlayer
|
var wallrun_player: AudioStreamPlayer
|
||||||
@@ -48,6 +49,25 @@ 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
|
||||||
|
@export var synced_weapon_path: String = ""
|
||||||
|
@export var synced_grapple_point: Vector3 = Vector3.ZERO
|
||||||
|
@export var synced_is_grapple_shooting: bool = false
|
||||||
|
|
||||||
|
# For homing rocket
|
||||||
|
@export var synced_is_targeting: bool = false
|
||||||
|
@export var synced_homing_target_pos: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
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,17 +81,31 @@ 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")
|
||||||
|
|
||||||
|
_setup_audio()
|
||||||
|
_setup_grapple()
|
||||||
|
|
||||||
|
if is_multiplayer_authority():
|
||||||
_damage_layer = CanvasLayer.new()
|
_damage_layer = CanvasLayer.new()
|
||||||
add_child(_damage_layer)
|
add_child(_damage_layer)
|
||||||
|
|
||||||
_setup_audio()
|
|
||||||
_setup_hit_marker()
|
_setup_hit_marker()
|
||||||
_setup_grapple()
|
|
||||||
_setup_hud()
|
_setup_hud()
|
||||||
|
else:
|
||||||
|
call_deferred("_hide_remote_weapons")
|
||||||
|
|
||||||
set_process(true)
|
set_process(true)
|
||||||
|
if is_multiplayer_authority():
|
||||||
set_physics_process(true)
|
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()
|
||||||
footstep_player.bus = "SFX"
|
footstep_player.bus = "SFX"
|
||||||
@@ -218,14 +252,244 @@ func apply_impulse(force: Vector3) -> void:
|
|||||||
# Tiny upward bump helps move_and_slide detach from the floor
|
# Tiny upward bump helps move_and_slide detach from the floor
|
||||||
global_position.y += 0.1
|
global_position.y += 0.1
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func server_apply_impulse(force: Vector3) -> void:
|
||||||
|
if not multiplayer.is_server(): return
|
||||||
|
var sender = multiplayer.get_remote_sender_id()
|
||||||
|
if sender != 1 and sender != str(name).to_int(): return
|
||||||
|
apply_impulse(force)
|
||||||
|
|
||||||
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:
|
||||||
|
var attacker_id = 0
|
||||||
|
var weapon_name = "Killed"
|
||||||
|
|
||||||
|
if _source:
|
||||||
|
if _source.has_method("get_multiplayer_authority"):
|
||||||
|
attacker_id = _source.get_multiplayer_authority()
|
||||||
|
elif "owner_player" in _source and _source.owner_player:
|
||||||
|
attacker_id = int(str(_source.owner_player.name))
|
||||||
|
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:
|
||||||
|
# Try to find the actual weapon to spawn its custom projectile so it looks identical!
|
||||||
|
if camera:
|
||||||
|
var wman = camera.get_node_or_null("WeaponManager")
|
||||||
|
if wman:
|
||||||
|
var target_weapon = null
|
||||||
|
for w in wman.weapons.values():
|
||||||
|
if "weapon_name" in w and w.weapon_name == weapon_name:
|
||||||
|
target_weapon = w
|
||||||
|
break
|
||||||
|
if target_weapon and target_weapon.has_method("_spawn_custom_projectile"):
|
||||||
|
target_weapon._spawn_custom_projectile(origin, fire_dir)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generic fallback if weapon not found (e.g. joined late and weapons didn't build yet)
|
||||||
|
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:
|
||||||
|
# Generic rocket fallback
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if impulse.length_squared() > 0.01:
|
||||||
|
apply_impulse(impulse)
|
||||||
|
|
||||||
|
# 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_dead:
|
||||||
if is_instance_valid(ragdoll_instance):
|
if is_instance_valid(ragdoll_instance):
|
||||||
ragdoll_instance.apply_initial_velocities(Vector3.ZERO, impulse)
|
ragdoll_instance.apply_initial_velocities(Vector3.ZERO, impulse)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show damage number for self-damage
|
# Show damage number for the attacker
|
||||||
spawn_damage_number(amount, _hit_pos)
|
if not multiplayer.has_multiplayer_peer() or attacker_id == multiplayer.get_unique_id():
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
spawn_damage_number(amount, hit_pos)
|
||||||
|
else:
|
||||||
|
var my_node = get_parent().get_node_or_null(str(attacker_id))
|
||||||
|
if my_node and my_node.has_method("spawn_damage_number"):
|
||||||
|
my_node.spawn_damage_number(amount, hit_pos)
|
||||||
|
|
||||||
|
# Show damage indicator and screen shake for the victim
|
||||||
|
if is_multiplayer_authority() and amount > 0.0 and (not multiplayer.has_multiplayer_peer() or attacker_id != multiplayer.get_unique_id()):
|
||||||
|
_show_damage_indicator(hit_pos)
|
||||||
|
|
||||||
|
if attacker_id != 0:
|
||||||
|
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
|
||||||
|
|
||||||
time_since_last_damage = 0.0
|
time_since_last_damage = 0.0
|
||||||
|
|
||||||
@@ -243,6 +507,88 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
|||||||
health = 0.0
|
health = 0.0
|
||||||
die(impulse)
|
die(impulse)
|
||||||
|
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm and multiplayer.has_multiplayer_peer():
|
||||||
|
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)
|
||||||
|
|
||||||
|
var real_victim_name = "Player " + str(name)
|
||||||
|
if "player_name" in self and self.player_name != "":
|
||||||
|
real_victim_name = self.player_name
|
||||||
|
elif str(name).to_int() == 1:
|
||||||
|
real_victim_name = "Host"
|
||||||
|
|
||||||
|
if nm.has_method("register_kill"):
|
||||||
|
var victim_id = str(name).to_int()
|
||||||
|
if victim_id == 0:
|
||||||
|
victim_id = 1 # Fallback to host if something is weird
|
||||||
|
nm.register_kill.rpc(victim_id, killer_id, weapon_name, real_victim_name, assist_ids)
|
||||||
|
|
||||||
|
func _show_damage_indicator(hit_pos: Vector3) -> void:
|
||||||
|
if not _damage_layer: return
|
||||||
|
|
||||||
|
var indicator = Polygon2D.new()
|
||||||
|
indicator.polygon = PackedVector2Array([
|
||||||
|
Vector2(0, -100),
|
||||||
|
Vector2(-20, -75),
|
||||||
|
Vector2(20, -75)
|
||||||
|
])
|
||||||
|
indicator.color = Color(1.0, 0.0, 0.0, 0.8)
|
||||||
|
indicator.position = get_viewport().get_visible_rect().size / 2.0
|
||||||
|
|
||||||
|
var forward = -global_transform.basis.z
|
||||||
|
forward.y = 0.0
|
||||||
|
if forward.length_squared() > 0.01:
|
||||||
|
forward = forward.normalized()
|
||||||
|
var right = global_transform.basis.x
|
||||||
|
right.y = 0.0
|
||||||
|
right = right.normalized()
|
||||||
|
|
||||||
|
var dir_to_hit = (hit_pos - global_position)
|
||||||
|
dir_to_hit.y = 0.0
|
||||||
|
if dir_to_hit.length_squared() > 0.01:
|
||||||
|
dir_to_hit = dir_to_hit.normalized()
|
||||||
|
var angle = atan2(dir_to_hit.dot(right), dir_to_hit.dot(forward))
|
||||||
|
indicator.rotation = angle
|
||||||
|
|
||||||
|
_damage_layer.add_child(indicator)
|
||||||
|
var tween = create_tween()
|
||||||
|
tween.tween_property(indicator, "color:a", 0.0, 1.0)
|
||||||
|
tween.tween_callback(indicator.queue_free)
|
||||||
|
|
||||||
|
if camera:
|
||||||
|
var shake_tween = create_tween()
|
||||||
|
shake_tween.tween_property(camera, "h_offset", randf_range(-0.2, 0.2), 0.05)
|
||||||
|
shake_tween.parallel().tween_property(camera, "v_offset", randf_range(-0.2, 0.2), 0.05)
|
||||||
|
shake_tween.chain().tween_property(camera, "h_offset", 0.0, 0.05)
|
||||||
|
shake_tween.parallel().tween_property(camera, "v_offset", 0.0, 0.05)
|
||||||
|
|
||||||
|
func get_weapon_name() -> String:
|
||||||
|
if camera:
|
||||||
|
var wman = camera.get_node_or_null("WeaponManager")
|
||||||
|
if wman:
|
||||||
|
var slot = wman.get("active_slot")
|
||||||
|
if slot != null and wman.weapons.has(slot):
|
||||||
|
var w = wman.weapons[slot]
|
||||||
|
if w and w.has_method("get_weapon_name"):
|
||||||
|
return w.get_weapon_name()
|
||||||
|
elif w:
|
||||||
|
var p = ""
|
||||||
|
if w.has_meta("script_path"):
|
||||||
|
p = w.get_meta("script_path").get_file().get_basename()
|
||||||
|
elif w.get_script():
|
||||||
|
p = w.get_script().resource_path.get_file().get_basename()
|
||||||
|
else:
|
||||||
|
p = "Killed"
|
||||||
|
return p.capitalize()
|
||||||
|
return "Killed"
|
||||||
|
|
||||||
func _ensure_machine() -> MovementStateMachine:
|
func _ensure_machine() -> MovementStateMachine:
|
||||||
if is_instance_valid(_machine):
|
if is_instance_valid(_machine):
|
||||||
return _machine
|
return _machine
|
||||||
@@ -254,67 +600,34 @@ func _ensure_machine() -> MovementStateMachine:
|
|||||||
_machine = null
|
_machine = null
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
var _client_input_dir: Vector2 = Vector2.ZERO
|
||||||
|
var _client_wish_dir_world: Vector3 = Vector3.ZERO
|
||||||
|
var _client_jump: bool = false
|
||||||
|
var _client_jump_just_pressed: bool = false
|
||||||
|
var _client_crouch: bool = false
|
||||||
|
var _client_dash: bool = false
|
||||||
|
var _client_grapple: bool = false
|
||||||
|
var _client_grapple_just_pressed: bool = false
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "unreliable")
|
||||||
|
func server_receive_inputs(input_dir: Vector2, wish_dir: Vector3, jump: bool, jump_just: bool, crouch: bool, dash: bool, grapple: bool, grapple_just: bool) -> void:
|
||||||
|
if not multiplayer.is_server(): return
|
||||||
|
var sender = multiplayer.get_remote_sender_id()
|
||||||
|
if sender != str(name).to_int() and sender != 1: return # Accept from owner or host self
|
||||||
|
|
||||||
|
_client_input_dir = input_dir
|
||||||
|
_client_wish_dir_world = wish_dir
|
||||||
|
_client_jump = jump
|
||||||
|
_client_jump_just_pressed = jump_just
|
||||||
|
_client_crouch = crouch
|
||||||
|
_client_dash = dash
|
||||||
|
_client_grapple = grapple
|
||||||
|
_client_grapple_just_pressed = grapple_just
|
||||||
|
|
||||||
func _physics_process(_delta: float) -> void:
|
func _physics_process(_delta: float) -> void:
|
||||||
var speed = velocity.length()
|
# Local client captures input and sends it
|
||||||
var wind_factor = clampf((speed - 10.0) / 25.0, 0.0, 1.0)
|
if is_multiplayer_authority():
|
||||||
# Decibel scale is non-linear, so lerping from -80 means it stays inaudible for a long time.
|
|
||||||
# -40 db is practically silent for a background loop, -15 db is our max quiet wind.
|
|
||||||
wind_player.volume_db = lerpf(-40.0, -15.0, wind_factor)
|
|
||||||
if not wind_player.playing and wind_factor > 0.0:
|
|
||||||
wind_player.play()
|
|
||||||
|
|
||||||
var sm := _ensure_machine()
|
|
||||||
if not sm:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Poll input in _physics_process so it's synchronized with the state machine tick
|
|
||||||
sm.input_jump_just_pressed = Input.is_action_just_pressed("jump")
|
|
||||||
sm.input_jump_pressed = Input.is_action_pressed("jump")
|
|
||||||
sm.input_crouch = Input.is_action_pressed("crouch")
|
|
||||||
sm.input_dash = Input.is_action_just_pressed("dash")
|
|
||||||
|
|
||||||
sm.input_grapple = Input.is_action_pressed("grapple")
|
|
||||||
sm.input_grapple_just_pressed = Input.is_action_just_pressed("grapple")
|
|
||||||
|
|
||||||
# Update grapple rope visuals
|
|
||||||
if (sm.current_state == "grapple" or sm.is_grapple_shooting) and camera:
|
|
||||||
grapple_rope.visible = true
|
|
||||||
|
|
||||||
# For shoot, start at right side of camera slightly below and closer
|
|
||||||
var start_pos = camera.global_position + camera.global_transform.basis * Vector3(0.3, -0.3, -0.15)
|
|
||||||
var end_pos = sm.grapple_point
|
|
||||||
|
|
||||||
if sm.is_grapple_shooting and sm.grapple_travel_time > 0.0:
|
|
||||||
# Interpolate end_pos
|
|
||||||
var t = clampf(sm.grapple_shoot_time / sm.grapple_travel_time, 0.0, 1.0)
|
|
||||||
end_pos = start_pos.lerp(sm.grapple_point, t)
|
|
||||||
|
|
||||||
var dist = start_pos.distance_to(end_pos)
|
|
||||||
if dist > 0.01:
|
|
||||||
grapple_rope.global_position = (start_pos + end_pos) / 2.0
|
|
||||||
# Look at end point
|
|
||||||
var up_vec = Vector3.UP
|
|
||||||
var dir = (end_pos - start_pos).normalized()
|
|
||||||
if abs(dir.dot(up_vec)) > 0.99:
|
|
||||||
up_vec = Vector3.RIGHT
|
|
||||||
# Set rotation first, which resets scale
|
|
||||||
grapple_rope.transform.basis = Basis.looking_at(dir, up_vec) * Basis.from_euler(Vector3(PI/2.0, 0, 0))
|
|
||||||
# Then apply the scale
|
|
||||||
grapple_rope.scale = Vector3(1.0, dist, 1.0)
|
|
||||||
else:
|
|
||||||
if grapple_rope:
|
|
||||||
grapple_rope.visible = false
|
|
||||||
|
|
||||||
# Swing sound
|
|
||||||
if sm.current_state == "grapple" and sm.player.velocity.length() > 5.0:
|
|
||||||
grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -15.0, _delta * 10.0)
|
|
||||||
else:
|
|
||||||
grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -80.0, _delta * 15.0)
|
|
||||||
|
|
||||||
# Raw 2D input
|
|
||||||
var raw_input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
var raw_input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||||
|
|
||||||
# Transform input direction by player yaw so movement is camera-relative
|
|
||||||
var forward := -global_transform.basis.z
|
var forward := -global_transform.basis.z
|
||||||
forward.y = 0.0
|
forward.y = 0.0
|
||||||
forward = forward.normalized()
|
forward = forward.normalized()
|
||||||
@@ -326,8 +639,69 @@ func _physics_process(_delta: float) -> void:
|
|||||||
if world_dir.length_squared() > 1.0:
|
if world_dir.length_squared() > 1.0:
|
||||||
world_dir = world_dir.normalized()
|
world_dir = world_dir.normalized()
|
||||||
|
|
||||||
sm.input_dir = raw_input
|
var input_jump = Input.is_action_pressed("jump")
|
||||||
sm.wish_dir_world = world_dir
|
var input_jump_just = Input.is_action_just_pressed("jump")
|
||||||
|
var input_crouch = Input.is_action_pressed("crouch")
|
||||||
|
var input_dash = Input.is_action_just_pressed("dash")
|
||||||
|
var input_grapple = Input.is_action_pressed("grapple")
|
||||||
|
var input_grapple_just = Input.is_action_just_pressed("grapple")
|
||||||
|
|
||||||
|
server_receive_inputs.rpc_id(1, raw_input, world_dir, input_jump, input_jump_just, input_crouch, input_dash, input_grapple, input_grapple_just)
|
||||||
|
|
||||||
|
var speed = velocity.length()
|
||||||
|
var wind_factor = clampf((speed - 10.0) / 25.0, 0.0, 1.0)
|
||||||
|
wind_player.volume_db = lerpf(-40.0, -15.0, wind_factor)
|
||||||
|
if not wind_player.playing and wind_factor > 0.0:
|
||||||
|
wind_player.play()
|
||||||
|
|
||||||
|
var sm := _ensure_machine()
|
||||||
|
if not sm:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only the Server sets input values to the StateMachine for evaluation
|
||||||
|
if multiplayer.is_server():
|
||||||
|
sm.input_dir = _client_input_dir
|
||||||
|
sm.wish_dir_world = _client_wish_dir_world
|
||||||
|
sm.input_jump_pressed = _client_jump
|
||||||
|
sm.input_jump_just_pressed = _client_jump_just_pressed
|
||||||
|
sm.input_crouch = _client_crouch
|
||||||
|
sm.input_dash = _client_dash
|
||||||
|
sm.input_grapple = _client_grapple
|
||||||
|
sm.input_grapple_just_pressed = _client_grapple_just_pressed
|
||||||
|
|
||||||
|
# Reset one-frame actions
|
||||||
|
_client_jump_just_pressed = false
|
||||||
|
_client_dash = false
|
||||||
|
_client_grapple_just_pressed = false
|
||||||
|
|
||||||
|
# Update synced properties for the grapple
|
||||||
|
synced_grapple_point = sm.grapple_point
|
||||||
|
synced_is_grapple_shooting = sm.is_grapple_shooting
|
||||||
|
|
||||||
|
# Update grapple rope visuals (runs on all clients for smooth interpolation)
|
||||||
|
if (synced_movement_state == "grapple" or synced_is_grapple_shooting) and camera:
|
||||||
|
grapple_rope.visible = true
|
||||||
|
var start_pos = camera.global_position + camera.global_transform.basis * Vector3(0.3, -0.3, -0.15)
|
||||||
|
var end_pos = synced_grapple_point
|
||||||
|
var dist = start_pos.distance_to(end_pos)
|
||||||
|
if dist > 0.01:
|
||||||
|
grapple_rope.global_position = (start_pos + end_pos) / 2.0
|
||||||
|
var up_vec = Vector3.UP
|
||||||
|
var dir = (end_pos - start_pos).normalized()
|
||||||
|
if abs(dir.dot(up_vec)) > 0.99:
|
||||||
|
up_vec = Vector3.RIGHT
|
||||||
|
grapple_rope.transform.basis = Basis.looking_at(dir, up_vec) * Basis.from_euler(Vector3(PI/2.0, 0, 0))
|
||||||
|
grapple_rope.scale = Vector3(1.0, dist, 1.0)
|
||||||
|
else:
|
||||||
|
if grapple_rope:
|
||||||
|
grapple_rope.visible = false
|
||||||
|
|
||||||
|
if synced_movement_state == "grapple" and velocity.length() > 5.0:
|
||||||
|
grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -15.0, _delta * 10.0)
|
||||||
|
else:
|
||||||
|
grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -80.0, _delta * 15.0)
|
||||||
|
|
||||||
|
# Update humanoid model animation state (runs everywhere)
|
||||||
|
|
||||||
# Update humanoid model animation state
|
# Update humanoid model animation state
|
||||||
var humanoid = get_node_or_null("HumanoidModel")
|
var humanoid = get_node_or_null("HumanoidModel")
|
||||||
@@ -335,6 +709,11 @@ func _physics_process(_delta: float) -> void:
|
|||||||
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:
|
||||||
if ev == "chain_updated":
|
if ev == "chain_updated":
|
||||||
@@ -345,6 +724,31 @@ 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:
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Update UI
|
||||||
|
if is_instance_valid(health_bar):
|
||||||
|
health_bar.value = health
|
||||||
|
health_label.text = "%d / %d" % [ceil(health), max_health]
|
||||||
|
if is_instance_valid(shield_bar):
|
||||||
|
shield_bar.value = shield
|
||||||
|
shield_label.text = "%d / %d" % [ceil(shield), max_shield]
|
||||||
|
|
||||||
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):
|
||||||
@@ -354,6 +758,11 @@ func _process(delta: float) -> void:
|
|||||||
if is_instance_valid(spring_arm):
|
if is_instance_valid(spring_arm):
|
||||||
# Follow torso position exactly to avoid lerp bobbing
|
# Follow torso position exactly to avoid lerp bobbing
|
||||||
spring_arm.global_position = torso.global_position
|
spring_arm.global_position = torso.global_position
|
||||||
|
|
||||||
|
if death_screen and death_screen.visible:
|
||||||
|
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) or Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or Input.is_key_pressed(KEY_SPACE) or Input.is_key_pressed(KEY_ENTER) or Input.is_key_pressed(KEY_ESCAPE) == false and (Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("jump") or Input.is_action_just_pressed("fire")):
|
||||||
|
_on_respawn_pressed()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Shield recharge logic
|
# Shield recharge logic
|
||||||
@@ -363,14 +772,6 @@ func _process(delta: float) -> void:
|
|||||||
if shield > max_shield:
|
if shield > max_shield:
|
||||||
shield = max_shield
|
shield = max_shield
|
||||||
|
|
||||||
# Update UI
|
|
||||||
if is_instance_valid(health_bar):
|
|
||||||
health_bar.value = health
|
|
||||||
health_label.text = "%d / %d" % [ceil(health), max_health]
|
|
||||||
if is_instance_valid(shield_bar):
|
|
||||||
shield_bar.value = shield
|
|
||||||
shield_label.text = "%d / %d" % [ceil(shield), max_shield]
|
|
||||||
|
|
||||||
func _setup_hud() -> void:
|
func _setup_hud() -> void:
|
||||||
var margin = MarginContainer.new()
|
var margin = MarginContainer.new()
|
||||||
margin.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
margin.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
||||||
@@ -381,6 +782,13 @@ func _setup_hud() -> void:
|
|||||||
var vbox = VBoxContainer.new()
|
var vbox = VBoxContainer.new()
|
||||||
margin.add_child(vbox)
|
margin.add_child(vbox)
|
||||||
|
|
||||||
|
# Add Match HUD overlay
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
var match_hud_scene = load("res://ui/match_hud.tscn")
|
||||||
|
if match_hud_scene:
|
||||||
|
var match_hud = match_hud_scene.instantiate()
|
||||||
|
add_child(match_hud)
|
||||||
|
|
||||||
# Shield Bar (Top)
|
# Shield Bar (Top)
|
||||||
var shield_box = VBoxContainer.new()
|
var shield_box = VBoxContainer.new()
|
||||||
var s_title = Label.new()
|
var s_title = Label.new()
|
||||||
@@ -458,12 +866,17 @@ func _setup_hud() -> void:
|
|||||||
d_title.add_theme_color_override("font_color", Color(1.0, 0.2, 0.2))
|
d_title.add_theme_color_override("font_color", Color(1.0, 0.2, 0.2))
|
||||||
d_vbox.add_child(d_title)
|
d_vbox.add_child(d_title)
|
||||||
|
|
||||||
var respawn_btn = Button.new()
|
var respawn_label = Label.new()
|
||||||
respawn_btn.text = "REBOOT"
|
respawn_label.text = "PRESS ANY KEY TO REBOOT"
|
||||||
respawn_btn.add_theme_font_size_override("font_size", 24)
|
respawn_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
respawn_btn.pressed.connect(_on_respawn_pressed)
|
respawn_label.add_theme_font_size_override("font_size", 24)
|
||||||
d_vbox.add_child(respawn_btn)
|
d_vbox.add_child(respawn_label)
|
||||||
|
|
||||||
|
var tw = create_tween().set_loops()
|
||||||
|
tw.tween_property(respawn_label, "modulate:a", 0.2, 0.8)
|
||||||
|
tw.tween_property(respawn_label, "modulate:a", 1.0, 0.8)
|
||||||
|
|
||||||
|
if is_multiplayer_authority():
|
||||||
_damage_layer.add_child(death_screen)
|
_damage_layer.add_child(death_screen)
|
||||||
|
|
||||||
func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
||||||
@@ -485,17 +898,16 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
|||||||
col.set_deferred("disabled", true)
|
col.set_deferred("disabled", true)
|
||||||
|
|
||||||
# Spawn true physics ragdoll
|
# Spawn true physics ragdoll
|
||||||
|
death_count += 1
|
||||||
var ragdoll = load("res://characters/procedural_ragdoll.gd").new()
|
var ragdoll = load("res://characters/procedural_ragdoll.gd").new()
|
||||||
|
ragdoll.name = "Ragdoll_%s_%d" % [self.name, death_count]
|
||||||
ragdoll_instance = ragdoll
|
ragdoll_instance = ragdoll
|
||||||
get_tree().current_scene.add_child(ragdoll)
|
get_tree().current_scene.add_child(ragdoll)
|
||||||
ragdoll.global_transform = global_transform
|
ragdoll.global_transform = global_transform
|
||||||
ragdoll.build_ragdoll(Color(0.2, 0.4, 0.8)) # Blueish player color
|
ragdoll.build_ragdoll(Color(0.2, 0.4, 0.8)) # Blueish player color
|
||||||
|
|
||||||
# Wait a frame for physics to initialize then apply velocity
|
# Wait a frame for physics to initialize then apply velocity
|
||||||
get_tree().create_timer(0.01).timeout.connect(func():
|
get_tree().create_timer(0.01).timeout.connect(_apply_ragdoll_velocity.bind(ragdoll, velocity, impulse))
|
||||||
if is_instance_valid(ragdoll):
|
|
||||||
ragdoll.apply_initial_velocities(velocity, impulse)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Move camera to 3rd person view using SpringArm3D
|
# Move camera to 3rd person view using SpringArm3D
|
||||||
if is_instance_valid(camera):
|
if is_instance_valid(camera):
|
||||||
@@ -503,6 +915,7 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
|||||||
var wman = camera.get_node_or_null("WeaponManager")
|
var wman = camera.get_node_or_null("WeaponManager")
|
||||||
if wman:
|
if wman:
|
||||||
wman.visible = false
|
wman.visible = false
|
||||||
|
wman.process_mode = Node.PROCESS_MODE_DISABLED
|
||||||
|
|
||||||
var cam_trans = camera.global_transform
|
var cam_trans = camera.global_transform
|
||||||
camera.get_parent().remove_child(camera)
|
camera.get_parent().remove_child(camera)
|
||||||
@@ -555,9 +968,109 @@ 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
|
||||||
|
if is_multiplayer_authority():
|
||||||
death_screen.visible = true
|
death_screen.visible = true
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
set_process_input(true)
|
||||||
|
|
||||||
|
func _apply_ragdoll_velocity(ragdoll, l_vel: Vector3, imp: Vector3) -> void:
|
||||||
|
if is_instance_valid(ragdoll) and ragdoll.has_method("apply_initial_velocities"):
|
||||||
|
ragdoll.apply_initial_velocities(l_vel, imp)
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not is_multiplayer_authority(): return
|
||||||
|
if is_dead and death_screen and death_screen.visible:
|
||||||
|
if event is InputEventKey or event is InputEventMouseButton or event is InputEventJoypadButton:
|
||||||
|
if event.is_pressed() and not event.is_echo():
|
||||||
|
_on_respawn_pressed()
|
||||||
|
|
||||||
func _on_respawn_pressed() -> void:
|
func _on_respawn_pressed() -> void:
|
||||||
# Reload scene
|
if death_screen:
|
||||||
get_tree().reload_current_scene()
|
death_screen.visible = false
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var spawn_pos = Vector3(randf_range(-5, 5), 5, randf_range(-5, 5))
|
||||||
|
rpc_respawn.rpc(spawn_pos)
|
||||||
|
else:
|
||||||
|
rpc_request_respawn.rpc_id(1)
|
||||||
|
|
||||||
|
var _last_respawn_request_time: float = 0.0
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func rpc_request_respawn() -> void:
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
var sender_id = multiplayer.get_remote_sender_id()
|
||||||
|
if sender_id != 1 and sender_id != str(name).to_int():
|
||||||
|
return
|
||||||
|
|
||||||
|
var now = Time.get_ticks_msec() / 1000.0
|
||||||
|
if now - _last_respawn_request_time < 0.5:
|
||||||
|
return
|
||||||
|
_last_respawn_request_time = now
|
||||||
|
|
||||||
|
if is_dead:
|
||||||
|
var spawn_pos = Vector3(randf_range(-5, 5), 5, randf_range(-5, 5))
|
||||||
|
rpc_respawn.rpc(spawn_pos)
|
||||||
|
else:
|
||||||
|
# Deal 9999 damage to trigger the full death sequence locally and over the network
|
||||||
|
take_damage(9999, global_position, null, Vector3.ZERO)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func rpc_respawn(spawn_pos: Vector3) -> void:
|
||||||
|
if is_dead:
|
||||||
|
is_dead = false
|
||||||
|
health = max_health
|
||||||
|
shield = max_shield
|
||||||
|
time_since_last_damage = 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()
|
||||||
|
|
||||||
|
visible = true
|
||||||
|
process_mode = Node.PROCESS_MODE_INHERIT
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Server sets the actual position to sync to everyone
|
||||||
|
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||||
|
position = spawn_pos
|
||||||
|
velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
# Local client resets UI and rebuilds weapons for their view
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
if death_screen:
|
||||||
|
death_screen.visible = false
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -20,6 +20,7 @@ config/icon="res://icon.svg"
|
|||||||
SettingsManager="*res://globals/settings_manager.gd"
|
SettingsManager="*res://globals/settings_manager.gd"
|
||||||
LoadoutManager="*res://globals/loadout_manager.gd"
|
LoadoutManager="*res://globals/loadout_manager.gd"
|
||||||
PauseMenu="*res://ui/pause_menu.gd"
|
PauseMenu="*res://ui/pause_menu.gd"
|
||||||
|
NetworkManager="*res://globals/network_manager.gd"
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
@@ -88,6 +89,11 @@ grapple={
|
|||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
scoreboard={
|
||||||
|
"deadzone": 0.0,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
var nm = load("res://globals/network_manager.gd").new()
|
||||||
|
nm.name = "NetworkManager"
|
||||||
|
root.add_child(nm)
|
||||||
|
|
||||||
|
var hud = load("res://ui/match_hud.gd").new()
|
||||||
|
hud.name = "MatchHUD"
|
||||||
|
root.add_child(hud)
|
||||||
|
|
||||||
|
# Mock network manager state
|
||||||
|
nm.match_active = true
|
||||||
|
nm._init_player_stats(1, "LocalTester")
|
||||||
|
|
||||||
|
# Try to call register_kill
|
||||||
|
nm.register_kill.rpc(-1, 1, "Rifle", "Target Dummy")
|
||||||
|
|
||||||
|
# Process one frame
|
||||||
|
call_deferred("quit")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://clrd47khl65oq
|
||||||
+270
-8
@@ -2,10 +2,18 @@ extends Control
|
|||||||
|
|
||||||
var _ui_vbox: VBoxContainer
|
var _ui_vbox: VBoxContainer
|
||||||
var _level_selector_panel: VBoxContainer
|
var _level_selector_panel: VBoxContainer
|
||||||
|
var _multiplayer_panel: VBoxContainer
|
||||||
|
var _direct_connect_panel: VBoxContainer
|
||||||
|
var _host_lobby_panel: Control
|
||||||
|
var _host_lobby_players_list: ItemList
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
|
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm:
|
||||||
|
nm.connection_succeeded.connect(_on_connection_succeeded)
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# BACKGROUND: 3D DIORAMA (Sliding Player)
|
# BACKGROUND: 3D DIORAMA (Sliding Player)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -97,21 +105,193 @@ func _ready() -> void:
|
|||||||
|
|
||||||
_add_button(_level_selector_panel, "Back", _on_back_pressed)
|
_add_button(_level_selector_panel, "Back", _on_back_pressed)
|
||||||
|
|
||||||
func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient) -> void:
|
# ==========================================
|
||||||
|
# MULTIPLAYER PANEL
|
||||||
|
# ==========================================
|
||||||
|
_multiplayer_panel = VBoxContainer.new()
|
||||||
|
_multiplayer_panel.add_theme_constant_override("separation", 20)
|
||||||
|
_multiplayer_panel.set_anchor_and_offset(SIDE_LEFT, 0.0, 50)
|
||||||
|
_multiplayer_panel.set_anchor_and_offset(SIDE_TOP, 0.0, 50)
|
||||||
|
_multiplayer_panel.set_anchor_and_offset(SIDE_RIGHT, 0.0, 600)
|
||||||
|
_multiplayer_panel.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50)
|
||||||
|
_multiplayer_panel.hide()
|
||||||
|
add_child(_multiplayer_panel)
|
||||||
|
|
||||||
|
var mp_title = Label.new()
|
||||||
|
mp_title.text = "Multiplayer"
|
||||||
|
mp_title.add_theme_font_size_override("font_size", 64)
|
||||||
|
mp_title.add_theme_color_override("font_color", Color(1, 1, 1))
|
||||||
|
mp_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||||
|
mp_title.add_theme_constant_override("outline_size", 8)
|
||||||
|
_multiplayer_panel.add_child(mp_title)
|
||||||
|
|
||||||
|
var mp_spacer = Control.new()
|
||||||
|
mp_spacer.custom_minimum_size = Vector2(0, 20)
|
||||||
|
_multiplayer_panel.add_child(mp_spacer)
|
||||||
|
|
||||||
|
_add_button(_multiplayer_panel, "Host Game", _on_host_pressed)
|
||||||
|
_add_button(_multiplayer_panel, "Direct Connect", _on_direct_connect_pressed)
|
||||||
|
_add_button(_multiplayer_panel, "Back", _on_multiplayer_back_pressed)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# DIRECT CONNECT PANEL
|
||||||
|
# ==========================================
|
||||||
|
_direct_connect_panel = VBoxContainer.new()
|
||||||
|
_direct_connect_panel.add_theme_constant_override("separation", 20)
|
||||||
|
_direct_connect_panel.set_anchor_and_offset(SIDE_LEFT, 0.0, 50)
|
||||||
|
_direct_connect_panel.set_anchor_and_offset(SIDE_TOP, 0.0, 50)
|
||||||
|
_direct_connect_panel.set_anchor_and_offset(SIDE_RIGHT, 0.0, 600)
|
||||||
|
_direct_connect_panel.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50)
|
||||||
|
_direct_connect_panel.hide()
|
||||||
|
add_child(_direct_connect_panel)
|
||||||
|
|
||||||
|
var dc_title = Label.new()
|
||||||
|
dc_title.text = "Direct Connect"
|
||||||
|
dc_title.add_theme_font_size_override("font_size", 64)
|
||||||
|
dc_title.add_theme_color_override("font_color", Color(1, 1, 1))
|
||||||
|
dc_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||||
|
dc_title.add_theme_constant_override("outline_size", 8)
|
||||||
|
_direct_connect_panel.add_child(dc_title)
|
||||||
|
|
||||||
|
var dc_ip_label = Label.new()
|
||||||
|
dc_ip_label.text = "IP Address:"
|
||||||
|
dc_ip_label.add_theme_font_size_override("font_size", 24)
|
||||||
|
_direct_connect_panel.add_child(dc_ip_label)
|
||||||
|
|
||||||
|
var dc_ip_input = LineEdit.new()
|
||||||
|
dc_ip_input.text = "127.0.0.1"
|
||||||
|
dc_ip_input.name = "IPInput"
|
||||||
|
dc_ip_input.add_theme_font_size_override("font_size", 24)
|
||||||
|
dc_ip_input.custom_minimum_size = Vector2(300, 50)
|
||||||
|
_direct_connect_panel.add_child(dc_ip_input)
|
||||||
|
|
||||||
|
var dc_port_label = Label.new()
|
||||||
|
dc_port_label.text = "Port:"
|
||||||
|
dc_port_label.add_theme_font_size_override("font_size", 24)
|
||||||
|
_direct_connect_panel.add_child(dc_port_label)
|
||||||
|
|
||||||
|
var dc_port_input = LineEdit.new()
|
||||||
|
dc_port_input.text = "31415" # Default Port
|
||||||
|
dc_port_input.name = "PortInput"
|
||||||
|
dc_port_input.add_theme_font_size_override("font_size", 24)
|
||||||
|
dc_port_input.custom_minimum_size = Vector2(300, 50)
|
||||||
|
_direct_connect_panel.add_child(dc_port_input)
|
||||||
|
|
||||||
|
var dc_spacer = Control.new()
|
||||||
|
dc_spacer.custom_minimum_size = Vector2(0, 20)
|
||||||
|
_direct_connect_panel.add_child(dc_spacer)
|
||||||
|
|
||||||
|
_add_button(_direct_connect_panel, "Connect", _on_connect_confirm_pressed)
|
||||||
|
_add_button(_direct_connect_panel, "Back", _on_dc_back_pressed)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# HOST LOBBY PANEL
|
||||||
|
# ==========================================
|
||||||
|
_host_lobby_panel = Control.new()
|
||||||
|
_host_lobby_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_host_lobby_panel.hide()
|
||||||
|
add_child(_host_lobby_panel)
|
||||||
|
|
||||||
|
var hl_hbox = HBoxContainer.new()
|
||||||
|
hl_hbox.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
hl_hbox.add_theme_constant_override("separation", 50)
|
||||||
|
_host_lobby_panel.add_child(hl_hbox)
|
||||||
|
|
||||||
|
# Left Side (Level Selector)
|
||||||
|
var hl_left = VBoxContainer.new()
|
||||||
|
hl_left.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
hl_left.set_anchor_and_offset(SIDE_LEFT, 0.0, 50)
|
||||||
|
hl_left.set_anchor_and_offset(SIDE_TOP, 0.0, 50)
|
||||||
|
hl_left.add_theme_constant_override("separation", 20)
|
||||||
|
|
||||||
|
var hl_margin = MarginContainer.new()
|
||||||
|
hl_margin.add_theme_constant_override("margin_left", 50)
|
||||||
|
hl_margin.add_theme_constant_override("margin_top", 50)
|
||||||
|
hl_margin.add_theme_constant_override("margin_bottom", 50)
|
||||||
|
hl_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
hl_hbox.add_child(hl_margin)
|
||||||
|
hl_margin.add_child(hl_left)
|
||||||
|
|
||||||
|
var hl_title = Label.new()
|
||||||
|
hl_title.text = "Host Lobby - Select Level"
|
||||||
|
hl_title.add_theme_font_size_override("font_size", 64)
|
||||||
|
hl_title.add_theme_color_override("font_color", Color(1, 1, 1))
|
||||||
|
hl_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||||
|
hl_title.add_theme_constant_override("outline_size", 8)
|
||||||
|
hl_left.add_child(hl_title)
|
||||||
|
|
||||||
|
var hl_cards = HBoxContainer.new()
|
||||||
|
hl_cards.add_theme_constant_override("separation", 20)
|
||||||
|
hl_left.add_child(hl_cards)
|
||||||
|
|
||||||
|
_build_level_card(hl_cards, "Dust 2", "res://scenes/dust2_level/dust2_level.tscn", d2_gradient, true, Vector2(200, 250))
|
||||||
|
_build_level_card(hl_cards, "Test Level", "res://scenes/test_level/test_level.tscn", tl_gradient, true, Vector2(200, 250))
|
||||||
|
|
||||||
|
var mode_label = Label.new()
|
||||||
|
mode_label.text = "Gamemode"
|
||||||
|
mode_label.add_theme_font_size_override("font_size", 32)
|
||||||
|
hl_left.add_child(mode_label)
|
||||||
|
|
||||||
|
var _gamemode_opt = OptionButton.new()
|
||||||
|
_gamemode_opt.add_theme_font_size_override("font_size", 24)
|
||||||
|
_gamemode_opt.add_item("Deathmatch", 0)
|
||||||
|
_gamemode_opt.add_to_group("host_only_ui")
|
||||||
|
_gamemode_opt.item_selected.connect(func(index: int):
|
||||||
|
if get_node_or_null("/root/NetworkManager"):
|
||||||
|
get_node("/root/NetworkManager").current_gamemode = _gamemode_opt.get_item_text(index)
|
||||||
|
)
|
||||||
|
hl_left.add_child(_gamemode_opt)
|
||||||
|
|
||||||
|
var hl_spacer = Control.new()
|
||||||
|
hl_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
hl_left.add_child(hl_spacer)
|
||||||
|
|
||||||
|
_add_button(hl_left, "Disband & Back", _on_lobby_back_pressed)
|
||||||
|
|
||||||
|
# Right Side (Player List)
|
||||||
|
var hl_right = VBoxContainer.new()
|
||||||
|
hl_right.custom_minimum_size = Vector2(400, 0)
|
||||||
|
var hl_right_margin = MarginContainer.new()
|
||||||
|
hl_right_margin.add_theme_constant_override("margin_top", 50)
|
||||||
|
hl_right_margin.add_theme_constant_override("margin_right", 50)
|
||||||
|
hl_right_margin.add_theme_constant_override("margin_bottom", 50)
|
||||||
|
hl_hbox.add_child(hl_right_margin)
|
||||||
|
hl_right_margin.add_child(hl_right)
|
||||||
|
|
||||||
|
var pl_title = Label.new()
|
||||||
|
pl_title.text = "Players Connected"
|
||||||
|
pl_title.add_theme_font_size_override("font_size", 48)
|
||||||
|
pl_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||||
|
pl_title.add_theme_constant_override("outline_size", 8)
|
||||||
|
hl_right.add_child(pl_title)
|
||||||
|
|
||||||
|
var port_label = Label.new()
|
||||||
|
if get_node_or_null("/root/NetworkManager"):
|
||||||
|
port_label.text = "Port: " + str(get_node("/root/NetworkManager").DEFAULT_PORT)
|
||||||
|
port_label.add_theme_font_size_override("font_size", 24)
|
||||||
|
port_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||||
|
hl_right.add_child(port_label)
|
||||||
|
|
||||||
|
_host_lobby_players_list = ItemList.new()
|
||||||
|
_host_lobby_players_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
_host_lobby_players_list.add_theme_font_size_override("font_size", 24)
|
||||||
|
hl_right.add_child(_host_lobby_players_list)
|
||||||
|
|
||||||
|
func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient, is_multiplayer: bool = false, card_size: Vector2 = Vector2(300, 400)) -> void:
|
||||||
var btn = TextureButton.new()
|
var btn = TextureButton.new()
|
||||||
btn.custom_minimum_size = Vector2(300, 400)
|
btn.custom_minimum_size = card_size
|
||||||
|
|
||||||
var tex = GradientTexture2D.new()
|
var tex = GradientTexture2D.new()
|
||||||
tex.gradient = gradient
|
tex.gradient = gradient
|
||||||
tex.width = 300
|
tex.width = int(card_size.x)
|
||||||
tex.height = 400
|
tex.height = int(card_size.y)
|
||||||
tex.fill_to = Vector2(1, 1)
|
tex.fill_to = Vector2(1, 1)
|
||||||
btn.texture_normal = tex
|
btn.texture_normal = tex
|
||||||
|
|
||||||
var tex_hover = GradientTexture2D.new()
|
var tex_hover = GradientTexture2D.new()
|
||||||
tex_hover.gradient = gradient
|
tex_hover.gradient = gradient
|
||||||
tex_hover.width = 300
|
tex_hover.width = int(card_size.x)
|
||||||
tex_hover.height = 400
|
tex_hover.height = int(card_size.y)
|
||||||
tex_hover.fill_to = Vector2(1, 1)
|
tex_hover.fill_to = Vector2(1, 1)
|
||||||
tex_hover.use_hdr = true # Slight bright effect on hover
|
tex_hover.use_hdr = true # Slight bright effect on hover
|
||||||
btn.texture_hover = tex_hover
|
btn.texture_hover = tex_hover
|
||||||
@@ -137,7 +317,16 @@ func _build_level_card(parent: Container, title: String, scene_path: String, gra
|
|||||||
lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
overlay.add_child(lbl)
|
overlay.add_child(lbl)
|
||||||
|
|
||||||
btn.pressed.connect(func(): get_tree().change_scene_to_file(scene_path))
|
if is_multiplayer:
|
||||||
|
btn.add_to_group("host_only_ui")
|
||||||
|
btn.pressed.connect(func(): get_node("/root/NetworkManager").rpc_load_level.rpc(scene_path))
|
||||||
|
else:
|
||||||
|
btn.pressed.connect(func():
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm:
|
||||||
|
nm.start_singleplayer_match("Deathmatch")
|
||||||
|
get_tree().change_scene_to_file(scene_path)
|
||||||
|
)
|
||||||
|
|
||||||
parent.add_child(btn)
|
parent.add_child(btn)
|
||||||
|
|
||||||
@@ -159,7 +348,80 @@ func _on_singleplayer_pressed() -> void:
|
|||||||
_level_selector_panel.show()
|
_level_selector_panel.show()
|
||||||
|
|
||||||
func _on_multiplayer_pressed() -> void:
|
func _on_multiplayer_pressed() -> void:
|
||||||
print("Multiplayer not implemented yet.")
|
_ui_vbox.hide()
|
||||||
|
_multiplayer_panel.show()
|
||||||
|
|
||||||
|
func _on_multiplayer_back_pressed() -> void:
|
||||||
|
_multiplayer_panel.hide()
|
||||||
|
_ui_vbox.show()
|
||||||
|
|
||||||
|
func _on_host_pressed() -> void:
|
||||||
|
var nm = get_node("/root/NetworkManager")
|
||||||
|
if nm.host_game() == OK:
|
||||||
|
_on_connection_succeeded()
|
||||||
|
|
||||||
|
func _on_direct_connect_pressed() -> void:
|
||||||
|
_multiplayer_panel.hide()
|
||||||
|
_direct_connect_panel.show()
|
||||||
|
|
||||||
|
func _on_dc_back_pressed() -> void:
|
||||||
|
_direct_connect_panel.hide()
|
||||||
|
_multiplayer_panel.show()
|
||||||
|
|
||||||
|
func _on_connect_confirm_pressed() -> void:
|
||||||
|
var ip = _direct_connect_panel.get_node("IPInput").text
|
||||||
|
var port = _direct_connect_panel.get_node("PortInput").text.to_int()
|
||||||
|
get_node("/root/NetworkManager").join_game(ip, port)
|
||||||
|
|
||||||
|
func _on_connection_succeeded() -> void:
|
||||||
|
_direct_connect_panel.hide()
|
||||||
|
_multiplayer_panel.hide()
|
||||||
|
_host_lobby_panel.show()
|
||||||
|
|
||||||
|
var nm = get_node("/root/NetworkManager")
|
||||||
|
var is_host = nm.multiplayer.is_server()
|
||||||
|
for node in get_tree().get_nodes_in_group("host_only_ui"):
|
||||||
|
if node is BaseButton:
|
||||||
|
node.disabled = not is_host
|
||||||
|
|
||||||
|
if not nm.player_connected.is_connected(_on_network_player_connected):
|
||||||
|
nm.player_connected.connect(_on_network_player_connected)
|
||||||
|
nm.player_disconnected.connect(_on_network_player_disconnected)
|
||||||
|
if not nm.stats_updated.is_connected(_update_lobby_players):
|
||||||
|
nm.stats_updated.connect(_update_lobby_players)
|
||||||
|
|
||||||
|
_update_lobby_players()
|
||||||
|
|
||||||
|
func _on_lobby_back_pressed() -> void:
|
||||||
|
_host_lobby_panel.hide()
|
||||||
|
_multiplayer_panel.show()
|
||||||
|
var nm = get_node("/root/NetworkManager")
|
||||||
|
nm.disconnect_game()
|
||||||
|
if nm.player_connected.is_connected(_on_network_player_connected):
|
||||||
|
nm.player_connected.disconnect(_on_network_player_connected)
|
||||||
|
nm.player_disconnected.disconnect(_on_network_player_disconnected)
|
||||||
|
if nm.stats_updated.is_connected(_update_lobby_players):
|
||||||
|
nm.stats_updated.disconnect(_update_lobby_players)
|
||||||
|
|
||||||
|
func _on_network_player_connected(_id: int) -> void:
|
||||||
|
_update_lobby_players()
|
||||||
|
|
||||||
|
func _on_network_player_disconnected(_id: int) -> void:
|
||||||
|
_update_lobby_players()
|
||||||
|
|
||||||
|
func _update_lobby_players() -> void:
|
||||||
|
if not _host_lobby_players_list: return
|
||||||
|
_host_lobby_players_list.clear()
|
||||||
|
var nm = get_node("/root/NetworkManager")
|
||||||
|
|
||||||
|
for pid in nm.player_stats.keys():
|
||||||
|
var p_name = nm.player_stats[pid].get("username", "Unknown")
|
||||||
|
var p_ping = nm.player_stats[pid].get("ping", 0)
|
||||||
|
|
||||||
|
if pid == 1:
|
||||||
|
_host_lobby_players_list.add_item("%s (Host) - %sms" % [p_name, p_ping])
|
||||||
|
else:
|
||||||
|
_host_lobby_players_list.add_item("%s - %sms" % [p_name, p_ping])
|
||||||
|
|
||||||
func _on_loadouts_pressed() -> void:
|
func _on_loadouts_pressed() -> void:
|
||||||
if has_node("/root/PauseMenu"):
|
if has_node("/root/PauseMenu"):
|
||||||
|
|||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
var _nm: Node
|
||||||
|
|
||||||
|
# Top HUD
|
||||||
|
var _time_label: Label
|
||||||
|
var _local_kills_label: Label
|
||||||
|
var _leader_label: Label
|
||||||
|
|
||||||
|
# Killfeed
|
||||||
|
var _killfeed_vbox: VBoxContainer
|
||||||
|
|
||||||
|
# Scoreboard
|
||||||
|
var _scoreboard_panel: PanelContainer
|
||||||
|
var _scoreboard_grid: GridContainer
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
|
||||||
|
# Top HUD
|
||||||
|
var top_hbox = HBoxContainer.new()
|
||||||
|
top_hbox.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||||
|
top_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
top_hbox.add_theme_constant_override("separation", 50)
|
||||||
|
var margin = MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_top", 20)
|
||||||
|
margin.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||||
|
margin.add_child(top_hbox)
|
||||||
|
add_child(margin)
|
||||||
|
|
||||||
|
_time_label = Label.new()
|
||||||
|
_time_label.add_theme_font_size_override("font_size", 48)
|
||||||
|
_time_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
_time_label.add_theme_constant_override("outline_size", 8)
|
||||||
|
top_hbox.add_child(_time_label)
|
||||||
|
|
||||||
|
_local_kills_label = Label.new()
|
||||||
|
_local_kills_label.text = "0 Kills"
|
||||||
|
_local_kills_label.add_theme_font_size_override("font_size", 64)
|
||||||
|
_local_kills_label.add_theme_color_override("font_color", Color(1, 0.8, 0.2))
|
||||||
|
_local_kills_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
_local_kills_label.add_theme_constant_override("outline_size", 12)
|
||||||
|
top_hbox.add_child(_local_kills_label)
|
||||||
|
|
||||||
|
_leader_label = Label.new()
|
||||||
|
_leader_label.text = "Leader: None"
|
||||||
|
_leader_label.add_theme_font_size_override("font_size", 32)
|
||||||
|
_leader_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
_leader_label.add_theme_constant_override("outline_size", 8)
|
||||||
|
top_hbox.add_child(_leader_label)
|
||||||
|
|
||||||
|
# Killfeed
|
||||||
|
var kf_margin = MarginContainer.new()
|
||||||
|
kf_margin.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
|
||||||
|
kf_margin.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||||
|
kf_margin.add_theme_constant_override("margin_top", 20)
|
||||||
|
kf_margin.add_theme_constant_override("margin_right", 20)
|
||||||
|
add_child(kf_margin)
|
||||||
|
|
||||||
|
_killfeed_vbox = VBoxContainer.new()
|
||||||
|
_killfeed_vbox.alignment = BoxContainer.ALIGNMENT_END
|
||||||
|
kf_margin.add_child(_killfeed_vbox)
|
||||||
|
|
||||||
|
# Scoreboard Overlay
|
||||||
|
var center_container = CenterContainer.new()
|
||||||
|
center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
add_child(center_container)
|
||||||
|
|
||||||
|
_scoreboard_panel = PanelContainer.new()
|
||||||
|
_scoreboard_panel.custom_minimum_size = Vector2(800, 600)
|
||||||
|
var sb_style = StyleBoxFlat.new()
|
||||||
|
sb_style.bg_color = Color(0, 0, 0, 0.8)
|
||||||
|
_scoreboard_panel.add_theme_stylebox_override("panel", sb_style)
|
||||||
|
_scoreboard_panel.hide()
|
||||||
|
center_container.add_child(_scoreboard_panel)
|
||||||
|
|
||||||
|
var sb_vbox = VBoxContainer.new()
|
||||||
|
var sb_margin = MarginContainer.new()
|
||||||
|
sb_margin.add_theme_constant_override("margin_left", 20)
|
||||||
|
sb_margin.add_theme_constant_override("margin_top", 20)
|
||||||
|
sb_margin.add_theme_constant_override("margin_right", 20)
|
||||||
|
sb_margin.add_theme_constant_override("margin_bottom", 20)
|
||||||
|
sb_margin.add_child(sb_vbox)
|
||||||
|
_scoreboard_panel.add_child(sb_margin)
|
||||||
|
|
||||||
|
var sb_title = Label.new()
|
||||||
|
sb_title.text = "Scoreboard"
|
||||||
|
sb_title.add_theme_font_size_override("font_size", 48)
|
||||||
|
sb_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
sb_vbox.add_child(sb_title)
|
||||||
|
|
||||||
|
_scoreboard_grid = GridContainer.new()
|
||||||
|
_scoreboard_grid.columns = 5
|
||||||
|
_scoreboard_grid.add_theme_constant_override("h_separation", 50)
|
||||||
|
_scoreboard_grid.add_theme_constant_override("v_separation", 10)
|
||||||
|
sb_vbox.add_child(_scoreboard_grid)
|
||||||
|
|
||||||
|
_populate_scoreboard_headers()
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
if _nm:
|
||||||
|
_nm.stats_updated.connect(_update_hud)
|
||||||
|
_nm.killfeed_event.connect(_on_killfeed_event)
|
||||||
|
_nm.match_state_updated.connect(_update_timer)
|
||||||
|
_update_hud()
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if Input.is_action_just_pressed("scoreboard"):
|
||||||
|
_scoreboard_panel.show()
|
||||||
|
_update_hud()
|
||||||
|
elif Input.is_action_just_released("scoreboard"):
|
||||||
|
_scoreboard_panel.hide()
|
||||||
|
|
||||||
|
if _nm and _nm.match_active:
|
||||||
|
_update_timer()
|
||||||
|
|
||||||
|
func _update_timer() -> void:
|
||||||
|
if not _nm: return
|
||||||
|
var t = int(_nm.match_time_remaining)
|
||||||
|
var m = int(t / 60.0)
|
||||||
|
var s = t % 60
|
||||||
|
_time_label.text = "%02d:%02d" % [m, s]
|
||||||
|
|
||||||
|
func _populate_scoreboard_headers() -> void:
|
||||||
|
for child in _scoreboard_grid.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
var headers = ["Player", "Kills", "Deaths", "Assists", "Ping"]
|
||||||
|
for h in headers:
|
||||||
|
var l = Label.new()
|
||||||
|
l.text = h
|
||||||
|
l.add_theme_font_size_override("font_size", 24)
|
||||||
|
l.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||||
|
_scoreboard_grid.add_child(l)
|
||||||
|
|
||||||
|
func _update_hud() -> void:
|
||||||
|
if not _nm: return
|
||||||
|
|
||||||
|
var my_id = multiplayer.get_unique_id()
|
||||||
|
|
||||||
|
var leader_name = "None"
|
||||||
|
var max_kills = -1
|
||||||
|
|
||||||
|
var stats = _nm.player_stats
|
||||||
|
|
||||||
|
# Sort players by kills for scoreboard
|
||||||
|
var players = stats.keys()
|
||||||
|
players.sort_custom(func(a, b): return stats[a].get("kills", 0) > stats[b].get("kills", 0))
|
||||||
|
|
||||||
|
_populate_scoreboard_headers()
|
||||||
|
|
||||||
|
for pid in players:
|
||||||
|
var p_data = stats[pid]
|
||||||
|
var p_kills = p_data.get("kills", 0)
|
||||||
|
|
||||||
|
# Find leader
|
||||||
|
if p_kills > max_kills:
|
||||||
|
max_kills = p_kills
|
||||||
|
leader_name = p_data.get("username", "Player " + str(pid))
|
||||||
|
|
||||||
|
# Update local kills
|
||||||
|
if pid == my_id:
|
||||||
|
_local_kills_label.text = str(p_kills) + " Kills"
|
||||||
|
|
||||||
|
# Build Scoreboard Row
|
||||||
|
var c_name = Label.new()
|
||||||
|
c_name.text = p_data.get("username", "Player")
|
||||||
|
c_name.add_theme_font_size_override("font_size", 24)
|
||||||
|
c_name.add_theme_color_override("font_color", Color(p_data.get("color", "cccccc")))
|
||||||
|
_scoreboard_grid.add_child(c_name)
|
||||||
|
|
||||||
|
var c_k = Label.new()
|
||||||
|
c_k.text = str(p_kills)
|
||||||
|
c_k.add_theme_font_size_override("font_size", 24)
|
||||||
|
_scoreboard_grid.add_child(c_k)
|
||||||
|
|
||||||
|
var c_d = Label.new()
|
||||||
|
c_d.text = str(p_data.get("deaths", 0))
|
||||||
|
c_d.add_theme_font_size_override("font_size", 24)
|
||||||
|
_scoreboard_grid.add_child(c_d)
|
||||||
|
|
||||||
|
var c_a = Label.new()
|
||||||
|
c_a.text = str(p_data.get("assists", 0))
|
||||||
|
c_a.add_theme_font_size_override("font_size", 24)
|
||||||
|
_scoreboard_grid.add_child(c_a)
|
||||||
|
|
||||||
|
var c_p = Label.new()
|
||||||
|
c_p.text = str(p_data.get("ping", 0)) + "ms"
|
||||||
|
c_p.add_theme_font_size_override("font_size", 24)
|
||||||
|
_scoreboard_grid.add_child(c_p)
|
||||||
|
|
||||||
|
_leader_label.text = "Leader: %s (%s Kills)" % [leader_name, max_kills]
|
||||||
|
|
||||||
|
func _on_killfeed_event(victim: String, killer: String, weapon: String, v_color: String = "cccccc", k_color: String = "cccccc") -> void:
|
||||||
|
var l = RichTextLabel.new()
|
||||||
|
l.bbcode_enabled = true
|
||||||
|
l.fit_content = true
|
||||||
|
l.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||||
|
|
||||||
|
if killer == "":
|
||||||
|
l.text = "[color=#%s]%s[/color] died." % [v_color, victim]
|
||||||
|
else:
|
||||||
|
l.text = "[color=#%s]%s[/color] [%s] [color=#%s]%s[/color]" % [k_color, killer, weapon, v_color, victim]
|
||||||
|
|
||||||
|
l.add_theme_font_size_override("normal_font_size", 20)
|
||||||
|
l.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
l.add_theme_constant_override("outline_size", 4)
|
||||||
|
_killfeed_vbox.add_child(l)
|
||||||
|
|
||||||
|
# Fade out and queue_free after 5 seconds
|
||||||
|
var t = get_tree().create_tween()
|
||||||
|
t.tween_interval(4.0)
|
||||||
|
t.tween_property(l, "modulate:a", 0.0, 1.0)
|
||||||
|
t.tween_callback(l.queue_free)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dymuoha2tdj63
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene format=3 uid="uid://c54h8k8u8p7q6"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/match_hud.gd" id="1_match"]
|
||||||
|
|
||||||
|
[node name="MatchHUD" type="CanvasLayer"]
|
||||||
|
script = ExtResource("1_match")
|
||||||
+43
-4
@@ -92,7 +92,20 @@ func _resume() -> void:
|
|||||||
|
|
||||||
func _respawn() -> void:
|
func _respawn() -> void:
|
||||||
_resume() # Unpause
|
_resume() # Unpause
|
||||||
get_tree().reload_current_scene()
|
|
||||||
|
# Find local player using their unique multiplayer ID
|
||||||
|
var local_player: Node3D = null
|
||||||
|
var pid = multiplayer.get_unique_id()
|
||||||
|
|
||||||
|
if get_tree().current_scene:
|
||||||
|
local_player = get_tree().current_scene.get_node_or_null(str(pid))
|
||||||
|
if not local_player:
|
||||||
|
var spawner = get_tree().current_scene.get_node_or_null("PlayerSpawner")
|
||||||
|
if spawner:
|
||||||
|
local_player = spawner.get_node_or_null(str(pid))
|
||||||
|
|
||||||
|
if local_player and local_player.has_method("rpc_request_respawn"):
|
||||||
|
local_player.rpc_request_respawn.rpc_id(1)
|
||||||
|
|
||||||
func _build_ui() -> void:
|
func _build_ui() -> void:
|
||||||
bg = ColorRect.new()
|
bg = ColorRect.new()
|
||||||
@@ -239,9 +252,32 @@ func _build_ui() -> void:
|
|||||||
var general_tab = VBoxContainer.new()
|
var general_tab = VBoxContainer.new()
|
||||||
general_tab.name = "General"
|
general_tab.name = "General"
|
||||||
tab_container.add_child(general_tab)
|
tab_container.add_child(general_tab)
|
||||||
var gen_label = Label.new()
|
var user_hbox = HBoxContainer.new()
|
||||||
gen_label.text = "Settings coming soon..."
|
general_tab.add_child(user_hbox)
|
||||||
general_tab.add_child(gen_label)
|
|
||||||
|
var user_lbl = Label.new()
|
||||||
|
user_lbl.text = "Username:"
|
||||||
|
user_lbl.custom_minimum_size = Vector2(150, 0)
|
||||||
|
user_hbox.add_child(user_lbl)
|
||||||
|
|
||||||
|
var user_input = LineEdit.new()
|
||||||
|
user_input.text = SettingsManager.username
|
||||||
|
user_input.custom_minimum_size = Vector2(200, 0)
|
||||||
|
user_hbox.add_child(user_input)
|
||||||
|
|
||||||
|
var user_save = Button.new()
|
||||||
|
user_save.text = "Save"
|
||||||
|
user_save.pressed.connect(func():
|
||||||
|
SettingsManager.username = user_input.text
|
||||||
|
SettingsManager._save_settings()
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm:
|
||||||
|
if nm.multiplayer.has_multiplayer_peer() and not nm.multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||||
|
nm.update_username.rpc_id(1, user_input.text)
|
||||||
|
else:
|
||||||
|
nm.update_username(user_input.text)
|
||||||
|
)
|
||||||
|
user_hbox.add_child(user_save)
|
||||||
|
|
||||||
var spacer_gen = Control.new()
|
var spacer_gen = Control.new()
|
||||||
spacer_gen.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
spacer_gen.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
@@ -649,6 +685,9 @@ func _show_return_dialog() -> void:
|
|||||||
|
|
||||||
func _return_to_main_menu() -> void:
|
func _return_to_main_menu() -> void:
|
||||||
_resume() # Reset pause state and cursor mode
|
_resume() # Reset pause state and cursor mode
|
||||||
|
var nm = get_node_or_null("/root/NetworkManager")
|
||||||
|
if nm:
|
||||||
|
nm.disconnect_game()
|
||||||
get_tree().change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
get_tree().change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
||||||
|
|
||||||
func _show_main_menu() -> void:
|
func _show_main_menu() -> void:
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ func _shoot_projectile() -> void:
|
|||||||
|
|
||||||
_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)
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -146,12 +146,11 @@ func _apply_impulse() -> void:
|
|||||||
var push_dir = camera.global_transform.basis.z.normalized()
|
var push_dir = camera.global_transform.basis.z.normalized()
|
||||||
|
|
||||||
# Apply impulse to player velocity
|
# Apply impulse to player velocity
|
||||||
player.velocity += push_dir * impulse_strength
|
var final_force = push_dir * impulse_strength
|
||||||
|
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||||
# Small hack to ensure we aren't considered on floor if we shoot down
|
player.server_apply_impulse.rpc_id(1, final_force)
|
||||||
if push_dir.y > 0:
|
else:
|
||||||
# Just slightly lift the player to break floor contact immediately
|
player.apply_impulse(final_force)
|
||||||
player.position.y += 0.05
|
|
||||||
|
|
||||||
func _play_muzzle_flash() -> void:
|
func _play_muzzle_flash() -> void:
|
||||||
if muzzle_flash:
|
if muzzle_flash:
|
||||||
|
|||||||
@@ -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 owner_player and owner_player.multiplayer.is_server():
|
||||||
|
result.collider.take_damage(damage, result.position, owner_player)
|
||||||
_explode(result.position)
|
_explode(result.position)
|
||||||
destroy()
|
destroy()
|
||||||
|
|
||||||
@@ -189,19 +191,26 @@ func _explode(pos: Vector3) -> void:
|
|||||||
# Add a tiny bit of upward bias to knockback so it lifts them
|
# Add a tiny bit of upward bias to knockback so it lifts them
|
||||||
dir = (dir + Vector3(0, 0.5, 0)).normalized()
|
dir = (dir + Vector3(0, 0.5, 0)).normalized()
|
||||||
|
|
||||||
# Apply knockback if it has apply_impulse
|
# Apply knockback if it has server_apply_impulse (for rocket jumping)
|
||||||
if col.has_method("apply_impulse"):
|
if col.has_method("server_apply_impulse") and col == owner_player:
|
||||||
|
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||||
|
col.server_apply_impulse.rpc_id(1, dir * final_knockback)
|
||||||
|
else:
|
||||||
col.apply_impulse(dir * final_knockback)
|
col.apply_impulse(dir * final_knockback)
|
||||||
|
|
||||||
# Apply damage
|
# Apply damage (and knockback for other players via take_damage)
|
||||||
if col.has_method("take_damage"):
|
if col.has_method("take_damage"):
|
||||||
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 (we already applied knockback above)
|
||||||
pass
|
pass
|
||||||
else:
|
elif owner_player:
|
||||||
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.is_multiplayer_authority() 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
|
||||||
|
|||||||
@@ -102,11 +102,18 @@ func _physics_process(delta: float) -> void:
|
|||||||
|
|
||||||
if ignited and is_instance_valid(weapon_source):
|
if ignited and is_instance_valid(weapon_source):
|
||||||
var target_dir: Vector3 = initial_aim_dir
|
var target_dir: Vector3 = initial_aim_dir
|
||||||
var is_painting = weapon_source.get("is_targeting")
|
var is_painting = false
|
||||||
|
var target: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
if weapon_source.get("player") and weapon_source.player:
|
||||||
|
is_painting = weapon_source.player.synced_is_targeting
|
||||||
|
target = weapon_source.player.synced_homing_target_pos
|
||||||
|
else:
|
||||||
|
is_painting = weapon_source.get("is_targeting")
|
||||||
if is_painting != null and is_painting:
|
if is_painting != null and is_painting:
|
||||||
var target: Vector3 = weapon_source.get("homing_target_pos")
|
target = weapon_source.get("homing_target_pos")
|
||||||
if target != null and target != Vector3.ZERO:
|
|
||||||
|
if is_painting and target != Vector3.ZERO:
|
||||||
target_dir = (target - global_position).normalized()
|
target_dir = (target - global_position).normalized()
|
||||||
|
|
||||||
if target_dir != Vector3.ZERO:
|
if target_dir != Vector3.ZERO:
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ 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"):
|
||||||
|
if owner_player and owner_player.multiplayer.is_server():
|
||||||
result.collider.take_damage(final_damage, result.position, owner_player)
|
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
|
||||||
|
|||||||
+22
-1
@@ -91,9 +91,18 @@ func _process(delta: float) -> void:
|
|||||||
(targeting_laser.mesh as CylinderMesh).height = 300.0
|
(targeting_laser.mesh as CylinderMesh).height = 300.0
|
||||||
targeting_laser.position.z = -150.0
|
targeting_laser.position.z = -150.0
|
||||||
|
|
||||||
|
if player and player.is_multiplayer_authority():
|
||||||
|
player.synced_is_targeting = true
|
||||||
|
player.synced_homing_target_pos = homing_target_pos
|
||||||
|
else:
|
||||||
|
if player and player.is_multiplayer_authority():
|
||||||
|
player.synced_is_targeting = false
|
||||||
|
|
||||||
func unequip() -> void:
|
func unequip() -> void:
|
||||||
is_targeting = false
|
is_targeting = false
|
||||||
targeting_laser.visible = false
|
targeting_laser.visible = false
|
||||||
|
if player and player.is_multiplayer_authority():
|
||||||
|
player.synced_is_targeting = false
|
||||||
|
|
||||||
# Override fire to shoot 8 rockets at once
|
# Override fire to shoot 8 rockets at once
|
||||||
func _fire() -> void:
|
func _fire() -> void:
|
||||||
@@ -141,6 +150,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
|
||||||
|
|
||||||
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||||
@@ -176,7 +188,16 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
|||||||
if player:
|
if player:
|
||||||
proj.velocity += fire_dir * max(0.0, player.velocity.dot(fire_dir))
|
proj.velocity += fire_dir * max(0.0, player.velocity.dot(fire_dir))
|
||||||
|
|
||||||
proj.initial_aim_dir = _current_aim_dir
|
var aim_dir = _current_aim_dir
|
||||||
|
# If remote player, use the synced head rotation to determine forward direction
|
||||||
|
if player and not player.is_multiplayer_authority():
|
||||||
|
var head = player.get_node_or_null("HeadPivot")
|
||||||
|
if head:
|
||||||
|
aim_dir = -head.global_transform.basis.z.normalized()
|
||||||
|
else:
|
||||||
|
aim_dir = fire_dir # Fallback
|
||||||
|
|
||||||
|
proj.initial_aim_dir = aim_dir
|
||||||
proj.cruise_speed = 65.0
|
proj.cruise_speed = 65.0
|
||||||
proj.ignition_timer = randf_range(0.3, 0.6) # Staggered ignition
|
proj.ignition_timer = randf_range(0.3, 0.6) # Staggered ignition
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ 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()
|
||||||
|
|
||||||
|
print("DEBUG [", multiplayer.get_unique_id(), "]: _build_loadout is_auth=", is_auth, " player=", str(player.name) if player else "null")
|
||||||
|
|
||||||
|
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):
|
||||||
@@ -68,6 +79,17 @@ func _build_loadout() -> void:
|
|||||||
weapons.clear()
|
weapons.clear()
|
||||||
|
|
||||||
var l = LoadoutManager.get_active_loadout()
|
var l = LoadoutManager.get_active_loadout()
|
||||||
|
print("DEBUG [", multiplayer.get_unique_id(), "]: Loadout primary1=", l["primary_1"])
|
||||||
|
|
||||||
|
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"])
|
||||||
@@ -77,17 +99,34 @@ 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):
|
||||||
|
print("DEBUG [", multiplayer.get_unique_id(), "]: _spawn_weapon failed check for ", weapon_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
var w_data = LoadoutManager.weapon_db[weapon_id]
|
var w_data = LoadoutManager.weapon_db[weapon_id]
|
||||||
var script_path = w_data["script"]
|
var script_path = w_data["script"]
|
||||||
if script_path == "":
|
if script_path == "":
|
||||||
|
print("DEBUG [", multiplayer.get_unique_id(), "]: _spawn_weapon failed script path for ", weapon_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
var script = load(script_path)
|
var script = load(script_path)
|
||||||
if script:
|
if script:
|
||||||
|
print("DEBUG [", multiplayer.get_unique_id(), "]: Successfully loaded script, adding weapon ", weapon_id)
|
||||||
var w = script.new()
|
var w = script.new()
|
||||||
w.name = "Weapon_" + str(slot) + "_" + weapon_id
|
w.name = "Weapon_" + str(slot) + "_" + weapon_id
|
||||||
# Assuming all weapons have player and camera vars
|
# Assuming all weapons have player and camera vars
|
||||||
@@ -184,6 +223,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