class_name CrawlerLeg extends RefCounted
## Single procedural leg for ground crawlers. Pure data — no nodes.
## Sphere-sweeps downward to find ground, steps with arc interpolation
## when foot drifts too far from ideal position. Uses gait groups so
## legs in the same group don't step simultaneously.
# ---- Configuration (set by parent creature) ----
var upper_length: float = 0.3
var lower_length: float = 0.3
var attach_offset: Vector3 = Vector3.ZERO ## Body-local offset from center
var step_threshold: float = 0.3 ## Drift distance before step triggers
var arc_height: float = 0.12 ## How far foot lifts during step
var arc_speed: float = 8.0 ## Step arc travel speed
var sweep_radius: float = 0.06 ## SphereSweep radius
var sweep_depth: float = 0.8 ## How far to sweep downward
var revalidate_interval: int = 4 ## Revalidate every Nth frame (staggered)
# ---- State ----
var foot_position: Vector3 = Vector3.ZERO ## Current world position
var foot_normal: Vector3 = Vector3.UP ## Ground normal at foot
var foot_target: Vector3 = Vector3.ZERO ## Step destination
var foot_arc_origin: Vector3 = Vector3.ZERO ## Step start point
var foot_valid: bool = false ## Is foot on a valid surface?
var is_stepping: bool = false ## Currently mid-step?
var arc_t: float = 1.0 ## 0..1 progress through step arc
var weight: float = 1.0 ## 0=moving, 1=planted (gait coordination)
var group_id: int = 0 ## Alternating group assignment
var leg_index: int = 0 ## Set by parent; staggers revalidation
var _frame_counter: int = 0 ## Incremented each update call
## Where the foot wants to be in world space.
func get_ideal_position(body_transform: Transform3D) -> Vector3:
return body_transform * attach_offset
## Main update. Returns true if foot is on a valid surface.
func update(space: PhysicsDirectSpaceState3D, delta: float,
body_transform: Transform3D, surface_normal: Vector3,
can_step: bool, exclude: Array[RID] = []) -> bool:
_frame_counter += 1
var ideal: Vector3 = get_ideal_position(body_transform)
# If currently stepping, advance arc
if is_stepping:
weight = 0.3
arc_t = clampf(arc_t + delta * arc_speed, 0.0, 1.0)
foot_position = _arc_interpolate(foot_arc_origin, foot_target, surface_normal, arc_t)
if arc_t >= 1.0:
# Step complete — plant
is_stepping = false
weight = 1.0
foot_position = foot_target
return foot_valid
# Foot is planted
weight = 1.0
var drift: float = foot_position.distance_to(ideal)
if drift > step_threshold and can_step:
# Initiate step: sweep for new surface at ideal position
var sweep_from: Vector3 = ideal + surface_normal * sweep_depth * 0.5
var sweep_to: Vector3 = ideal - surface_normal * sweep_depth
var result: Dictionary = SphereSweep.sweep(space, sweep_from, sweep_to,
sweep_radius, exclude)
if not result.is_empty():
foot_arc_origin = foot_position
foot_target = result["point"]
foot_normal = result["normal"]
arc_t = 0.0
is_stepping = true
foot_valid = true
else:
foot_valid = false
else:
# Still close enough — re-validate surface position (staggered)
if (_frame_counter + leg_index) % revalidate_interval == 0:
_revalidate(space, ideal, surface_normal, delta, exclude)
return foot_valid
## Initialize foot position directly (e.g. on spawn).
func plant_at(pos: Vector3, normal: Vector3) -> void:
foot_position = pos
foot_normal = normal
foot_target = pos
foot_arc_origin = pos
foot_valid = true
is_stepping = false
arc_t = 1.0
weight = 1.0
## Parabolic arc lift: linear interpolation plus normal-direction bump.
func _arc_interpolate(from: Vector3, to: Vector3, normal: Vector3, t: float) -> Vector3:
var linear: Vector3 = from.lerp(to, t)
var arc_offset: float = 4.0 * t * (1.0 - t) * arc_height
return linear + normal * arc_offset
## Re-sweep a planted foot to track surface curvature.
func _revalidate(space: PhysicsDirectSpaceState3D, ideal: Vector3,
surface_normal: Vector3, delta: float, exclude: Array[RID]) -> void:
var sweep_from: Vector3 = ideal + surface_normal * sweep_depth * 0.5
var sweep_to: Vector3 = ideal - surface_normal * sweep_depth
var result: Dictionary = SphereSweep.sweep(space, sweep_from, sweep_to,
sweep_radius, exclude)
if not result.is_empty():
foot_position = foot_position.lerp(result["point"], 5.0 * delta)
foot_normal = result["normal"]
foot_valid = true
else:
foot_valid = false