Files
tactical-shooter/scripts/combat/damage_processor.gd
T
shawn 705b068ed2 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()
2026-07-01 20:26:18 -04:00

153 lines
5.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## DamageProcessor — Apply hit results and track kills.
##
## Receives hit results from WeaponServer (via LagCompensation), applies
## damage to the target entity, and emits events for scoreboard/UI updates.
##
## Usage (add as child of GameServer or combat manager):
##
## var dp = DamageProcessor.new()
## add_child(dp)
## dp.register_player(entity_id, 100.0)
## dp.process_hit(victim_id, shooter_id, 30.0)
##
class_name DamageProcessor
extends Node
# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------
## Emitted when a player takes damage.
## victim_id: entity_id of the damaged player
## shooter_id: entity_id of the player who dealt the damage (-1 if world/fall)
## damage: raw damage amount before any modifiers
## killed: true if this damage reduced health to 0
signal player_damaged(victim_id: int, shooter_id: int, damage: float, killed: bool)
## Emitted when a player dies from damage.
## victim_id: entity_id of the killed player
## shooter_id: entity_id of the killer (-1 if world/fall)
signal player_killed(victim_id: int, shooter_id: int)
# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------
## Current health per entity_id.
var _health: Dictionary = {} # entity_id (int) → current_health (float)
## Maximum health per entity_id (for respawn reset).
var _max_health: Dictionary = {} # entity_id (int) → max_health (float)
## Kill count per shooter entity_id.
var _kills: Dictionary = {} # shooter_id (int) → kill_count (int)
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
## Register a player for damage tracking with the given maximum health.
func register_player(entity_id: int, max_health: float = 100.0) -> void:
_health[entity_id] = max_health
_max_health[entity_id] = max_health
# Preserve existing kill count if re-registering
## Remove a player from damage tracking (on disconnect / respawn).
func unregister_player(entity_id: int) -> void:
_health.erase(entity_id)
_max_health.erase(entity_id)
_kills.erase(entity_id)
## Reset a player's health to their maximum (on respawn).
## Does NOT reset kill count.
func reset_health(entity_id: int) -> void:
if _max_health.has(entity_id):
_health[entity_id] = _max_health[entity_id]
## Process a hit result: apply damage, check for kill, emit signals.
##
## hit_result format (from WeaponServer.fire()):
## {hit: bool, position: Vector3, target_id: int,
## damage: float, weapon_id: String}
##
## shooter_id is the entity_id of the player who fired.
func process_hit(hit_result: Dictionary, shooter_id: int) -> void:
if not hit_result.get("hit", false):
return
var victim_id: int = hit_result.get("target_id", -1)
var damage: float = hit_result.get("damage", 0.0)
if victim_id < 0 or damage <= 0.0:
return
# Apply damage
var current: float = _health.get(victim_id, 100.0)
current -= damage
var killed: bool = false
if current <= 0.0:
current = 0.0
killed = true
# Track kill for the shooter
if shooter_id >= 0:
_kills[shooter_id] = _kills.get(shooter_id, 0) + 1
player_killed.emit(victim_id, shooter_id)
_health[victim_id] = max(current, 0.0)
# Emit damage event (always, even on kill)
player_damaged.emit(victim_id, shooter_id, damage, killed)
## Convenience: process a hit result with distance-based damage falloff.
## hit_result must contain a "position" key. damage_falloff_start is the
## distance in Godot units beyond which damage begins to decrease.
## damage_falloff_min is the minimum damage multiplier (clamped 0.01.0).
func process_hit_with_falloff(
hit_result: Dictionary,
shooter_id: int,
origin: Vector3,
falloff_start: float = 100.0,
falloff_min: float = 0.5
) -> void:
if not hit_result.get("hit", false):
return
var damage: float = hit_result.get("damage", 0.0)
var hit_pos: Vector3 = hit_result.get("position", Vector3.ZERO)
var distance: float = origin.distance_to(hit_pos)
if distance > falloff_start:
# Linear falloff from falloff_start to some max range (2x falloff_start)
var max_range: float = falloff_start * 2.0
var t: float = clamp(
(distance - falloff_start) / (max_range - falloff_start),
0.0, 1.0
)
var multiplier: float = 1.0 - (1.0 - falloff_min) * t
damage *= multiplier
var modified_result: Dictionary = hit_result.duplicate()
modified_result["damage"] = damage
process_hit(modified_result, shooter_id)
# ---------------------------------------------------------------------------
# Query
# ---------------------------------------------------------------------------
## Get current health for an entity, or -1 if not registered.
func get_health(entity_id: int) -> float:
return _health.get(entity_id, -1.0)
## Get kill count for a shooter entity_id.
func get_kills(player_id: int) -> int:
return _kills.get(player_id, 0)
## Get all kills (shooter_id → kill_count).
func get_all_kills() -> Dictionary:
return _kills.duplicate()
## Reset all state (for round restart).
func reset_all() -> void:
_health.clear()
_max_health.clear()
_kills.clear()