OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / wall_run_ground_gate.gd

176 lines · gd ref:
extends Node3D
## When the player is ON THE GROUND, do the feet still find the GROUND — even if
## wall_run_active happens to be set?
##
## WHY THIS EXISTS. _detect_walls runs in the AIRBORNE state and its Method 2
## sphere-sweeps 2 m in three directions whenever sprint is held — which is how
## you run. On a mountainous map is_on_floor() flickers as you cross steep
## ground, so the player dips into AIRBORNE constantly, and anything steeper than
## 60 degrees passes the wall test (`absf(n.dot(up_direction)) < 0.5`). So
## wall_run_active can latch while you are, to every appearance, walking.
##
## That was harmless for as long as wall_run_active only added an 8 degree
## cosmetic lean. It stopped being harmless when the animator started routing
## FootIK's sweep direction off it: procedural_animator passes
## `ik_up = (-wall_run_normal).normalized() if wall_run_active else local_up`, so
## the feet hunt SIDEWAYS along the wall normal while the player stands on a floor.
##
## The invariant: GROUND CONTACT WINS. If is_grounded, the feet sweep down.
##
## THE RIG RUNS A LIVE PLAYER, and that is the fix to this gate rather than to the
## game. It used to set process_mode = PROCESS_MODE_DISABLED and then call
## move_and_slide() itself — but a disabled node is removed from the physics
## space, so every one of those calls failed with "body->get_space() is null" and
## did nothing. The player never landed, is_on_floor() was never true, and the
## gate reported FAIL for a reason that had nothing to do with the invariant. It
## was not measuring the game at all. (#47 section 2.1, again.)
##
## Bone poses are read in the skeleton_updated signal, NOT in _process:
## get_bone_global_pose() does not see a SkeletonModifier3D's writes, because
## Godot restores the pre-modifier pose after the skeleton update.
##
## Run:
##   "$HYPER_TPS_GODOT" --headless res://scenes/test/wall_run_ground_gate.tscn

const FLOOR_Y := 0.0
const WALL_X := 0.6
const SETTLE := 70          ## land, and let the landing crouch spring settle
const HOLD := 30            ## frames with wall_run_active forced on

var _player: CharacterBody3D = null
var _f := 0
var _skel: Node = null
var _done := false
var _sampled := false
var _hits: Array = [false, false]
var _pts: Array = [Vector3.ZERO, Vector3.ZERO]
var _on_floor := false
var _wr := false


func _ready() -> void:
	process_priority = 100
	var floor_body := StaticBody3D.new()
	var fcs := CollisionShape3D.new()
	var fbox := BoxShape3D.new()
	fbox.size = Vector3(40.0, 1.0, 40.0)
	fcs.shape = fbox
	floor_body.add_child(fcs)
	floor_body.position = Vector3(0.0, FLOOR_Y - 0.5, 0.0)
	add_child(floor_body)

	# A wall beside it, so a sideways sweep has something to find. Without it the
	# broken behaviour is a MISS rather than a wrong hit, and the gate would pass
	# for the wrong reason.
	var wall := StaticBody3D.new()
	var wcs := CollisionShape3D.new()
	var wbox := BoxShape3D.new()
	wbox.size = Vector3(0.4, 20.0, 20.0)
	wcs.shape = wbox
	wall.add_child(wcs)
	wall.position = Vector3(WALL_X + 0.2, 5.0, 0.0)
	add_child(wall)

	var scene: PackedScene = load("res://scenes/player/player_procedural.tscn")
	_player = scene.instantiate()
	add_child(_player)
	_player.global_position = Vector3(0.0, FLOOR_Y + 0.6, 0.0)
	_player.camera_head.global_transform = Transform3D(Basis.IDENTITY, Vector3.ZERO)


func _physics_process(_d: float) -> void:
	if _done or _player == null:
		return
	if _skel == null:
		_skel = _find_skel(_player)
		if _skel != null and _skel.has_signal("skeleton_updated"):
			_skel.skeleton_updated.connect(_on_skeleton_updated)
	_f += 1
	if _f <= SETTLE:
		return
	# The state _detect_walls can latch on steep ground while sprint is held.
	# Set every frame, before the player's own _physics_process runs.
	_player.wall_run_active = true
	_player.wall_run_normal = Vector3(1.0, 0.0, 0.0)   # into the wall


func _on_skeleton_updated() -> void:
	# Sample over a WINDOW, not one frame: the skeleton update does not line up
	# with any particular physics frame, so a single-frame trigger silently misses.
	if _done or _player == null or _f < SETTLE + HOLD - 10:
		return
	var anim: Node = _find(_player, "ProceduralAnimator")
	if anim == null:
		return
	var fik: Variant = anim.get("_foot_ik")
	if fik == null:
		return
	_hits = (fik.get("_ground_hit") as Array).duplicate()
	_pts = (fik.get("debug_ground_points") as Array).duplicate()
	_on_floor = _player.is_on_floor()
	_wr = _player.wall_run_active
	_sampled = true


func _process(_d: float) -> void:
	if _done or _player == null or _f < SETTLE + HOLD:
		return
	if not _sampled and _f < SETTLE + HOLD + 60:
		return        # give the skeleton update a chance to arrive
	_done = true
	print("")
	if not _sampled:
		print("RIG FAILED: never sampled the skeleton — animator=%s skel=%s."
				% [str(_find(_player, "ProceduralAnimator") != null), str(_skel != null)])
		get_tree().quit(1)
		return
	print("on_floor = %s, wall_run_active = %s" % [str(_on_floor), str(_wr)])
	if not _on_floor:
		print("")
		print("RIG FAILED: the player is not standing on the floor, so this says")
		print("nothing about whether ground contact wins.")
		get_tree().quit(1)
		return
	if not _wr:
		print("")
		print("RIG FAILED: wall_run_active did not stay set, so the conflict this")
		print("gate exists to test never happened.")
		get_tree().quit(1)
		return
	var ok := true
	for i in range(2):
		var p: Vector3 = _pts[i]
		var on_ground: bool = bool(_hits[i]) and absf(p.y - FLOOR_Y) < 0.15
		var on_wall: bool = bool(_hits[i]) and absf(p.x - WALL_X) < 0.15
		print("  foot %d hit=%s point=%s  -> %s" % [i, str(_hits[i]), str(p),
				"GROUND" if on_ground else ("WALL" if on_wall else "neither")])
		if not on_ground:
			ok = false
	print("")
	if ok:
		print("PASS  grounded feet find the ground even with wall_run_active set")
		get_tree().quit(0)
	else:
		print("FAIL  the feet are not on the ground — ground contact must win")
		get_tree().quit(1)


func _find(n: Node, nm: String) -> Node:
	if n.name == nm:
		return n
	for c in n.get_children():
		var f: Node = _find(c, nm)
		if f != null:
			return f
	return null


func _find_skel(n: Node) -> Node:
	if n.get_class() == "Skeleton3D":
		return n
	for c in n.get_children():
		var f: Node = _find_skel(c)
		if f != null:
			return f
	return null