t_p1_hitscan: Add lag-compensated hitscan weapon system

NEW: server/scripts/combat/lag_compensation.gd
  - 128-entry ring buffer of player positions per server tick
  - rewind_and_raycast(tick, origin, direction, range, exclude):
    saves current positions, rewinds to tick-time positions,
    performs PhysicsDirectSpaceState3D.intersect_ray(), restores
  - Falls back to normal raycast if history missing for tick
  - shot_processed signal

NEW: scripts/combat/damage_processor.gd
  - Processes WeaponServer hit results: applies damage, tracks kills
  - Optional distance-based damage falloff
  - player_damaged / player_killed signals
  - Health and kill-count queries

MODIFY: server/scripts/weapons/weapon_server.gd
  - fire() now takes tick parameter: fire(tick, player_id, ...)
  - _perform_hitscan() uses LagCompensation.rewind_and_raycast()
    when lag_compensation reference is set and tick >= 0
  - Non-compensated fallback path preserved

MODIFY: server/scripts/game_server.gd
  - Adds _current_tick counter, incremented each simulation tick
  - Creates WeaponServer + LagCompensation + DamageProcessor as children
  - record_tick() called before each tick's simulation
  - register/unregister_player_node() for player lifecycle
  - _on_player_killed() relays kill events via player_damaged signal

MODIFY: scripts/network/server_main.gd
  - _spawn_player() registers node with GameServer.register_player_node()
  - _despawn_player() unregisters via unregister_player_node()
This commit is contained in:
2026-07-01 20:26:18 -04:00
parent 3465922be4
commit 705b068ed2
5 changed files with 457 additions and 8 deletions
+31 -8
View File
@@ -43,6 +43,11 @@ var _state: Dictionary = {}
## Set automatically if the node enters the tree; can also be injected.
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
# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------
@@ -208,8 +213,11 @@ func _complete_reload(player_id: int, weapon_id: String, st: Dictionary) -> void
# ---------------------------------------------------------------------------
## Fire the weapon from the given origin along direction.
## Performs a hit-scan raycast through the physics world.
## Performs a hit-scan raycast through the physics world with optional
## server-side lag compensation rewinding.
##
## tick: server tick when the shot occurred (used for lag compensation).
## Pass -1 to skip lag compensation and use current-frame physics.
## hit_result = {
## hit: bool,
## position: Vector3, # world-space hit point
@@ -218,7 +226,7 @@ func _complete_reload(player_id: int, weapon_id: String, st: Dictionary) -> void
## weapon_id: String, # the weapon used
## }
##
func fire(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 := WeaponDefinitions.get_weapon(weapon_id)
if data == null:
return _miss_result(weapon_id)
@@ -241,13 +249,14 @@ func fire(player_id: int, weapon_id: String, origin: Vector3, direction: Vector3
st["reload_remaining"] = data.reload_time
# Perform hit-scan — supports multi-pellet weapons
return _perform_hitscan(player_id, weapon_id, data, origin, direction, st)
return _perform_hitscan(tick, player_id, weapon_id, data, origin, direction, st)
# ---------------------------------------------------------------------------
# Internal: hitscan
# ---------------------------------------------------------------------------
func _perform_hitscan(
tick: int,
player_id: int,
weapon_id: String,
data: WeaponData,
@@ -273,12 +282,26 @@ func _perform_hitscan(
for i in range(pellets):
var dir: Vector3 = _apply_spread(base_direction, spread_rad)
var query := PhysicsRayQueryParameters3D.create(origin, origin + dir * max_range)
query.exclude = exclude
query.collide_with_bodies = true
query.collide_with_areas = false
var result: Dictionary = space_state.intersect_ray(query)
# --- Lag-compensated raycast ---
# If lag_compensation is available and a valid tick is provided,
# rewind to the target tick before raycasting. Otherwise fall
# back to the current-frame physics world.
var result: Dictionary = {}
if lag_compensation != null and tick >= 0:
result = lag_compensation.rewind_and_raycast(
tick, origin, dir, max_range, exclude
)
else:
# Fallback: no lag compensation — raycast against current positions
var query := PhysicsRayQueryParameters3D.create(
origin, origin + dir * max_range
)
query.exclude = exclude
query.collide_with_bodies = true
query.collide_with_areas = false
result = space_state.intersect_ray(query)
if result.is_empty():
continue