4a5264c5b0
- Replaced TeamData.Team type hints with int in all scripts - Added explicit preloads for headless mode class resolution - Created stub LagCompensation and DamageProcessor scripts - Fixed PluginManager, SpawnManager, EconomyManager, BuyZone, RoundManager, BuyMenuHandler, BombObjective class_name references - Updated server config to match ServerConfig.gd format Gray screen root cause: server scripts failed to parse in headless mode due to Godot 4 class_name loading order (Resource after Node), leaving server_main.gd non-functional — accepted connections but never spawned players.
251 lines
8.7 KiB
GDScript
251 lines
8.7 KiB
GDScript
## TeamManager — Team assignment, tracking, and auto-balance.
|
|
##
|
|
## Manages which player belongs to which team, tracks team scores,
|
|
## and provides auto-balancing logic to keep team sizes fair.
|
|
##
|
|
## Teams are round-independent — player assignments persist across rounds.
|
|
##
|
|
## Usage:
|
|
## var tm = TeamManager.new()
|
|
## add_child(tm)
|
|
## tm.assign_player(player_id, Team.COUNTER_TERRORIST)
|
|
## var team: Team = tm.get_player_team(player_id)
|
|
## var ct_players: Array[int] = tm.get_team_players(Team.COUNTER_TERRORIST)
|
|
##
|
|
## Signals:
|
|
## player_joined_team(player_id, team)
|
|
## player_left_team(player_id, team)
|
|
## team_scores_updated(team_scores)
|
|
|
|
extends Node
|
|
class_name TeamManager
|
|
|
|
# Force TeamData to be loaded first (Godot 4 class_name loading order: Resources after Nodes)
|
|
const _team_data_ref = preload("res://scripts/teams/team_data.gd")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signals
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Emitted when a player is assigned to a team (including on team switch).
|
|
## team is an int (TeamData.Team enum value).
|
|
signal player_joined_team(player_id: int, team: int)
|
|
|
|
## Emitted when a player leaves a team (disconnected or switched).
|
|
## team is an int (TeamData.Team enum value).
|
|
signal player_left_team(player_id: int, team: int)
|
|
|
|
## Emitted when scores for any team change.
|
|
signal team_scores_updated(team_scores: Dictionary)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Mapping: player_id (int) → Team enum value.
|
|
var _player_teams: Dictionary = {}
|
|
|
|
## Mapping: Team enum → Array of player IDs.
|
|
var _team_players: Dictionary = {}
|
|
|
|
## Score per team: Team enum → int (rounds won, kills, etc.).
|
|
var _team_scores: Dictionary = {}
|
|
|
|
## Maximum players allowed on a single team (0 = unlimited).
|
|
## Set from ServerConfig on _ready if available.
|
|
var max_players_per_team: int = 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _ready() -> void:
|
|
var cfg = get_node_or_null("/root/ServerConfig")
|
|
if cfg and cfg.has_method(&"get_config_path"):
|
|
max_players_per_team = cfg.players_per_team
|
|
else:
|
|
max_players_per_team = 8
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API — Assignment
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Assign a player to a team. Returns true if successful, false if the team is full.
|
|
## Emits player_left_team for the old team (if any) and player_joined_team for the new team.
|
|
func assign_player(player_id: int, team: int) -> bool:
|
|
# Validate team
|
|
if team < 0 or team > TeamData.Team.size() - 1:
|
|
push_warning("[TeamManager] Invalid team value: %d" % team)
|
|
return false
|
|
|
|
# Check if team is full (skip for spectator)
|
|
if team != TeamData.Team.SPECTATOR and max_players_per_team > 0:
|
|
var current_count: int = _team_players.get(team, []).size()
|
|
if current_count >= max_players_per_team:
|
|
# Allow if the player is already on this team
|
|
if _player_teams.get(player_id, TeamData.Team.SPECTATOR) != team:
|
|
push_warning("[TeamManager] Team %s is full (%d/%d)" % [TeamData.get_team_name(team), current_count, max_players_per_team])
|
|
return false
|
|
|
|
# Remove from old team if assigned
|
|
var old_team: int = _player_teams.get(player_id, TeamData.Team.SPECTATOR)
|
|
if old_team != team:
|
|
_remove_from_team(player_id, old_team)
|
|
|
|
# Add to new team
|
|
_player_teams[player_id] = team
|
|
if not _team_players.has(team):
|
|
_team_players[team] = []
|
|
if player_id not in _team_players[team]:
|
|
_team_players[team].append(player_id)
|
|
|
|
player_joined_team.emit(player_id, team)
|
|
return true
|
|
|
|
|
|
## Remove a player from any team (equivalent to assigning to SPECTATOR).
|
|
func remove_player(player_id: int) -> void:
|
|
var current_team: int = _player_teams.get(player_id, TeamData.Team.SPECTATOR)
|
|
_remove_from_team(player_id, current_team)
|
|
|
|
|
|
## Unregister a player entirely (on disconnect).
|
|
func unregister_player(player_id: int) -> void:
|
|
remove_player(player_id)
|
|
_player_teams.erase(player_id)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API — Querying
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Get the team a player belongs to. Returns SPECTATOR if unassigned.
|
|
## Returns an int (TeamData.Team enum value).
|
|
func get_player_team(player_id: int) -> int:
|
|
return _player_teams.get(player_id, TeamData.Team.SPECTATOR)
|
|
|
|
|
|
## Get an array of all player IDs on a given team.
|
|
func get_team_players(team: int) -> Array[int]:
|
|
return _team_players.get(team, []).duplicate()
|
|
|
|
|
|
## Get the total number of players on a given team.
|
|
func get_team_player_count(team: int) -> int:
|
|
return _team_players.get(team, []).size()
|
|
|
|
|
|
## Check if a player is on a specific team.
|
|
func is_player_on_team(player_id: int, team: int) -> bool:
|
|
return _player_teams.get(player_id, TeamData.Team.SPECTATOR) == team
|
|
|
|
|
|
## Get a dictionary of all player-to-team mappings.
|
|
func get_all_assignments() -> Dictionary:
|
|
return _player_teams.duplicate()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API — Auto-balance
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Assign a player to the team with the fewest members (ties: random).
|
|
## Returns the team as an int (TeamData.Team enum value) the player was assigned to,
|
|
## or SPECTATOR if both teams are full.
|
|
func assign_to_team_with_fewest(player_id: int) -> int:
|
|
var ct_count: int = _team_players.get(TeamData.Team.COUNTER_TERRORIST, []).size()
|
|
var t_count: int = _team_players.get(TeamData.Team.TERRORIST, []).size()
|
|
|
|
# If one team is at capacity, force to the other
|
|
if max_players_per_team > 0:
|
|
if ct_count >= max_players_per_team and t_count < max_players_per_team:
|
|
assign_player(player_id, TeamData.Team.TERRORIST)
|
|
return TeamData.Team.TERRORIST
|
|
if t_count >= max_players_per_team and ct_count < max_players_per_team:
|
|
assign_player(player_id, TeamData.Team.COUNTER_TERRORIST)
|
|
return TeamData.Team.COUNTER_TERRORIST
|
|
if ct_count >= max_players_per_team and t_count >= max_players_per_team:
|
|
push_warning("[TeamManager] Both teams are full")
|
|
return TeamData.Team.SPECTATOR
|
|
|
|
# Assign to team with fewer players (ties: randomly pick one)
|
|
var team: int = TeamData.Team.COUNTER_TERRORIST
|
|
if ct_count < t_count:
|
|
team = TeamData.Team.COUNTER_TERRORIST
|
|
elif t_count < ct_count:
|
|
team = TeamData.Team.TERRORIST
|
|
else:
|
|
# Tied — pick randomly
|
|
team = TeamData.Team.COUNTER_TERRORIST if randi() % 2 == 0 else TeamData.Team.TERRORIST
|
|
|
|
assign_player(player_id, team)
|
|
return team
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API — Scoring
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Set the score for a given team.
|
|
func set_team_score(team: int, score: int) -> void:
|
|
_team_scores[team] = score
|
|
team_scores_updated.emit(_team_scores.duplicate())
|
|
|
|
|
|
## Add to a team's current score.
|
|
func add_team_score(team: int, amount: int = 1) -> void:
|
|
var current: int = _team_scores.get(team, 0)
|
|
_team_scores[team] = current + amount
|
|
team_scores_updated.emit(_team_scores.duplicate())
|
|
|
|
|
|
## Get the score for a given team.
|
|
func get_team_score(team: int) -> int:
|
|
return _team_scores.get(team, 0)
|
|
|
|
|
|
## Get all team scores as a dictionary: Team enum → int.
|
|
func get_all_scores() -> Dictionary:
|
|
return _team_scores.duplicate()
|
|
|
|
|
|
## Reset all scores to zero.
|
|
func reset_scores() -> void:
|
|
for team in [TeamData.Team.COUNTER_TERRORIST, TeamData.Team.TERRORIST]:
|
|
_team_scores[team] = 0
|
|
team_scores_updated.emit(_team_scores.duplicate())
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API — Utility
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Reset all teams (clears all assignments and scores).
|
|
func reset_all() -> void:
|
|
# Emit leave signals for all assigned players
|
|
for player_id in _player_teams.keys():
|
|
var team: int = _player_teams[player_id]
|
|
player_left_team.emit(player_id, team)
|
|
|
|
_player_teams.clear()
|
|
_team_players.clear()
|
|
reset_scores()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _remove_from_team(player_id: int, team: int) -> void:
|
|
if _player_teams.has(player_id):
|
|
_player_teams.erase(player_id)
|
|
|
|
if _team_players.has(team):
|
|
var arr: Array = _team_players[team]
|
|
var idx: int = arr.find(player_id)
|
|
if idx >= 0:
|
|
arr.remove_at(idx)
|
|
player_left_team.emit(player_id, team)
|