extends RefCounted ## Ground movement with lightweight separation, without navigation dependencies. func step(unit, combatants: Array, delta: float, lane_center: float) -> void: var heading = Vector3.ZERO var travel_limit = INF if is_instance_valid(unit.target) and unit.target.alive: var offset: Vector3 = unit.target.position - unit.position offset.y = 0.0 var gap = offset.length() if gap > float(unit.data.attack_range): heading = offset.normalized() travel_limit = maxf(0.0, gap - float(unit.data.attack_range) + 0.02) elif unit.is_attacker: heading = Vector3.BACK var separation = Vector3.ZERO for other in combatants: if other == unit or not other.alive or other.team_id != unit.team_id: continue var offset: Vector3 = unit.position - other.position offset.y = 0.0 var distance = offset.length() # Space allies according to the visible stone bodies. Opponents can still # enter the shortest melee range (1.0 m); separation must not prevent hits. var spacing: float = 1.30 if other.is_attacker == unit.is_attacker else .90 if distance < spacing: if distance < 0.0001: offset = Vector3.RIGHT if unit.uid < other.uid else Vector3.LEFT distance = 0.001 separation += offset.normalized() * (spacing - distance) * 2.0 var displacement = heading * minf(float(unit.data.speed) * unit.movement_factor() * delta, travel_limit) displacement += separation.limit_length(0.9) * delta unit.position += displacement unit.position.x = clampf(unit.position.x, lane_center - 5.6, lane_center + 5.6) unit.position.z = clampf(unit.position.z, -20.0, 16.1) if heading.length_squared() > 0.001: unit.facing = atan2(heading.x, heading.z)