feat: implement multiplayer network manager and player movement controller with base game entities and UI
This commit is contained in:
@@ -13,6 +13,7 @@ var shield: float = 100.0
|
|||||||
var is_dead: bool = false
|
var is_dead: bool = false
|
||||||
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")
|
||||||
@@ -42,6 +43,18 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
|
|||||||
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
|
||||||
|
|
||||||
var dmg_taken = amount
|
var dmg_taken = amount
|
||||||
if shield > 0:
|
if shield > 0:
|
||||||
if shield >= amount:
|
if shield >= amount:
|
||||||
@@ -76,7 +89,14 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
|
|||||||
|
|
||||||
if source.has_method("get_weapon_name"):
|
if source.has_method("get_weapon_name"):
|
||||||
weapon_name = source.get_weapon_name()
|
weapon_name = source.get_weapon_name()
|
||||||
nm.register_kill.rpc(-1, killer_id, weapon_name, "Target Dummy")
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 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
|
||||||
|
|
||||||
@@ -85,6 +86,18 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
|
|||||||
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
|
||||||
|
|
||||||
var dmg_taken = amount
|
var dmg_taken = amount
|
||||||
if shield > 0:
|
if shield > 0:
|
||||||
if shield >= amount:
|
if shield >= amount:
|
||||||
@@ -119,7 +132,14 @@ func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> v
|
|||||||
|
|
||||||
if source.has_method("get_weapon_name"):
|
if source.has_method("get_weapon_name"):
|
||||||
weapon_name = source.get_weapon_name()
|
weapon_name = source.get_weapon_name()
|
||||||
nm.register_kill.rpc(-1, killer_id, weapon_name, "Walking Dummy")
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|||||||
+28
-10
@@ -6,7 +6,7 @@ signal connection_failed
|
|||||||
signal connection_succeeded
|
signal connection_succeeded
|
||||||
signal match_state_updated
|
signal match_state_updated
|
||||||
signal stats_updated
|
signal stats_updated
|
||||||
signal killfeed_event(victim: String, killer: String, weapon: String)
|
signal killfeed_event(victim: String, killer: String, weapon: String, v_color: String, k_color: String)
|
||||||
|
|
||||||
const DEFAULT_PORT = 31415
|
const DEFAULT_PORT = 31415
|
||||||
|
|
||||||
@@ -77,12 +77,16 @@ func start_singleplayer_match(gamemode: String = "Deathmatch") -> void:
|
|||||||
rpc("sync_match_state", match_active, match_time_remaining, current_gamemode)
|
rpc("sync_match_state", match_active, match_time_remaining, current_gamemode)
|
||||||
|
|
||||||
func _init_player_stats(id: int, username: String) -> void:
|
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] = {
|
player_stats[id] = {
|
||||||
"username": username,
|
"username": username,
|
||||||
"kills": 0,
|
"kills": 0,
|
||||||
"deaths": 0,
|
"deaths": 0,
|
||||||
"assists": 0,
|
"assists": 0,
|
||||||
"ping": 0
|
"ping": 0,
|
||||||
|
"color": p_color
|
||||||
}
|
}
|
||||||
stats_updated.emit()
|
stats_updated.emit()
|
||||||
|
|
||||||
@@ -191,26 +195,40 @@ func receive_ping_response(server_time: int) -> void:
|
|||||||
|
|
||||||
# --- GAMEPLAY EVENTS ---
|
# --- GAMEPLAY EVENTS ---
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func register_kill(victim_id: int, killer_id: int, weapon_name: String, custom_victim_name: String = "") -> void:
|
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 multiplayer.is_server():
|
||||||
if player_stats.has(victim_id):
|
if killer_id != 0 and killer_id != victim_id:
|
||||||
player_stats[victim_id]["deaths"] += 1
|
if player_stats.has(killer_id):
|
||||||
if player_stats.has(killer_id) and killer_id != victim_id:
|
player_stats[killer_id]["kills"] += 1
|
||||||
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 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 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
|
# Auto-fallback for singleplayer testing where player_stats might not be initialized
|
||||||
if not player_stats.has(killer_id) and killer_id == 1:
|
if not player_stats.has(killer_id) and killer_id == 1:
|
||||||
k_name = SettingsManager.username
|
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:
|
if killer_id == 0 or killer_id == victim_id:
|
||||||
k_name = "" # Suicide or world death
|
k_name = "" # Suicide or world death
|
||||||
|
|
||||||
broadcast_killfeed.rpc(v_name, k_name, weapon_name)
|
broadcast_killfeed.rpc(v_name, k_name, weapon_name, v_color, k_color)
|
||||||
sync_player_stats.rpc(player_stats)
|
sync_player_stats.rpc(player_stats)
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func broadcast_killfeed(victim: String, killer: String, weapon: String) -> void:
|
func broadcast_killfeed(victim: String, killer: String, weapon: String, v_color: String = "cccccc", k_color: String = "cccccc") -> void:
|
||||||
killfeed_event.emit(victim, killer, weapon)
|
killfeed_event.emit(victim, killer, weapon, v_color, k_color)
|
||||||
|
|||||||
@@ -26,10 +26,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
|
||||||
@@ -227,6 +227,19 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
|||||||
# Show damage number for self-damage
|
# Show damage number for self-damage
|
||||||
spawn_damage_number(amount, _hit_pos)
|
spawn_damage_number(amount, _hit_pos)
|
||||||
|
|
||||||
|
# Handle damage tracking for assists
|
||||||
|
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
|
time_since_last_damage = 0.0
|
||||||
|
|
||||||
if shield > 0.0:
|
if shield > 0.0:
|
||||||
@@ -247,6 +260,13 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
|||||||
var victim_id = multiplayer.get_unique_id() if is_multiplayer_authority() else int(str(name))
|
var victim_id = multiplayer.get_unique_id() if is_multiplayer_authority() else int(str(name))
|
||||||
var killer_id = 0
|
var killer_id = 0
|
||||||
var weapon_name = "Killed"
|
var weapon_name = "Killed"
|
||||||
|
|
||||||
|
var assist_ids: Array = []
|
||||||
|
var now = Time.get_ticks_msec() / 1000.0
|
||||||
|
for aid in recent_attackers.keys():
|
||||||
|
if aid != killer_id and now - recent_attackers[aid] <= 10.0:
|
||||||
|
assist_ids.append(aid)
|
||||||
|
|
||||||
if _source and _source is Node:
|
if _source and _source is Node:
|
||||||
if _source.has_method("get_multiplayer_authority"):
|
if _source.has_method("get_multiplayer_authority"):
|
||||||
killer_id = _source.get_multiplayer_authority()
|
killer_id = _source.get_multiplayer_authority()
|
||||||
@@ -257,7 +277,7 @@ func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impul
|
|||||||
|
|
||||||
if _source.has_method("get_weapon_name"):
|
if _source.has_method("get_weapon_name"):
|
||||||
weapon_name = _source.get_weapon_name()
|
weapon_name = _source.get_weapon_name()
|
||||||
get_node("/root/NetworkManager").register_kill.rpc(victim_id, killer_id, weapon_name, "")
|
get_node("/root/NetworkManager").register_kill.rpc(victim_id, killer_id, weapon_name, "", assist_ids)
|
||||||
|
|
||||||
func get_weapon_name() -> String:
|
func get_weapon_name() -> String:
|
||||||
if camera:
|
if camera:
|
||||||
@@ -269,8 +289,13 @@ func get_weapon_name() -> String:
|
|||||||
if w and w.has_method("get_weapon_name"):
|
if w and w.has_method("get_weapon_name"):
|
||||||
return w.get_weapon_name()
|
return w.get_weapon_name()
|
||||||
elif w:
|
elif w:
|
||||||
# Format "res://weapons/assault_rifle.tscn" -> "Assault Rifle"
|
var p = ""
|
||||||
var p = w.scene_file_path.get_file().get_basename()
|
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 p.capitalize()
|
||||||
return "Killed"
|
return "Killed"
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,13 @@ func _ready() -> void:
|
|||||||
pl_title.add_theme_constant_override("outline_size", 8)
|
pl_title.add_theme_constant_override("outline_size", 8)
|
||||||
hl_right.add_child(pl_title)
|
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 = ItemList.new()
|
||||||
_host_lobby_players_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
_host_lobby_players_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
_host_lobby_players_list.add_theme_font_size_override("font_size", 24)
|
_host_lobby_players_list.add_theme_font_size_override("font_size", 24)
|
||||||
|
|||||||
+14
-8
@@ -51,7 +51,8 @@ func _ready() -> void:
|
|||||||
|
|
||||||
# Killfeed
|
# Killfeed
|
||||||
var kf_margin = MarginContainer.new()
|
var kf_margin = MarginContainer.new()
|
||||||
kf_margin.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
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_top", 20)
|
||||||
kf_margin.add_theme_constant_override("margin_right", 20)
|
kf_margin.add_theme_constant_override("margin_right", 20)
|
||||||
add_child(kf_margin)
|
add_child(kf_margin)
|
||||||
@@ -164,6 +165,7 @@ func _update_hud() -> void:
|
|||||||
var c_name = Label.new()
|
var c_name = Label.new()
|
||||||
c_name.text = p_data.get("username", "Player")
|
c_name.text = p_data.get("username", "Player")
|
||||||
c_name.add_theme_font_size_override("font_size", 24)
|
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)
|
_scoreboard_grid.add_child(c_name)
|
||||||
|
|
||||||
var c_k = Label.new()
|
var c_k = Label.new()
|
||||||
@@ -188,14 +190,18 @@ func _update_hud() -> void:
|
|||||||
|
|
||||||
_leader_label.text = "Leader: %s (%s Kills)" % [leader_name, max_kills]
|
_leader_label.text = "Leader: %s (%s Kills)" % [leader_name, max_kills]
|
||||||
|
|
||||||
func _on_killfeed_event(victim: String, killer: String, weapon: String) -> void:
|
func _on_killfeed_event(victim: String, killer: String, weapon: String, v_color: String = "cccccc", k_color: String = "cccccc") -> void:
|
||||||
var l = Label.new()
|
var l = RichTextLabel.new()
|
||||||
if killer == "":
|
l.bbcode_enabled = true
|
||||||
l.text = "%s died." % victim
|
l.fit_content = true
|
||||||
else:
|
l.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||||
l.text = "%s [%s] %s" % [killer, weapon, victim]
|
|
||||||
|
|
||||||
l.add_theme_font_size_override("font_size", 20)
|
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_color_override("font_outline_color", Color.BLACK)
|
||||||
l.add_theme_constant_override("outline_size", 4)
|
l.add_theme_constant_override("outline_size", 4)
|
||||||
_killfeed_vbox.add_child(l)
|
_killfeed_vbox.add_child(l)
|
||||||
|
|||||||
Reference in New Issue
Block a user