extends Node3D
## A bot's visible body that GLIDES to its published position (#93 §5; #80 "it snaps").
##
## Position bodies publish a realtime row at ~10 Hz (patrol only after moving a set
## distance) and the marker used to be SET to each row: a visible step every row, on
## every peer, including the one simulating it. Now each row is a TARGET:
## - the first row, and any jump over SNAP_M, places the marker exactly (spawn,
## teleport, map entry — sliding across the map would be wrong);
## - otherwise the marker eases toward target + velocity * (time since the row),
## velocity estimated from the last two rows, extrapolation capped at
## EXTRAPOLATE_S so a bot that stopped publishing does not wander off.
## The brains keep their own position state (pathwalker _pos, patrol p) and never
## read this node back, so smoothing it cannot feed lag into their logic.
const SNAP_M := 5.0 # same threshold as player dead reckoning
const RATE := 15.0 # 1/s: ~67 ms time constant against a 100 ms row interval
const EXTRAPOLATE_S := 0.15
var _has_target := false
var _target := Vector3.ZERO
var _vel := Vector3.ZERO
var _t_row := 0.0
static func _now() -> float:
return Time.get_ticks_msec() / 1000.0
## A new published position. `now` (seconds) is for gates; play uses the clock.
func set_target(pos: Vector3, now: float = -1.0) -> void:
var t := now if now >= 0.0 else _now()
if not _has_target or global_position.distance_to(pos) > SNAP_M:
_has_target = true
_target = pos
_vel = Vector3.ZERO
_t_row = t
global_position = pos
return
var dt := t - _t_row
_vel = (pos - _target) / dt if dt > 0.02 else Vector3.ZERO
_target = pos
_t_row = t
func step(delta: float, now: float) -> void:
if not _has_target:
return
var ahead := clampf(now - _t_row, 0.0, EXTRAPOLATE_S)
var want := _target + _vel * ahead
global_position = global_position.lerp(want, 1.0 - exp(-RATE * delta))
func _process(delta: float) -> void:
step(delta, _now())