91b878f347
- Added 6 Quaternius weapon models (Pistol, Revolver, Rifle, Shotgun, P90, SniperRifle) - Created weapon resource files with balanced stats - Wired Pistol + Rifle into player.tscn replacing Kenney blasters - Fixed WASD input (matched input map action names) - Fixed Escape key toggles mouse capture - Added Audio autoload singleton - Fixed broken sound asset paths across all scripts - Fixed all scene ext_resource text paths (nested under assets/kenney-fps/) - Fixed all .import source_file paths - Deleted stale .import files (will be regenerated by editor on open)
207 lines
6.7 KiB
GDScript
207 lines
6.7 KiB
GDScript
extends Node
|
|
|
|
@export var player_scene: PackedScene
|
|
|
|
## Whether to spawn an avatar for the host (peer 1) when on_server_start fires.
|
|
## Set to false for dedicated server operation.
|
|
@export var spawn_host_avatar: bool = true
|
|
|
|
## When true, this is a dedicated server. Peer 1 (the server itself) never
|
|
## gets a player avatar. Clients also skip spawning peer 1 locally.
|
|
var _dedicated_server: bool = false
|
|
|
|
## Mark this spawner as running on a dedicated server.
|
|
## The host avatar (peer 1) is skipped entirely — no avatar is spawned for the
|
|
## server process itself.
|
|
func set_dedicated_server() -> void:
|
|
_dedicated_server = true
|
|
print("[PlayerSpawner] Dedicated server mode enabled")
|
|
|
|
var t_spawn_points: Array[Marker3D] = []
|
|
var ct_spawn_points: Array[Marker3D] = []
|
|
var team_manager: TeamManager
|
|
var round_manager: RoundManager
|
|
var economy_manager: EconomyManager
|
|
var bomb: Bomb
|
|
var avatars: Dictionary = {}
|
|
|
|
func _ready():
|
|
team_manager = $"../TeamManager"
|
|
round_manager = $"../RoundManager"
|
|
economy_manager = $"../EconomyManager"
|
|
bomb = get_tree().current_scene.get_node_or_null("Network/Bomb")
|
|
|
|
# Discover spawn points from scene containers
|
|
var scene_root := get_tree().current_scene
|
|
var t_container := scene_root.get_node_or_null("T Spawns")
|
|
if t_container:
|
|
for child in t_container.get_children():
|
|
if child is Marker3D:
|
|
t_spawn_points.append(child)
|
|
var ct_container := scene_root.get_node_or_null("CT Spawns")
|
|
if ct_container:
|
|
for child in ct_container.get_children():
|
|
if child is Marker3D:
|
|
ct_spawn_points.append(child)
|
|
|
|
NetworkEvents.on_client_start.connect(_handle_connected)
|
|
NetworkEvents.on_server_start.connect(_handle_host)
|
|
NetworkEvents.on_peer_join.connect(_handle_new_peer)
|
|
NetworkEvents.on_peer_leave.connect(_handle_leave)
|
|
NetworkEvents.on_client_stop.connect(_handle_stop)
|
|
NetworkEvents.on_server_stop.connect(_handle_stop)
|
|
|
|
if round_manager:
|
|
round_manager.round_started.connect(_handle_round_start)
|
|
|
|
func _handle_connected(id: int):
|
|
_spawn(id)
|
|
|
|
func _handle_host():
|
|
if _dedicated_server:
|
|
print("[PlayerSpawner] Dedicated server — skipping host avatar entirely")
|
|
# Match starts when the first client connects (handled by headless_server)
|
|
return
|
|
if not spawn_host_avatar:
|
|
print("[PlayerSpawner] spawn_host_avatar=false — skipping host avatar")
|
|
if round_manager:
|
|
round_manager.start_match()
|
|
return
|
|
_spawn(1)
|
|
# Auto-start match
|
|
if round_manager:
|
|
round_manager.start_match()
|
|
|
|
func _handle_new_peer(id: int):
|
|
# On a dedicated server, peer 1 (the server process) has no avatar.
|
|
# When a client connects, it receives on_peer_join for peer 1, but
|
|
# should skip spawning because the server doesn't have that avatar.
|
|
# This also applies to listen-server: the server replicates peer 1's
|
|
# avatar to clients; spawning a local copy would duplicate it.
|
|
if id == 1 and not multiplayer.is_server():
|
|
print("[PlayerSpawner] Client skipping spawn for peer 1 (server has no dedicated avatar)")
|
|
return
|
|
if _dedicated_server and id == 1:
|
|
return
|
|
if id == 1 and not spawn_host_avatar:
|
|
print("[PlayerSpawner] Skipping spawn for peer 1 (spawn_host_avatar=false)")
|
|
return
|
|
_spawn(id)
|
|
if multiplayer.is_server():
|
|
team_manager.sync_to_peer(id)
|
|
if economy_manager:
|
|
economy_manager.sync_to_peer(id)
|
|
if bomb:
|
|
bomb.sync_to_peer(id)
|
|
if avatars.size() <= 2:
|
|
# Dedicated server handles match start via headless_server.gd
|
|
if not _dedicated_server:
|
|
round_manager.start_round()
|
|
else:
|
|
# 3+ players: sync state, spectate until next round
|
|
round_manager.sync_to_peer(id)
|
|
var avatar = avatars.get(id)
|
|
if avatar and avatar.has_method("set_spectator"):
|
|
avatar.call_deferred("set_spectator")
|
|
|
|
func _handle_leave(id: int):
|
|
if not avatars.has(id):
|
|
return
|
|
|
|
var avatar = avatars[id] as Node
|
|
avatar.queue_free()
|
|
avatars.erase(id)
|
|
if economy_manager:
|
|
economy_manager.remove_player(id)
|
|
|
|
func _handle_stop():
|
|
for avatar in avatars.values():
|
|
avatar.queue_free()
|
|
avatars.clear()
|
|
|
|
func _handle_round_start(_round_number: int):
|
|
if not multiplayer.is_server():
|
|
return
|
|
# Respawn all players at their team spawn points
|
|
for peer_id in avatars:
|
|
var avatar = avatars[peer_id] as CharacterBody3D
|
|
if avatar == null:
|
|
continue
|
|
# Fresh spawns (_has_played_round=false) always get default loadout
|
|
var survived: bool = not avatar.is_dead and avatar._has_played_round
|
|
var spawn_pos := get_team_spawn_point(peer_id)
|
|
avatar.round_respawn(spawn_pos, survived)
|
|
|
|
# Assign bomb to a random T player
|
|
if bomb:
|
|
var t_members := team_manager.get_team_members(TeamManager.Team.T)
|
|
if not t_members.is_empty():
|
|
var idx := hash(_round_number * 31) % t_members.size()
|
|
bomb.assign_to_player(t_members[idx])
|
|
|
|
func _spawn(id: int):
|
|
# Auto-assign team on server
|
|
if multiplayer.is_server():
|
|
team_manager.auto_assign(id)
|
|
|
|
var avatar = player_scene.instantiate() as Node
|
|
avatars[id] = avatar
|
|
avatar.name += " #%d" % id
|
|
add_child(avatar)
|
|
avatar.global_position = get_team_spawn_point(id)
|
|
|
|
# Avatar is always owned by server
|
|
avatar.set_multiplayer_authority(1)
|
|
|
|
# Pass references
|
|
if avatar.has_method("setup_references"):
|
|
avatar.setup_references(team_manager, round_manager)
|
|
|
|
# Init economy
|
|
if multiplayer.is_server() and economy_manager:
|
|
economy_manager.init_player(id)
|
|
|
|
print("Spawned avatar %s at %s" % [avatar.name, multiplayer.get_unique_id()])
|
|
|
|
# Avatar's input object is owned by player
|
|
var input = avatar.find_child("Input")
|
|
if input != null:
|
|
input.set_multiplayer_authority(id)
|
|
print("Set input(%s) ownership to %s" % [input.name, id])
|
|
|
|
# Notify RollbackSynchronizer about ownership changes
|
|
var rollback_sync = avatar.find_child("RollbackSynchronizer")
|
|
if rollback_sync and rollback_sync.has_method("process_settings"):
|
|
rollback_sync.process_settings()
|
|
|
|
# On the client side, initialize weapon loadout locally so the player
|
|
# has weapons immediately. The server will reconcile via rollback state sync.
|
|
if not multiplayer.is_server():
|
|
var weapon_mgr = avatar.find_child("WeaponManager")
|
|
if weapon_mgr and weapon_mgr.has_method("set_default_loadout"):
|
|
# Default to T team — server will correct via state sync
|
|
weapon_mgr.set_default_loadout(1) # TeamManager.Team.T = 1
|
|
print("[Spawner] Client-side weapon loadout initialized for peer %s" % id)
|
|
|
|
func get_team_spawn_point(peer_id: int, spawn_idx: int = 0) -> Vector3:
|
|
var team := team_manager.get_team(peer_id)
|
|
var points: Array[Marker3D]
|
|
|
|
match team:
|
|
TeamManager.Team.T:
|
|
points = t_spawn_points
|
|
TeamManager.Team.CT:
|
|
points = ct_spawn_points
|
|
_:
|
|
# Fallback: use T spawns
|
|
points = t_spawn_points
|
|
|
|
if points.is_empty():
|
|
return Vector3.ZERO
|
|
|
|
var idx := hash(peer_id * 37 + spawn_idx * 19) % points.size()
|
|
return points[idx].global_position
|
|
|
|
func get_next_spawn_point(peer_id: int, spawn_idx: int = 0) -> Vector3:
|
|
return get_team_spawn_point(peer_id, spawn_idx)
|