OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / creatures / crawler_leg.gd

117 lines · gd ref:
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