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
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
## 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
|
||||
Reference in New Issue
Block a user