Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07a15fe998 | ||
|
|
dbbfbef4a4 | ||
|
|
ff3e01a9e5 |
@@ -36,6 +36,8 @@ var movement_speed: float = 0.0
|
||||
var _anim_time: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
rotation_degrees.y = 180
|
||||
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
mat.roughness = 0.8
|
||||
@@ -197,7 +199,6 @@ func set_weapon(script_path: String) -> void:
|
||||
_set_shadows_recursive(w)
|
||||
|
||||
# Force position after the weapon's _build_model() sets it for 1st person
|
||||
# root_pivot is rotated 180 deg around Y, so local +Z is FORWARD and local -X is RIGHT
|
||||
w.position = Vector3(-0.15, 1.0, 0.4)
|
||||
w.rotation_degrees = Vector3(0, 180, 0)
|
||||
)
|
||||
|
||||
@@ -9,18 +9,18 @@ func build_ragdoll(color: Color) -> void:
|
||||
mat.roughness = 0.8
|
||||
|
||||
# Create bodies
|
||||
torso_body = _create_body(Vector3(0.4, 0.7, 0.25), mat, Vector3(0, 1.15, 0), 20.0)
|
||||
var head_body = _create_body(Vector3(0.25, 0.25, 0.25), mat, Vector3(0, 1.65, 0), 5.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, "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 lower_arm_l_body = _create_body(Vector3(0.1, 0.35, 0.1), mat, Vector3(-0.28, 0.925, 0), 2.0)
|
||||
var upper_arm_r_body = _create_body(Vector3(0.12, 0.35, 0.12), mat, Vector3(0.28, 1.275, 0), 3.0)
|
||||
var lower_arm_r_body = _create_body(Vector3(0.1, 0.35, 0.1), mat, Vector3(0.28, 0.925, 0), 2.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, "LowerArmL")
|
||||
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, "LowerArmR")
|
||||
|
||||
var thigh_l_body = _create_body(Vector3(0.15, 0.45, 0.15), mat, Vector3(-0.12, 0.625, 0), 5.0)
|
||||
var calf_l_body = _create_body(Vector3(0.13, 0.45, 0.13), mat, Vector3(-0.12, 0.175, 0), 3.0)
|
||||
var thigh_r_body = _create_body(Vector3(0.15, 0.45, 0.15), mat, Vector3(0.12, 0.625, 0), 5.0)
|
||||
var calf_r_body = _create_body(Vector3(0.13, 0.45, 0.13), mat, Vector3(0.12, 0.175, 0), 3.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, "CalfL")
|
||||
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, "CalfR")
|
||||
|
||||
add_child(torso_body)
|
||||
add_child(head_body)
|
||||
@@ -33,6 +33,21 @@ func build_ragdoll(color: Color) -> void:
|
||||
add_child(thigh_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
|
||||
call_deferred("_setup_joints", head_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):
|
||||
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()
|
||||
body.name = bname
|
||||
body.position = pos
|
||||
body.mass = mass
|
||||
|
||||
|
||||
+41
-12
@@ -293,16 +293,40 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
# Use a slightly random spawn position to avoid exact overlapping
|
||||
player.position = Vector3(randf_range(-2, 2), 2.0, randf_range(-2, 2))
|
||||
|
||||
# Network Synchronizer
|
||||
var sync = MultiplayerSynchronizer.new()
|
||||
sync.name = "MultiplayerSynchronizer"
|
||||
sync.set_multiplayer_authority(pid)
|
||||
var rep_config = SceneReplicationConfig.new()
|
||||
rep_config.add_property(":position")
|
||||
rep_config.add_property(":rotation")
|
||||
rep_config.add_property("HeadPivot:rotation")
|
||||
sync.replication_config = rep_config
|
||||
player.add_child(sync)
|
||||
# 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
|
||||
var col_shape := CollisionShape3D.new()
|
||||
@@ -395,9 +419,10 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
player.set_process(true)
|
||||
sm.set_physics_process(true)
|
||||
else:
|
||||
# Disable processing/input for remote players
|
||||
# Disable physics processing/input for remote players
|
||||
# but keep _process enabled so synced state updates the HumanoidModel
|
||||
player.set_physics_process(false)
|
||||
player.set_process(false)
|
||||
player.set_process(true)
|
||||
player.set_process_input(false)
|
||||
sm.set_physics_process(false)
|
||||
sm.set_process(false)
|
||||
@@ -410,6 +435,10 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
wman.set_process_input(false)
|
||||
wman.set_process(false)
|
||||
wman.set_physics_process(false)
|
||||
wman.visible = false
|
||||
var cv = wman.get("canvas_layer")
|
||||
if cv:
|
||||
cv.visible = false
|
||||
|
||||
return player
|
||||
|
||||
|
||||
+81
-43
@@ -1,13 +1,14 @@
|
||||
extends Area3D
|
||||
class_name CombatArea
|
||||
|
||||
var time_left: float = 5.0
|
||||
var time_safe: float = 0.0
|
||||
var out_of_bounds_players: Dictionary = {} # body -> { time_left: 5.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_label: Label = null
|
||||
var _ui_overlay: ColorRect = null
|
||||
var _is_local_player_out: bool = false
|
||||
var _local_player_body: Node3D = null
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 0
|
||||
@@ -17,50 +18,85 @@ func _ready() -> void:
|
||||
set_process(false)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _player_out_of_bounds:
|
||||
# If the player dies while out of bounds, stop the timer and remove the UI
|
||||
if "is_dead" in _player_out_of_bounds and _player_out_of_bounds.is_dead:
|
||||
_player_out_of_bounds = null
|
||||
# Process players who are actively out of bounds
|
||||
var to_kill: Array = []
|
||||
for body in out_of_bounds_players.keys():
|
||||
if "is_dead" in body and body.is_dead:
|
||||
out_of_bounds_players.erase(body)
|
||||
if body == _local_player_body:
|
||||
_remove_ui()
|
||||
return
|
||||
continue
|
||||
|
||||
# Player is outside the combat area!
|
||||
time_left -= delta
|
||||
if time_left <= 0.0:
|
||||
time_left = 0.0
|
||||
_kill_player()
|
||||
var data = out_of_bounds_players[body]
|
||||
data.time_left -= delta
|
||||
if data.time_left <= 0.0:
|
||||
data.time_left = 0.0
|
||||
to_kill.append(body)
|
||||
|
||||
# Update UI
|
||||
if _ui_label and _ui_overlay:
|
||||
_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 - (time_left / 5.0)))
|
||||
else:
|
||||
# Player is inside the combat area, but might have a damaged timer
|
||||
if time_left < 5.0:
|
||||
time_safe += delta
|
||||
if time_safe >= 10.0:
|
||||
time_left = 5.0
|
||||
time_safe = 0.0
|
||||
set_process(false) # Fully recovered, sleep
|
||||
if body == _local_player_body and _is_local_player_out and _ui_label and _ui_overlay:
|
||||
_ui_label.text = "WARNING: RETURN TO COMBAT AREA\n%.1f" % data.time_left
|
||||
_ui_overlay.color = Color(1.0, 0.0, 0.0, 0.8 * (1.0 - (data.time_left / 5.0)))
|
||||
|
||||
for body in to_kill:
|
||||
_kill_player(body)
|
||||
|
||||
# Process players who returned but are recovering
|
||||
var recovered: Array = []
|
||||
for body in recovery_players.keys():
|
||||
if "is_dead" in body and body.is_dead:
|
||||
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:
|
||||
if body.name == "Player":
|
||||
# Don't trigger if the player is already dead or the scene is tearing down
|
||||
if body is CharacterBody3D and body.has_method("take_damage") and str(body.name).is_valid_int():
|
||||
if "is_dead" in body and body.is_dead:
|
||||
return
|
||||
if body.is_queued_for_deletion() or not is_inside_tree():
|
||||
return
|
||||
|
||||
_player_out_of_bounds = body
|
||||
time_safe = 0.0 # Reset recovery timer
|
||||
var start_time = 5.0
|
||||
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()
|
||||
|
||||
set_process(true)
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if body == _player_out_of_bounds:
|
||||
_player_out_of_bounds = null
|
||||
if out_of_bounds_players.has(body):
|
||||
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()
|
||||
# set_process(true) stays on to tick the time_safe recovery
|
||||
_is_local_player_out = false
|
||||
_local_player_body = null
|
||||
|
||||
func _create_ui() -> void:
|
||||
if _ui_layer: return
|
||||
@@ -83,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_outline_color", Color(0, 0, 0))
|
||||
_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)
|
||||
|
||||
@@ -97,15 +136,14 @@ func _remove_ui() -> void:
|
||||
_ui_label = null
|
||||
_ui_overlay = null
|
||||
|
||||
func _kill_player() -> void:
|
||||
if _player_out_of_bounds:
|
||||
# PlayerMovementController's take_damage requires (amount, source)
|
||||
if _player_out_of_bounds.has_method("take_damage"):
|
||||
_player_out_of_bounds.take_damage(9999, 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
|
||||
func _kill_player(body: Node3D) -> void:
|
||||
if body:
|
||||
if multiplayer.is_server():
|
||||
if body.has_method("take_damage"):
|
||||
body.take_damage(9999, Vector3.ZERO, null, Vector3.ZERO)
|
||||
|
||||
_player_out_of_bounds = null
|
||||
out_of_bounds_players.erase(body)
|
||||
if body == _local_player_body and _is_local_player_out:
|
||||
_remove_ui()
|
||||
_is_local_player_out = false
|
||||
_local_player_body = null
|
||||
|
||||
@@ -39,7 +39,7 @@ func _ready() -> void:
|
||||
|
||||
_update_label()
|
||||
|
||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> void:
|
||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null, _impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
@@ -141,14 +141,13 @@ func _die(_hit_position: Vector3, source: Node, damage_amount: float) -> void:
|
||||
linear_vel = hit_dir * (damage_amount * 0.02)
|
||||
impulse = hit_dir * (damage_amount * 0.5)
|
||||
|
||||
get_tree().create_timer(0.01).timeout.connect(func():
|
||||
if is_instance_valid(ragdoll_instance):
|
||||
ragdoll_instance.apply_initial_velocities(linear_vel, impulse)
|
||||
)
|
||||
get_tree().create_timer(0.01).timeout.connect(_apply_ragdoll_velocity.bind(ragdoll_instance, linear_vel, impulse))
|
||||
|
||||
get_tree().create_timer(3.0).timeout.connect(func():
|
||||
_respawn()
|
||||
)
|
||||
get_tree().create_timer(3.0).timeout.connect(_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:
|
||||
if is_instance_valid(ragdoll_instance):
|
||||
|
||||
@@ -33,7 +33,7 @@ func _ready() -> void:
|
||||
dps_label.modulate = Color(1, 0.8, 0)
|
||||
add_child(dps_label)
|
||||
|
||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> void:
|
||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
var now = Time.get_ticks_msec() / 1000.0
|
||||
damage_history.append({"time": now, "amount": amount})
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ func _physics_process(delta: float) -> void:
|
||||
var current_h_speed = Vector2(velocity.x, velocity.z).length()
|
||||
visual_node.update_state("ground", current_h_speed)
|
||||
|
||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> void:
|
||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
@@ -185,15 +185,13 @@ func _die(_hit_position: Vector3, source: Node, damage_amount: float) -> void:
|
||||
linear_vel = hit_dir * (damage_amount * 0.02)
|
||||
impulse = hit_dir * (damage_amount * 0.5)
|
||||
|
||||
get_tree().create_timer(0.01).timeout.connect(func():
|
||||
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(0.01).timeout.connect(_apply_ragdoll_velocity.bind(ragdoll_instance, velocity + linear_vel, impulse))
|
||||
|
||||
get_tree().create_timer(3.0).timeout.connect(func():
|
||||
_respawn()
|
||||
)
|
||||
get_tree().create_timer(3.0).timeout.connect(_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:
|
||||
if is_instance_valid(ragdoll_instance):
|
||||
|
||||
@@ -64,6 +64,8 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if player and not player.multiplayer.is_server(): return
|
||||
|
||||
# Update coyote time
|
||||
if on_ground:
|
||||
coyote_timer = params.coyote_time
|
||||
|
||||
@@ -16,6 +16,7 @@ var health: float = 100.0
|
||||
var max_shield: float = 100.0
|
||||
var shield: float = 100.0
|
||||
var time_since_last_damage: float = 0.0
|
||||
var death_count: int = 0
|
||||
var is_dead: bool = false
|
||||
|
||||
# UI
|
||||
@@ -48,6 +49,25 @@ var _hit_marker_tween: Tween
|
||||
# Grapple
|
||||
var grapple_rope: MeshInstance3D
|
||||
|
||||
# ── Multiplayer Sync Variables ─────────────────────────────────────────────
|
||||
# These are written by the authority each frame and replicated to remote peers
|
||||
# via the MultiplayerSynchronizer properties.
|
||||
var synced_movement_state: String = "idle"
|
||||
var synced_movement_speed: float = 0.0
|
||||
var synced_is_crouching: bool = false
|
||||
@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:
|
||||
var sm := _ensure_machine()
|
||||
@@ -61,17 +81,31 @@ func _ready() -> void:
|
||||
if head_pivot:
|
||||
camera = head_pivot.get_node_or_null("Camera3D")
|
||||
|
||||
_setup_audio()
|
||||
_setup_grapple()
|
||||
|
||||
if is_multiplayer_authority():
|
||||
_damage_layer = CanvasLayer.new()
|
||||
add_child(_damage_layer)
|
||||
|
||||
_setup_audio()
|
||||
_setup_hit_marker()
|
||||
_setup_grapple()
|
||||
_setup_hud()
|
||||
else:
|
||||
call_deferred("_hide_remote_weapons")
|
||||
|
||||
set_process(true)
|
||||
if is_multiplayer_authority():
|
||||
set_physics_process(true)
|
||||
|
||||
func _hide_remote_weapons() -> void:
|
||||
if camera:
|
||||
var wman = camera.get_node_or_null("WeaponManager")
|
||||
if wman:
|
||||
wman.visible = false
|
||||
wman.process_mode = Node.PROCESS_MODE_DISABLED
|
||||
var cv = wman.get("canvas_layer")
|
||||
if cv:
|
||||
cv.visible = false
|
||||
|
||||
func _setup_audio() -> void:
|
||||
footstep_player = AudioStreamPlayer.new()
|
||||
footstep_player.bus = "SFX"
|
||||
@@ -219,16 +253,9 @@ func apply_impulse(force: Vector3) -> void:
|
||||
global_position.y += 0.1
|
||||
|
||||
func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
if is_dead:
|
||||
if is_instance_valid(ragdoll_instance):
|
||||
ragdoll_instance.apply_initial_velocities(Vector3.ZERO, impulse)
|
||||
return
|
||||
|
||||
# Show damage number for self-damage
|
||||
spawn_damage_number(amount, _hit_pos)
|
||||
|
||||
# Handle damage tracking for assists
|
||||
var attacker_id = 0
|
||||
var weapon_name = "Killed"
|
||||
|
||||
if _source:
|
||||
if _source.has_method("get_multiplayer_authority"):
|
||||
attacker_id = _source.get_multiplayer_authority()
|
||||
@@ -237,6 +264,210 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
||||
elif _source is CharacterBody3D:
|
||||
attacker_id = int(str(_source.name))
|
||||
|
||||
if _source.has_method("get_weapon_name"):
|
||||
weapon_name = _source.get_weapon_name()
|
||||
|
||||
# If this is singleplayer offline, just call it directly
|
||||
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||
rpc_take_damage(amount, _hit_pos, attacker_id, weapon_name, impulse)
|
||||
else:
|
||||
# Tell the server to handle the damage and broadcast it to everyone!
|
||||
server_take_damage.rpc_id(1, amount, _hit_pos, attacker_id, weapon_name, impulse)
|
||||
|
||||
@rpc("any_peer", "call_local", "unreliable")
|
||||
func server_play_fire_effects(origin: Vector3, target_or_dir: Vector3, weapon_name: String, is_hitscan: bool) -> void:
|
||||
if not multiplayer.is_server(): return
|
||||
rpc_play_fire_effects.rpc(origin, target_or_dir, weapon_name, is_hitscan)
|
||||
|
||||
@rpc("any_peer", "call_local", "unreliable")
|
||||
func rpc_play_fire_effects(origin: Vector3, target_or_dir: Vector3, _weapon_name: String, is_hitscan: bool) -> void:
|
||||
# Local player already spawned effects locally with 0 latency
|
||||
if is_multiplayer_authority(): return
|
||||
|
||||
if is_hitscan:
|
||||
# Spawn a standalone tracer directly into the scene
|
||||
_spawn_remote_tracer(origin, target_or_dir)
|
||||
else:
|
||||
# Spawn a visual-only projectile flying from origin along the direction
|
||||
_spawn_remote_projectile(origin, target_or_dir, _weapon_name)
|
||||
|
||||
func _spawn_remote_tracer(origin: Vector3, target: Vector3) -> void:
|
||||
var tracer = Node3D.new()
|
||||
tracer.set_script(load("res://weapons/hitscan_tracer.gd"))
|
||||
|
||||
var t_mesh_inst = MeshInstance3D.new()
|
||||
var t_mesh = CapsuleMesh.new()
|
||||
t_mesh.radius = 0.02
|
||||
t_mesh.height = 1.0
|
||||
var t_mat = StandardMaterial3D.new()
|
||||
t_mat.albedo_color = Color(1.0, 0.8, 0.2)
|
||||
t_mat.emission_enabled = true
|
||||
t_mat.emission = Color(2.0, 1.5, 0.5)
|
||||
t_mesh.material = t_mat
|
||||
|
||||
t_mesh_inst.mesh = t_mesh
|
||||
t_mesh_inst.rotation.x = deg_to_rad(90)
|
||||
t_mesh_inst.position = Vector3(0, 0, -0.5)
|
||||
tracer.add_child(t_mesh_inst)
|
||||
|
||||
tracer.target_position = target
|
||||
|
||||
if origin.distance_squared_to(target) > 0.1:
|
||||
get_tree().current_scene.add_child(tracer)
|
||||
tracer.global_position = origin
|
||||
|
||||
var up_vec = Vector3.UP
|
||||
if abs((target - origin).normalized().dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
tracer.look_at(target, up_vec)
|
||||
|
||||
func _spawn_remote_projectile(origin: Vector3, fire_dir: Vector3, weapon_name: String) -> void:
|
||||
# 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
|
||||
# Broadcast damage event to all peers so the victim dies on all screens
|
||||
rpc_take_damage.rpc(amount, hit_pos, attacker_id, weapon_name, impulse)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func rpc_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_name: String, impulse: Vector3) -> void:
|
||||
if is_dead:
|
||||
if is_instance_valid(ragdoll_instance):
|
||||
ragdoll_instance.apply_initial_velocities(Vector3.ZERO, impulse)
|
||||
return
|
||||
|
||||
# Show damage number for the attacker
|
||||
if not multiplayer.has_multiplayer_peer() or attacker_id == multiplayer.get_unique_id():
|
||||
spawn_damage_number(amount, hit_pos)
|
||||
|
||||
if attacker_id != 0:
|
||||
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
|
||||
|
||||
@@ -256,10 +487,10 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
||||
health = 0.0
|
||||
die(impulse)
|
||||
|
||||
if multiplayer.is_server():
|
||||
if get_node_or_null("/root/NetworkManager") and multiplayer.has_multiplayer_peer():
|
||||
var victim_id = multiplayer.get_unique_id() if is_multiplayer_authority() else int(str(name))
|
||||
var killer_id = 0
|
||||
var weapon_name = "Killed"
|
||||
var victim_id = multiplayer.get_unique_id()
|
||||
var killer_id = attacker_id
|
||||
|
||||
var assist_ids: Array = []
|
||||
var now = Time.get_ticks_msec() / 1000.0
|
||||
@@ -267,16 +498,6 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
||||
if aid != killer_id and now - recent_attackers[aid] <= 10.0:
|
||||
assist_ids.append(aid)
|
||||
|
||||
if _source and _source is Node:
|
||||
if _source.has_method("get_multiplayer_authority"):
|
||||
killer_id = _source.get_multiplayer_authority()
|
||||
elif "owner_player" in _source and _source.owner_player:
|
||||
killer_id = int(str(_source.owner_player.name))
|
||||
elif _source is PlayerMovementController:
|
||||
killer_id = int(str(_source.name))
|
||||
|
||||
if _source.has_method("get_weapon_name"):
|
||||
weapon_name = _source.get_weapon_name()
|
||||
get_node("/root/NetworkManager").register_kill.rpc(victim_id, killer_id, weapon_name, "", assist_ids)
|
||||
|
||||
func get_weapon_name() -> String:
|
||||
@@ -310,68 +531,34 @@ func _ensure_machine() -> MovementStateMachine:
|
||||
_machine = 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:
|
||||
if not is_multiplayer_authority(): return
|
||||
var speed = velocity.length()
|
||||
var wind_factor = clampf((speed - 10.0) / 25.0, 0.0, 1.0)
|
||||
# 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
|
||||
# Local client captures input and sends it
|
||||
if is_multiplayer_authority():
|
||||
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
|
||||
forward.y = 0.0
|
||||
forward = forward.normalized()
|
||||
@@ -383,8 +570,70 @@ func _physics_process(_delta: float) -> void:
|
||||
if world_dir.length_squared() > 1.0:
|
||||
world_dir = world_dir.normalized()
|
||||
|
||||
sm.input_dir = raw_input
|
||||
sm.wish_dir_world = world_dir
|
||||
var input_jump = Input.is_action_pressed("jump")
|
||||
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
|
||||
if sm.current_state == "grapple" or sm.is_grapple_shooting:
|
||||
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
|
||||
var humanoid = get_node_or_null("HumanoidModel")
|
||||
@@ -392,6 +641,11 @@ func _physics_process(_delta: float) -> void:
|
||||
var h_speed = Vector2(velocity.x, velocity.z).length()
|
||||
humanoid.update_state(sm.current_state, h_speed, sm.input_crouch)
|
||||
|
||||
# Write synced state for remote peers
|
||||
synced_movement_state = sm.current_state
|
||||
synced_movement_speed = Vector2(velocity.x, velocity.z).length()
|
||||
synced_is_crouching = sm.input_crouch
|
||||
|
||||
|
||||
func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||
if ev == "chain_updated":
|
||||
@@ -402,7 +656,22 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||
grapple_latch_player.play()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not is_multiplayer_authority(): return
|
||||
# Remote players: apply synced movement state to their HumanoidModel
|
||||
if not is_multiplayer_authority():
|
||||
if synced_loadout_ready and not has_meta("remote_weapons_built"):
|
||||
set_meta("remote_weapons_built", true)
|
||||
var wman = camera.get_node_or_null("WeaponManager")
|
||||
if wman and wman.has_method("_build_remote_loadout"):
|
||||
wman._build_remote_loadout(synced_loadout_p1, synced_loadout_p2, synced_loadout_sp, synced_loadout_melee)
|
||||
|
||||
var humanoid = get_node_or_null("HumanoidModel")
|
||||
if humanoid:
|
||||
humanoid.update_state(synced_movement_state, synced_movement_speed, synced_is_crouching)
|
||||
# Check for weapon changes
|
||||
if synced_weapon_path != "" and synced_weapon_path != humanoid.get_meta("current_weapon_path", ""):
|
||||
humanoid.set_weapon(synced_weapon_path)
|
||||
humanoid.set_meta("current_weapon_path", synced_weapon_path)
|
||||
return
|
||||
if is_dead:
|
||||
# Continuously follow the ragdoll torso
|
||||
if is_instance_valid(ragdoll_instance) and is_instance_valid(camera):
|
||||
@@ -538,6 +807,7 @@ func _setup_hud() -> void:
|
||||
tw.tween_property(respawn_label, "modulate:a", 0.2, 0.8)
|
||||
tw.tween_property(respawn_label, "modulate:a", 1.0, 0.8)
|
||||
|
||||
if is_multiplayer_authority():
|
||||
_damage_layer.add_child(death_screen)
|
||||
|
||||
func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
@@ -559,17 +829,16 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
col.set_deferred("disabled", true)
|
||||
|
||||
# Spawn true physics ragdoll
|
||||
death_count += 1
|
||||
var ragdoll = load("res://characters/procedural_ragdoll.gd").new()
|
||||
ragdoll.name = "Ragdoll_%s_%d" % [self.name, death_count]
|
||||
ragdoll_instance = ragdoll
|
||||
get_tree().current_scene.add_child(ragdoll)
|
||||
ragdoll.global_transform = global_transform
|
||||
ragdoll.build_ragdoll(Color(0.2, 0.4, 0.8)) # Blueish player color
|
||||
|
||||
# Wait a frame for physics to initialize then apply velocity
|
||||
get_tree().create_timer(0.01).timeout.connect(func():
|
||||
if is_instance_valid(ragdoll):
|
||||
ragdoll.apply_initial_velocities(velocity, impulse)
|
||||
)
|
||||
get_tree().create_timer(0.01).timeout.connect(_apply_ragdoll_velocity.bind(ragdoll, velocity, impulse))
|
||||
|
||||
# Move camera to 3rd person view using SpringArm3D
|
||||
if is_instance_valid(camera):
|
||||
@@ -630,9 +899,14 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
ragdoll_instance.set_meta("spring_arm", spring_arm)
|
||||
|
||||
# Show death screen
|
||||
if is_multiplayer_authority():
|
||||
death_screen.visible = true
|
||||
set_process_input(true)
|
||||
|
||||
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 _input(event: InputEvent) -> void:
|
||||
if not is_multiplayer_authority(): return
|
||||
if is_dead and death_screen and death_screen.visible:
|
||||
@@ -641,5 +915,84 @@ func _input(event: InputEvent) -> void:
|
||||
_on_respawn_pressed()
|
||||
|
||||
func _on_respawn_pressed() -> void:
|
||||
# Reload scene
|
||||
get_tree().reload_current_scene()
|
||||
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)
|
||||
|
||||
@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
|
||||
|
||||
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()
|
||||
|
||||
+2
-2
@@ -104,8 +104,8 @@ func _respawn() -> void:
|
||||
if spawner:
|
||||
local_player = spawner.get_node_or_null(str(pid))
|
||||
|
||||
if local_player and local_player.has_method("take_damage"):
|
||||
local_player.take_damage(9999, Vector3.ZERO)
|
||||
if local_player and local_player.has_method("rpc_request_respawn"):
|
||||
local_player.rpc_request_respawn.rpc_id(1)
|
||||
|
||||
func _build_ui() -> void:
|
||||
bg = ColorRect.new()
|
||||
|
||||
@@ -161,7 +161,12 @@ func _shoot_hitscan() -> void:
|
||||
hits_remaining -= 1
|
||||
current_damage_mult *= penetration_damage_penalty
|
||||
final_target = result.position
|
||||
_spawn_tracer(origin, final_target)
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||
player.server_play_fire_effects.rpc_id(1, origin, final_target, name, true)
|
||||
|
||||
func _spawn_tracer(_origin: Vector3, final_target: Vector3) -> void:
|
||||
# Spawn cosmetic tracer
|
||||
var tracer = Node3D.new()
|
||||
tracer.set_script(load("res://weapons/hitscan_tracer.gd"))
|
||||
|
||||
@@ -121,6 +121,9 @@ func _shoot_projectile() -> void:
|
||||
|
||||
_spawn_custom_projectile(origin, fire_dir)
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||
player.server_play_fire_effects.rpc_id(1, origin, fire_dir, weapon_name, false)
|
||||
|
||||
func _spawn_custom_projectile(_origin: Vector3, _fire_dir: Vector3) -> void:
|
||||
# Virtual method for custom projectiles (e.g., bouncing, homing)
|
||||
pass
|
||||
|
||||
@@ -137,6 +137,8 @@ func _on_hit(result: Dictionary) -> void:
|
||||
if not result.collider.has_method("take_damage"):
|
||||
if result.collider is StaticBody3D or result.collider is CSGShape3D:
|
||||
ImpactSpawner.spawn(get_tree(), "crater", result.position, result.normal, explosion_radius * 1.5)
|
||||
elif owner_player and owner_player.multiplayer.is_server():
|
||||
result.collider.take_damage(damage, result.position, owner_player)
|
||||
_explode(result.position)
|
||||
destroy()
|
||||
|
||||
@@ -198,10 +200,14 @@ func _explode(pos: Vector3) -> void:
|
||||
if col == owner_player and not can_self_damage:
|
||||
# Skip self damage, but we still applied the knockback!
|
||||
pass
|
||||
else:
|
||||
col.take_damage(final_damage, target_pos, owner_player)
|
||||
elif owner_player and owner_player.multiplayer.is_server():
|
||||
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 s_mesh = SphereMesh.new()
|
||||
s_mesh.radius = explosion_radius
|
||||
|
||||
@@ -102,11 +102,18 @@ func _physics_process(delta: float) -> void:
|
||||
|
||||
if ignited and is_instance_valid(weapon_source):
|
||||
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:
|
||||
var target: Vector3 = weapon_source.get("homing_target_pos")
|
||||
if target != null and target != Vector3.ZERO:
|
||||
target = weapon_source.get("homing_target_pos")
|
||||
|
||||
if is_painting and target != Vector3.ZERO:
|
||||
target_dir = (target - global_position).normalized()
|
||||
|
||||
if target_dir != Vector3.ZERO:
|
||||
|
||||
@@ -58,6 +58,7 @@ func _on_hit(result: Dictionary) -> void:
|
||||
final_damage = lerpf(damage, min_damage, falloff_factor)
|
||||
|
||||
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)
|
||||
elif result.collider is StaticBody3D or result.collider is CSGShape3D:
|
||||
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.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:
|
||||
is_targeting = 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
|
||||
func _fire() -> void:
|
||||
@@ -141,6 +150,9 @@ func _fire() -> void:
|
||||
var fire_dir = (base_dir + right * x_dir + up * y_dir).normalized()
|
||||
_spawn_custom_projectile(proj_origin, fire_dir)
|
||||
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer:
|
||||
player.server_play_fire_effects.rpc_id(1, proj_origin, fire_dir, weapon_name, false)
|
||||
|
||||
var _current_aim_dir: Vector3 = Vector3.ZERO
|
||||
|
||||
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:
|
||||
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.ignition_timer = randf_range(0.3, 0.6) # Staggered ignition
|
||||
|
||||
|
||||
@@ -61,6 +61,17 @@ func _process(_delta: float) -> void:
|
||||
vm_camera.fov = camera.fov
|
||||
|
||||
func _build_loadout() -> void:
|
||||
var is_auth = false
|
||||
if player:
|
||||
is_auth = player.is_multiplayer_authority()
|
||||
else:
|
||||
is_auth = is_multiplayer_authority()
|
||||
|
||||
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
|
||||
for w in weapons.values():
|
||||
if is_instance_valid(w):
|
||||
@@ -68,6 +79,17 @@ func _build_loadout() -> void:
|
||||
weapons.clear()
|
||||
|
||||
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(2, l["primary_2"])
|
||||
@@ -77,17 +99,34 @@ func _build_loadout() -> void:
|
||||
|
||||
_equip_slot(1)
|
||||
|
||||
func _build_remote_loadout(p1: String, p2: String, sp: String, melee: String) -> void:
|
||||
for w in weapons.values():
|
||||
if is_instance_valid(w):
|
||||
w.queue_free()
|
||||
weapons.clear()
|
||||
|
||||
_spawn_weapon(1, p1)
|
||||
_spawn_weapon(2, p2)
|
||||
_spawn_weapon(3, sp)
|
||||
if melee != "":
|
||||
_spawn_weapon(4, melee)
|
||||
|
||||
_equip_slot(1)
|
||||
|
||||
func _spawn_weapon(slot: int, weapon_id: String) -> void:
|
||||
if weapon_id == "" or weapon_id == "none" or not LoadoutManager.weapon_db.has(weapon_id):
|
||||
print("DEBUG [", multiplayer.get_unique_id(), "]: _spawn_weapon failed check for ", weapon_id)
|
||||
return
|
||||
|
||||
var w_data = LoadoutManager.weapon_db[weapon_id]
|
||||
var script_path = w_data["script"]
|
||||
if script_path == "":
|
||||
print("DEBUG [", multiplayer.get_unique_id(), "]: _spawn_weapon failed script path for ", weapon_id)
|
||||
return
|
||||
|
||||
var script = load(script_path)
|
||||
if script:
|
||||
print("DEBUG [", multiplayer.get_unique_id(), "]: Successfully loaded script, adding weapon ", weapon_id)
|
||||
var w = script.new()
|
||||
w.name = "Weapon_" + str(slot) + "_" + weapon_id
|
||||
# Assuming all weapons have player and camera vars
|
||||
@@ -184,6 +223,9 @@ func _equip_slot(slot: int) -> void:
|
||||
var humanoid = player.get_node("HumanoidModel")
|
||||
if humanoid.has_method("set_weapon") and w.has_meta("script_path"):
|
||||
humanoid.set_weapon(w.get_meta("script_path"))
|
||||
# Update the synced variable so remote peers pick it up
|
||||
if "synced_weapon_path" in player:
|
||||
player.synced_weapon_path = w.get_meta("script_path")
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||
|
||||
Reference in New Issue
Block a user