Files
tactical-shooter/scripts/teams/team_manager.gd
T

243 lines
8.4 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
# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------
## Emitted when a player is assigned to a team (including on team switch).
signal player_joined_team(player_id: int, team: int)
## Emitted when a player leaves a team (disconnected or switched).
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:
if ServerConfig and ServerConfig.has_method(&\"get_config_path\"):
max_players_per_team = ServerConfig.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: TeamData.Team) -> 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: TeamData.Team = _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: TeamData.Team = _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.
func get_player_team(player_id: int) -> TeamData.Team:
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: TeamData.Team) -> Array[int]:
return _team_players.get(team, []).duplicate()
## Get the total number of players on a given team.
func get_team_player_count(team: TeamData.Team) -> 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: TeamData.Team) -> 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 the player was assigned to, or SPECTATOR if both teams are full.
func assign_to_team_with_fewest(player_id: int) -> TeamData.Team:
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: TeamData.Team
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: TeamData.Team, 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: TeamData.Team, 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: TeamData.Team) -> 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: TeamData.Team = _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: TeamData.Team) -> 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)