Files
tactical-shooter/server/scripts/zones/buy_zone.gd
T
shawn 46ff83325f t_p2: Add team, spawn, and buy-zone system
- 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
2026-07-01 20:24:16 -04:00

182 lines
6.0 KiB
GDScript

## BuyZone — Area3D that defines where players can buy weapons/equipment.
##
## A trigger zone that tracks which players are inside it and emits
## enter/exit signals. Buy zones can be filtered by team, so only
## the appropriate team can purchase within a given zone.
##
## In the map scene, place BuyZone nodes (Area3D) near each team's
## spawn area. Set zone_team to restrict which team can use it.
##
## Usage (map tscn):
## [node name="BuyZone_CT" type="Area3D" parent="."]
## script = ExtResource("...buy_zone.gd")
## zone_team = 1 # Team.COUNTER_TERRORIST
## zone_radius = 10.0
##
## (Add a CollisionShape3D child with a SphereShape3D for the trigger volume.)
##
## Signals:
## player_entered_buyzone(player_id) — emitted when a player enters the zone
## player_exited_buyzone(player_id) — emitted when a player leaves the zone
##
## Usage (server runtime):
## var bz: BuyZone = $BuyZone_CT
## if bz.is_in_buyzone(player_id):
## show_buy_menu(player_id)
extends Area3D
class_name BuyZone
# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------
## Emitted when a player enters the buy zone.
signal player_entered_buyzone(player_id: int)
## Emitted when a player exits the buy zone.
signal player_exited_buyzone(player_id: int)
# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------
## Which team(s) can use this buy zone.
## Team.COUNTER_TERRORIST = 1, Team.TERRORIST = 2, Team.SPECTATOR = 0.
## Use the Team enum: TeamData.Team.COUNTER_TERRORIST, etc.
@export var zone_team: TeamData.Team = TeamData.Team.COUNTER_TERRORIST
## Radius of the buy zone in Godot units (visual hint only; actual collision
## shape determines the trigger volume).
@export var zone_radius: float = 10.0
## If true, any team can use this zone (overrides zone_team).
@export var allow_all_teams: bool = false
# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------
## Set of player IDs currently inside this buy zone.
var _players_in_zone: Dictionary = {} # player_id (int) → true
# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------
func _ready() -> void:
# Connect area signals
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
area_entered.connect(_on_area_entered)
area_exited.connect(_on_area_exited)
# Set collision layer/mask for buy zones (layer 4 by convention)
collision_layer = 0 # Buy zones don't collide with physics
collision_mask = 0 # They detect bodies via signals only
# Ensure we have at least a basic collision shape hint
_setup_collision_shape()
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
## Check if a specific player is inside this buy zone.
func is_in_buyzone(player_id: int) -> bool:
return _players_in_zone.has(player_id)
## Get an array of player IDs currently inside this buy zone.
func get_players_in_buyzone() -> Array[int]:
return _players_in_zone.keys()
## Check if the given team is allowed to use this buy zone.
func is_team_allowed(team: TeamData.Team) -> bool:
if allow_all_teams:
return true
return team == zone_team
## Get the number of players currently in the zone.
func get_player_count() -> int:
return _players_in_zone.size()
# ---------------------------------------------------------------------------
# Internal: Signal handlers
# ---------------------------------------------------------------------------
func _on_body_entered(body: Node) -> void:
var player_id: int = _extract_player_id(body)
if player_id >= 0:
_players_in_zone[player_id] = true
player_entered_buyzone.emit(player_id)
func _on_body_exited(body: Node) -> void:
var player_id: int = _extract_player_id(body)
if player_id >= 0 and _players_in_zone.erase(player_id):
player_exited_buyzone.emit(player_id)
func _on_area_entered(_area: Area3D) -> void:
# Areas can also trigger if the player is an Area3D (e.g. a trigger zone
# on the player). For now, body detection is the primary path.
pass
func _on_area_exited(_area: Area3D) -> void:
pass
# ---------------------------------------------------------------------------
# Internal: helpers
# ---------------------------------------------------------------------------
## Try to extract a multiplayer peer ID from a body node.
## The body is expected to be a Player node or have a "get_peer_id" method,
## or the body's owner is a Player node with multiplayer authority info.
func _extract_player_id(body: Node) -> int:
# If the body has a peer_id property or method
if body.has_method(&"get_peer_id"):
return body.get_peer_id()
# If the body is a Player node with multiplayer authority
if body.has_method(&"get_multiplayer_authority"):
return body.get_multiplayer_authority()
# Check for a direct peer_id variable
if "peer_id" in body and body.get("peer_id") is int:
return body.get("peer_id")
# Try the body's owner
var owner_node: Node = body.owner if body.owner else body.get_parent()
if owner_node and owner_node != body:
return _extract_player_id(owner_node)
return -1
## Ensure there's a collision shape for the trigger volume.
## If no CollisionShape3D child exists, create a SphereShape3D one.
func _setup_collision_shape() -> void:
# Check if we already have a CollisionShape3D
for child in get_children():
if child is CollisionShape3D:
return # Already have one
# Create a default sphere shape
var shape := SphereShape3D.new()
shape.radius = zone_radius
var col_shape := CollisionShape3D.new()
col_shape.shape = shape
add_child(col_shape)
col_shape.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner