- Clients now handle the host closing the arena: bank winnings if still alive (retire), reset the HUD, and return to the camp menu instead of being stranded in a dead world - Death in co-op offers Spectate when a teammate is still fighting: closes the results overlay and watches the run; the escape menu gains a "Back to Camp" option for dead spectators to leave later - Late joiners receive the current wave number on connect so their HUD doesn't sit on "Prepare..." until the next wave starts
855 lines
32 KiB
GDScript
855 lines
32 KiB
GDScript
extends Node3D
|
|
|
|
@onready var skin_input: LineEdit = $Menu/MainContainer/MainMenu/Option2/SkinInput
|
|
@onready var nick_input: LineEdit = $Menu/MainContainer/MainMenu/Option1/NickInput
|
|
@onready var address_input: LineEdit = $Menu/MainContainer/MainMenu/Option3/AddressInput
|
|
@onready var players_container: Node3D = $PlayersContainer
|
|
@onready var weapons_container: Node3D = $WeaponsContainer
|
|
@onready var enemies_container: Node3D = $EnemiesContainer
|
|
var orbs_container: Node3D = null
|
|
@onready var enemy_spawner: EnemySpawner = $EnemySpawner
|
|
@onready var player_spawn_points: Node3D = $PlayerSpawnPoints
|
|
@onready var menu: Control = $Menu
|
|
@onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu
|
|
@export var player_scene: PackedScene
|
|
@export var practice_dummy_scene: PackedScene
|
|
@export var armed_enemy_scene: PackedScene
|
|
|
|
# Weapon spawning counter (server-side only)
|
|
var _weapon_spawn_counter: int = 0
|
|
# Track active weapons for late-join sync (server-side only)
|
|
var _active_weapons: Dictionary = {} # weapon_id -> WorldWeapon reference
|
|
# Track if we've already initialized to prevent double-spawning
|
|
var _multiplayer_initialized: bool = false
|
|
# Track next spawn point for round-robin spawning (server-side only)
|
|
var _next_spawn_index: int = 0
|
|
|
|
# multiplayer chat
|
|
@onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message
|
|
@onready var send: Button = $MultiplayerChat/VBoxContainer/HBoxContainer/Send
|
|
@onready var chat: TextEdit = $MultiplayerChat/VBoxContainer/Chat
|
|
@onready var multiplayer_chat: Control = $MultiplayerChat
|
|
|
|
var chat_visible = false
|
|
|
|
func _ready():
|
|
print("[Level] _ready() called. Peer ID: ", multiplayer.get_unique_id(), " Is server: ", multiplayer.is_server())
|
|
|
|
multiplayer_chat.hide()
|
|
menu.show()
|
|
multiplayer_chat.set_process_input(true)
|
|
|
|
# Add quick-fill preset buttons
|
|
_create_preset_buttons()
|
|
|
|
# Reshape the menu into the gladiator outfitting screen
|
|
_setup_outfitting_menu()
|
|
|
|
# Clients: if the host closes the arena, bank winnings and return to camp
|
|
Network.server_disconnected.connect(_on_server_closed)
|
|
|
|
# Create or find weapons container
|
|
if has_node("WeaponsContainer"):
|
|
weapons_container = get_node("WeaponsContainer")
|
|
else:
|
|
weapons_container = Node3D.new()
|
|
weapons_container.name = "WeaponsContainer"
|
|
add_child(weapons_container)
|
|
print("Created WeaponsContainer")
|
|
|
|
# Create or find enemies container
|
|
if has_node("EnemiesContainer"):
|
|
enemies_container = get_node("EnemiesContainer")
|
|
else:
|
|
enemies_container = Node3D.new()
|
|
enemies_container.name = "EnemiesContainer"
|
|
add_child(enemies_container)
|
|
print("Created EnemiesContainer")
|
|
|
|
# Create or find health orbs container
|
|
if has_node("OrbsContainer"):
|
|
orbs_container = get_node("OrbsContainer")
|
|
else:
|
|
orbs_container = Node3D.new()
|
|
orbs_container.name = "OrbsContainer"
|
|
add_child(orbs_container)
|
|
print("Created OrbsContainer")
|
|
|
|
# Don't initialize weapons in _ready() - wait for Host/Join to be pressed
|
|
# This is handled in initialize_multiplayer() which is called after the
|
|
# multiplayer peer is properly set up
|
|
print("[Level] _ready() complete - waiting for Host/Join")
|
|
|
|
func _on_connected_to_server():
|
|
print("[Level] Connected to server! Cleaning up manual weapons")
|
|
_cleanup_manual_weapons_on_client()
|
|
|
|
## The host closed the arena while we were connected (client side).
|
|
## Leaving alive counts as retiring - bank everything, then back to camp.
|
|
func _on_server_closed():
|
|
print("[Level] Server disconnected - returning to camp")
|
|
if GameState.run_active:
|
|
GameState.retire_run()
|
|
|
|
if has_node("/root/HUD"):
|
|
get_node("/root/HUD").reset()
|
|
|
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
get_tree().reload_current_scene()
|
|
|
|
## Called by Network when multiplayer peer is set up
|
|
func initialize_multiplayer():
|
|
print("[Level] initialize_multiplayer called. is_server: ", multiplayer.is_server())
|
|
|
|
# Prevent double initialization
|
|
if _multiplayer_initialized:
|
|
print("[Level] Already initialized, skipping")
|
|
return
|
|
|
|
_multiplayer_initialized = true
|
|
|
|
# Begin this player's arena run (starts the survival clock + gold tracking)
|
|
GameState.start_run()
|
|
|
|
if multiplayer.is_server():
|
|
print("[Level] Running server initialization")
|
|
Network.connect("player_connected", Callable(self, "_on_player_connected"))
|
|
multiplayer.peer_disconnected.connect(_remove_player)
|
|
|
|
# Remove any manually placed weapons - free floor loot undermines the shop.
|
|
# Weapons only enter the arena via the armory or dropped by slain enemies.
|
|
_remove_manual_weapons_on_server()
|
|
|
|
# Spawn practice dummies
|
|
_spawn_practice_dummies()
|
|
|
|
# Spawn armed enemies
|
|
_spawn_armed_enemies()
|
|
|
|
# Scatter a few health orbs so the pickup is visible from the start
|
|
_spawn_initial_health_orbs()
|
|
|
|
# Begin automatic timed enemy waves
|
|
if enemy_spawner:
|
|
enemy_spawner.start_auto_waves()
|
|
|
|
# Spawn the host player (peer ID 1)
|
|
print("[Level] Spawning host player")
|
|
var host_info = Network.players.get(1, {"nick": "Host", "skin": "blue"})
|
|
_add_player(1, host_info)
|
|
else:
|
|
# Client initialization - clean up manual weapons immediately
|
|
print("[Level] Running client initialization - cleaning up manual weapons")
|
|
_cleanup_manual_weapons_on_client()
|
|
|
|
func _cleanup_manual_weapons_on_client():
|
|
"""Remove manually placed weapons on clients (server will sync them via RPC)"""
|
|
print("[Client ", multiplayer.get_unique_id(), "] _cleanup_manual_weapons_on_client called")
|
|
|
|
if not weapons_container:
|
|
print("[Client] No weapons_container found!")
|
|
return
|
|
|
|
print("[Client] WeaponsContainer has ", weapons_container.get_child_count(), " children")
|
|
|
|
var weapons_to_remove = []
|
|
for child in weapons_container.get_children():
|
|
print("[Client] Checking child: ", child.name, " (type: ", child.get_class(), ")")
|
|
if child is WorldWeapon:
|
|
print("[Client] - Is WorldWeapon with weapon_id: ", child.weapon_id)
|
|
if child.weapon_id == -1:
|
|
weapons_to_remove.append(child)
|
|
print("[Client] - Marked for removal")
|
|
|
|
print("[Client] Found ", weapons_to_remove.size(), " weapons to remove")
|
|
for weapon in weapons_to_remove:
|
|
print("[Client] Removing manually placed weapon: ", weapon.name)
|
|
# Use immediate removal to prevent RPC errors
|
|
weapons_container.remove_child(weapon)
|
|
weapon.free()
|
|
|
|
func _remove_manual_weapons_on_server():
|
|
"""Remove WorldWeapon nodes manually placed in level.tscn (server side).
|
|
Clients already remove their local copies in _cleanup_manual_weapons_on_client.
|
|
The arena economy requires gear to be bought or looted from slain enemies."""
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
if not weapons_container:
|
|
return
|
|
|
|
var removed = 0
|
|
for child in weapons_container.get_children():
|
|
if child is WorldWeapon and child.weapon_id == -1:
|
|
weapons_container.remove_child(child)
|
|
child.free()
|
|
removed += 1
|
|
|
|
if removed > 0:
|
|
print("[Server] Removed ", removed, " manually placed floor weapon(s)")
|
|
|
|
func _on_player_connected(peer_id, player_info):
|
|
print("[Server] _on_player_connected called for peer ", peer_id, " with info: ", player_info)
|
|
_add_player(peer_id, player_info)
|
|
|
|
# Sync existing players to the newly joined player
|
|
if multiplayer.is_server() and peer_id != 1:
|
|
print("[Server] Syncing existing players to newly connected peer: ", peer_id)
|
|
# Wait a frame to ensure new player is fully initialized
|
|
await get_tree().process_frame
|
|
for existing_player in players_container.get_children():
|
|
var existing_id = int(existing_player.name)
|
|
if existing_id != peer_id: # Don't sync the player to themselves
|
|
print("[Server] Syncing existing player ", existing_id, " to peer ", peer_id)
|
|
rpc_id(peer_id, "_spawn_player_local", existing_id, existing_player.position)
|
|
|
|
# Sync existing weapons to the newly joined player (but not to server itself)
|
|
if multiplayer.is_server() and peer_id != 1:
|
|
print("[Server] Syncing weapons to newly connected peer: ", peer_id)
|
|
print("[Server] Active weapons in _active_weapons: ", _active_weapons.keys())
|
|
print("[Server] Active weapons count: ", _active_weapons.size())
|
|
for weapon_id in _active_weapons.keys():
|
|
var weapon = _active_weapons[weapon_id]
|
|
if is_instance_valid(weapon) and weapon.weapon_data:
|
|
print("[Server] Sending weapon ", weapon_id, " (", weapon.weapon_data.weapon_name, ") at position ", weapon.global_position, " to peer ", peer_id)
|
|
# Send current position and zero velocity for syncing
|
|
rpc_id(peer_id, "_client_spawn_weapon",
|
|
weapon.weapon_data.resource_path,
|
|
weapon.global_position,
|
|
Vector3.ZERO,
|
|
weapon_id
|
|
)
|
|
else:
|
|
print("[Server] Skipping invalid weapon ", weapon_id)
|
|
|
|
# Sync existing enemies to the newly joined player
|
|
print("[Server] Syncing enemies to newly connected peer: ", peer_id)
|
|
if enemies_container:
|
|
for enemy in enemies_container.get_children():
|
|
if enemy is BaseEnemy:
|
|
# Check if it's an ArmedEnemy or PracticeDummy
|
|
if enemy.name.begins_with("ArmedEnemy_"):
|
|
# Sync armed enemy with its weapons
|
|
var main_weapon_path = ""
|
|
var offhand_weapon_path = ""
|
|
if "equipped_weapon" in enemy and enemy.equipped_weapon and enemy.equipped_weapon.weapon_data:
|
|
main_weapon_path = enemy.equipped_weapon.weapon_data.resource_path
|
|
if "equipped_offhand" in enemy and enemy.equipped_offhand and enemy.equipped_offhand.weapon_data:
|
|
offhand_weapon_path = enemy.equipped_offhand.weapon_data.resource_path
|
|
print("[Server] Syncing armed enemy ", enemy.name, " to peer ", peer_id)
|
|
rpc_id(peer_id, "_spawn_armed_enemy_local", enemy.name, enemy.global_position, main_weapon_path, offhand_weapon_path)
|
|
elif enemy.name.begins_with("PracticeDummy_"):
|
|
# Extract ID from name (e.g., "PracticeDummy_1" -> 1)
|
|
var enemy_name_parts = enemy.name.split("_")
|
|
if enemy_name_parts.size() >= 2:
|
|
var enemy_id = enemy_name_parts[-1].to_int()
|
|
print("[Server] Syncing enemy ", enemy.name, " at position ", enemy.global_position, " to peer ", peer_id)
|
|
rpc_id(peer_id, "_spawn_dummy_local", enemy_id, enemy.global_position)
|
|
|
|
# Sync existing health orbs to the newly joined player
|
|
print("[Server] Syncing health orbs to newly connected peer: ", peer_id)
|
|
for orb_id in _active_orbs.keys():
|
|
rpc_id(peer_id, "_spawn_health_orb_local", orb_id, _active_orbs[orb_id])
|
|
|
|
# Sync the current wave number so the late joiner's HUD isn't stuck on "Prepare..."
|
|
GameState.rpc_id(peer_id, "sync_wave", GameState.current_wave)
|
|
|
|
# Sync equipped weapons for all existing players to the newly joined player
|
|
print("[Server] Syncing equipped weapons to newly connected peer: ", peer_id)
|
|
for player_node in players_container.get_children():
|
|
var player = player_node as Character
|
|
if player and is_instance_valid(player):
|
|
# Skip the newly joined player (they don't have weapons yet)
|
|
if int(player.name) == peer_id:
|
|
continue
|
|
|
|
# Sync main hand weapon
|
|
if player.equipped_weapon and player.equipped_weapon.weapon_data:
|
|
print("[Server] Syncing main hand weapon for player ", player.name, " to peer ", peer_id)
|
|
player.rpc_id(peer_id, "equip_weapon_from_world",
|
|
player.equipped_weapon.weapon_data.resource_path)
|
|
|
|
# Sync off-hand weapon
|
|
if player.equipped_offhand and player.equipped_offhand.weapon_data:
|
|
print("[Server] Syncing off-hand weapon for player ", player.name, " to peer ", peer_id)
|
|
player.rpc_id(peer_id, "equip_weapon_from_world",
|
|
player.equipped_offhand.weapon_data.resource_path)
|
|
|
|
func _on_host_pressed():
|
|
print("[Level] Host button pressed")
|
|
menu.hide()
|
|
print("[Level] Calling Network.start_host()")
|
|
Network.start_host(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower())
|
|
print("[Level] Waiting one frame...")
|
|
await get_tree().process_frame
|
|
print("[Level] Calling initialize_multiplayer()")
|
|
initialize_multiplayer()
|
|
|
|
func _on_join_pressed():
|
|
print("[Level] Join button pressed")
|
|
menu.hide()
|
|
print("[Level] Calling Network.join_game()")
|
|
Network.join_game(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower(), address_input.text.strip_edges())
|
|
print("[Level] Waiting one frame...")
|
|
await get_tree().process_frame
|
|
print("[Level] Calling initialize_multiplayer()")
|
|
initialize_multiplayer()
|
|
|
|
func _add_player(id: int, player_info : Dictionary):
|
|
print("[Level] _add_player called for peer ", id, " with info: ", player_info)
|
|
|
|
# Server spawns player and replicates to all clients via RPC
|
|
if multiplayer.is_server():
|
|
if players_container.has_node(str(id)):
|
|
print("[Level] Player ", id, " already exists, skipping")
|
|
return
|
|
|
|
var spawn_pos = get_spawn_point()
|
|
print("[Level] Server spawning player ", id, " at ", spawn_pos)
|
|
|
|
# Spawn on server and all clients (call_local does both)
|
|
rpc("_spawn_player_local", id, spawn_pos)
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func _spawn_player_local(id: int, spawn_pos: Vector3):
|
|
if players_container.has_node(str(id)):
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Player ", id, " already exists, skipping")
|
|
return
|
|
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Creating player instance for peer ", id)
|
|
var player = player_scene.instantiate()
|
|
player.name = str(id)
|
|
player.position = spawn_pos
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Adding player to PlayersContainer")
|
|
players_container.add_child(player, true)
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Player ", id, " spawned at ", player.position)
|
|
|
|
# Get player info from Network
|
|
var player_info = Network.players.get(id, {"nick": "Player", "skin": "blue"})
|
|
|
|
var nick = player_info["nick"]
|
|
# Access nickname directly via node path since @onready hasn't loaded yet
|
|
var nickname_label = player.get_node_or_null("PlayerNick/Nickname")
|
|
if nickname_label:
|
|
nickname_label.text = nick
|
|
# player.rpc("change_nick", nick)
|
|
|
|
# Set up HUD for local player
|
|
if id == multiplayer.get_unique_id():
|
|
print("[Level] Setting up HUD for local player")
|
|
# Wait a frame to ensure HUD autoload is ready
|
|
await get_tree().process_frame
|
|
if has_node("/root/HUD"):
|
|
get_node("/root/HUD").set_local_player(player)
|
|
else:
|
|
push_warning("[Level] HUD autoload not found! Make sure to restart Godot to register the new autoload.")
|
|
|
|
# Equip the loadout chosen in the outfitting screen (broadcasts to all peers)
|
|
var loadout = GameState.get_selected_loadout()
|
|
if loadout.get("main", "") != "":
|
|
player.rpc("equip_weapon_from_world", loadout["main"])
|
|
if loadout.get("offhand", "") != "":
|
|
player.rpc("equip_weapon_from_world", loadout["offhand"])
|
|
|
|
var skin_enum = player_info["skin"]
|
|
player.set_player_skin(skin_enum)
|
|
# rpc("sync_player_skin", id, skin_enum)
|
|
|
|
# rpc("sync_player_position", id, player.position)
|
|
|
|
func get_spawn_point() -> Vector3:
|
|
# Use PlayerSpawnPoint container if available
|
|
if player_spawn_points and player_spawn_points.get_child_count() > 0:
|
|
var spawn_children = player_spawn_points.get_children()
|
|
|
|
# Use round-robin to distribute players across spawn points
|
|
var spawn_index = _next_spawn_index % spawn_children.size()
|
|
_next_spawn_index = (spawn_index + 1) % spawn_children.size()
|
|
|
|
var spawn_node = spawn_children[spawn_index]
|
|
print("[Level] Using spawn point ", spawn_index, " at ", spawn_node.global_position)
|
|
return spawn_node.global_position
|
|
else:
|
|
# Fallback to random circle spawn if no spawn points defined
|
|
print("[Level] Warning: No PlayerSpawnPoint container found, using fallback random spawn")
|
|
var spawn_point = Vector2.from_angle(randf() * 2 * PI) * 10 # spawn radius
|
|
return Vector3(spawn_point.x, 0, spawn_point.y)
|
|
|
|
func _remove_player(id):
|
|
if not multiplayer.is_server() or not players_container.has_node(str(id)):
|
|
return
|
|
var player_node = players_container.get_node(str(id))
|
|
if player_node:
|
|
player_node.queue_free()
|
|
|
|
# @rpc("any_peer", "call_local")
|
|
# func sync_player_position(id: int, new_position: Vector3):
|
|
# var player = players_container.get_node(str(id))
|
|
# if player:
|
|
# player.position = new_position
|
|
|
|
# @rpc("any_peer", "call_local")
|
|
# func sync_player_skin(id: int, skin_color: Character.SkinColor):
|
|
# var player = players_container.get_node(str(id))
|
|
# if player:
|
|
# player.set_player_skin(skin_color)
|
|
|
|
func _on_quit_pressed() -> void:
|
|
get_tree().quit()
|
|
|
|
# ---------- MULTIPLAYER CHAT ----------
|
|
func toggle_chat():
|
|
if menu.visible:
|
|
return
|
|
|
|
chat_visible = !chat_visible
|
|
if chat_visible:
|
|
multiplayer_chat.show()
|
|
message.grab_focus()
|
|
else:
|
|
multiplayer_chat.hide()
|
|
get_viewport().set_input_as_handled()
|
|
|
|
func is_chat_visible() -> bool:
|
|
return chat_visible
|
|
|
|
func _input(event):
|
|
if event.is_action_pressed("toggle_chat"):
|
|
toggle_chat()
|
|
elif event is InputEventKey and event.keycode == KEY_ENTER:
|
|
_on_send_pressed()
|
|
elif event is InputEventKey and event.keycode == KEY_N and event.pressed:
|
|
# Start next wave (server only, press N)
|
|
if multiplayer.is_server() and enemy_spawner:
|
|
enemy_spawner.start_next_wave()
|
|
print("[Level] Starting wave via N key")
|
|
elif event is InputEventKey and event.keycode == KEY_H and event.pressed:
|
|
# Toggle hitbox/hurtbox debug visualization
|
|
HitBox.debug_visible = not HitBox.debug_visible
|
|
HurtBox.debug_visible = not HurtBox.debug_visible
|
|
print("[Level] Hitbox/Hurtbox debug visibility: ", HitBox.debug_visible)
|
|
|
|
func _on_send_pressed() -> void:
|
|
var trimmed_message = message.text.strip_edges()
|
|
if trimmed_message == "":
|
|
return # do not send empty messages
|
|
|
|
var nick = Network.players[multiplayer.get_unique_id()]["nick"]
|
|
|
|
rpc("msg_rpc", nick, trimmed_message)
|
|
message.text = ""
|
|
message.grab_focus()
|
|
|
|
@rpc("any_peer", "call_local")
|
|
func msg_rpc(nick, msg):
|
|
chat.text += str(nick, " : ", msg, "\n")
|
|
|
|
# ---------- OUTFITTING MENU ----------
|
|
## Turn the plain host/join menu into the gladiator camp:
|
|
## menu options on the left, armory shop + character sheet preview on the right.
|
|
func _setup_outfitting_menu():
|
|
# Retitle and shrink the header so it fits the narrowed left column
|
|
var title = menu.get_node_or_null("MainContainer/Label")
|
|
if title:
|
|
title.text = "SURVIVAL OF\nTHE SNIPPEST"
|
|
title.add_theme_font_size_override("font_size", 42)
|
|
|
|
# Narrow the existing menu column to the left third of the screen
|
|
var main_container = menu.get_node_or_null("MainContainer")
|
|
if main_container:
|
|
main_container.anchor_right = 0.34
|
|
|
|
# Rename buttons to fit the arena fiction
|
|
var host_button = menu.get_node_or_null("MainContainer/MainMenu/Buttons/Host")
|
|
if host_button:
|
|
host_button.text = "ENTER ARENA"
|
|
var join_button = menu.get_node_or_null("MainContainer/MainMenu/Buttons/Join")
|
|
if join_button:
|
|
join_button.text = "JOIN FRIEND"
|
|
|
|
# Armory + preview fill the rest of the screen
|
|
var outfitting_script = load("res://level/ui/scripts/outfitting_screen.gd")
|
|
var outfitting = outfitting_script.new()
|
|
outfitting.name = "OutfittingScreen"
|
|
outfitting.anchor_left = 0.35
|
|
outfitting.anchor_top = 0.04
|
|
outfitting.anchor_right = 0.985
|
|
outfitting.anchor_bottom = 0.96
|
|
menu.add_child(outfitting)
|
|
outfitting.set_nick_input(nick_input)
|
|
|
|
# ---------- PRESET BUTTONS ----------
|
|
func _create_preset_buttons():
|
|
# Create a container for preset buttons
|
|
var preset_container = HBoxContainer.new()
|
|
preset_container.name = "PresetContainer"
|
|
|
|
var preset_label = Label.new()
|
|
preset_label.text = "Quick Fill:"
|
|
preset_container.add_child(preset_label)
|
|
|
|
# Scott button
|
|
var scott_button = Button.new()
|
|
scott_button.text = "Scott"
|
|
scott_button.pressed.connect(_on_scott_preset)
|
|
preset_container.add_child(scott_button)
|
|
|
|
# Jemz button
|
|
var jemz_button = Button.new()
|
|
jemz_button.text = "Jemz"
|
|
jemz_button.pressed.connect(_on_jemz_preset)
|
|
preset_container.add_child(jemz_button)
|
|
|
|
# Add to main menu at the top
|
|
main_menu.add_child(preset_container)
|
|
main_menu.move_child(preset_container, 0)
|
|
|
|
func _on_scott_preset():
|
|
nick_input.text = "Scott"
|
|
skin_input.text = "Blue"
|
|
address_input.text = "127.0.0.1"
|
|
|
|
func _on_jemz_preset():
|
|
nick_input.text = "Jemz"
|
|
skin_input.text = "Red"
|
|
address_input.text = "127.0.0.1"
|
|
|
|
# ---------- ENEMY SPAWNING ----------
|
|
func _spawn_practice_dummies():
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
if not practice_dummy_scene:
|
|
push_warning("Practice dummy scene not assigned!")
|
|
return
|
|
|
|
# Wait a frame for everything to be ready
|
|
await get_tree().process_frame
|
|
|
|
print("[Server] Spawning practice dummies")
|
|
|
|
# Spawn dummies at different positions
|
|
var dummy_positions = [
|
|
Vector3(10, 0, 0),
|
|
Vector3(-10, 0, 0),
|
|
Vector3(0, 0, 10),
|
|
Vector3(0, 0, -10),
|
|
]
|
|
|
|
var dummy_counter = 0
|
|
for pos in dummy_positions:
|
|
dummy_counter += 1
|
|
rpc("_spawn_dummy_local", dummy_counter, pos)
|
|
|
|
## Spawn a practice dummy on all clients
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func _spawn_dummy_local(dummy_id: int, spawn_pos: Vector3):
|
|
if not practice_dummy_scene:
|
|
push_error("Practice dummy scene not loaded!")
|
|
return
|
|
|
|
if not enemies_container:
|
|
push_error("EnemiesContainer not found!")
|
|
return
|
|
|
|
var dummy_name = "PracticeDummy_" + str(dummy_id)
|
|
|
|
# Don't spawn duplicates
|
|
if enemies_container.has_node(dummy_name):
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " already exists")
|
|
return
|
|
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Spawning dummy ", dummy_name, " at ", spawn_pos)
|
|
|
|
var dummy = practice_dummy_scene.instantiate()
|
|
dummy.name = dummy_name
|
|
dummy.position = spawn_pos
|
|
|
|
# Set multiplayer authority to server
|
|
dummy.set_multiplayer_authority(1)
|
|
|
|
enemies_container.add_child(dummy, true)
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " spawned successfully")
|
|
|
|
# ---------- WEAPON SPAWNING ----------
|
|
## Spawn a weapon in the world (called from server, syncs to all clients)
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func spawn_world_weapon(weapon_data_path: String, spawn_position: Vector3, initial_velocity: Vector3, weapon_id: int):
|
|
print("[Client ", multiplayer.get_unique_id(), "] spawn_world_weapon called for weapon_id: ", weapon_id)
|
|
|
|
if not weapons_container:
|
|
push_error("WeaponsContainer not found in level!")
|
|
return
|
|
|
|
# Load the weapon data resource
|
|
var weapon_data = load(weapon_data_path) as WeaponData
|
|
if not weapon_data:
|
|
push_error("Failed to load weapon data from: " + weapon_data_path)
|
|
return
|
|
|
|
# Create WorldWeapon instance
|
|
var world_weapon = WorldWeapon.new()
|
|
world_weapon.weapon_data = weapon_data
|
|
print("[DEBUG] About to set weapon_id. Parameter weapon_id = ", weapon_id)
|
|
world_weapon.weapon_id = weapon_id # Store the ID
|
|
print("[DEBUG] After setting weapon_id. world_weapon.weapon_id = ", world_weapon.weapon_id)
|
|
world_weapon.name = "WorldWeapon_" + str(weapon_id) # Deterministic name
|
|
print("[DEBUG] Set weapon name to: ", world_weapon.name, " using weapon_id: ", weapon_id)
|
|
world_weapon.position = spawn_position
|
|
|
|
# Remove existing weapon with same name if it exists (prevents duplicates)
|
|
var weapon_path = NodePath(world_weapon.name)
|
|
if weapons_container.has_node(weapon_path):
|
|
print("[DEBUG] Weapon ", world_weapon.name, " already exists, removing old one first")
|
|
var old_weapon = weapons_container.get_node(weapon_path)
|
|
weapons_container.remove_child(old_weapon)
|
|
old_weapon.queue_free()
|
|
|
|
# Add to weapons container
|
|
weapons_container.add_child(world_weapon)
|
|
print("[DEBUG] After add_child, weapon name in tree: ", world_weapon.name, " weapon_id: ", world_weapon.weapon_id)
|
|
|
|
# Track this weapon on the server (AFTER adding to tree so signals work)
|
|
if multiplayer.is_server():
|
|
_active_weapons[weapon_id] = world_weapon
|
|
print("[Server] Added weapon ", weapon_id, " to _active_weapons. Total: ", _active_weapons.size())
|
|
# Connect to the weapon's removal signal
|
|
world_weapon.tree_exiting.connect(_on_weapon_removed.bind(weapon_id))
|
|
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Spawned weapon: ", world_weapon.name, " at ", spawn_position)
|
|
|
|
# Apply velocity after physics is ready
|
|
if initial_velocity != Vector3.ZERO:
|
|
await get_tree().process_frame
|
|
world_weapon.linear_velocity = initial_velocity
|
|
|
|
## Remove a world weapon from all clients (called by WorldWeapon when picked up)
|
|
func remove_world_weapon(weapon_id: int):
|
|
print("[Server] remove_world_weapon called for weapon_id: ", weapon_id)
|
|
if not multiplayer.is_server():
|
|
print("[ERROR] remove_world_weapon called on client!")
|
|
return
|
|
|
|
# Immediately remove from active weapons to prevent late-join sync issues
|
|
if _active_weapons.has(weapon_id):
|
|
_active_weapons.erase(weapon_id)
|
|
print("[Server] Removed weapon ", weapon_id, " from _active_weapons. Remaining: ", _active_weapons.size())
|
|
print("[Server] Remaining weapon IDs: ", _active_weapons.keys())
|
|
else:
|
|
print("[Server] WARNING: Weapon ", weapon_id, " not found in _active_weapons!")
|
|
|
|
# Broadcast removal to all clients
|
|
print("[Server] Broadcasting removal RPC to all clients")
|
|
rpc("_remove_weapon_on_clients", weapon_id)
|
|
|
|
## RPC to remove weapon on all clients
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func _remove_weapon_on_clients(weapon_id: int):
|
|
print("[Peer ", multiplayer.get_unique_id(), "] _remove_weapon_on_clients called for weapon_id: ", weapon_id)
|
|
var weapon_name = "WorldWeapon_" + str(weapon_id)
|
|
if weapons_container and weapons_container.has_node(weapon_name):
|
|
var weapon = weapons_container.get_node(weapon_name)
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Removing weapon ", weapon_name)
|
|
weapon.queue_free()
|
|
else:
|
|
print("[Peer ", multiplayer.get_unique_id(), "] WARNING: Weapon ", weapon_name, " not found in WeaponsContainer")
|
|
|
|
## Called when a weapon is removed (picked up or destroyed)
|
|
func _on_weapon_removed(weapon_id: int):
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
# Remove from tracking
|
|
if _active_weapons.has(weapon_id):
|
|
_active_weapons.erase(weapon_id)
|
|
print("Removed weapon ", weapon_id, " from active tracking")
|
|
|
|
## Client-only spawn (called via rpc_id for late-join sync)
|
|
@rpc("any_peer", "reliable")
|
|
func _client_spawn_weapon(weapon_data_path: String, spawn_position: Vector3, initial_velocity: Vector3, weapon_id: int):
|
|
print("[Client ", multiplayer.get_unique_id(), "] _client_spawn_weapon received for weapon_id: ", weapon_id)
|
|
# This only runs on clients, not server (no call_local)
|
|
if multiplayer.is_server():
|
|
print("[ERROR] _client_spawn_weapon called on server!")
|
|
return
|
|
|
|
# Call the regular spawn function to create the weapon
|
|
print("[Client ", multiplayer.get_unique_id(), "] Calling spawn_world_weapon locally")
|
|
spawn_world_weapon(weapon_data_path, spawn_position, initial_velocity, weapon_id)
|
|
|
|
# ---------- HEALTH ORBS ----------
|
|
var _orb_spawn_counter: int = 0
|
|
var _active_orbs: Dictionary = {} # orb_id -> spawn position (Vector3), for late-join sync
|
|
|
|
## Spawn a health orb at a position (server only, replicates to all clients)
|
|
func spawn_health_orb(spawn_pos: Vector3):
|
|
if not multiplayer.is_server():
|
|
return
|
|
_orb_spawn_counter += 1
|
|
rpc("_spawn_health_orb_local", _orb_spawn_counter, spawn_pos)
|
|
|
|
## Spawn a health orb on all clients (call_local spawns on server too)
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func _spawn_health_orb_local(orb_id: int, spawn_pos: Vector3):
|
|
if not orbs_container:
|
|
push_error("[Level] OrbsContainer not found!")
|
|
return
|
|
|
|
var orb_name = "HealthOrb_" + str(orb_id)
|
|
if orbs_container.has_node(orb_name):
|
|
return # Already spawned (e.g. duplicate sync)
|
|
|
|
var orb_scene = load("res://level/scenes/health_orb.tscn")
|
|
if not orb_scene:
|
|
push_error("[Level] Failed to load health_orb.tscn")
|
|
return
|
|
|
|
var orb = orb_scene.instantiate()
|
|
orb.orb_id = orb_id
|
|
orb.name = orb_name
|
|
orb.position = spawn_pos
|
|
orbs_container.add_child(orb, true)
|
|
|
|
# Server tracks active orbs for late-join sync
|
|
if multiplayer.is_server():
|
|
_active_orbs[orb_id] = spawn_pos
|
|
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Spawned health orb ", orb_name, " at ", spawn_pos)
|
|
|
|
## Remove a health orb from all clients (called by HealthOrb when collected)
|
|
func remove_health_orb(orb_id: int):
|
|
if not multiplayer.is_server():
|
|
return
|
|
if _active_orbs.has(orb_id):
|
|
_active_orbs.erase(orb_id)
|
|
rpc("_remove_orb_on_clients", orb_id)
|
|
|
|
## RPC to remove an orb on all clients
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func _remove_orb_on_clients(orb_id: int):
|
|
var orb_name = "HealthOrb_" + str(orb_id)
|
|
if orbs_container and orbs_container.has_node(orb_name):
|
|
orbs_container.get_node(orb_name).queue_free()
|
|
|
|
## Scatter a few health orbs at server start
|
|
func _spawn_initial_health_orbs():
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
# Wait a frame for everything to be ready
|
|
await get_tree().process_frame
|
|
|
|
var orb_positions = [
|
|
Vector3(6, 1, 6),
|
|
Vector3(-6, 1, -6),
|
|
Vector3(-6, 1, 6),
|
|
]
|
|
for pos in orb_positions:
|
|
spawn_health_orb(pos)
|
|
|
|
# ---------- ARMED ENEMY SPAWNING ----------
|
|
var _armed_enemy_counter: int = 0
|
|
|
|
## Spawn an armed enemy (server only, replicates to all clients)
|
|
func spawn_armed_enemy(spawn_pos: Vector3, main_weapon_path: String = "", offhand_weapon_path: String = ""):
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
if not armed_enemy_scene:
|
|
push_warning("[Level] Armed enemy scene not assigned!")
|
|
return
|
|
|
|
_armed_enemy_counter += 1
|
|
var enemy_name = "ArmedEnemy_" + str(_armed_enemy_counter)
|
|
|
|
rpc("_spawn_armed_enemy_local", enemy_name, spawn_pos, main_weapon_path, offhand_weapon_path)
|
|
|
|
## Spawn armed enemy on all clients
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func _spawn_armed_enemy_local(enemy_name: String, spawn_pos: Vector3, main_weapon_path: String, offhand_weapon_path: String):
|
|
if not armed_enemy_scene:
|
|
push_error("[Level] Armed enemy scene not loaded!")
|
|
return
|
|
|
|
if not enemies_container:
|
|
push_error("[Level] EnemiesContainer not found!")
|
|
return
|
|
|
|
# Don't spawn duplicates
|
|
if enemies_container.has_node(enemy_name):
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Armed enemy ", enemy_name, " already exists")
|
|
return
|
|
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Spawning armed enemy ", enemy_name, " at ", spawn_pos)
|
|
|
|
var enemy = armed_enemy_scene.instantiate()
|
|
enemy.name = enemy_name
|
|
enemy.position = spawn_pos
|
|
|
|
# Set multiplayer authority to server
|
|
enemy.set_multiplayer_authority(1)
|
|
|
|
# Set starting weapons (for server to trigger RPCs)
|
|
if main_weapon_path != "":
|
|
var weapon_data = load(main_weapon_path) as WeaponData
|
|
if weapon_data:
|
|
enemy.starting_weapon = weapon_data
|
|
|
|
if offhand_weapon_path != "":
|
|
var offhand_data = load(offhand_weapon_path) as WeaponData
|
|
if offhand_data:
|
|
enemy.starting_offhand = offhand_data
|
|
|
|
enemies_container.add_child(enemy, true)
|
|
print("[Peer ", multiplayer.get_unique_id(), "] Armed enemy ", enemy_name, " spawned successfully")
|
|
|
|
# Server credits kill gold for armed enemies spawned outside the wave spawner
|
|
if multiplayer.is_server():
|
|
enemy.died.connect(_on_level_enemy_died.bind(enemy))
|
|
|
|
## Kill-gold payout for enemies spawned directly by the level (not the wave spawner).
|
|
## Practice dummies are deliberately NOT connected - they respawn and would be farmable.
|
|
func _on_level_enemy_died(killer_id: int, enemy: Node):
|
|
if not multiplayer.is_server():
|
|
return
|
|
if killer_id > 0 and Network.players.has(killer_id) and enemy is BaseEnemy:
|
|
GameState.server_credit_kill(killer_id, enemy.gold_reward)
|
|
|
|
## Spawn initial armed enemies when server starts
|
|
func _spawn_armed_enemies():
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
if not armed_enemy_scene:
|
|
push_warning("[Level] Armed enemy scene not assigned - skipping armed enemy spawn")
|
|
return
|
|
|
|
# Wait a frame for everything to be ready
|
|
await get_tree().process_frame
|
|
|
|
print("[Server] Spawning armed enemies")
|
|
|
|
# Find enemy spawn points from EnemySpawner
|
|
var enemy_spawner = get_node_or_null("EnemySpawner")
|
|
if not enemy_spawner:
|
|
push_warning("[Level] EnemySpawner not found - skipping armed enemy spawn")
|
|
return
|
|
|
|
# Get all spawn points (children of EnemySpawner that start with "EnemySpawnPoint")
|
|
var spawn_points: Array[Node3D] = []
|
|
for child in enemy_spawner.get_children():
|
|
if child is Node3D and child.name.begins_with("EnemySpawnPoint"):
|
|
spawn_points.append(child)
|
|
|
|
if spawn_points.is_empty():
|
|
push_warning("[Level] No enemy spawn points found in EnemySpawner")
|
|
return
|
|
|
|
print("[Server] Found ", spawn_points.size(), " enemy spawn points")
|
|
|
|
# Spawn an unarmed enemy at each spawn point
|
|
for spawn_point in spawn_points:
|
|
var spawn_pos = spawn_point.global_position
|
|
print("[Server] Spawning armed enemy at ", spawn_pos)
|
|
spawn_armed_enemy(spawn_pos, "", "")
|