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:
@@ -0,0 +1,152 @@
|
||||
## 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.0–1.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()
|
||||
Reference in New Issue
Block a user