## 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)