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()`
|
- ✅ Duck-typed netfox checks via `has_method()`, `Engine.has_singleton()`
|
||||||
|
|
||||||
**REMAINING:**
|
**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:**
|
**REMAINING:**
|
||||||
- Creates full Player nodes with RollbackSynchronizer for each remote player (✅ correct pattern)
|
- 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 | ✅ |
|
| Input client→server | RollbackSynchronizer.input_properties | ✅ |
|
||||||
| Tick-driven movement | `_rollback_tick(delta, tick, is_input)` | ✅ |
|
| Tick-driven movement | `_rollback_tick(delta, tick, is_input)` | ✅ |
|
||||||
| Client prediction | `enable_prediction = true` | ✅ |
|
| Client prediction | `enable_prediction = true` | ✅ |
|
||||||
| Smooth interpolation | TickInterpolator | ❌ P7.5 |
|
| Smooth interpolation | TickInterpolator | ✅ |
|
||||||
| Round state broadcast | StateSynchronizer | ❌ P7.7 |
|
| Round state broadcast | StateSynchronizer | ❌ P7.7 |
|
||||||
| Rollback-safe weapon fire | RewindableAction | ❌ P7.6 |
|
| Rollback-safe weapon fire | RewindableAction | ❌ P7.6 |
|
||||||
| Lag comp / hitscan | RewindableAction + mutation | ❌ P7.6 |
|
| Lag comp / hitscan | RewindableAction + mutation | ❌ P7.6 |
|
||||||
| Visual rollback | TickInterpolator + _rollback_tick | ❌ P7.5 |
|
| Visual rollback | TickInterpolator + _rollback_tick | ✅ |
|
||||||
| Lifecycle signals | NetworkEvents | ✅ |
|
| 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.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.7 (Round state netfox) ── needs ── NetworkEvents working (✅ DONE)
|
||||||
P7.8 (FPSController refactor) ── needs ── P7.6 input flow clarified
|
P7.8 (FPSController refactor) ── needs ── P7.6 input flow clarified
|
||||||
P7.9 (LAN test + deploy) ── needs ── all of the above
|
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 |
|
| File | Lines | Netfox Status | RPCs | Signals | Changes Needed |
|
||||||
|------|-------|---------------|------|---------|----------------|
|
|------|-------|---------------|------|---------|----------------|
|
||||||
| network_manager.gd | 245 | ✅ overlay wired | 6 ❌ | 9 ✅ | Remove 6 RPCs post-P7.7 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| client_main.gd | 124 | ✅ thin layer | 0 | 2 signal connects | None atm |
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ dedicated_server=true
|
|||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
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"
|
export_path="build/tactical-shooter-server.x86_64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
## print(data.display_name) # "Assault Rifle"
|
## print(data.display_name) # "Assault Rifle"
|
||||||
##
|
##
|
||||||
extends Resource
|
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
|
# Properties
|
||||||
@@ -46,31 +48,34 @@ class_name WeaponData
|
|||||||
@export var pellets_per_shot: int = 1
|
@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.
|
## 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",
|
"rifle", "Assault Rifle",
|
||||||
30.0, 10.0, 30, 2.1, true, 0.5, 200.0, 1
|
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.
|
## 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",
|
"pistol", "Pistol",
|
||||||
25.0, 4.0, 12, 1.5, false, 0.3, 80.0, 1
|
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.
|
## 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",
|
"shotgun", "Shotgun",
|
||||||
8.0, 1.0, 8, 3.0, false, 3.0, 30.0, 8
|
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.
|
## 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",
|
"smg", "Submachine Gun",
|
||||||
18.0, 12.0, 25, 1.8, true, 1.5, 100.0, 1
|
18.0, 12.0, 25, 1.8, true, 1.5, 100.0, 1
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -80,26 +85,26 @@ const SMG: Resource = pref(
|
|||||||
## Internal helper that constructs a WeaponData instance from raw values.
|
## Internal helper that constructs a WeaponData instance from raw values.
|
||||||
## This avoids repeating the constructor boilerplate for every constant.
|
## This avoids repeating the constructor boilerplate for every constant.
|
||||||
static func pref(
|
static func pref(
|
||||||
_weapon_id: String,
|
_weapon_id: String,
|
||||||
_display_name: String,
|
_display_name: String,
|
||||||
_damage: float,
|
_damage: float,
|
||||||
_fire_rate: float,
|
_fire_rate: float,
|
||||||
_mag_size: int,
|
_mag_size: int,
|
||||||
_reload_time: float,
|
_reload_time: float,
|
||||||
_is_automatic: bool,
|
_is_automatic: bool,
|
||||||
_spread_degrees: float,
|
_spread_degrees: float,
|
||||||
_range: float,
|
_range: float,
|
||||||
_pellets: int = 1
|
_pellets: int = 1
|
||||||
) -> WeaponData:
|
): # -> WeaponData (untyped for headless compat)
|
||||||
var w := WeaponData.new()
|
var w = WeaponData.new()
|
||||||
w.weapon_id = _weapon_id
|
w.weapon_id = _weapon_id
|
||||||
w.display_name = _display_name
|
w.display_name = _display_name
|
||||||
w.damage = _damage
|
w.damage = _damage
|
||||||
w.fire_rate = _fire_rate
|
w.fire_rate = _fire_rate
|
||||||
w.mag_size = _mag_size
|
w.mag_size = _mag_size
|
||||||
w.reload_time = _reload_time
|
w.reload_time = _reload_time
|
||||||
w.is_automatic = _is_automatic
|
w.is_automatic = _is_automatic
|
||||||
w.spread_degrees = _spread_degrees
|
w.spread_degrees = _spread_degrees
|
||||||
w.range = _range
|
w.range = _range
|
||||||
w.pellets_per_shot = _pellets
|
w.pellets_per_shot = _pellets
|
||||||
return w
|
return w
|
||||||
|
|||||||
@@ -12,23 +12,38 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
class_name WeaponDefinitions
|
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
|
## All weapons indexed by weapon_id — the single source of truth for the
|
||||||
## game's starter arsenal. Add new weapons here and in WeaponData.
|
## game's starter arsenal. Populated on first access via _ensure_init().
|
||||||
const WEAPONS: Dictionary = {
|
static var WEAPONS: Dictionary = {}
|
||||||
"rifle": WeaponData.RIFLE,
|
|
||||||
"pistol": WeaponData.PISTOL,
|
static func _ensure_init() -> void:
|
||||||
"shotgun": WeaponData.SHOTGUN,
|
if _initialized:
|
||||||
"smg": WeaponData.SMG,
|
return
|
||||||
}
|
_initialized = true
|
||||||
|
WEAPONS = {
|
||||||
|
"rifle": _WD.RIFLE,
|
||||||
|
"pistol": _WD.PISTOL,
|
||||||
|
"shotgun": _WD.SHOTGUN,
|
||||||
|
"smg": _WD.SMG,
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Lookup
|
# Lookup
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
## Return the WeaponData for the given weapon_id, or null if unknown.
|
## Return the WeaponData for the given weapon_id, or null if unknown.
|
||||||
static func get_weapon(id: String) -> WeaponData:
|
static func get_weapon(id: String):
|
||||||
return WEAPONS.get(id, null) as WeaponData
|
_ensure_init()
|
||||||
|
return WEAPONS.get(id, null)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ signal player_despawned(peer_id: int)
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Exports
|
# Exports
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@export var player_scene: PackedScene = preload("res://scenes/player.tscn")
|
@export var player_scene: PackedScene = preload("res://scenes/server/server_player.tscn")
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# State
|
# State
|
||||||
@@ -58,13 +58,16 @@ func _ready() -> void:
|
|||||||
# Use load() instead of preload() so GameServer's dev dependencies
|
# Use load() instead of preload() so GameServer's dev dependencies
|
||||||
# (weapons, economy, objectives, teams) don't block compilation.
|
# (weapons, economy, objectives, teams) don't block compilation.
|
||||||
var GameServerClass = load("res://server/scripts/game_server.gd")
|
var GameServerClass = load("res://server/scripts/game_server.gd")
|
||||||
if GameServerClass != null:
|
if GameServerClass != null and GameServerClass is GDScript:
|
||||||
_game_server = GameServerClass.new()
|
_game_server = GameServerClass.new()
|
||||||
add_child(_game_server)
|
if _game_server != null:
|
||||||
# GameServer registers SimulationServer as a singleton on _ready
|
add_child(_game_server)
|
||||||
_game_server.start_simulation()
|
# GameServer registers SimulationServer as a singleton on _ready
|
||||||
|
_game_server.start_simulation()
|
||||||
|
else:
|
||||||
|
push_warning("[ServerMain] GameServer instantiation failed")
|
||||||
else:
|
else:
|
||||||
push_warning("[ServerMain] GameServer not available — running without simulation server")
|
push_warning("[ServerMain] GameServer class not available — running without simulation server")
|
||||||
|
|
||||||
# Check if ServerConfig has finished loading.
|
# Check if ServerConfig has finished loading.
|
||||||
|
|
||||||
@@ -94,8 +97,6 @@ func _ready() -> void:
|
|||||||
## Check if ServerConfig has finished loading.
|
## Check if ServerConfig has finished loading.
|
||||||
func _server_config_loaded() -> bool:
|
func _server_config_loaded() -> bool:
|
||||||
return ServerConfig and ServerConfig.has_method(&"get_config_path") and not ServerConfig.get_config_path().is_empty()
|
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:
|
func _exit_tree() -> void:
|
||||||
NetworkManager.stop()
|
NetworkManager.stop()
|
||||||
|
|||||||
@@ -7,9 +7,17 @@ extends Node
|
|||||||
var physics_world = null
|
var physics_world = null
|
||||||
var _history: Dictionary = {} # tick → { entity_id: position }
|
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:
|
func record_position(tick: int, entity_id: int, position: Vector3) -> void:
|
||||||
_history[tick] = {entity_id: position}
|
if not _history.has(tick):
|
||||||
# Keep only 128 most recent ticks
|
_history[tick] = {}
|
||||||
|
_history[tick][entity_id] = position
|
||||||
if _history.size() > 128:
|
if _history.size() > 128:
|
||||||
var oldest = _history.keys().min()
|
var oldest = _history.keys().min()
|
||||||
if oldest != null:
|
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).
|
## Process a buy request (also callable directly from server-side code).
|
||||||
func _process_buy_request(player_id: int, weapon_id: String) -> void:
|
func _process_buy_request(player_id: int, weapon_id: String) -> void:
|
||||||
# ── 1. Validate weapon exists ──
|
# ── 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:
|
if weapon_data == null:
|
||||||
purchase_denied.emit(player_id, weapon_id, "unknown_weapon")
|
purchase_denied.emit(player_id, weapon_id, "unknown_weapon")
|
||||||
_send_buy_denied(player_id, "Unknown weapon: \"%s\"" % weapon_id)
|
_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():
|
for wid in EconomyManager.WEAPON_COSTS.keys():
|
||||||
var cost: int = EconomyManager.WEAPON_COSTS[wid]
|
var cost: int = EconomyManager.WEAPON_COSTS[wid]
|
||||||
if money >= cost:
|
if money >= cost:
|
||||||
var data: WeaponData = WeaponDefinitions.get_weapon(wid)
|
var data = WeaponDefinitions.get_weapon(wid) # WeaponData — untyped for headless
|
||||||
affordable.append({
|
affordable.append({
|
||||||
"weapon_id": wid,
|
"weapon_id": wid,
|
||||||
"display_name": data.display_name if data else 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.
|
## BombObjective — server-authoritative bomb plant/defuse logic.
|
||||||
var bomb_objective = null
|
var bomb_objective = null
|
||||||
|
|
||||||
## TeamData reference — cached with load() to avoid class_name resolution in headless.
|
## TeamData reference — cached reference, populated at runtime if needed.
|
||||||
var _td = null
|
var _team_data_ref = null
|
||||||
|
|
||||||
## Current server tick counter, incremented each time tick() is called.
|
## Current server tick counter, incremented each time tick() is called.
|
||||||
var _current_tick: int = 0
|
var _current_tick: int = 0
|
||||||
@@ -130,34 +130,34 @@ func _ready() -> void:
|
|||||||
# Create and wire the WeaponServer with LagCompensation and DamageProcessor.
|
# Create and wire the WeaponServer with LagCompensation and DamageProcessor.
|
||||||
# These work alongside the C++ SimulationServer for the GDScript
|
# These work alongside the C++ SimulationServer for the GDScript
|
||||||
# weapon path.
|
# 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()
|
weapon_server.physics_world = get_viewport().get_world_3d()
|
||||||
add_child(weapon_server)
|
add_child(weapon_server)
|
||||||
|
|
||||||
lag_compensation = _lc.new()
|
lag_compensation = load("res://server/scripts/combat/lag_compensation.gd").new()
|
||||||
add_child(lag_compensation)
|
add_child(lag_compensation)
|
||||||
|
|
||||||
damage_processor = _dp.new()
|
damage_processor = load("res://server/scripts/combat/damage_processor.gd").new()
|
||||||
add_child(damage_processor)
|
add_child(damage_processor)
|
||||||
|
|
||||||
# --- Round / Match lifecycle ---
|
# --- Round / Match lifecycle ---
|
||||||
round_manager = _rm.new()
|
round_manager = load("res://server/scripts/round/round_manager.gd").new()
|
||||||
add_child(round_manager)
|
add_child(round_manager)
|
||||||
|
|
||||||
# Wire up TeamManager reference (find it in the tree or create it)
|
# Wire up TeamManager reference (find it in the tree or create it)
|
||||||
team_manager = get_node_or_null("/root/TeamManager")
|
team_manager = get_node_or_null("/root/TeamManager")
|
||||||
if not team_manager:
|
if not team_manager:
|
||||||
team_manager = _tm.new()
|
team_manager = load("res://scripts/teams/team_manager.gd").new()
|
||||||
add_child(team_manager)
|
add_child(team_manager)
|
||||||
|
|
||||||
round_manager.team_manager = team_manager
|
round_manager.team_manager = team_manager
|
||||||
round_manager.damage_processor = damage_processor
|
round_manager.damage_processor = damage_processor
|
||||||
|
|
||||||
# --- Economy system ---
|
# --- Economy system ---
|
||||||
economy_manager = _em.new()
|
economy_manager = load("res://server/scripts/economy/economy_manager.gd").new()
|
||||||
add_child(economy_manager)
|
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)
|
add_child(buy_menu_handler)
|
||||||
buy_menu_handler.initialise(economy_manager, weapon_server, team_manager)
|
buy_menu_handler.initialise(economy_manager, weapon_server, team_manager)
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ func _ready() -> void:
|
|||||||
Engine.register_singleton("SimulationServer", simulation_server)
|
Engine.register_singleton("SimulationServer", simulation_server)
|
||||||
|
|
||||||
# --- Bomb / Defuse Objective ---
|
# --- Bomb / Defuse Objective ---
|
||||||
bomb_objective = _bo.new()
|
bomb_objective = load("res://server/scripts/objectives/bomb_objective.gd").new()
|
||||||
add_child(bomb_objective)
|
add_child(bomb_objective)
|
||||||
bomb_objective.round_manager = round_manager
|
bomb_objective.round_manager = round_manager
|
||||||
bomb_objective.team_manager = team_manager
|
bomb_objective.team_manager = team_manager
|
||||||
|
|||||||
@@ -81,13 +81,13 @@ const DEFAULT_EXPLOSION_DAMAGE: float = 500.0
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
## Reference to the RoundManager node.
|
## Reference to the RoundManager node.
|
||||||
var round_manager: RoundManager = null
|
var round_manager = null
|
||||||
|
|
||||||
## Reference to the TeamManager for checking player teams.
|
## 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.
|
## 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).
|
## Mapping: entity_id → peer_id (from GameServer).
|
||||||
var entity_to_peer: Dictionary = {}
|
var entity_to_peer: Dictionary = {}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
## Signals connect round events to economy, HUD, scoreboard, etc.
|
## Signals connect round events to economy, HUD, scoreboard, etc.
|
||||||
|
|
||||||
extends Node
|
extends Node
|
||||||
class_name RoundManager
|
|
||||||
const _td_rm = preload("res://scripts/teams/team_data.gd")
|
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.
|
## Reference to the TeamManager node/singleton.
|
||||||
var team_manager: TeamManager = null
|
var team_manager = null
|
||||||
|
|
||||||
## Reference to the DamageProcessor node/singleton.
|
## Reference to the DamageProcessor node/singleton.
|
||||||
var damage_processor: DamageProcessor = null
|
var damage_processor = null # DamageProcessor — untyped for headless compat
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ var _weapon_config: Dictionary = {}
|
|||||||
var _history_depth: int = 64
|
var _history_depth: int = 64
|
||||||
var _next_entity_id: int = 1
|
var _next_entity_id: int = 1
|
||||||
var _entities: Dictionary = {} # entity_id → StubEntity
|
var _entities: Dictionary = {} # entity_id → StubEntity
|
||||||
|
var _running: bool = false
|
||||||
|
var _last_hit_result: Dictionary = {}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# SimulationServer API
|
# SimulationServer API
|
||||||
@@ -50,6 +52,28 @@ func set_weapon_config(cfg: Dictionary) -> void:
|
|||||||
func set_history_depth(depth: int) -> void:
|
func set_history_depth(depth: int) -> void:
|
||||||
_history_depth = depth
|
_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:
|
func apply_input(entity_id: int, input_dict: Dictionary) -> void:
|
||||||
var e: StubEntity = _entities.get(entity_id)
|
var e: StubEntity = _entities.get(entity_id)
|
||||||
if e == null:
|
if e == null:
|
||||||
@@ -75,10 +99,9 @@ func fire_weapon(entity_id: int) -> void:
|
|||||||
func get_entity(entity_id: int) -> StubEntity:
|
func get_entity(entity_id: int) -> StubEntity:
|
||||||
return _entities.get(entity_id)
|
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()
|
var e = StubEntity.new()
|
||||||
e.id = _next_entity_id
|
e.id = _next_entity_id
|
||||||
e.peer_id = peer_id
|
|
||||||
e.position = pos
|
e.position = pos
|
||||||
e.alive = true
|
e.alive = true
|
||||||
e.health = 100.0
|
e.health = 100.0
|
||||||
@@ -86,12 +109,15 @@ func spawn_player_entity(peer_id: int, pos: Vector3) -> int:
|
|||||||
_next_entity_id += 1
|
_next_entity_id += 1
|
||||||
return e.id
|
return e.id
|
||||||
|
|
||||||
func despawn_player_entity(entity_id: int) -> void:
|
func spawn_player_entity(peer_id: int, pos: Vector3) -> int:
|
||||||
_entities.erase(entity_id)
|
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:
|
func despawn_entity(entity_id: int) -> void:
|
||||||
# Stub: in the real C++ SimulationServer this runs the 128Hz simulation
|
_entities.erase(entity_id)
|
||||||
pass
|
|
||||||
|
|
||||||
func get_entity_count() -> int:
|
func get_entity_count() -> int:
|
||||||
return _entities.size()
|
return _entities.size()
|
||||||
|
|||||||
@@ -56,15 +56,16 @@ var lag_compensation = null
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if physics_world == null:
|
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).
|
## Public initialiser (call after adding to tree, or inject a World3D).
|
||||||
func initialise(world: World3D = null) -> void:
|
func initialise(world: World3D = null) -> void:
|
||||||
if world != null:
|
if world != null:
|
||||||
physics_world = world
|
physics_world = world
|
||||||
elif physics_world == null:
|
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
|
# Per-player state management
|
||||||
@@ -73,47 +74,47 @@ func initialise(world: World3D = null) -> void:
|
|||||||
## Ensure a player has an entry in the state dictionary.
|
## Ensure a player has an entry in the state dictionary.
|
||||||
## Called automatically by can_fire / fire.
|
## Called automatically by can_fire / fire.
|
||||||
func register_player(player_id: int) -> void:
|
func register_player(player_id: int) -> void:
|
||||||
if not _state.has(player_id):
|
if not _state.has(player_id):
|
||||||
_state[player_id] = {}
|
_state[player_id] = {}
|
||||||
|
|
||||||
## Remove a player's state (on disconnect / respawn).
|
## Remove a player's state (on disconnect / respawn).
|
||||||
func unregister_player(player_id: int) -> void:
|
func unregister_player(player_id: int) -> void:
|
||||||
_state.erase(player_id)
|
_state.erase(player_id)
|
||||||
|
|
||||||
## Give a player a weapon — allocates initial ammo for it.
|
## Give a player a weapon — allocates initial ammo for it.
|
||||||
## Also calls register_player implicitly.
|
## Also calls register_player implicitly.
|
||||||
func give_weapon(player_id: int, weapon_id: String) -> void:
|
func give_weapon(player_id: int, weapon_id: String) -> void:
|
||||||
register_player(player_id)
|
register_player(player_id)
|
||||||
var data := _wd.get_weapon(weapon_id)
|
var data = _wd.get_weapon(weapon_id)
|
||||||
if data == null:
|
if data == null:
|
||||||
push_warning("WeaponServer: unknown weapon '%s'" % weapon_id)
|
push_warning("WeaponServer: unknown weapon '%s'" % weapon_id)
|
||||||
return
|
return
|
||||||
var ws: Dictionary = _state[player_id]
|
var ws: Dictionary = _state[player_id]
|
||||||
ws[weapon_id] = {
|
ws[weapon_id] = {
|
||||||
ammo = data.mag_size,
|
ammo = data.mag_size,
|
||||||
reserve = data.mag_size * 3, # 3 spare magazines
|
reserve = data.mag_size * 3, # 3 spare magazines
|
||||||
is_reloading = false,
|
is_reloading = false,
|
||||||
reload_remaining = 0.0,
|
reload_remaining = 0.0,
|
||||||
last_fire_time = -INF,
|
last_fire_time = -INF,
|
||||||
}
|
}
|
||||||
|
|
||||||
## Return the per-weapon state dictionary for a player+weapon, or null.
|
## Return the per-weapon state dictionary for a player+weapon, or null.
|
||||||
func get_weapon_state(player_id: int, weapon_id: String) -> Dictionary:
|
func get_weapon_state(player_id: int, weapon_id: String) -> Dictionary:
|
||||||
var ps: Dictionary = _state.get(player_id, {})
|
var ps: Dictionary = _state.get(player_id, {})
|
||||||
return ps.get(weapon_id, {}) as Dictionary
|
return ps.get(weapon_id, {}) as Dictionary
|
||||||
|
|
||||||
## Return the current ammo and reserve for a player's weapon.
|
## Return the current ammo and reserve for a player's weapon.
|
||||||
## Returns {ammo: int, reserve: int, max_ammo: int} or null.
|
## Returns {ammo: int, reserve: int, max_ammo: int} or null.
|
||||||
func get_ammo_info(player_id: int, weapon_id: String) -> Dictionary:
|
func get_ammo_info(player_id: int, weapon_id: String) -> Dictionary:
|
||||||
var st := get_weapon_state(player_id, weapon_id)
|
var st := get_weapon_state(player_id, weapon_id)
|
||||||
if st.is_empty():
|
if st.is_empty():
|
||||||
return {}
|
return {}
|
||||||
var data := _wd.get_weapon(weapon_id)
|
var data = _wd.get_weapon(weapon_id)
|
||||||
return {
|
return {
|
||||||
ammo = st.get("ammo", 0),
|
ammo = st.get("ammo", 0),
|
||||||
reserve = st.get("reserve", 0),
|
reserve = st.get("reserve", 0),
|
||||||
max_ammo = data.mag_size if data else 0,
|
max_ammo = data.mag_size if data else 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Can-fire check
|
# Can-fire check
|
||||||
@@ -122,33 +123,33 @@ func get_ammo_info(player_id: int, weapon_id: String) -> Dictionary:
|
|||||||
## Returns true if the player can fire the specified weapon right now.
|
## Returns true if the player can fire the specified weapon right now.
|
||||||
## Checks: weapon known, ammo available, not reloading, cooldown elapsed.
|
## Checks: weapon known, ammo available, not reloading, cooldown elapsed.
|
||||||
func can_fire(player_id: int, weapon_id: String) -> bool:
|
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:
|
if data == null:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
var st := get_weapon_state(player_id, weapon_id)
|
var st := get_weapon_state(player_id, weapon_id)
|
||||||
if st.is_empty():
|
if st.is_empty():
|
||||||
# Player doesn't have this weapon yet — give it to them
|
# Player doesn't have this weapon yet — give it to them
|
||||||
give_weapon(player_id, weapon_id)
|
give_weapon(player_id, weapon_id)
|
||||||
st = get_weapon_state(player_id, weapon_id)
|
st = get_weapon_state(player_id, weapon_id)
|
||||||
|
|
||||||
# Check reload
|
# Check reload
|
||||||
if st.get("is_reloading", false):
|
if st.get("is_reloading", false):
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Check ammo
|
# Check ammo
|
||||||
if st.get("ammo", 0) <= 0:
|
if st.get("ammo", 0) <= 0:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Check fire rate cooldown
|
# Check fire rate cooldown
|
||||||
var fire_interval: float = 1.0 / max(data.fire_rate, 0.001)
|
var fire_interval: float = 1.0 / max(data.fire_rate, 0.001)
|
||||||
var last_fire: float = st.get("last_fire_time", -INF)
|
var last_fire: float = st.get("last_fire_time", -INF)
|
||||||
var now: float = Time.get_unix_time_from_system()
|
var now: float = Time.get_unix_time_from_system()
|
||||||
|
|
||||||
if now - last_fire < fire_interval:
|
if now - last_fire < fire_interval:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Reload
|
# Reload
|
||||||
@@ -156,60 +157,60 @@ func can_fire(player_id: int, weapon_id: String) -> bool:
|
|||||||
|
|
||||||
## Start reloading the player's weapon. Returns true if reload initiated.
|
## Start reloading the player's weapon. Returns true if reload initiated.
|
||||||
func start_reload(player_id: int, weapon_id: String) -> bool:
|
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:
|
if data == null:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
var st := get_weapon_state(player_id, weapon_id)
|
var st := get_weapon_state(player_id, weapon_id)
|
||||||
if st.is_empty():
|
if st.is_empty():
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Already reloading
|
# Already reloading
|
||||||
if st.get("is_reloading", false):
|
if st.get("is_reloading", false):
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Magazine already full
|
# Magazine already full
|
||||||
if st.get("ammo", 0) >= data.mag_size:
|
if st.get("ammo", 0) >= data.mag_size:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# No reserve ammo
|
# No reserve ammo
|
||||||
if st.get("reserve", 0) <= 0:
|
if st.get("reserve", 0) <= 0:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
st["is_reloading"] = true
|
st["is_reloading"] = true
|
||||||
st["reload_remaining"] = data.reload_time
|
st["reload_remaining"] = data.reload_time
|
||||||
return true
|
return true
|
||||||
|
|
||||||
## Process reload timers. Called each server tick with delta.
|
## Process reload timers. Called each server tick with delta.
|
||||||
## Returns true if any reload completed this tick.
|
## Returns true if any reload completed this tick.
|
||||||
func process_reloads(player_id: int, delta: float) -> bool:
|
func process_reloads(player_id: int, delta: float) -> bool:
|
||||||
var any_completed: bool = false
|
var any_completed: bool = false
|
||||||
var ps: Dictionary = _state.get(player_id, {})
|
var ps: Dictionary = _state.get(player_id, {})
|
||||||
for weapon_id in ps.keys():
|
for weapon_id in ps.keys():
|
||||||
var st: Dictionary = ps[weapon_id]
|
var st: Dictionary = ps[weapon_id]
|
||||||
if not st.get("is_reloading", false):
|
if not st.get("is_reloading", false):
|
||||||
continue
|
continue
|
||||||
var remaining: float = st.get("reload_remaining", 0.0) - delta
|
var remaining: float = st.get("reload_remaining", 0.0) - delta
|
||||||
if remaining <= 0.0:
|
if remaining <= 0.0:
|
||||||
# Reload complete
|
# Reload complete
|
||||||
_complete_reload(player_id, weapon_id, st)
|
_complete_reload(player_id, weapon_id, st)
|
||||||
any_completed = true
|
any_completed = true
|
||||||
else:
|
else:
|
||||||
st["reload_remaining"] = remaining
|
st["reload_remaining"] = remaining
|
||||||
return any_completed
|
return any_completed
|
||||||
|
|
||||||
func _complete_reload(player_id: int, weapon_id: String, st: Dictionary) -> void:
|
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:
|
if data == null:
|
||||||
return
|
return
|
||||||
|
|
||||||
var needed: int = data.mag_size - st.get("ammo", 0)
|
var needed: int = data.mag_size - st.get("ammo", 0)
|
||||||
var from_reserve: int = mini(needed, st.get("reserve", 0))
|
var from_reserve: int = mini(needed, st.get("reserve", 0))
|
||||||
|
|
||||||
st["ammo"] = st.get("ammo", 0) + from_reserve
|
st["ammo"] = st.get("ammo", 0) + from_reserve
|
||||||
st["reserve"] = st.get("reserve", 0) - from_reserve
|
st["reserve"] = st.get("reserve", 0) - from_reserve
|
||||||
st["is_reloading"] = false
|
st["is_reloading"] = false
|
||||||
st["reload_remaining"] = 0.0
|
st["reload_remaining"] = 0.0
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Fire
|
# Fire
|
||||||
@@ -230,113 +231,113 @@ 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:
|
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:
|
if data == null:
|
||||||
return _miss_result(weapon_id)
|
return _miss_result(weapon_id)
|
||||||
|
|
||||||
var st := get_weapon_state(player_id, weapon_id)
|
var st := get_weapon_state(player_id, weapon_id)
|
||||||
if st.is_empty():
|
if st.is_empty():
|
||||||
return _miss_result(weapon_id)
|
return _miss_result(weapon_id)
|
||||||
|
|
||||||
# Authoritative can-fire check (belt and suspenders)
|
# Authoritative can-fire check (belt and suspenders)
|
||||||
if not _hard_can_fire(player_id, weapon_id, data, st):
|
if not _hard_can_fire(player_id, weapon_id, data, st):
|
||||||
return _miss_result(weapon_id)
|
return _miss_result(weapon_id)
|
||||||
|
|
||||||
# Deduct ammo
|
# Deduct ammo
|
||||||
st["ammo"] = st.get("ammo", 1) - 1
|
st["ammo"] = st.get("ammo", 1) - 1
|
||||||
st["last_fire_time"] = Time.get_unix_time_from_system()
|
st["last_fire_time"] = Time.get_unix_time_from_system()
|
||||||
|
|
||||||
# Auto-reload if magazine empty
|
# Auto-reload if magazine empty
|
||||||
if st["ammo"] <= 0 and st.get("reserve", 0) > 0:
|
if st["ammo"] <= 0 and st.get("reserve", 0) > 0:
|
||||||
st["is_reloading"] = true
|
st["is_reloading"] = true
|
||||||
st["reload_remaining"] = data.reload_time
|
st["reload_remaining"] = data.reload_time
|
||||||
|
|
||||||
# Perform hit-scan — supports multi-pellet weapons
|
# Perform hit-scan — supports multi-pellet weapons
|
||||||
return _perform_hitscan(tick, player_id, weapon_id, data, origin, direction, st)
|
return _perform_hitscan(tick, player_id, weapon_id, data, origin, direction, st)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Internal: hitscan
|
# Internal: hitscan
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _perform_hitscan(
|
func _perform_hitscan(
|
||||||
tick: int,
|
tick: int,
|
||||||
player_id: int,
|
player_id: int,
|
||||||
weapon_id: String,
|
weapon_id: String,
|
||||||
data: WeaponData,
|
data, # WeaponData — untyped for headless compat (Resource class_name order)
|
||||||
origin: Vector3,
|
origin: Vector3,
|
||||||
base_direction: Vector3,
|
base_direction: Vector3,
|
||||||
st: Dictionary
|
st: Dictionary
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
var space_state: PhysicsDirectSpaceState3D = _get_space_state()
|
var space_state: PhysicsDirectSpaceState3D = _get_space_state()
|
||||||
if space_state == null:
|
if space_state == null:
|
||||||
return _miss_result(weapon_id)
|
return _miss_result(weapon_id)
|
||||||
|
|
||||||
var total_damage: float = 0.0
|
var total_damage: float = 0.0
|
||||||
var latest_hit_pos: Vector3 = Vector3.ZERO
|
var latest_hit_pos: Vector3 = Vector3.ZERO
|
||||||
var latest_target_id: int = -1
|
var latest_target_id: int = -1
|
||||||
var any_hit: bool = false
|
var any_hit: bool = false
|
||||||
|
|
||||||
var pellets: int = max(1, data.pellets_per_shot)
|
var pellets: int = max(1, data.pellets_per_shot)
|
||||||
var max_range: float = max(1.0, data.range)
|
var max_range: float = max(1.0, data.range)
|
||||||
var spread_rad: float = deg_to_rad(data.spread_degrees)
|
var spread_rad: float = deg_to_rad(data.spread_degrees)
|
||||||
|
|
||||||
# Build a collision exception list so we don't hit the shooter
|
# Build a collision exception list so we don't hit the shooter
|
||||||
var exclude: Array[RID] = _get_shooter_exclusions(player_id)
|
var exclude: Array[RID] = _get_shooter_exclusions(player_id)
|
||||||
|
|
||||||
for i in range(pellets):
|
for i in range(pellets):
|
||||||
var dir: Vector3 = _apply_spread(base_direction, spread_rad)
|
var dir: Vector3 = _apply_spread(base_direction, spread_rad)
|
||||||
|
|
||||||
# --- Lag-compensated raycast ---
|
# --- Lag-compensated raycast ---
|
||||||
# If lag_compensation is available and a valid tick is provided,
|
# If lag_compensation is available and a valid tick is provided,
|
||||||
# rewind to the target tick before raycasting. Otherwise fall
|
# rewind to the target tick before raycasting. Otherwise fall
|
||||||
# back to the current-frame physics world.
|
# back to the current-frame physics world.
|
||||||
var result: Dictionary = {}
|
var result: Dictionary = {}
|
||||||
if lag_compensation != null and tick >= 0:
|
if lag_compensation != null and tick >= 0:
|
||||||
result = lag_compensation.rewind_and_raycast(
|
result = lag_compensation.rewind_and_raycast(
|
||||||
tick, origin, dir, max_range, exclude
|
tick, origin, dir, max_range, exclude
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Fallback: no lag compensation — raycast against current positions
|
# Fallback: no lag compensation — raycast against current positions
|
||||||
var query := PhysicsRayQueryParameters3D.create(
|
var query := PhysicsRayQueryParameters3D.create(
|
||||||
origin, origin + dir * max_range
|
origin, origin + dir * max_range
|
||||||
)
|
)
|
||||||
query.exclude = exclude
|
query.exclude = exclude
|
||||||
query.collide_with_bodies = true
|
query.collide_with_bodies = true
|
||||||
query.collide_with_areas = false
|
query.collide_with_areas = false
|
||||||
result = space_state.intersect_ray(query)
|
result = space_state.intersect_ray(query)
|
||||||
|
|
||||||
if result.is_empty():
|
if result.is_empty():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
any_hit = true
|
any_hit = true
|
||||||
latest_hit_pos = result.get("position", Vector3.ZERO)
|
latest_hit_pos = result.get("position", Vector3.ZERO)
|
||||||
var collider: Object = result.get("collider", null)
|
var collider: Object = result.get("collider", null)
|
||||||
if collider and collider.has_method(&"get_entity_id"):
|
if collider and collider.has_method(&"get_entity_id"):
|
||||||
latest_target_id = collider.get_entity_id()
|
latest_target_id = collider.get_entity_id()
|
||||||
elif collider:
|
elif collider:
|
||||||
latest_target_id = collider.get_instance_id()
|
latest_target_id = collider.get_instance_id()
|
||||||
|
|
||||||
total_damage += data.damage
|
total_damage += data.damage
|
||||||
|
|
||||||
if not any_hit:
|
if not any_hit:
|
||||||
return _miss_result(weapon_id)
|
return _miss_result(weapon_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hit = true,
|
hit = true,
|
||||||
position = latest_hit_pos,
|
position = latest_hit_pos,
|
||||||
target_id = latest_target_id,
|
target_id = latest_target_id,
|
||||||
damage = total_damage,
|
damage = total_damage,
|
||||||
weapon_id = weapon_id,
|
weapon_id = weapon_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
func _miss_result(weapon_id: String) -> Dictionary:
|
func _miss_result(weapon_id: String) -> Dictionary:
|
||||||
return {
|
return {
|
||||||
hit = false,
|
hit = false,
|
||||||
position = Vector3.ZERO,
|
position = Vector3.ZERO,
|
||||||
target_id = -1,
|
target_id = -1,
|
||||||
damage = 0.0,
|
damage = 0.0,
|
||||||
weapon_id = weapon_id,
|
weapon_id = weapon_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Internal: helpers
|
# Internal: helpers
|
||||||
@@ -344,50 +345,45 @@ func _miss_result(weapon_id: String) -> Dictionary:
|
|||||||
|
|
||||||
## Strict can-fire that does NOT auto-grant the weapon.
|
## Strict can-fire that does NOT auto-grant the weapon.
|
||||||
func _hard_can_fire(
|
func _hard_can_fire(
|
||||||
player_id: int,
|
player_id: int,
|
||||||
weapon_id: String,
|
weapon_id: String,
|
||||||
data: WeaponData,
|
data, # WeaponData — untyped for headless compat (Resource class_name order)
|
||||||
st: Dictionary
|
st: Dictionary
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if st.is_empty():
|
if st.is_empty():
|
||||||
return false
|
return false
|
||||||
if st.get("is_reloading", false):
|
if st.get("is_reloading", false):
|
||||||
return false
|
return false
|
||||||
if st.get("ammo", 0) <= 0:
|
if st.get("ammo", 0) <= 0:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
var fire_interval: float = 1.0 / max(data.fire_rate, 0.001)
|
var fire_interval: float = 1.0 / max(data.fire_rate, 0.001)
|
||||||
var last_fire: float = st.get("last_fire_time", -INF)
|
var last_fire: float = st.get("last_fire_time", -INF)
|
||||||
var now: float = Time.get_unix_time_from_system()
|
var now: float = Time.get_unix_time_from_system()
|
||||||
if now - last_fire < fire_interval:
|
if now - last_fire < fire_interval:
|
||||||
return false
|
return false
|
||||||
return true
|
return true
|
||||||
|
|
||||||
func _get_space_state() -> PhysicsDirectSpaceState3D:
|
func _get_space_state() -> PhysicsDirectSpaceState3D:
|
||||||
if physics_world != null:
|
if physics_world != null:
|
||||||
return physics_world.direct_space_state
|
return physics_world.direct_space_state
|
||||||
# Fallback: try from the scene tree
|
return null
|
||||||
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]:
|
func _get_shooter_exclusions(player_id: int) -> Array[RID]:
|
||||||
# Try to find the player's collision body via the entity system
|
# Try to find the player's collision body via the entity system
|
||||||
# For now, return an empty array (no self-exclusion beyond what Godot does)
|
# For now, return an empty array (no self-exclusion beyond what Godot does)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
## Apply spread to a base direction vector.
|
## Apply spread to a base direction vector.
|
||||||
func _apply_spread(base: Vector3, spread_rad: float) -> Vector3:
|
func _apply_spread(base: Vector3, spread_rad: float) -> Vector3:
|
||||||
if spread_rad <= 0.001:
|
if spread_rad <= 0.001:
|
||||||
return base
|
return base
|
||||||
var theta: float = randf() * TAU
|
var theta: float = randf() * TAU
|
||||||
var phi: float = randf() * spread_rad
|
var phi: float = randf() * spread_rad
|
||||||
var up := Vector3.UP
|
var up := Vector3.UP
|
||||||
if abs(base.dot(up)) > 0.99:
|
if abs(base.dot(up)) > 0.99:
|
||||||
up = Vector3.RIGHT
|
up = Vector3.RIGHT
|
||||||
var right := base.cross(up).normalized()
|
var right := base.cross(up).normalized()
|
||||||
up = right.cross(base).normalized()
|
up = right.cross(base).normalized()
|
||||||
var offset := right * sin(theta) * sin(phi) + up * cos(theta) * sin(phi)
|
var offset := right * sin(theta) * sin(phi) + up * cos(theta) * sin(phi)
|
||||||
return (base + offset).normalized()
|
return (base + offset).normalized()
|
||||||
|
|||||||
Reference in New Issue
Block a user