Compare commits

..
4 Commits
6 changed files with 142 additions and 33 deletions
+13
View File
@@ -11,6 +11,7 @@ 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 = {} var recent_attackers: Dictionary = {}
@@ -39,6 +40,16 @@ func _ready() -> void:
_update_label() _update_label()
func _process(delta: float) -> void:
if is_dead:
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: func take_damage(amount: float, hit_position: Vector3, source: Node = null, _impulse: Vector3 = Vector3.ZERO) -> void:
if is_dead: if is_dead:
return return
@@ -55,6 +66,8 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null, _imp
if attacker_id != 0: if attacker_id != 0:
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0 recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
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:
+13
View File
@@ -11,6 +11,7 @@ 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 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
@@ -51,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
@@ -98,6 +107,8 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null, impu
if attacker_id != 0: if attacker_id != 0:
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0 recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
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:
@@ -109,6 +120,8 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null, impu
if amount > 0: if amount > 0:
health -= amount health -= amount
if health <= 0:
health = 0
_update_label() _update_label()
+7 -4
View File
@@ -94,11 +94,14 @@ func _physics_process(delta: float) -> void:
_try_start_grapple() _try_start_grapple()
if is_grapple_shooting: if is_grapple_shooting:
grapple_shoot_time += delta if not input_grapple:
if grapple_shoot_time >= grapple_travel_time:
is_grapple_shooting = false is_grapple_shooting = false
movement_event.emit("grapple_latch", {}) else:
switch_to("grapple") grapple_shoot_time += delta
if grapple_shoot_time >= grapple_travel_time:
is_grapple_shooting = false
movement_event.emit("grapple_latch", {})
switch_to("grapple")
# Manage global crouch state # Manage global crouch state
var want_crouch = input_crouch var want_crouch = input_crouch
+95 -17
View File
@@ -252,6 +252,13 @@ 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 attacker_id = 0
var weapon_name = "Killed" var weapon_name = "Killed"
@@ -454,6 +461,10 @@ func rpc_play_explosion(pos: Vector3, radius: float) -> void:
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func server_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_name: String, impulse: Vector3) -> void: 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 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 # 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_take_damage.rpc(amount, hit_pos, attacker_id, weapon_name, impulse)
@@ -466,7 +477,16 @@ func rpc_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_n
# Show damage number for the attacker # Show damage number for the attacker
if not multiplayer.has_multiplayer_peer() or attacker_id == multiplayer.get_unique_id(): if not multiplayer.has_multiplayer_peer() or attacker_id == multiplayer.get_unique_id():
spawn_damage_number(amount, hit_pos) 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: if attacker_id != 0:
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0 recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
@@ -488,8 +508,8 @@ func rpc_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_n
die(impulse) die(impulse)
if multiplayer.is_server(): if multiplayer.is_server():
if get_node_or_null("/root/NetworkManager") and multiplayer.has_multiplayer_peer(): var nm = get_node_or_null("/root/NetworkManager")
var victim_id = multiplayer.get_unique_id() if nm and multiplayer.has_multiplayer_peer():
var killer_id = attacker_id var killer_id = attacker_id
var assist_ids: Array = [] var assist_ids: Array = []
@@ -498,7 +518,56 @@ func rpc_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_n
if aid != killer_id and now - recent_attackers[aid] <= 10.0: if aid != killer_id and now - recent_attackers[aid] <= 10.0:
assist_ids.append(aid) assist_ids.append(aid)
get_node("/root/NetworkManager").register_kill.rpc(victim_id, killer_id, weapon_name, "", assist_ids) 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: func get_weapon_name() -> String:
if camera: if camera:
@@ -606,9 +675,8 @@ func _physics_process(_delta: float) -> void:
_client_grapple_just_pressed = false _client_grapple_just_pressed = false
# Update synced properties for the grapple # Update synced properties for the grapple
if sm.current_state == "grapple" or sm.is_grapple_shooting: synced_grapple_point = sm.grapple_point
synced_grapple_point = sm.grapple_point synced_is_grapple_shooting = sm.is_grapple_shooting
synced_is_grapple_shooting = sm.is_grapple_shooting
# Update grapple rope visuals (runs on all clients for smooth interpolation) # Update grapple rope visuals (runs on all clients for smooth interpolation)
if (synced_movement_state == "grapple" or synced_is_grapple_shooting) and camera: if (synced_movement_state == "grapple" or synced_is_grapple_shooting) and camera:
@@ -672,6 +740,15 @@ func _process(delta: float) -> void:
humanoid.set_weapon(synced_weapon_path) humanoid.set_weapon(synced_weapon_path)
humanoid.set_meta("current_weapon_path", synced_weapon_path) humanoid.set_meta("current_weapon_path", synced_weapon_path)
return 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):
@@ -694,14 +771,6 @@ func _process(delta: float) -> void:
shield += 20.0 * delta shield += 20.0 * delta
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()
@@ -903,8 +972,8 @@ func die(impulse: Vector3 = Vector3.ZERO) -> void:
death_screen.visible = true death_screen.visible = true
set_process_input(true) set_process_input(true)
func _apply_ragdoll_velocity(ragdoll: Node3D, l_vel: Vector3, imp: Vector3) -> void: func _apply_ragdoll_velocity(ragdoll, l_vel: Vector3, imp: Vector3) -> void:
if is_instance_valid(ragdoll): if is_instance_valid(ragdoll) and ragdoll.has_method("apply_initial_velocities"):
ragdoll.apply_initial_velocities(l_vel, imp) ragdoll.apply_initial_velocities(l_vel, imp)
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
@@ -915,12 +984,16 @@ func _input(event: InputEvent) -> void:
_on_respawn_pressed() _on_respawn_pressed()
func _on_respawn_pressed() -> void: func _on_respawn_pressed() -> void:
if death_screen:
death_screen.visible = false
if multiplayer.is_server(): if multiplayer.is_server():
var spawn_pos = Vector3(randf_range(-5, 5), 5, randf_range(-5, 5)) var spawn_pos = Vector3(randf_range(-5, 5), 5, randf_range(-5, 5))
rpc_respawn.rpc(spawn_pos) rpc_respawn.rpc(spawn_pos)
else: else:
rpc_request_respawn.rpc_id(1) rpc_request_respawn.rpc_id(1)
var _last_respawn_request_time: float = 0.0
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func rpc_request_respawn() -> void: func rpc_request_respawn() -> void:
if not multiplayer.is_server(): if not multiplayer.is_server():
@@ -929,6 +1002,11 @@ func rpc_request_respawn() -> void:
if sender_id != 1 and sender_id != str(name).to_int(): if sender_id != 1 and sender_id != str(name).to_int():
return 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: if is_dead:
var spawn_pos = Vector3(randf_range(-5, 5), 5, randf_range(-5, 5)) var spawn_pos = Vector3(randf_range(-5, 5), 5, randf_range(-5, 5))
rpc_respawn.rpc(spawn_pos) rpc_respawn.rpc(spawn_pos)
+5 -6
View File
@@ -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:
+9 -6
View File
@@ -191,16 +191,19 @@ 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:
col.apply_impulse(dir * final_knockback) 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)
# 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
elif owner_player and owner_player.multiplayer.is_server(): elif owner_player:
col.take_damage(final_damage, target_pos, owner_player, dir * final_knockback) col.take_damage(final_damage, target_pos, owner_player, dir * final_knockback)
# Broadcast Visual Explosion Effect to other peers # Broadcast Visual Explosion Effect to other peers