261 lines
9.0 KiB
GDScript
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")
|