neon-sprawl/client/scripts/player_locomotion_wasd.gd

261 lines
9.0 KiB
GDScript

extends RefCounted
## NEO-22 WASD wall scrape / floor seam tick (extracted from [code]player.gd[/code] for
## [kbd]gdlint[/kbd] [code]max-file-lines[/code]). Owns coyote + scrape feet state; calls back into
## the [CharacterBody3D] player for physics and walk helpers.
const LOCOMOTION_FLOOR_COYOTE_TICKS: int = 8
const LOCOMOTION_WALL_SCRAPE_PROBE_HOLD_VY_MAX: float = -1.05
const LOCOMOTION_WALL_SCRAPE_SNAP_FALLBACK: float = 0.12
const LOCOMOTION_SCRAPE_FEET_LOCK_LERP: float = 0.2
const LOCOMOTION_SCRAPE_FEET_CORRECT_MAX: float = 0.008
const LOCOMOTION_SCRAPE_FEET_ERR_GATE: float = 0.075
const LOCOMOTION_SCRAPE_RAY_FEET_ABSORB_MAX: float = 0.1
const LOCOMOTION_SCRAPE_RAY_PROBE_Y_LERP: float = 0.42
const LOCOMOTION_SCRAPE_RAY_FEET_MEDIAN_SPAN: float = 0.055
const LOCOMOTION_SCRAPE_WISH_LINE_MICRO_PERP_M: float = 0.032
const _FLOOR_RAY_FEET_INVALID: float = -1.0e9
## Must match [code]player.gd[/code] walk continuation probe (same ray math as
## [method _walk_ray_hit_up_floor]).
const _WALK_SUPPORT_PROBE_DEPTH: float = 0.32
const _WALK_SUPPORT_PROBE_MIN_UP_DOT: float = 0.42
const _WALK_CONTINUATION_MAX_BELOW_FEET: float = 0.108
var _player: CharacterBody3D
var _cap_half: float
var _cap_radius: float
var _move_speed: float
var _floor_snap_moving: float
var _floor_coyote: int = 0
var _scrape_feet_lock_valid: bool = false
var _scrape_feet_y_lock: float = 0.0
var _scrape_probe_y_ema_valid: bool = false
var _scrape_probe_y_ema: float = 0.0
func _init(
player: CharacterBody3D,
cap_half: float,
cap_radius: float,
move_speed: float,
floor_snap_moving: float
) -> void:
_player = player
_cap_half = cap_half
_cap_radius = cap_radius
_move_speed = move_speed
_floor_snap_moving = floor_snap_moving
func reset_scrape_feet_lock() -> void:
_scrape_feet_lock_valid = false
_scrape_probe_y_ema_valid = false
func reset_for_nav_or_snap() -> void:
_floor_coyote = 0
reset_scrape_feet_lock()
func _feet_y() -> float:
return _player.global_position.y - _cap_half - _cap_radius
func _has_scrape_vertical_contact() -> bool:
if _player.is_on_wall():
return true
for i: int in _player.get_slide_collision_count():
var n: Vector3 = _player.get_slide_collision(i).get_normal()
if n.y < 0.48 and n.y > -0.22:
return true
return false
func _project_global_xz_on_wish_line_if_micro_slip(anchor_xz: Vector2, wish_xz: Vector2) -> void:
var w2s: float = wish_xz.length_squared()
if w2s < 1e-12:
return
var w2n: Vector2 = wish_xz / sqrt(w2s)
var p2: Vector2 = Vector2(_player.global_position.x, _player.global_position.z)
var along: float = (p2 - anchor_xz).dot(w2n)
var p2_on: Vector2 = anchor_xz + w2n * along
var perp_len: float = (p2 - p2_on).length()
if perp_len > LOCOMOTION_SCRAPE_WISH_LINE_MICRO_PERP_M or perp_len <= 1e-7:
return
_player.global_position.x = p2_on.x
_player.global_position.z = p2_on.y
func _align_velocity_xz_to_wish() -> void:
var wish: Vector3 = _player._locomotion_wish_world_xz
var w2 := Vector2(wish.x, wish.z)
if w2.length_squared() < 1e-10:
return
w2 = w2.normalized()
var v2 := Vector2(_player.velocity.x, _player.velocity.z)
var along: float = v2.dot(w2)
if along < 0.0:
along = 0.0
along = minf(_move_speed, along)
_player.velocity.x = w2.x * along
_player.velocity.z = w2.y * along
func _floor_ray_feet_y(
space: PhysicsDirectSpaceState3D, anchor_feet_xz: Vector2, off_xz: Vector2
) -> float:
var feet_y: float = _player.global_position.y - _cap_half - _cap_radius
var feet_x: float = anchor_feet_xz.x + off_xz.x
var feet_z: float = anchor_feet_xz.y + off_xz.y
var probe_from := Vector3(feet_x, feet_y + 0.04, feet_z)
var probe_end := Vector3(feet_x, feet_y - _WALK_SUPPORT_PROBE_DEPTH, feet_z)
var q := PhysicsRayQueryParameters3D.create(probe_from, probe_end)
q.collision_mask = _player.collision_mask
q.exclude = [_player.get_rid()]
var res: Dictionary = space.intersect_ray(q)
if res.is_empty() or not res.has("normal") or not res.has("position"):
return _FLOOR_RAY_FEET_INVALID
var hit_y: float = (res["position"] as Vector3).y
if hit_y < feet_y - _WALK_CONTINUATION_MAX_BELOW_FEET:
return _FLOOR_RAY_FEET_INVALID
var n: Vector3 = res["normal"]
if n.dot(Vector3.UP) <= _WALK_SUPPORT_PROBE_MIN_UP_DOT:
return _FLOOR_RAY_FEET_INVALID
return hit_y
func _floor_ray_feet_y_median(
space: PhysicsDirectSpaceState3D, anchor_feet_xz: Vector2, wish_probe: Vector2
) -> float:
var offs: Array[Vector2] = [Vector2.ZERO]
var ws2: float = wish_probe.length_squared()
if ws2 > 1e-10:
var wp: Vector2 = wish_probe / sqrt(ws2)
var perp: Vector2 = Vector2(-wp.y, wp.x) * LOCOMOTION_SCRAPE_RAY_FEET_MEDIAN_SPAN
offs.append(perp)
offs.append(-perp)
var y_vals: Array[float] = []
for k: int in range(offs.size()):
var y_one: float = _floor_ray_feet_y(space, anchor_feet_xz, offs[k])
if y_one != _FLOOR_RAY_FEET_INVALID:
y_vals.append(y_one)
if y_vals.is_empty():
return _FLOOR_RAY_FEET_INVALID
y_vals.sort()
var n_hit: int = y_vals.size()
var mid: int = (n_hit - 1) >> 1
return y_vals[mid]
func run_wasd(delta: float) -> void:
if _player._has_walk_goal:
_player.clear_nav_goal()
_player._idle_anchor_active = false
_player.floor_block_on_wall = true
var scrape_ray_anchor_feet_xz := Vector2(_player.global_position.x, _player.global_position.z)
var feet_y: float = _feet_y()
var wish: Vector3 = _player._locomotion_wish_world_xz
_player.velocity.x = wish.x * _move_speed
_player.velocity.z = wish.z * _move_speed
var wish_probe: Vector2 = Vector2(wish.x, wish.z)
var wish_probe_active: bool = wish_probe.length_squared() > 1e-10
var locomotion_probe_floor: bool = (
wish_probe_active and _player._walk_has_close_floor_probe_below(wish_probe)
)
var scrape_probe_hold: bool = (
locomotion_probe_floor
and _has_scrape_vertical_contact()
and _player.velocity.y >= LOCOMOTION_WALL_SCRAPE_PROBE_HOLD_VY_MAX
)
if _player.is_on_floor():
_floor_coyote = LOCOMOTION_FLOOR_COYOTE_TICKS
elif scrape_probe_hold:
_floor_coyote = LOCOMOTION_FLOOR_COYOTE_TICKS
else:
_floor_coyote = maxi(0, _floor_coyote - 1)
var locomotion_grounded_y: bool = _player.is_on_floor() or _floor_coyote > 0
var scrape_micro_slip_xz: bool = (
wish_probe_active and locomotion_grounded_y and _has_scrape_vertical_contact()
)
if locomotion_grounded_y:
_player.velocity.y = 0.0
else:
_player._apply_walk_air_gravity(delta, feet_y, Vector2.ZERO)
_player._walk_clip_horizontal_velocity_against_vertical_contacts(wish)
_player.floor_snap_length = _floor_snap_moving
if locomotion_grounded_y and wish_probe_active and _has_scrape_vertical_contact():
if locomotion_probe_floor:
_player.floor_snap_length = 0.0
else:
_player.floor_snap_length = minf(
_floor_snap_moving, LOCOMOTION_WALL_SCRAPE_SNAP_FALLBACK
)
_player.move_and_slide()
if scrape_micro_slip_xz:
_project_global_xz_on_wish_line_if_micro_slip(scrape_ray_anchor_feet_xz, wish_probe)
var feet_after: float = _feet_y()
if wish_probe_active and _player.is_on_floor() and _has_scrape_vertical_contact():
var ray_absorbed: bool = false
if locomotion_probe_floor:
var w3d := _player.get_world_3d()
if w3d != null:
var probe_feet_y: float = _floor_ray_feet_y_median(
w3d.direct_space_state, scrape_ray_anchor_feet_xz, wish_probe
)
if probe_feet_y != _FLOOR_RAY_FEET_INVALID:
var raw_absorb_dy: float = probe_feet_y - feet_after
var absorb_dy: float = raw_absorb_dy
if absf(raw_absorb_dy) > LOCOMOTION_SCRAPE_RAY_FEET_ABSORB_MAX:
_scrape_probe_y_ema_valid = false
else:
if _scrape_probe_y_ema_valid:
_scrape_probe_y_ema = lerpf(
_scrape_probe_y_ema,
probe_feet_y,
LOCOMOTION_SCRAPE_RAY_PROBE_Y_LERP
)
else:
_scrape_probe_y_ema = probe_feet_y
_scrape_probe_y_ema_valid = true
absorb_dy = _scrape_probe_y_ema - feet_after
if (
absf(absorb_dy) <= LOCOMOTION_SCRAPE_RAY_FEET_ABSORB_MAX
and absf(absorb_dy) > 1e-5
):
_player.global_position.y += absorb_dy
feet_after = _feet_y()
_scrape_feet_y_lock = feet_after
_scrape_feet_lock_valid = true
ray_absorbed = true
if not ray_absorbed:
if not _scrape_feet_lock_valid:
_scrape_feet_y_lock = feet_after
_scrape_feet_lock_valid = true
else:
if absf(_scrape_feet_y_lock - feet_after) > LOCOMOTION_SCRAPE_FEET_ERR_GATE:
_scrape_feet_y_lock = feet_after
else:
_scrape_feet_y_lock = lerpf(
_scrape_feet_y_lock, feet_after, LOCOMOTION_SCRAPE_FEET_LOCK_LERP
)
var pull_feet: float = _scrape_feet_y_lock - feet_after
var step_y: float = clampf(
pull_feet,
-LOCOMOTION_SCRAPE_FEET_CORRECT_MAX,
LOCOMOTION_SCRAPE_FEET_CORRECT_MAX
)
if absf(step_y) > 1e-6:
_player.global_position.y += step_y
if scrape_micro_slip_xz:
_project_global_xz_on_wish_line_if_micro_slip(scrape_ray_anchor_feet_xz, wish_probe)
else:
reset_scrape_feet_lock()
if wish_probe_active and _player.is_on_floor() and _has_scrape_vertical_contact():
if locomotion_probe_floor:
_align_velocity_xz_to_wish()
if locomotion_grounded_y:
_player.velocity.y = 0.0
_player._snap_capsule_upright()
_player._debug_trace_transform("physics")