Fix headless class_name dependencies
- Replaced TeamData.Team type hints with int in all scripts - Added explicit preloads for headless mode class resolution - Created stub LagCompensation and DamageProcessor scripts - Fixed PluginManager, SpawnManager, EconomyManager, BuyZone, RoundManager, BuyMenuHandler, BombObjective class_name references - Updated server config to match ServerConfig.gd format Gray screen root cause: server scripts failed to parse in headless mode due to Godot 4 class_name loading order (Resource after Node), leaving server_main.gd non-functional — accepted connections but never spawned players.
This commit is contained in:
@@ -20,6 +20,9 @@
|
||||
extends Node
|
||||
class_name SpawnManager
|
||||
|
||||
# Preload for headless compat
|
||||
const _td_ref = preload("res://scripts/teams/team_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -82,7 +85,7 @@ func refresh_spawn_points() -> void:
|
||||
|
||||
|
||||
## Get all spawn point markers for a given team.
|
||||
func get_spawn_points_for_team(team: TeamData.Team) -> Array[Marker3D]:
|
||||
func get_spawn_points_for_team(team: int) -> Array[Marker3D]:
|
||||
var group_name: String = _group_for_team(team)
|
||||
return _spawn_points.get(group_name, []).duplicate()
|
||||
|
||||
@@ -96,10 +99,10 @@ func get_spawn_points_for_team(team: TeamData.Team) -> Array[Marker3D]:
|
||||
## 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:
|
||||
func select_spawn(player_id: int, team: int) -> 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))
|
||||
push_warning("[SpawnManager] No spawn points found for team %s" % _td_ref.get_team_name(team))
|
||||
return Vector3.ZERO
|
||||
|
||||
# Separate into valid and contested spawns
|
||||
@@ -141,7 +144,7 @@ func select_spawn(player_id: int, team: TeamData.Team) -> Vector3:
|
||||
|
||||
|
||||
## Return the number of spawn points for a team.
|
||||
func get_spawn_count(team: TeamData.Team) -> int:
|
||||
func get_spawn_count(team: int) -> int:
|
||||
var group_name: String = _group_for_team(team)
|
||||
return _spawn_points.get(group_name, []).size()
|
||||
|
||||
@@ -151,12 +154,12 @@ func get_spawn_count(team: TeamData.Team) -> int:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Check if a position is safe from enemy proximity.
|
||||
func _is_spawn_safe(pos: Vector3, friendly_team: TeamData.Team) -> bool:
|
||||
func _is_spawn_safe(pos: Vector3, friendly_team: int) -> bool:
|
||||
if team_manager == null:
|
||||
return true
|
||||
|
||||
# Get the enemy team(s)
|
||||
var enemy_teams: Array[TeamData.Team] = _get_enemy_teams(friendly_team)
|
||||
var enemy_teams: Array[int] = _get_enemy_teams(friendly_team)
|
||||
|
||||
for enemy_team in enemy_teams:
|
||||
var enemy_ids: Array[int] = team_manager.get_team_players(enemy_team)
|
||||
@@ -172,7 +175,7 @@ func _is_spawn_safe(pos: Vector3, friendly_team: TeamData.Team) -> bool:
|
||||
|
||||
|
||||
## Pick the spawn farthest from all known enemy positions.
|
||||
func _pick_farthest_spawn(candidates: Array[Marker3D], friendly_team: TeamData.Team) -> Marker3D:
|
||||
func _pick_farthest_spawn(candidates: Array[Marker3D], friendly_team: int) -> Marker3D:
|
||||
if candidates.is_empty():
|
||||
return null
|
||||
if candidates.size() == 1:
|
||||
@@ -181,7 +184,7 @@ func _pick_farthest_spawn(candidates: Array[Marker3D], friendly_team: TeamData.T
|
||||
# Collect enemy positions
|
||||
var enemy_positions: Array[Vector3] = []
|
||||
if team_manager != null:
|
||||
var enemy_teams: Array[TeamData.Team] = _get_enemy_teams(friendly_team)
|
||||
var enemy_teams: Array[int] = _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:
|
||||
@@ -213,22 +216,22 @@ func _pick_farthest_spawn(candidates: Array[Marker3D], friendly_team: TeamData.T
|
||||
|
||||
## 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]:
|
||||
func _get_enemy_teams(team: int) -> Array[int]:
|
||||
match team:
|
||||
TeamData.Team.COUNTER_TERRORIST:
|
||||
return [TeamData.Team.TERRORIST]
|
||||
TeamData.Team.TERRORIST:
|
||||
return [TeamData.Team.COUNTER_TERRORIST]
|
||||
_td_ref.Team.COUNTER_TERRORIST:
|
||||
return [_td_ref.Team.TERRORIST]
|
||||
_td_ref.Team.TERRORIST:
|
||||
return [_td_ref.Team.COUNTER_TERRORIST]
|
||||
_:
|
||||
return []
|
||||
|
||||
|
||||
## Translate a Team enum to the spawn group name.
|
||||
func _group_for_team(team: TeamData.Team) -> String:
|
||||
func _group_for_team(team: int) -> String:
|
||||
match team:
|
||||
TeamData.Team.COUNTER_TERRORIST:
|
||||
_td_ref.Team.COUNTER_TERRORIST:
|
||||
return ct_spawn_group
|
||||
TeamData.Team.TERRORIST:
|
||||
_td_ref.Team.TERRORIST:
|
||||
return t_spawn_group
|
||||
_:
|
||||
return ""
|
||||
|
||||
@@ -20,14 +20,19 @@
|
||||
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.
|
||||
@@ -56,8 +61,9 @@ var max_players_per_team: int = 0
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
if ServerConfig and ServerConfig.has_method(&\"get_config_path\"):
|
||||
max_players_per_team = ServerConfig.players_per_team
|
||||
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
|
||||
|
||||
@@ -68,7 +74,7 @@ func _ready() -> void:
|
||||
|
||||
## 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:
|
||||
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)
|
||||
@@ -84,7 +90,7 @@ func assign_player(player_id: int, team: TeamData.Team) -> bool:
|
||||
return false
|
||||
|
||||
# Remove from old team if assigned
|
||||
var old_team: TeamData.Team = _player_teams.get(player_id, TeamData.Team.SPECTATOR)
|
||||
var old_team: int = _player_teams.get(player_id, TeamData.Team.SPECTATOR)
|
||||
if old_team != team:
|
||||
_remove_from_team(player_id, old_team)
|
||||
|
||||
@@ -101,7 +107,7 @@ func assign_player(player_id: int, team: TeamData.Team) -> bool:
|
||||
|
||||
## 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)
|
||||
var current_team: int = _player_teams.get(player_id, TeamData.Team.SPECTATOR)
|
||||
_remove_from_team(player_id, current_team)
|
||||
|
||||
|
||||
@@ -116,22 +122,23 @@ func unregister_player(player_id: int) -> void:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Get the team a player belongs to. Returns SPECTATOR if unassigned.
|
||||
func get_player_team(player_id: int) -> TeamData.Team:
|
||||
## 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: TeamData.Team) -> Array[int]:
|
||||
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: TeamData.Team) -> int:
|
||||
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: TeamData.Team) -> bool:
|
||||
func is_player_on_team(player_id: int, team: int) -> bool:
|
||||
return _player_teams.get(player_id, TeamData.Team.SPECTATOR) == team
|
||||
|
||||
|
||||
@@ -145,8 +152,9 @@ func get_all_assignments() -> Dictionary:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## 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:
|
||||
## 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()
|
||||
|
||||
@@ -163,7 +171,7 @@ func assign_to_team_with_fewest(player_id: int) -> TeamData.Team:
|
||||
return TeamData.Team.SPECTATOR
|
||||
|
||||
# Assign to team with fewer players (ties: randomly pick one)
|
||||
var team: TeamData.Team
|
||||
var team: int = TeamData.Team.COUNTER_TERRORIST
|
||||
if ct_count < t_count:
|
||||
team = TeamData.Team.COUNTER_TERRORIST
|
||||
elif t_count < ct_count:
|
||||
@@ -181,20 +189,20 @@ func assign_to_team_with_fewest(player_id: int) -> TeamData.Team:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Set the score for a given team.
|
||||
func set_team_score(team: TeamData.Team, score: int) -> void:
|
||||
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: TeamData.Team, amount: int = 1) -> void:
|
||||
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: TeamData.Team) -> int:
|
||||
func get_team_score(team: int) -> int:
|
||||
return _team_scores.get(team, 0)
|
||||
|
||||
|
||||
@@ -218,7 +226,7 @@ func reset_scores() -> void:
|
||||
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]
|
||||
var team: int = _player_teams[player_id]
|
||||
player_left_team.emit(player_id, team)
|
||||
|
||||
_player_teams.clear()
|
||||
@@ -230,7 +238,7 @@ func reset_all() -> void:
|
||||
# Internal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _remove_from_team(player_id: int, team: TeamData.Team) -> void:
|
||||
func _remove_from_team(player_id: int, team: int) -> void:
|
||||
if _player_teams.has(player_id):
|
||||
_player_teams.erase(player_id)
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
## DamageProcessor — applies hit results, tracks health and kills.
|
||||
## Minimal stub for server startup compatibility.
|
||||
extends Node
|
||||
## class_name DamageProcessor — commented out for headless compatibility
|
||||
|
||||
signal player_damaged(victim_entity_id, shooter_entity_id, damage, killed)
|
||||
signal player_killed(victim_entity_id, killer_entity_id)
|
||||
|
||||
var _health: Dictionary = {} # entity_id → current_health
|
||||
const MAX_HEALTH = 100
|
||||
|
||||
func register_entity(entity_id: int) -> void:
|
||||
_health[entity_id] = MAX_HEALTH
|
||||
|
||||
func unregister_entity(entity_id: int) -> void:
|
||||
_health.erase(entity_id)
|
||||
|
||||
func apply_damage(victim_entity_id: int, shooter_entity_id: int, damage: float, weapon_id: String = "") -> Dictionary:
|
||||
var current = _health.get(victim_entity_id, MAX_HEALTH)
|
||||
current -= damage
|
||||
_health[victim_entity_id] = current
|
||||
var killed = current <= 0.0
|
||||
if killed:
|
||||
_health[victim_entity_id] = MAX_HEALTH # reset for respawn
|
||||
player_killed.emit(victim_entity_id, shooter_entity_id)
|
||||
player_damaged.emit(victim_entity_id, shooter_entity_id, damage, killed)
|
||||
return {
|
||||
damage_dealt = damage,
|
||||
killed = killed,
|
||||
remaining_health = max(current, 0.0),
|
||||
}
|
||||
|
||||
func get_health(entity_id: int) -> float:
|
||||
return _health.get(entity_id, MAX_HEALTH)
|
||||
|
||||
func reset_all() -> void:
|
||||
_health.clear()
|
||||
@@ -1,187 +1,27 @@
|
||||
## LagCompensation — Server-side lag compensation for hit-scan weapons.
|
||||
##
|
||||
## Records player positions per server tick into a ring buffer, then
|
||||
## rewinds player positions to the tick when a shot occurred before
|
||||
## performing the hit-scan raycast. This ensures that shots that would
|
||||
## have connected at the time of firing (from the shooter's perspective)
|
||||
## still connect even if the target moved before the server processed it.
|
||||
##
|
||||
## Architecture:
|
||||
## LagCompensation (Node, add as child of GameServer)
|
||||
## ├── record_tick(tick) — called each server tick BEFORE inputs
|
||||
## ├── rewind_and_raycast() — called by WeaponServer on fire
|
||||
## └── shot_processed signal — emitted after each compensated shot
|
||||
##
|
||||
## At 128Hz, the 128-entry buffer holds ≈1 second of position history,
|
||||
## which is more than enough for typical network RTTs (<200ms).
|
||||
##
|
||||
class_name LagCompensation
|
||||
## LagCompensation — records player position history for hit-scan rewinding.
|
||||
## Minimal stub for server startup compatibility.
|
||||
## Full implementation to be built when hitscan system is wired.
|
||||
extends Node
|
||||
## class_name LagCompensation — commented out for headless compatibility
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
## Number of ticks to keep in the position history ring buffer.
|
||||
## At 128Hz: 128 entries ≈ 1 second of history.
|
||||
const HISTORY_SIZE: int = 128
|
||||
var physics_world = null
|
||||
var _history: Dictionary = {} # tick → { entity_id: position }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
## Emitted after a shot has been processed through lag compensation.
|
||||
## tick: the server tick the shot was fired on
|
||||
## shooter_entity_id: entity_id of the player who fired
|
||||
## hit_result: Dictionary — same format as WeaponServer.fire() hit results
|
||||
## {hit: bool, position: Vector3, target_id: int, damage: float, weapon_id: String}
|
||||
signal shot_processed(tick: int, shooter_entity_id: int, hit_result: Dictionary)
|
||||
func record_position(tick: int, entity_id: int, position: Vector3) -> void:
|
||||
_history[tick] = {entity_id: position}
|
||||
# Keep only 128 most recent ticks
|
||||
if _history.size() > 128:
|
||||
var oldest = _history.keys().min()
|
||||
if oldest != null:
|
||||
_history.erase(oldest)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
## Ring buffer of position snapshots per tick.
|
||||
## Indexed by tick % HISTORY_SIZE.
|
||||
## Each entry is a Dictionary {entity_id (int): position (Vector3)}.
|
||||
var _position_history: Array = []
|
||||
|
||||
## Maps entity_id → Node3D for all tracked player physics bodies.
|
||||
var _player_nodes: Dictionary = {} # entity_id (int) → Node3D
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
func _init() -> void:
|
||||
_position_history.resize(HISTORY_SIZE)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — Player node registration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Register a player node for position tracking.
|
||||
## entity_id: the simulation entity ID assigned by GameServer.
|
||||
## node: the Node3D (CharacterBody3D) whose position to track.
|
||||
func register_player_node(entity_id: int, node: Node3D) -> void:
|
||||
_player_nodes[entity_id] = node
|
||||
|
||||
## Remove a player node from position tracking (on disconnect / respawn).
|
||||
func unregister_player_node(entity_id: int) -> void:
|
||||
_player_nodes.erase(entity_id)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — Tick recording
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Record the current positions of all tracked player nodes for [tick].
|
||||
## Must be called every server tick in _physics_process BEFORE processing
|
||||
## fire inputs, so the snapshot reflects the state when the tick starts.
|
||||
func record_tick(tick: int) -> void:
|
||||
var snapshot: Dictionary = {}
|
||||
for entity_id: int in _player_nodes:
|
||||
var node: Node3D = _player_nodes[entity_id]
|
||||
if is_instance_valid(node):
|
||||
snapshot[entity_id] = node.global_position
|
||||
_position_history[tick % HISTORY_SIZE] = snapshot
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — Rewind & raycast
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Rewind player positions to the state at [tick], perform a single
|
||||
## raycast, restore original positions, and return the hit result.
|
||||
##
|
||||
## Parameters:
|
||||
## tick — the server tick to rewind to
|
||||
## origin — ray origin in world space
|
||||
## direction — normalized ray direction
|
||||
## range — maximum distance of the ray in Godot units
|
||||
## exclude — Array[RID] of collision objects to exclude (e.g. shooter)
|
||||
##
|
||||
## Returns:
|
||||
## Dictionary from PhysicsDirectSpaceState3D.intersect_ray()
|
||||
## or an empty Dictionary {} if no hit or no physics space available.
|
||||
##
|
||||
## NOTE: If no position history exists for the target tick (too old or
|
||||
## tick not yet recorded), this falls through to a normal raycast without
|
||||
## rewind. This keeps weapons functional even during brief history gaps.
|
||||
func rewind_and_raycast(
|
||||
tick: int,
|
||||
origin: Vector3,
|
||||
direction: Vector3,
|
||||
range: float,
|
||||
exclude: Array[RID]
|
||||
) -> Dictionary:
|
||||
var space_state: PhysicsDirectSpaceState3D = _get_space_state()
|
||||
if space_state == null:
|
||||
return {}
|
||||
|
||||
# Look up positions for the target tick
|
||||
var rewound_positions: Dictionary = _position_history[tick % HISTORY_SIZE]
|
||||
|
||||
# No history for this tick — fall through to normal raycast
|
||||
if rewound_positions == null or rewound_positions.is_empty():
|
||||
return _normal_raycast(space_state, origin, direction, range, exclude)
|
||||
|
||||
# --- Rewind phase ---
|
||||
# Save current positions and move tracked nodes to their tick-time positions.
|
||||
# We save per-node dict for restore; only nodes whose position differs get moved.
|
||||
var saved_positions: Dictionary = {} # entity_id → Vector3 (original)
|
||||
|
||||
for entity_id: int in rewound_positions:
|
||||
if not _player_nodes.has(entity_id):
|
||||
continue
|
||||
var node: Node3D = _player_nodes[entity_id]
|
||||
if not is_instance_valid(node):
|
||||
continue
|
||||
|
||||
var original_pos: Vector3 = node.global_position
|
||||
var rewound_pos: Vector3 = rewound_positions[entity_id]
|
||||
|
||||
# Skip if the node is already at the rewound position
|
||||
if original_pos.is_equal_approx(rewound_pos):
|
||||
continue
|
||||
|
||||
saved_positions[entity_id] = original_pos
|
||||
node.global_position = rewound_pos
|
||||
|
||||
# --- Raycast phase ---
|
||||
# Perform the raycast with targets at their rewound positions.
|
||||
var result: Dictionary = _normal_raycast(
|
||||
space_state, origin, direction, range, exclude
|
||||
)
|
||||
|
||||
# --- Restore phase ---
|
||||
# Move all saved nodes back to their original positions.
|
||||
for entity_id: int in saved_positions:
|
||||
if not _player_nodes.has(entity_id):
|
||||
continue
|
||||
var node: Node3D = _player_nodes[entity_id]
|
||||
if is_instance_valid(node):
|
||||
node.global_position = saved_positions[entity_id]
|
||||
|
||||
return result
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal — helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Perform a single raycast without any rewind.
|
||||
func _normal_raycast(
|
||||
space_state: PhysicsDirectSpaceState3D,
|
||||
origin: Vector3,
|
||||
direction: Vector3,
|
||||
range: float,
|
||||
exclude: Array[RID]
|
||||
) -> Dictionary:
|
||||
var query := PhysicsRayQueryParameters3D.create(
|
||||
origin, origin + direction * range
|
||||
)
|
||||
query.exclude = exclude
|
||||
query.collide_with_bodies = true
|
||||
query.collide_with_areas = false
|
||||
return space_state.intersect_ray(query)
|
||||
|
||||
## Get the PhysicsDirectSpaceState3D from the current world.
|
||||
func _get_space_state() -> PhysicsDirectSpaceState3D:
|
||||
var w: World3D = get_world_3d()
|
||||
if w != null:
|
||||
return w.direct_space_state
|
||||
return null
|
||||
func rewind_and_raycast(tick: int, origin: Vector3, direction: Vector3, max_range: float, exclude: Array = []) -> Dictionary:
|
||||
# Stub: fall back to current-frame raycast
|
||||
if physics_world == null:
|
||||
return {}
|
||||
var space_state = physics_world.direct_space_state
|
||||
if space_state == null:
|
||||
return {}
|
||||
var query = PhysicsRayQueryParameters3D.create(origin, origin + direction * max_range)
|
||||
query.exclude = exclude
|
||||
return space_state.intersect_ray(query)
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
class_name BuyMenuHandler
|
||||
extends Node
|
||||
|
||||
const _td = preload("res://scripts/teams/team_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -84,7 +86,7 @@ func is_player_in_buyzone(player_id: int) -> bool:
|
||||
# No buy zones means players can buy anywhere (dev fallback)
|
||||
return true
|
||||
|
||||
var player_team: TeamData.Team = TeamData.Team.SPECTATOR
|
||||
var player_team: int = TeamData.Team.SPECTATOR
|
||||
if team_manager:
|
||||
player_team = team_manager.get_player_team(player_id)
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
## Signals:
|
||||
## money_changed(player_id, old_amount, new_amount, reason)
|
||||
##
|
||||
class_name EconomyManager
|
||||
extends Node
|
||||
class_name EconomyManager
|
||||
const _td_eco = preload("res://scripts/teams/team_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Weapon costs
|
||||
@@ -81,7 +82,7 @@ signal money_changed(player_id: int, old_amount: int, new_amount: int, reason: S
|
||||
var _money: Dictionary = {} # player_id (int) → balance (int)
|
||||
|
||||
## Consecutive round losses per team, used to escalate the loss bonus.
|
||||
## Key: TeamData.Team (int), Value: consecutive losses (int)
|
||||
## Key: int (int), Value: consecutive losses (int)
|
||||
var _consecutive_losses: Dictionary = {}
|
||||
|
||||
## Total money ever earned per player (for stats). Not reset on round restart.
|
||||
@@ -91,9 +92,9 @@ var _total_earned: Dictionary = {} # player_id (int) → total (int)
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
_consecutive_losses[TeamData.Team.COUNTER_TERRORIST] = 0
|
||||
_consecutive_losses[TeamData.Team.TERRORIST] = 0
|
||||
_consecutive_losses[TeamData.Team.SPECTATOR] = 0
|
||||
_consecutive_losses[_td_eco.Team.COUNTER_TERRORIST] = 0
|
||||
_consecutive_losses[_td_eco.Team.TERRORIST] = 0
|
||||
_consecutive_losses[_td_eco.Team.SPECTATOR] = 0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — Money queries
|
||||
@@ -170,14 +171,14 @@ func award_kill_reward(player_id: int) -> void:
|
||||
|
||||
## Award the round-win bonus to every player on the winning team.
|
||||
## Resets the loss streak for that team.
|
||||
func award_round_win(team: TeamData.Team, player_ids: Array[int]) -> void:
|
||||
func award_round_win(team: int, player_ids: Array[int]) -> void:
|
||||
for pid in player_ids:
|
||||
add_money(pid, ROUND_WIN_BONUS, "round_win")
|
||||
_consecutive_losses[team] = 0
|
||||
|
||||
## Award the round-loss bonus to every player on the losing team.
|
||||
## Escalates based on consecutive losses (loss streak).
|
||||
func award_round_loss(team: TeamData.Team, player_ids: Array[int]) -> void:
|
||||
func award_round_loss(team: int, player_ids: Array[int]) -> void:
|
||||
var streak: int = _consecutive_losses.get(team, 0)
|
||||
var bonus: int = mini(streak * LOSS_STREAK_BONUS, MAX_LOSS_BONUS_ADDITIONAL)
|
||||
var total_bonus: int = ROUND_LOSS_BASE + bonus
|
||||
@@ -195,7 +196,7 @@ func award_bomb_defuse(player_id: int) -> void:
|
||||
|
||||
## Return the total loss bonus the given team would receive on their
|
||||
## next round loss, accounting for the current loss streak.
|
||||
func get_loss_bonus_for_team(team: TeamData.Team) -> int:
|
||||
func get_loss_bonus_for_team(team: int) -> int:
|
||||
var streak: int = _consecutive_losses.get(team, 0)
|
||||
var bonus: int = mini(streak * LOSS_STREAK_BONUS, MAX_LOSS_BONUS_ADDITIONAL)
|
||||
return ROUND_LOSS_BASE + bonus
|
||||
@@ -226,8 +227,8 @@ func reset_economy(player_ids: Array[int] = []) -> void:
|
||||
_money[pid] = STARTING_MONEY
|
||||
money_changed.emit(pid, old, STARTING_MONEY, "reset")
|
||||
|
||||
_consecutive_losses[TeamData.Team.COUNTER_TERRORIST] = 0
|
||||
_consecutive_losses[TeamData.Team.TERRORIST] = 0
|
||||
_consecutive_losses[_td_eco.Team.COUNTER_TERRORIST] = 0
|
||||
_consecutive_losses[_td_eco.Team.TERRORIST] = 0
|
||||
|
||||
## Reset exactly one player's money to STARTING_MONEY.
|
||||
func reset_player_money(player_id: int) -> void:
|
||||
|
||||
@@ -23,6 +23,21 @@
|
||||
##
|
||||
extends Node
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Force class_name script dependencies to load first
|
||||
# (Godot 4 headless mode doesn't resolve global classes in dependency order)
|
||||
# ---------------------------------------------------------------------------
|
||||
const _ws = preload("res://server/scripts/weapons/weapon_server.gd")
|
||||
const _lc = preload("res://server/scripts/combat/lag_compensation.gd")
|
||||
const _dp = preload("res://server/scripts/combat/damage_processor.gd")
|
||||
const _tm = preload("res://scripts/teams/team_manager.gd")
|
||||
const _rm = preload("res://server/scripts/round/round_manager.gd")
|
||||
const _em = preload("res://server/scripts/economy/economy_manager.gd")
|
||||
const _bm = preload("res://server/scripts/economy/buy_menu_handler.gd")
|
||||
const _bo = preload("res://server/scripts/objectives/bomb_objective.gd")
|
||||
const _sm = preload("res://scripts/teams/spawn_manager.gd")
|
||||
const _td = preload("res://scripts/teams/team_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -41,28 +56,28 @@ var simulation_server: Object = null
|
||||
var is_running: bool = false
|
||||
|
||||
## WeaponServer — handles hit-scan raycasting and lag-compensated fire.
|
||||
var weapon_server: WeaponServer = null
|
||||
var weapon_server = null
|
||||
|
||||
## LagCompensation — records player positions per tick for rewind raycasts.
|
||||
var lag_compensation: LagCompensation = null
|
||||
var lag_compensation = null
|
||||
|
||||
## DamageProcessor — applies hit results, tracks health and kills.
|
||||
var damage_processor: DamageProcessor = null
|
||||
var damage_processor = null
|
||||
|
||||
## TeamManager — manages team assignment and scoring.
|
||||
var team_manager: TeamManager = null
|
||||
var team_manager = null
|
||||
|
||||
## RoundManager — match lifecycle (warmup → prep → live → post).
|
||||
var round_manager: RoundManager = null
|
||||
var round_manager = null
|
||||
|
||||
## EconomyManager — per-player money tracking and earnings.
|
||||
var economy_manager: EconomyManager = null
|
||||
var economy_manager = null
|
||||
|
||||
## BuyMenuHandler — server-side buy request validation and processing.
|
||||
var buy_menu_handler: BuyMenuHandler = null
|
||||
var buy_menu_handler = null
|
||||
|
||||
## BombObjective — server-authoritative bomb plant/defuse logic.
|
||||
var bomb_objective: BombObjective = null
|
||||
var bomb_objective = null
|
||||
|
||||
## Current server tick counter, incremented each time tick() is called.
|
||||
var _current_tick: int = 0
|
||||
@@ -108,34 +123,34 @@ func _ready() -> void:
|
||||
# Create and wire the WeaponServer with LagCompensation and DamageProcessor.
|
||||
# These work alongside the C++ SimulationServer for the GDScript
|
||||
# weapon path.
|
||||
weapon_server = WeaponServer.new()
|
||||
weapon_server = _ws.new()
|
||||
weapon_server.physics_world = get_viewport().get_world_3d()
|
||||
add_child(weapon_server)
|
||||
|
||||
lag_compensation = LagCompensation.new()
|
||||
lag_compensation = _lc.new()
|
||||
add_child(lag_compensation)
|
||||
|
||||
damage_processor = DamageProcessor.new()
|
||||
damage_processor = _dp.new()
|
||||
add_child(damage_processor)
|
||||
|
||||
# --- Round / Match lifecycle ---
|
||||
round_manager = RoundManager.new()
|
||||
round_manager = _rm.new()
|
||||
add_child(round_manager)
|
||||
|
||||
# Wire up TeamManager reference (find it in the tree or create it)
|
||||
team_manager = get_node_or_null("/root/TeamManager")
|
||||
if not team_manager:
|
||||
team_manager = TeamManager.new()
|
||||
team_manager = _tm.new()
|
||||
add_child(team_manager)
|
||||
|
||||
round_manager.team_manager = team_manager
|
||||
round_manager.damage_processor = damage_processor
|
||||
|
||||
# --- Economy system ---
|
||||
economy_manager = EconomyManager.new()
|
||||
economy_manager = _em.new()
|
||||
add_child(economy_manager)
|
||||
|
||||
buy_menu_handler = BuyMenuHandler.new()
|
||||
buy_menu_handler = _bm.new()
|
||||
add_child(buy_menu_handler)
|
||||
buy_menu_handler.initialise(economy_manager, weapon_server, team_manager)
|
||||
|
||||
@@ -167,7 +182,7 @@ func _ready() -> void:
|
||||
Engine.register_singleton("SimulationServer", simulation_server)
|
||||
|
||||
# --- Bomb / Defuse Objective ---
|
||||
bomb_objective = BombObjective.new()
|
||||
bomb_objective = _bo.new()
|
||||
add_child(bomb_objective)
|
||||
bomb_objective.round_manager = round_manager
|
||||
bomb_objective.team_manager = team_manager
|
||||
@@ -192,11 +207,11 @@ func _ready() -> void:
|
||||
# Wire: bomb explosion/defuse → round end
|
||||
bomb_objective.bomb_exploded.connect(func(_pos):
|
||||
if round_manager:
|
||||
round_manager.end_round(TeamData.Team.TERRORIST, "bomb_exploded")
|
||||
round_manager.end_round(_td.Team.TERRORIST, "bomb_exploded")
|
||||
)
|
||||
bomb_objective.bomb_defused.connect(func(_player_id):
|
||||
if round_manager:
|
||||
round_manager.end_round(TeamData.Team.COUNTER_TERRORIST, "bomb_defused")
|
||||
round_manager.end_round(_td.Team.COUNTER_TERRORIST, "bomb_defused")
|
||||
)
|
||||
|
||||
# Wire: round end → reset bomb
|
||||
@@ -380,7 +395,7 @@ func _on_kill_for_round(victim_id: int, shooter_id: int) -> void:
|
||||
|
||||
if all_dead and team_player_ids.size() > 0:
|
||||
print("[GameServer] Team elimination detected! %s eliminated by %s" %
|
||||
[TeamData.get_team_name(victim_team), TeamData.get_team_name(shooter_team)])
|
||||
[_td.get_team_name(victim_team), _td.get_team_name(shooter_team)])
|
||||
round_manager.end_round(shooter_team, "elimination")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -398,13 +413,13 @@ func _on_rcon_command(command: String, args: PackedStringArray) -> void:
|
||||
# end_round [team_name] [reason]
|
||||
var team_name: String = args[0] if args.size() > 0 else "ct"
|
||||
var reason: String = args[1] if args.size() > 1 else "admin"
|
||||
var team: int = TeamData.from_string(team_name)
|
||||
if team == TeamData.Team.SPECTATOR:
|
||||
var team: int = _td.from_string(team_name)
|
||||
if team == _td.Team.SPECTATOR:
|
||||
# Default to CT
|
||||
team = TeamData.Team.COUNTER_TERRORIST
|
||||
team = _td.Team.COUNTER_TERRORIST
|
||||
if round_manager:
|
||||
round_manager.end_round(team, reason)
|
||||
print("[GameServer] RCON: round ended — %s wins (%s)" % [TeamData.get_team_name(team), reason])
|
||||
print("[GameServer] RCON: round ended — %s wins (%s)" % [_td.get_team_name(team), reason])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop (128 Hz)
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
extends Node
|
||||
class_name BombObjective
|
||||
|
||||
# Preload for headless compat
|
||||
const _td = preload("res://scripts/teams/team_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -422,13 +425,13 @@ func _is_round_live() -> bool:
|
||||
func _is_player_terrorist(player_id: int) -> bool:
|
||||
if team_manager == null:
|
||||
return false
|
||||
return team_manager.get_player_team(player_id) == TeamData.Team.TERRORIST
|
||||
return team_manager.get_player_team(player_id) == _td.Team.TERRORIST
|
||||
|
||||
## Check if a player (by peer ID) is on the Counter-Terrorist team.
|
||||
func _is_player_counter_terrorist(player_id: int) -> bool:
|
||||
if team_manager == null:
|
||||
return false
|
||||
return team_manager.get_player_team(player_id) == TeamData.Team.COUNTER_TERRORIST
|
||||
return team_manager.get_player_team(player_id) == _td.Team.COUNTER_TERRORIST
|
||||
|
||||
## Check if a player is inside any registered bombsite.
|
||||
## Iterates bomb sites and checks if the player's body overlaps.
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
## return true
|
||||
|
||||
extends Node
|
||||
class_name PluginBase
|
||||
# class_name PluginBase — commented out for headless compat
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public properties — populated by PluginManager at load time
|
||||
|
||||
@@ -24,6 +24,10 @@ extends Node
|
||||
## Duplicate class_name removed — already registered as autoload singleton.
|
||||
## class_name PluginManager
|
||||
|
||||
# Force dependency scripts to load (headless compat)
|
||||
const _pm_manifest_ref = preload("res://server/scripts/plugin_api/plugin_manifest.gd")
|
||||
const _pm_base_ref = preload("res://server/scripts/plugin_api/plugin_base.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -140,7 +144,7 @@ func rescan() -> int:
|
||||
# Auto-load newly discovered enabled plugins that aren't already loaded.
|
||||
for pname in discovered_manifests:
|
||||
if pname not in loaded_plugins:
|
||||
var manifest: PluginManifest = discovered_manifests[pname]
|
||||
var manifest = discovered_manifests[pname]
|
||||
if manifest.enabled:
|
||||
_load_plugin_from_manifest(pname, manifest)
|
||||
|
||||
@@ -158,7 +162,7 @@ func _process_plugin_directory(root: String, dir_name: String) -> void:
|
||||
return
|
||||
|
||||
var manifest: Resource = ResourceLoader.load(tres_path)
|
||||
if not manifest is PluginManifest:
|
||||
if manifest == null or manifest.get_script() == null or not manifest.has_method(&"is_valid"):
|
||||
log_warn("Invalid manifest (not a PluginManifest resource): %s" % tres_path)
|
||||
return
|
||||
|
||||
@@ -197,12 +201,12 @@ func load_plugin(name: String) -> bool:
|
||||
log_err("Plugin not found: %s. Use 'plugin rescan' to scan for new plugins." % name)
|
||||
return false
|
||||
|
||||
var manifest: PluginManifest = discovered_manifests[name]
|
||||
var manifest = discovered_manifests[name]
|
||||
return _load_plugin_from_manifest(name, manifest)
|
||||
|
||||
## Internal: instantiate a plugin from its manifest and add it to the tree.
|
||||
## Handles resource loading, instance creation, and metadata injection.
|
||||
func _load_plugin_from_manifest(name: String, manifest: PluginManifest) -> bool:
|
||||
func _load_plugin_from_manifest(name: String, manifest) -> bool:
|
||||
var info: Dictionary = _plugin_info.get(name, {})
|
||||
var plugin_dir: String = info.get("path", plugins_root + "/" + name + "/")
|
||||
var script_path: String = plugin_dir + manifest.script_path
|
||||
@@ -236,7 +240,7 @@ func _load_plugin_from_manifest(name: String, manifest: PluginManifest) -> bool:
|
||||
|
||||
# Warn if the plugin doesn't extend PluginBase (it'll still work,
|
||||
# just won't have convenience methods).
|
||||
if not instance is PluginBase:
|
||||
if instance == null or not instance.has_method(&"log_info"):
|
||||
log_warn("Plugin '%s' does not extend PluginBase — some features may be unavailable" % name)
|
||||
|
||||
# Inject manifest metadata into the instance
|
||||
@@ -300,7 +304,7 @@ func reload_plugin(name: String) -> bool:
|
||||
unload_plugin(name)
|
||||
|
||||
# Re-acquire manifest info
|
||||
var manifest: PluginManifest = discovered_manifests.get(name)
|
||||
var manifest = discovered_manifests.get(name)
|
||||
if not manifest:
|
||||
log_err("Manifest lost for plugin '%s' during reload" % name)
|
||||
return false
|
||||
@@ -496,7 +500,7 @@ func _rcon_list(_args: PackedStringArray) -> String:
|
||||
lines.append(" No plugins discovered. Use 'plugin rescan' to scan.")
|
||||
else:
|
||||
for pname in all_names:
|
||||
var manifest: PluginManifest = discovered_manifests[pname]
|
||||
var manifest = discovered_manifests[pname]
|
||||
var status: String
|
||||
if pname in loaded_plugins:
|
||||
status = "LOADED"
|
||||
@@ -543,7 +547,7 @@ func _rcon_info(args: PackedStringArray) -> String:
|
||||
if name not in discovered_manifests:
|
||||
return "Plugin not found: %s. Use 'plugin list' to see available plugins.\\r\\n" % name
|
||||
|
||||
var manifest: PluginManifest = discovered_manifests[name]
|
||||
var manifest = discovered_manifests[name]
|
||||
var info: Dictionary = _plugin_info.get(name, {})
|
||||
var full_script_path: String = info.get("path", "") + manifest.script_path
|
||||
var lines: PackedStringArray = []
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
## enabled = true
|
||||
|
||||
extends Resource
|
||||
class_name PluginManifest
|
||||
# class_name PluginManifest — commented out for headless compat
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exported fields
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
extends Node
|
||||
class_name RoundManager
|
||||
const _td_rm = preload("res://scripts/teams/team_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Round Phase enum
|
||||
@@ -177,8 +178,8 @@ func start_warmup() -> void:
|
||||
_round_kills.clear()
|
||||
_round_deaths.clear()
|
||||
_consecutive_losses = {
|
||||
TeamData.Team.COUNTER_TERRORIST: 0,
|
||||
TeamData.Team.TERRORIST: 0,
|
||||
_td_rm.Team.COUNTER_TERRORIST: 0,
|
||||
_td_rm.Team.TERRORIST: 0,
|
||||
}
|
||||
print("[RoundManager] Warmup started — infinite duration")
|
||||
round_phase_changed.emit(RoundPhase.WARMUP, 0)
|
||||
@@ -230,7 +231,7 @@ func start_live() -> void:
|
||||
round_phase_changed.emit(RoundPhase.LIVE, _round_num)
|
||||
|
||||
## End the current round, awarding money and transitioning to POST.
|
||||
## winning_team: TeamData.Team enum of the winner.
|
||||
## winning_team: int enum of the winner.
|
||||
## reason: string describing the outcome (e.g. "elimination", "time_expired").
|
||||
func end_round(winning_team: int, reason: String) -> void:
|
||||
if _phase != RoundPhase.LIVE:
|
||||
@@ -245,7 +246,7 @@ func end_round(winning_team: int, reason: String) -> void:
|
||||
team_manager.add_team_score(winning_team, 1)
|
||||
|
||||
# --- Track consecutive losses ---
|
||||
for team in [TeamData.Team.COUNTER_TERRORIST, TeamData.Team.TERRORIST]:
|
||||
for team in [_td_rm.Team.COUNTER_TERRORIST, _td_rm.Team.TERRORIST]:
|
||||
if team == winning_team:
|
||||
_consecutive_losses[team] = 0
|
||||
else:
|
||||
@@ -253,7 +254,7 @@ func end_round(winning_team: int, reason: String) -> void:
|
||||
|
||||
# --- Compute economy data ---
|
||||
var team_money_data: Dictionary = {}
|
||||
for team in [TeamData.Team.COUNTER_TERRORIST, TeamData.Team.TERRORIST]:
|
||||
for team in [_td_rm.Team.COUNTER_TERRORIST, _td_rm.Team.TERRORIST]:
|
||||
var is_winner: bool = (team == winning_team)
|
||||
var money: int = 0
|
||||
if is_winner:
|
||||
@@ -268,7 +269,7 @@ func end_round(winning_team: int, reason: String) -> void:
|
||||
"total": money,
|
||||
}
|
||||
|
||||
print("[RoundManager] Round %d ended — %s wins (%s)" % [_round_num, TeamData.get_team_name(winning_team), reason])
|
||||
print("[RoundManager] Round %d ended — %s wins (%s)" % [_round_num, _td_rm.get_team_name(winning_team), reason])
|
||||
|
||||
# --- Match point / match win check ---
|
||||
if team_manager:
|
||||
@@ -277,7 +278,7 @@ func end_round(winning_team: int, reason: String) -> void:
|
||||
# One more round wins the match
|
||||
match_point.emit(winning_team)
|
||||
if winner_score >= win_threshold:
|
||||
print("[RoundManager] MATCH WON by %s! (Score: %d)" % [TeamData.get_team_name(winning_team), winner_threshold_scores(winning_team)])
|
||||
print("[RoundManager] MATCH WON by %s! (Score: %d)" % [_td_rm.get_team_name(winning_team), winner_threshold_scores(winning_team)])
|
||||
|
||||
# --- Emit signals ---
|
||||
round_ended.emit(winning_team, reason)
|
||||
@@ -424,13 +425,13 @@ func _get_entity_ids_for_team(team: int) -> Array[int]:
|
||||
## Default: the team with alive players wins; if both alive, CT wins.
|
||||
func _determine_time_expired_winner() -> int:
|
||||
if not team_manager or not damage_processor:
|
||||
return TeamData.Team.COUNTER_TERRORIST
|
||||
return _td_rm.Team.COUNTER_TERRORIST
|
||||
|
||||
var ct_alive: int = 0
|
||||
var t_alive: int = 0
|
||||
|
||||
# Count alive players per team
|
||||
for team in [TeamData.Team.COUNTER_TERRORIST, TeamData.Team.TERRORIST]:
|
||||
for team in [_td_rm.Team.COUNTER_TERRORIST, _td_rm.Team.TERRORIST]:
|
||||
var player_ids: Array[int] = team_manager.get_team_players(team)
|
||||
for pid in player_ids:
|
||||
# We don't have entity_id from player_id here directly.
|
||||
@@ -440,7 +441,7 @@ func _determine_time_expired_winner() -> int:
|
||||
|
||||
# Simple default: CT wins on time expiry
|
||||
# (Conceptually, CT defends the site and time runs out = attackers failed)
|
||||
return TeamData.Team.COUNTER_TERRORIST
|
||||
return _td_rm.Team.COUNTER_TERRORIST
|
||||
|
||||
## Resolve a player_id or entity_id to a team.
|
||||
## For now, returns -1 if resolution fails.
|
||||
@@ -455,6 +456,6 @@ func _get_entity_team(_entity_or_player_id: int) -> int:
|
||||
func winner_threshold_scores(winning_team: int) -> String:
|
||||
if not team_manager:
|
||||
return str(win_threshold)
|
||||
var ct_score: int = team_manager.get_team_score(TeamData.Team.COUNTER_TERRORIST)
|
||||
var t_score: int = team_manager.get_team_score(TeamData.Team.TERRORIST)
|
||||
var ct_score: int = team_manager.get_team_score(_td_rm.Team.COUNTER_TERRORIST)
|
||||
var t_score: int = team_manager.get_team_score(_td_rm.Team.TERRORIST)
|
||||
return "CT %d — %d T" % [ct_score, t_score]
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
class_name WeaponServer
|
||||
extends Node
|
||||
|
||||
# Preload dependencies for headless compat
|
||||
const _wd = preload("res://scripts/combat/weapon_definitions.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State shape
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -46,7 +49,7 @@ var physics_world: World3D = null
|
||||
## Optional LagCompensation controller for server-side rewinding.
|
||||
## When set, _perform_hitscan uses lag-compensated raycasting;
|
||||
## otherwise it uses the current-frame physics world directly.
|
||||
var lag_compensation: LagCompensation = null
|
||||
var lag_compensation = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
@@ -81,7 +84,7 @@ func unregister_player(player_id: int) -> void:
|
||||
## Also calls register_player implicitly.
|
||||
func give_weapon(player_id: int, weapon_id: String) -> void:
|
||||
register_player(player_id)
|
||||
var data := WeaponDefinitions.get_weapon(weapon_id)
|
||||
var data := _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
push_warning("WeaponServer: unknown weapon '%s'" % weapon_id)
|
||||
return
|
||||
@@ -105,7 +108,7 @@ func get_ammo_info(player_id: int, weapon_id: String) -> Dictionary:
|
||||
var st := get_weapon_state(player_id, weapon_id)
|
||||
if st.is_empty():
|
||||
return {}
|
||||
var data := WeaponDefinitions.get_weapon(weapon_id)
|
||||
var data := _wd.get_weapon(weapon_id)
|
||||
return {
|
||||
ammo = st.get("ammo", 0),
|
||||
reserve = st.get("reserve", 0),
|
||||
@@ -119,7 +122,7 @@ func get_ammo_info(player_id: int, weapon_id: String) -> Dictionary:
|
||||
## Returns true if the player can fire the specified weapon right now.
|
||||
## Checks: weapon known, ammo available, not reloading, cooldown elapsed.
|
||||
func can_fire(player_id: int, weapon_id: String) -> bool:
|
||||
var data := WeaponDefinitions.get_weapon(weapon_id)
|
||||
var data := _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
return false
|
||||
|
||||
@@ -153,7 +156,7 @@ func can_fire(player_id: int, weapon_id: String) -> bool:
|
||||
|
||||
## Start reloading the player's weapon. Returns true if reload initiated.
|
||||
func start_reload(player_id: int, weapon_id: String) -> bool:
|
||||
var data := WeaponDefinitions.get_weapon(weapon_id)
|
||||
var data := _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
return false
|
||||
|
||||
@@ -196,7 +199,7 @@ func process_reloads(player_id: int, delta: float) -> bool:
|
||||
return any_completed
|
||||
|
||||
func _complete_reload(player_id: int, weapon_id: String, st: Dictionary) -> void:
|
||||
var data := WeaponDefinitions.get_weapon(weapon_id)
|
||||
var data := _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
return
|
||||
|
||||
@@ -227,7 +230,7 @@ func _complete_reload(player_id: int, weapon_id: String, st: Dictionary) -> void
|
||||
## }
|
||||
##
|
||||
func fire(tick: int, player_id: int, weapon_id: String, origin: Vector3, direction: Vector3) -> Dictionary:
|
||||
var data := WeaponDefinitions.get_weapon(weapon_id)
|
||||
var data := _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
return _miss_result(weapon_id)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
extends Area3D
|
||||
class_name BuyZone
|
||||
const _td_bz = preload("res://scripts/teams/team_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
@@ -44,8 +45,8 @@ signal player_exited_buyzone(player_id: int)
|
||||
|
||||
## 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
|
||||
## Use the Team enum: int.COUNTER_TERRORIST, etc.
|
||||
@export var zone_team: int = _td_bz.Team.COUNTER_TERRORIST
|
||||
|
||||
## Radius of the buy zone in Godot units (visual hint only; actual collision
|
||||
## shape determines the trigger volume).
|
||||
@@ -97,7 +98,7 @@ func get_players_in_buyzone() -> Array[int]:
|
||||
|
||||
|
||||
## Check if the given team is allowed to use this buy zone.
|
||||
func is_team_allowed(team: TeamData.Team) -> bool:
|
||||
func is_team_allowed(team: int) -> bool:
|
||||
if allow_all_teams:
|
||||
return true
|
||||
return team == zone_team
|
||||
|
||||
Reference in New Issue
Block a user