46ff83325f
- team_data.gd: Team enum (SPECTATOR/CT/Terrorist), constants for names, colors, starting money, and spawn groups; TeamData resource class - team_manager.gd: Player-to-team assignment, auto-balancing, score tracking; round-independent (persists across rounds) - spawn_manager.gd: Scans scene for Marker3D nodes in spawn_points_ct and spawn_points_t groups; selects valid spawns (farthest from enemies with proximity check, fallback to first available) - buy_zone.gd: Area3D-based trigger zone with team filtering, player enter/exit tracking and signals - test_range.tscn: Added 2 CT spawn markers and 2 T spawn markers with appropriate groups
235 lines
7.9 KiB
GDScript
235 lines
7.9 KiB
GDScript
## SpawnManager — Spawn point selection for team-based spawning.
|
|
##
|
|
## Scans the current scene for Marker3D nodes in team-specific groups
|
|
## ("spawn_points_ct", "spawn_points_t") and selects valid spawns.
|
|
##
|
|
## Spawn validity rules:
|
|
## - No enemies within a configurable minimum distance (proximity_check_radius)
|
|
## - Fallback: first available spawn if all are contested
|
|
## - Preference: farthest from known enemy positions
|
|
##
|
|
## Usage:
|
|
## var sm = SpawnManager.new()
|
|
## add_child(sm)
|
|
## sm.team_manager = get_node("/root/TeamManager")
|
|
## var spawn_pos: Vector3 = sm.select_spawn(player_id, Team.COUNTER_TERRORIST)
|
|
##
|
|
## Spawn points are found at runtime by scanning scene groups.
|
|
## Map authors place Marker3D nodes and add them to the appropriate group.
|
|
|
|
extends Node
|
|
class_name SpawnManager
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Exports
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Minimum distance (in Godot units) from any enemy player for a spawn to be valid.
|
|
@export var proximity_check_radius: float = 15.0
|
|
|
|
## If true, fall back to first available spawn when all are contested.
|
|
@export var allow_fallback: bool = true
|
|
|
|
## Group name for Counter-Terrorist spawn markers.
|
|
@export var ct_spawn_group: String = "spawn_points_ct"
|
|
|
|
## Group name for Terrorist spawn markers.
|
|
@export var t_spawn_group: String = "spawn_points_t"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dependencies
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Reference to the TeamManager singleton or node.
|
|
## If not set, spawn safety checks are skipped (all spawns considered valid).
|
|
var team_manager: TeamManager = null
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Cached spawn points for the current map.
|
|
## Keyed by group name → Array of Marker3D nodes.
|
|
var _spawn_points: Dictionary = {}
|
|
|
|
## Tracks which player last occupied which spawn (for spread).
|
|
## player_id (int) → Marker3D global position
|
|
var _last_spawn_positions: Dictionary = {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## (Re)scan the scene tree for spawn point markers.
|
|
## Call this when the map changes.
|
|
func refresh_spawn_points() -> void:
|
|
_spawn_points.clear()
|
|
var groups_to_scan: Array[String] = [ct_spawn_group, t_spawn_group]
|
|
|
|
for group_name in groups_to_scan:
|
|
var markers: Array[Marker3D] = []
|
|
var nodes: Array[Node] = get_tree().get_nodes_in_group(group_name)
|
|
for node in nodes:
|
|
if node is Marker3D:
|
|
markers.append(node as Marker3D)
|
|
_spawn_points[group_name] = markers
|
|
|
|
print("[SpawnManager] Refreshed spawn points — CT: %d, T: %d" % [
|
|
_spawn_points.get(ct_spawn_group, []).size(),
|
|
_spawn_points.get(t_spawn_group, []).size(),
|
|
])
|
|
|
|
|
|
## Get all spawn point markers for a given team.
|
|
func get_spawn_points_for_team(team: TeamData.Team) -> Array[Marker3D]:
|
|
var group_name: String = _group_for_team(team)
|
|
return _spawn_points.get(group_name, []).duplicate()
|
|
|
|
|
|
## Select a valid spawn position for a player on a given team.
|
|
##
|
|
## Strategy:
|
|
## 1. Filter to valid spawns (no enemies within proximity_check_radius).
|
|
## 2. Among valid spawns, pick the one farthest from known enemies.
|
|
## 3. If no spawn is valid, use fallback behaviour.
|
|
## 4. If no spawns exist at all, return Vector3.ZERO.
|
|
##
|
|
## Returns the global position of the chosen spawn point.
|
|
func select_spawn(player_id: int, team: TeamData.Team) -> Vector3:
|
|
var markers: Array[Marker3D] = get_spawn_points_for_team(team)
|
|
if markers.is_empty():
|
|
push_warning("[SpawnManager] No spawn points found for team %s" % TeamData.get_team_name(team))
|
|
return Vector3.ZERO
|
|
|
|
# Separate into valid and contested spawns
|
|
var valid_spawns: Array[Marker3D] = []
|
|
var contested_spawns: Array[Marker3D] = []
|
|
|
|
if team_manager != null:
|
|
for marker in markers:
|
|
if _is_spawn_safe(marker.global_position, team):
|
|
valid_spawns.append(marker)
|
|
else:
|
|
contested_spawns.append(marker)
|
|
else:
|
|
# No team manager — all spawns are valid
|
|
valid_spawns = markers.duplicate()
|
|
|
|
# Choose the best spawn
|
|
var chosen: Marker3D = null
|
|
|
|
if not valid_spawns.is_empty():
|
|
# Pick the farthest from known enemy last-positions
|
|
chosen = _pick_farthest_spawn(valid_spawns, team)
|
|
elif allow_fallback:
|
|
# Fallback: pick the first contested spawn
|
|
if not contested_spawns.is_empty():
|
|
chosen = contested_spawns[0]
|
|
print("[SpawnManager] Fallback: all spawns contested, using first available")
|
|
else:
|
|
chosen = markers[0]
|
|
print("[SpawnManager] Fallback: no spawns available, using first marker")
|
|
else:
|
|
chosen = markers[0]
|
|
print("[SpawnManager] No valid spawns and fallback disabled, using first marker")
|
|
|
|
# Record this spawn as the player's last position
|
|
var pos: Vector3 = chosen.global_position
|
|
_last_spawn_positions[player_id] = pos
|
|
return pos
|
|
|
|
|
|
## Return the number of spawn points for a team.
|
|
func get_spawn_count(team: TeamData.Team) -> int:
|
|
var group_name: String = _group_for_team(team)
|
|
return _spawn_points.get(group_name, []).size()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal: Spawn selection helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Check if a position is safe from enemy proximity.
|
|
func _is_spawn_safe(pos: Vector3, friendly_team: TeamData.Team) -> bool:
|
|
if team_manager == null:
|
|
return true
|
|
|
|
# Get the enemy team(s)
|
|
var enemy_teams: Array[TeamData.Team] = _get_enemy_teams(friendly_team)
|
|
|
|
for enemy_team in enemy_teams:
|
|
var enemy_ids: Array[int] = team_manager.get_team_players(enemy_team)
|
|
for enemy_id in enemy_ids:
|
|
# Check last known position
|
|
var enemy_pos: Vector3 = _last_spawn_positions.get(enemy_id, Vector3.ZERO)
|
|
if enemy_pos != Vector3.ZERO:
|
|
var dist: float = pos.distance_to(enemy_pos)
|
|
if dist < proximity_check_radius:
|
|
return false
|
|
|
|
return true
|
|
|
|
|
|
## Pick the spawn farthest from all known enemy positions.
|
|
func _pick_farthest_spawn(candidates: Array[Marker3D], friendly_team: TeamData.Team) -> Marker3D:
|
|
if candidates.is_empty():
|
|
return null
|
|
if candidates.size() == 1:
|
|
return candidates[0]
|
|
|
|
# Collect enemy positions
|
|
var enemy_positions: Array[Vector3] = []
|
|
if team_manager != null:
|
|
var enemy_teams: Array[TeamData.Team] = _get_enemy_teams(friendly_team)
|
|
for enemy_team in enemy_teams:
|
|
var enemy_ids: Array[int] = team_manager.get_team_players(enemy_team)
|
|
for enemy_id in enemy_ids:
|
|
var enemy_pos: Vector3 = _last_spawn_positions.get(enemy_id, Vector3.ZERO)
|
|
if enemy_pos != Vector3.ZERO:
|
|
enemy_positions.append(enemy_pos)
|
|
|
|
if enemy_positions.is_empty():
|
|
# No known enemy positions — pick randomly among candidates
|
|
return candidates[randi() % candidates.size()]
|
|
|
|
# Pick candidate with max minimum-distance to any enemy
|
|
var best: Marker3D = candidates[0]
|
|
var best_min_dist: float = 0.0
|
|
|
|
for marker in candidates:
|
|
var pos: Vector3 = marker.global_position
|
|
var min_dist: float = INF
|
|
for epos in enemy_positions:
|
|
var d: float = pos.distance_to(epos)
|
|
if d < min_dist:
|
|
min_dist = d
|
|
if min_dist > best_min_dist:
|
|
best_min_dist = min_dist
|
|
best = marker
|
|
|
|
return best
|
|
|
|
|
|
## Get the teams that are enemies of the given team.
|
|
## For now, CT and T are enemies of each other; SPECTATOR has no enemies.
|
|
func _get_enemy_teams(team: TeamData.Team) -> Array[TeamData.Team]:
|
|
match team:
|
|
TeamData.Team.COUNTER_TERRORIST:
|
|
return [TeamData.Team.TERRORIST]
|
|
TeamData.Team.TERRORIST:
|
|
return [TeamData.Team.COUNTER_TERRORIST]
|
|
_:
|
|
return []
|
|
|
|
|
|
## Translate a Team enum to the spawn group name.
|
|
func _group_for_team(team: TeamData.Team) -> String:
|
|
match team:
|
|
TeamData.Team.COUNTER_TERRORIST:
|
|
return ct_spawn_group
|
|
TeamData.Team.TERRORIST:
|
|
return t_spawn_group
|
|
_:
|
|
return ""
|