P7.9: fix headless export — resolve Resource class_name ordering + duplicate RoundManager + simulation stub API
- Fix dead code in server_main.gd (unreachable lag compensation print) - Remove WeaponData type hints from weapon_server.gd (Resource class_name in Node scripts = headless fail) - Use preload() instead of WeaponData class_name in weapon_definitions.gd for const refs - Lazy-init WEAPONS dict (const can't reference preload members) - Remove duplicate class_name RoundManager from server/scripts/round/round_manager.gd - Remove DamageProcessor type hints from round/round_manager.gd and bomb_objective.gd - Add start()/stop()/can_tick()/tick()/spawn_entity()/despawn_entity() to simulation_server_stub.gd - Fix get_world_3d() → get_tree().root.world_3d in weapon_server.gd Node context - Fix var data := → var data = for untyped WeaponDefinitions.get_weapon() returns - Clean export cache and verify server starts with zero parse errors
This commit is contained in:
@@ -83,7 +83,8 @@ Task: P7.1 (research — netfox migration plan)
|
||||
- ✅ Duck-typed netfox checks via `has_method()`, `Engine.has_singleton()`
|
||||
|
||||
**REMAINING:**
|
||||
- No `TickInterpolator` child for smooth interpolation → P7.5
|
||||
- ✅ `TickInterpolator` child for smooth interpolation (dynamically created in `_setup_tick_interpolator()`)
|
||||
- ✅ `TickInterpolator` on remote player instances for visual rollback
|
||||
|
||||
---
|
||||
|
||||
@@ -130,7 +131,7 @@ Task: P7.1 (research — netfox migration plan)
|
||||
|
||||
**REMAINING:**
|
||||
- Creates full Player nodes with RollbackSynchronizer for each remote player (✅ correct pattern)
|
||||
- May want TickInterpolator on remote player instances later (P7.5)
|
||||
- ✅ `TickInterpolator` on remote player instances (dynamically created in `_setup_tick_interpolator()`)
|
||||
|
||||
---
|
||||
|
||||
@@ -174,11 +175,11 @@ Task: P7.1 (research — netfox migration plan)
|
||||
| Input client→server | RollbackSynchronizer.input_properties | ✅ |
|
||||
| Tick-driven movement | `_rollback_tick(delta, tick, is_input)` | ✅ |
|
||||
| Client prediction | `enable_prediction = true` | ✅ |
|
||||
| Smooth interpolation | TickInterpolator | ❌ P7.5 |
|
||||
| Smooth interpolation | TickInterpolator | ✅ |
|
||||
| Round state broadcast | StateSynchronizer | ❌ P7.7 |
|
||||
| Rollback-safe weapon fire | RewindableAction | ❌ P7.6 |
|
||||
| Lag comp / hitscan | RewindableAction + mutation | ❌ P7.6 |
|
||||
| Visual rollback | TickInterpolator + _rollback_tick | ❌ P7.5 |
|
||||
| Visual rollback | TickInterpolator + _rollback_tick | ✅ |
|
||||
| Lifecycle signals | NetworkEvents | ✅ |
|
||||
|
||||
---
|
||||
@@ -200,7 +201,7 @@ All access guarded — no class_name references in our code.
|
||||
|
||||
```
|
||||
P7.6 (RewindableAction) ── needs ── RollbackSynchronizer working (✅ DONE)
|
||||
P7.5 (TickInterpolator) ── needs ── RollbackSynchronizer working (✅ DONE)
|
||||
P7.5 (TickInterpolator) ── needs ── RollbackSynchronizer working (✅ DONE)── [[DONE]] ✅
|
||||
P7.7 (Round state netfox) ── needs ── NetworkEvents working (✅ DONE)
|
||||
P7.8 (FPSController refactor) ── needs ── P7.6 input flow clarified
|
||||
P7.9 (LAN test + deploy) ── needs ── all of the above
|
||||
@@ -238,7 +239,7 @@ P7.9 (LAN test + deploy) ── needs ── all of the above
|
||||
| File | Lines | Netfox Status | RPCs | Signals | Changes Needed |
|
||||
|------|-------|---------------|------|---------|----------------|
|
||||
| network_manager.gd | 245 | ✅ overlay wired | 6 ❌ | 9 ✅ | Remove 6 RPCs post-P7.7 |
|
||||
| player.gd | 251 | ✅ rollback + tick | 0 | 0 | Add TickInterpolator P7.5 |
|
||||
| player.gd | 251 | ✅ rollback + tick + interpolator | 0 | 0 | ✅ |
|
||||
| player_net_input.gd | 91 | ✅ tick gather | 0 | 0 | None atm |
|
||||
| server_main.gd | 342 | ✅ thin layer | 7 call sites ❌ | 2 | Migrate to StateSynchronizer P7.7 |
|
||||
| client_main.gd | 124 | ✅ thin layer | 0 | 2 signal connects | None atm |
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ dedicated_server=true
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter="client/**, server/server-browser-api/venv/**, **/venv/**"
|
||||
exclude_filter="client/**, gdextension.disabled/**, server/server-browser-api/venv/**, **/venv/**"
|
||||
export_path="build/tactical-shooter-server.x86_64"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
## print(data.display_name) # "Assault Rifle"
|
||||
##
|
||||
extends Resource
|
||||
class_name WeaponData
|
||||
# NOTE: no class_name — the client has WeaponData class_name at
|
||||
# client/weapons/data/weapon_data.gd with different field names.
|
||||
# Server code uses preload() + untyped references to avoid conflicts.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Properties
|
||||
@@ -46,29 +48,32 @@ class_name WeaponData
|
||||
@export var pellets_per_shot: int = 1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-configured weapon instances (class-level constants)
|
||||
# Pre-configured weapon instances (class-level static vars)
|
||||
# ---------------------------------------------------------------------------
|
||||
# NOTE: static var initializers CAN call functions (unlike const).
|
||||
# These run when the script is loaded, which avoids the "not a constant
|
||||
# expression" error that occurs with const + .new().
|
||||
|
||||
## Assault Rifle — 30 dmg, 10 rps, 30 mag, 2.1s reload, automatic, 0.5° spread, 200m range.
|
||||
const RIFLE: Resource = pref(
|
||||
static var RIFLE: Resource = pref(
|
||||
"rifle", "Assault Rifle",
|
||||
30.0, 10.0, 30, 2.1, true, 0.5, 200.0, 1
|
||||
)
|
||||
|
||||
## Pistol — 25 dmg, 4 rps, 12 mag, 1.5s reload, semi-auto, 0.3° spread, 80m range.
|
||||
const PISTOL: Resource = pref(
|
||||
static var PISTOL: Resource = pref(
|
||||
"pistol", "Pistol",
|
||||
25.0, 4.0, 12, 1.5, false, 0.3, 80.0, 1
|
||||
)
|
||||
|
||||
## Shotgun — 8 dmg × 8 pellets, 1 rps, 8 mag, 3.0s reload, semi-auto, 3° spread, 30m range.
|
||||
const SHOTGUN: Resource = pref(
|
||||
static var SHOTGUN: Resource = pref(
|
||||
"shotgun", "Shotgun",
|
||||
8.0, 1.0, 8, 3.0, false, 3.0, 30.0, 8
|
||||
)
|
||||
|
||||
## SMG — 18 dmg, 12 rps, 25 mag, 1.8s reload, automatic, 1.5° spread, 100m range.
|
||||
const SMG: Resource = pref(
|
||||
static var SMG: Resource = pref(
|
||||
"smg", "Submachine Gun",
|
||||
18.0, 12.0, 25, 1.8, true, 1.5, 100.0, 1
|
||||
)
|
||||
@@ -90,8 +95,8 @@ static func pref(
|
||||
_spread_degrees: float,
|
||||
_range: float,
|
||||
_pellets: int = 1
|
||||
) -> WeaponData:
|
||||
var w := WeaponData.new()
|
||||
): # -> WeaponData (untyped for headless compat)
|
||||
var w = WeaponData.new()
|
||||
w.weapon_id = _weapon_id
|
||||
w.display_name = _display_name
|
||||
w.damage = _damage
|
||||
|
||||
@@ -12,23 +12,38 @@
|
||||
extends RefCounted
|
||||
class_name WeaponDefinitions
|
||||
|
||||
# Preload WeaponData script directly (no class_name dependency — the server
|
||||
# weapon_data.gd intentionally avoids class_name to avoid conflicts with
|
||||
# client/weapons/data/weapon_data.gd which also registers WeaponData).
|
||||
const _WD = preload("res://scripts/combat/weapon_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Weapon Registry
|
||||
# Weapon Registry — lazily initialised to avoid const + .new() errors
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Whether the WEAPONS dictionary has been populated.
|
||||
static var _initialized: bool = false
|
||||
|
||||
## All weapons indexed by weapon_id — the single source of truth for the
|
||||
## game's starter arsenal. Add new weapons here and in WeaponData.
|
||||
const WEAPONS: Dictionary = {
|
||||
"rifle": WeaponData.RIFLE,
|
||||
"pistol": WeaponData.PISTOL,
|
||||
"shotgun": WeaponData.SHOTGUN,
|
||||
"smg": WeaponData.SMG,
|
||||
}
|
||||
## game's starter arsenal. Populated on first access via _ensure_init().
|
||||
static var WEAPONS: Dictionary = {}
|
||||
|
||||
static func _ensure_init() -> void:
|
||||
if _initialized:
|
||||
return
|
||||
_initialized = true
|
||||
WEAPONS = {
|
||||
"rifle": _WD.RIFLE,
|
||||
"pistol": _WD.PISTOL,
|
||||
"shotgun": _WD.SHOTGUN,
|
||||
"smg": _WD.SMG,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lookup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Return the WeaponData for the given weapon_id, or null if unknown.
|
||||
static func get_weapon(id: String) -> WeaponData:
|
||||
return WEAPONS.get(id, null) as WeaponData
|
||||
static func get_weapon(id: String):
|
||||
_ensure_init()
|
||||
return WEAPONS.get(id, null)
|
||||
|
||||
@@ -24,7 +24,7 @@ signal player_despawned(peer_id: int)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports
|
||||
# ---------------------------------------------------------------------------
|
||||
@export var player_scene: PackedScene = preload("res://scenes/player.tscn")
|
||||
@export var player_scene: PackedScene = preload("res://scenes/server/server_player.tscn")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
@@ -58,13 +58,16 @@ func _ready() -> void:
|
||||
# Use load() instead of preload() so GameServer's dev dependencies
|
||||
# (weapons, economy, objectives, teams) don't block compilation.
|
||||
var GameServerClass = load("res://server/scripts/game_server.gd")
|
||||
if GameServerClass != null:
|
||||
if GameServerClass != null and GameServerClass is GDScript:
|
||||
_game_server = GameServerClass.new()
|
||||
if _game_server != null:
|
||||
add_child(_game_server)
|
||||
# GameServer registers SimulationServer as a singleton on _ready
|
||||
_game_server.start_simulation()
|
||||
else:
|
||||
push_warning("[ServerMain] GameServer not available — running without simulation server")
|
||||
push_warning("[ServerMain] GameServer instantiation failed")
|
||||
else:
|
||||
push_warning("[ServerMain] GameServer class not available — running without simulation server")
|
||||
|
||||
# Check if ServerConfig has finished loading.
|
||||
|
||||
@@ -94,8 +97,6 @@ func _ready() -> void:
|
||||
## Check if ServerConfig has finished loading.
|
||||
func _server_config_loaded() -> bool:
|
||||
return ServerConfig and ServerConfig.has_method(&"get_config_path") and not ServerConfig.get_config_path().is_empty()
|
||||
var lag_comp_ms: float = 64.0 * 1000.0 / ServerConfig.tick_rate
|
||||
print("[ServerMain] Lag comp: Enabled (64-tick history = %.1fms)" % lag_comp_ms)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
NetworkManager.stop()
|
||||
|
||||
@@ -7,9 +7,17 @@ extends Node
|
||||
var physics_world = null
|
||||
var _history: Dictionary = {} # tick → { entity_id: position }
|
||||
|
||||
## Record the start of a new tick for lag compensation.
|
||||
## Called by GameServer each tick before processing inputs.
|
||||
## Individual player positions are recorded by the game server loop.
|
||||
func record_tick(tick: int) -> void:
|
||||
_history[tick] = {}
|
||||
|
||||
## Record a player's position at a given tick (used during per-entity processing).
|
||||
func record_position(tick: int, entity_id: int, position: Vector3) -> void:
|
||||
_history[tick] = {entity_id: position}
|
||||
# Keep only 128 most recent ticks
|
||||
if not _history.has(tick):
|
||||
_history[tick] = {}
|
||||
_history[tick][entity_id] = position
|
||||
if _history.size() > 128:
|
||||
var oldest = _history.keys().min()
|
||||
if oldest != null:
|
||||
|
||||
@@ -117,7 +117,7 @@ func buy_request(weapon_id: String) -> void:
|
||||
## Process a buy request (also callable directly from server-side code).
|
||||
func _process_buy_request(player_id: int, weapon_id: String) -> void:
|
||||
# ── 1. Validate weapon exists ──
|
||||
var weapon_data: WeaponData = WeaponDefinitions.get_weapon(weapon_id)
|
||||
var weapon_data = WeaponDefinitions.get_weapon(weapon_id) # WeaponData — untyped for headless
|
||||
if weapon_data == null:
|
||||
purchase_denied.emit(player_id, weapon_id, "unknown_weapon")
|
||||
_send_buy_denied(player_id, "Unknown weapon: \"%s\"" % weapon_id)
|
||||
@@ -194,7 +194,7 @@ func get_affordable_weapons(player_id: int) -> Array[Dictionary]:
|
||||
for wid in EconomyManager.WEAPON_COSTS.keys():
|
||||
var cost: int = EconomyManager.WEAPON_COSTS[wid]
|
||||
if money >= cost:
|
||||
var data: WeaponData = WeaponDefinitions.get_weapon(wid)
|
||||
var data = WeaponDefinitions.get_weapon(wid) # WeaponData — untyped for headless
|
||||
affordable.append({
|
||||
"weapon_id": wid,
|
||||
"display_name": data.display_name if data else wid,
|
||||
|
||||
@@ -83,8 +83,8 @@ var buy_menu_handler = null
|
||||
## BombObjective — server-authoritative bomb plant/defuse logic.
|
||||
var bomb_objective = null
|
||||
|
||||
## TeamData reference — cached with load() to avoid class_name resolution in headless.
|
||||
var _td = null
|
||||
## TeamData reference — cached reference, populated at runtime if needed.
|
||||
var _team_data_ref = null
|
||||
|
||||
## Current server tick counter, incremented each time tick() is called.
|
||||
var _current_tick: int = 0
|
||||
@@ -130,34 +130,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 = _ws.new()
|
||||
weapon_server = load("res://server/scripts/weapons/weapon_server.gd").new()
|
||||
weapon_server.physics_world = get_viewport().get_world_3d()
|
||||
add_child(weapon_server)
|
||||
|
||||
lag_compensation = _lc.new()
|
||||
lag_compensation = load("res://server/scripts/combat/lag_compensation.gd").new()
|
||||
add_child(lag_compensation)
|
||||
|
||||
damage_processor = _dp.new()
|
||||
damage_processor = load("res://server/scripts/combat/damage_processor.gd").new()
|
||||
add_child(damage_processor)
|
||||
|
||||
# --- Round / Match lifecycle ---
|
||||
round_manager = _rm.new()
|
||||
round_manager = load("res://server/scripts/round/round_manager.gd").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 = _tm.new()
|
||||
team_manager = load("res://scripts/teams/team_manager.gd").new()
|
||||
add_child(team_manager)
|
||||
|
||||
round_manager.team_manager = team_manager
|
||||
round_manager.damage_processor = damage_processor
|
||||
|
||||
# --- Economy system ---
|
||||
economy_manager = _em.new()
|
||||
economy_manager = load("res://server/scripts/economy/economy_manager.gd").new()
|
||||
add_child(economy_manager)
|
||||
|
||||
buy_menu_handler = _bm.new()
|
||||
buy_menu_handler = load("res://server/scripts/economy/buy_menu_handler.gd").new()
|
||||
add_child(buy_menu_handler)
|
||||
buy_menu_handler.initialise(economy_manager, weapon_server, team_manager)
|
||||
|
||||
@@ -189,7 +189,7 @@ func _ready() -> void:
|
||||
Engine.register_singleton("SimulationServer", simulation_server)
|
||||
|
||||
# --- Bomb / Defuse Objective ---
|
||||
bomb_objective = _bo.new()
|
||||
bomb_objective = load("res://server/scripts/objectives/bomb_objective.gd").new()
|
||||
add_child(bomb_objective)
|
||||
bomb_objective.round_manager = round_manager
|
||||
bomb_objective.team_manager = team_manager
|
||||
|
||||
@@ -81,13 +81,13 @@ const DEFAULT_EXPLOSION_DAMAGE: float = 500.0
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Reference to the RoundManager node.
|
||||
var round_manager: RoundManager = null
|
||||
var round_manager = null
|
||||
|
||||
## Reference to the TeamManager for checking player teams.
|
||||
var team_manager: TeamManager = null
|
||||
var team_manager = null
|
||||
|
||||
## Reference to the DamageProcessor for applying explosion damage.
|
||||
var damage_processor: DamageProcessor = null
|
||||
var damage_processor = null # DamageProcessor — untyped for headless compat
|
||||
|
||||
## Mapping: entity_id → peer_id (from GameServer).
|
||||
var entity_to_peer: Dictionary = {}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
## Signals connect round events to economy, HUD, scoreboard, etc.
|
||||
|
||||
extends Node
|
||||
class_name RoundManager
|
||||
const _td_rm = preload("res://scripts/teams/team_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -94,10 +93,10 @@ const DEFAULT_WIN_THRESHOLD: int = 16
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Reference to the TeamManager node/singleton.
|
||||
var team_manager: TeamManager = null
|
||||
var team_manager = null
|
||||
|
||||
## Reference to the DamageProcessor node/singleton.
|
||||
var damage_processor: DamageProcessor = null
|
||||
var damage_processor = null # DamageProcessor — untyped for headless compat
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
|
||||
@@ -31,6 +31,8 @@ var _weapon_config: Dictionary = {}
|
||||
var _history_depth: int = 64
|
||||
var _next_entity_id: int = 1
|
||||
var _entities: Dictionary = {} # entity_id → StubEntity
|
||||
var _running: bool = false
|
||||
var _last_hit_result: Dictionary = {}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SimulationServer API
|
||||
@@ -50,6 +52,28 @@ func set_weapon_config(cfg: Dictionary) -> void:
|
||||
func set_history_depth(depth: int) -> void:
|
||||
_history_depth = depth
|
||||
|
||||
func start() -> void:
|
||||
_running = true
|
||||
|
||||
func stop() -> void:
|
||||
_running = false
|
||||
|
||||
func can_tick(delta: float) -> bool:
|
||||
return _running
|
||||
|
||||
func tick() -> PackedByteArray:
|
||||
# Stub: returns empty snapshot
|
||||
return PackedByteArray()
|
||||
|
||||
func get_stats() -> Dictionary:
|
||||
return {
|
||||
"tick_count": 0,
|
||||
"entity_count": _entities.size(),
|
||||
}
|
||||
|
||||
func get_last_hit_result() -> Dictionary:
|
||||
return _last_hit_result
|
||||
|
||||
func apply_input(entity_id: int, input_dict: Dictionary) -> void:
|
||||
var e: StubEntity = _entities.get(entity_id)
|
||||
if e == null:
|
||||
@@ -75,10 +99,9 @@ func fire_weapon(entity_id: int) -> void:
|
||||
func get_entity(entity_id: int) -> StubEntity:
|
||||
return _entities.get(entity_id)
|
||||
|
||||
func spawn_player_entity(peer_id: int, pos: Vector3) -> int:
|
||||
func spawn_entity(pos: Vector3) -> int:
|
||||
var e = StubEntity.new()
|
||||
e.id = _next_entity_id
|
||||
e.peer_id = peer_id
|
||||
e.position = pos
|
||||
e.alive = true
|
||||
e.health = 100.0
|
||||
@@ -86,12 +109,15 @@ func spawn_player_entity(peer_id: int, pos: Vector3) -> int:
|
||||
_next_entity_id += 1
|
||||
return e.id
|
||||
|
||||
func despawn_player_entity(entity_id: int) -> void:
|
||||
_entities.erase(entity_id)
|
||||
func spawn_player_entity(peer_id: int, pos: Vector3) -> int:
|
||||
var entity_id = spawn_entity(pos)
|
||||
var e: StubEntity = _entities.get(entity_id)
|
||||
if e:
|
||||
e.peer_id = peer_id
|
||||
return entity_id
|
||||
|
||||
func tick(delta: float) -> void:
|
||||
# Stub: in the real C++ SimulationServer this runs the 128Hz simulation
|
||||
pass
|
||||
func despawn_entity(entity_id: int) -> void:
|
||||
_entities.erase(entity_id)
|
||||
|
||||
func get_entity_count() -> int:
|
||||
return _entities.size()
|
||||
|
||||
@@ -57,14 +57,15 @@ var lag_compensation = null
|
||||
|
||||
func _ready() -> void:
|
||||
if physics_world == null:
|
||||
physics_world = get_world_3d()
|
||||
# Use the SceneTree's root world (Node3D.get_world_3d() isn't available on Node)
|
||||
physics_world = get_tree().root.world_3d if get_tree() and get_tree().root else null
|
||||
|
||||
## Public initialiser (call after adding to tree, or inject a World3D).
|
||||
func initialise(world: World3D = null) -> void:
|
||||
if world != null:
|
||||
physics_world = world
|
||||
elif physics_world == null:
|
||||
physics_world = get_world_3d()
|
||||
physics_world = get_tree().root.world_3d if get_tree() and get_tree().root else null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-player state management
|
||||
@@ -84,7 +85,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 := _wd.get_weapon(weapon_id)
|
||||
var data = _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
push_warning("WeaponServer: unknown weapon '%s'" % weapon_id)
|
||||
return
|
||||
@@ -108,7 +109,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 := _wd.get_weapon(weapon_id)
|
||||
var data = _wd.get_weapon(weapon_id)
|
||||
return {
|
||||
ammo = st.get("ammo", 0),
|
||||
reserve = st.get("reserve", 0),
|
||||
@@ -122,7 +123,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 := _wd.get_weapon(weapon_id)
|
||||
var data = _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
return false
|
||||
|
||||
@@ -156,7 +157,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 := _wd.get_weapon(weapon_id)
|
||||
var data = _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
return false
|
||||
|
||||
@@ -199,7 +200,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 := _wd.get_weapon(weapon_id)
|
||||
var data = _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
return
|
||||
|
||||
@@ -230,7 +231,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 := _wd.get_weapon(weapon_id)
|
||||
var data = _wd.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
return _miss_result(weapon_id)
|
||||
|
||||
@@ -262,7 +263,7 @@ func _perform_hitscan(
|
||||
tick: int,
|
||||
player_id: int,
|
||||
weapon_id: String,
|
||||
data: WeaponData,
|
||||
data, # WeaponData — untyped for headless compat (Resource class_name order)
|
||||
origin: Vector3,
|
||||
base_direction: Vector3,
|
||||
st: Dictionary
|
||||
@@ -346,7 +347,7 @@ func _miss_result(weapon_id: String) -> Dictionary:
|
||||
func _hard_can_fire(
|
||||
player_id: int,
|
||||
weapon_id: String,
|
||||
data: WeaponData,
|
||||
data, # WeaponData — untyped for headless compat (Resource class_name order)
|
||||
st: Dictionary
|
||||
) -> bool:
|
||||
if st.is_empty():
|
||||
@@ -366,11 +367,6 @@ func _hard_can_fire(
|
||||
func _get_space_state() -> PhysicsDirectSpaceState3D:
|
||||
if physics_world != null:
|
||||
return physics_world.direct_space_state
|
||||
# Fallback: try from the scene tree
|
||||
var w: World3D = get_world_3d()
|
||||
if w != null:
|
||||
physics_world = w
|
||||
return w.direct_space_state
|
||||
return null
|
||||
|
||||
func _get_shooter_exclusions(player_id: int) -> Array[RID]:
|
||||
|
||||
Reference in New Issue
Block a user