MultiplayerFighter
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

386 lines
13 KiB

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 = null # Will be created if doesn't exist
@onready var menu: Control = $Menu
@onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu
@export var player_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
# 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():
multiplayer_chat.hide()
menu.show()
multiplayer_chat.set_process_input(true)
# Add quick-fill preset buttons
_create_preset_buttons()
# 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")
# Clients: Remove manually placed weapons (server will sync them via RPC)
if not multiplayer.is_server():
_cleanup_manual_weapons_on_client()
return
Network.connect("player_connected", Callable(self, "_on_player_connected"))
multiplayer.peer_disconnected.connect(_remove_player)
# Initialize any manually placed weapons in the scene
_initialize_manual_weapons()
# Spawn initial weapons when server starts
_spawn_initial_weapons()
func _cleanup_manual_weapons_on_client():
"""Remove manually placed weapons on clients (server will sync them via RPC)"""
if not weapons_container:
return
var weapons_to_remove = []
for child in weapons_container.get_children():
if child is WorldWeapon and child.weapon_id == -1:
weapons_to_remove.append(child)
for weapon in weapons_to_remove:
print("[Client] Removing manually placed weapon: ", weapon.name)
weapon.queue_free()
func _initialize_manual_weapons():
"""Initialize any WorldWeapon nodes manually placed in the level scene"""
if not multiplayer.is_server():
return
if not weapons_container:
return
# Find all WorldWeapon nodes in the weapons container
var manual_weapons = []
for child in weapons_container.get_children():
if child is WorldWeapon:
manual_weapons.append(child)
if manual_weapons.is_empty():
print("[Server] No manually placed weapons found")
return
print("[Server] Found ", manual_weapons.size(), " manually placed weapon(s)")
# Initialize each manually placed weapon
for weapon in manual_weapons:
# Skip if already initialized (weapon_id != -1)
if weapon.weapon_id != -1:
continue
# Assign unique ID
_weapon_spawn_counter += 1
weapon.weapon_id = _weapon_spawn_counter
# Set deterministic name for networking
var old_name = weapon.name
weapon.name = "WorldWeapon_" + str(weapon.weapon_id)
# Track in active weapons
_active_weapons[weapon.weapon_id] = weapon
# Connect cleanup signal
weapon.tree_exiting.connect(_on_weapon_removed.bind(weapon.weapon_id))
print("[Server] Initialized manual weapon '", old_name, "' with ID: ", weapon.weapon_id, " at position: ", weapon.global_position)
# Verify weapon_data is set
if not weapon.weapon_data:
push_error("Manual weapon '", old_name, "' has no WeaponData assigned!")
continue
func _spawn_initial_weapons():
if not multiplayer.is_server():
return
# Wait a frame for everything to be ready
await get_tree().process_frame
print("[Server] _spawn_initial_weapons - Connected peers: ", multiplayer.get_peers())
# Spawn a sword
_weapon_spawn_counter += 1
print("[Server] Calling RPC to spawn sword with ID: ", _weapon_spawn_counter)
rpc("spawn_world_weapon",
"res://level/resources/weapon_sword.tres",
Vector3(5, 1, 0),
Vector3.ZERO,
_weapon_spawn_counter
)
# Spawn a shield
_weapon_spawn_counter += 1
print("[Server] Calling RPC to spawn shield with ID: ", _weapon_spawn_counter)
rpc("spawn_world_weapon",
"res://level/resources/weapon_shield.tres",
Vector3(-5, 1, 0),
Vector3.ZERO,
_weapon_spawn_counter
)
func _on_player_connected(peer_id, player_info):
_add_player(peer_id, player_info)
# 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 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, " 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
)
func _on_host_pressed():
menu.hide()
Network.start_host(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower())
func _on_join_pressed():
menu.hide()
Network.join_game(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower(), address_input.text.strip_edges())
func _add_player(id: int, player_info : Dictionary):
if players_container.has_node(str(id)):
return
var player = player_scene.instantiate()
player.name = str(id)
player.position = get_spawn_point()
players_container.add_child(player, true)
var nick = Network.players[id]["nick"]
player.nickname.text = nick
# player.rpc("change_nick", nick)
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:
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()
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")
# ---------- 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"
# ---------- 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
# 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)