OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / wall_run_curve_gate.gd

181 lines · gd ref:
extends Node3D
## Does the model's FACING wind while wall running on a wall that is not flat?
##
## wall_run_gate passes on a single flat plane at a steady 8 m/s along it. #47
## named that blind spot exactly: "a gate that can stage a FLICKERING wall
## contact... Both wall-run failures lived exactly where no gate could reach."
##
## THE MECHANISM UNDER TEST. update_model_orientation's wall-run branch builds its
## target from the model's OWN current forward, projected onto the leaned plane:
##
##     wl_fwd = -model.basis.z ;  wl_fwd -= wl_up * wl_fwd.dot(wl_up)
##
## That cannot INVERT, which is what it was written to fix. But it is unanchored:
## with wl_up steady the target equals the current basis, so yaw is a free
## integrator with no restoring term. wall_run_normal is re-read from the detected
## wall EVERY frame (player_procedural.gd:1364), so on any surface that is not a
## plane, wl_up swings, the old forward is re-projected onto a new plane, and the
## yaw keeps whatever error that leaves. Permanently.
##
## THE RIG RUNS A LIVE PLAYER. It does NOT set process_mode = DISABLED: that
## removes the body from the physics space, so move_and_slide errors with
## "body->get_space() is null" and does nothing. wall_run_gate does disable it and
## throws that error on all 40 of its frames -- it has only ever tested the sphere
## sweeps, never motion. This gate lets the player's own _physics_process run.
##
## TWO WALLS, one constant expected facing (both run along -Z), so any difference
## is the mechanism and not the geometry:
##   CONTROL  one flat plane -- what wall_run_gate already covers.
##   FACETED  the same plane in segments alternating +/- 3 degrees, which is what
##            a dual-contoured surface looks like to a sweep.
## If the rows agree, the gate is not seeing the facets and proves nothing.
##
## Run:
##   "$HYPER_TPS_GODOT" --headless res://scenes/test/wall_run_curve_gate.tscn

const WALL_X := 0.6
const SETTLE := 20
const FRAMES := 160
const FACET_DEG := 3.0

var _player: CharacterBody3D = null
var _walls: Array = []
var _phase := 0
var _f := 0
var _yaw_prev := 0.0
var _wind := 0.0
var _worst := 0.0
var _active := 0
var _rows: Array = []
var _done := false


func _ready() -> void:
	process_priority = 100
	_build()


func _build() -> void:
	for w in _walls:
		w.queue_free()
	_walls.clear()
	if _phase == 0:
		var wall := StaticBody3D.new()
		var cs := CollisionShape3D.new()
		var box := BoxShape3D.new()
		box.size = Vector3(0.4, 400.0, 400.0)
		cs.shape = box
		wall.add_child(cs)
		wall.position = Vector3(WALL_X + 0.2, 0.0, -180.0)
		add_child(wall)
		_walls.append(wall)
	else:
		for i in range(200):
			var seg := StaticBody3D.new()
			var cs2 := CollisionShape3D.new()
			var box2 := BoxShape3D.new()
			box2.size = Vector3(0.4, 400.0, 2.2)
			cs2.shape = box2
			seg.add_child(cs2)
			seg.position = Vector3(WALL_X + 0.2, 0.0, 20.0 - float(i) * 2.0)
			seg.rotation.y = deg_to_rad(FACET_DEG if i % 2 == 0 else -FACET_DEG)
			add_child(seg)
			_walls.append(seg)

	var scene: PackedScene = load("res://scenes/player/player_procedural.tscn")
	_player = scene.instantiate()
	add_child(_player)
	_player.global_position = Vector3(WALL_X - 0.3, 40.0, 16.0)
	_player.camera_head.global_transform = Transform3D(Basis.IDENTITY, Vector3.ZERO)
	_player.velocity = Vector3(3.0, 0.0, -8.0)
	_f = 0
	_wind = 0.0
	_worst = 0.0
	_active = 0
	_yaw_prev = _model_yaw()


func _physics_process(_d: float) -> void:
	if _done or _player == null:
		return
	Input.action_press("sprint")
	_player.input_sync.input_direction = Vector2(0.0, -1.0)
	_f += 1
	if _f <= SETTLE:
		_yaw_prev = _model_yaw()
		return
	if _player.wall_run_active:
		_active += 1
	var y: float = _model_yaw()
	var prev_y: float = _yaw_prev
	_wind += absf(rad_to_deg(angle_difference(_yaw_prev, y)))
	_yaw_prev = y
	# Both walls run along -Z, so the tangent is the same constant in both rows.
	if OS.has_environment("HTPS_WR") and _phase == 1:
		var n: Vector3 = _player.wall_run_normal
		var vv: Vector3 = _player.velocity
		var mm: Node3D = _player.get_node("Model") as Node3D
		var mf: Vector3 = -mm.global_transform.basis.z
		var mu: Vector3 = mm.global_transform.basis.y
		var horiz: float = Vector2(mf.x, mf.z).length()
		var step: float = absf(rad_to_deg(angle_difference(prev_y, y)))
		if step > 3.0 or horiz < 0.5:
			print("[WR] f=%3d step=%6.2f  yaw=%7.2f  fwd=(%.3f,%.3f,%.3f) horiz=%.3f  up.y=%.3f  n_yaw=%7.2f  active=%s"
					% [_f, step, rad_to_deg(y), mf.x, mf.y, mf.z, horiz, mu.y,
					   rad_to_deg(atan2(n.x, -n.z)), str(_player.wall_run_active)])
	var dev: float = absf(rad_to_deg(angle_difference(y, 0.0)))
	if dev > _worst:
		_worst = dev


func _process(_d: float) -> void:
	if _done or _player == null or _f < SETTLE + FRAMES:
		return
	_rows.append({"name": "flat control" if _phase == 0 else "faceted +/-3 deg",
			"wind": _wind, "worst": _worst,
			"pct": 100.0 * float(_active) / float(FRAMES)})
	_player.queue_free()
	_player = null
	_phase += 1
	if _phase < 2:
		_build()
		return
	_report()
	_done = true
	get_tree().quit(0)


func _report() -> void:
	print("")
	print("wall                 wall_run   total yaw travelled   worst off tangent")
	print("--------------------------------------------------------------------------")
	for r in _rows:
		print("%-20s %6.1f %%   %13.1f deg %17.1f deg"
				% [r["name"], r["pct"], r["wind"], r["worst"]])
	print("")
	var a: float = float(_rows[0]["wind"])
	var b: float = float(_rows[1]["wind"])
	if float(_rows[0]["pct"]) < 1.0 and float(_rows[1]["pct"]) < 1.0:
		print("RIG FAILED: wall_run never engaged in either row, so neither number is")
		print("about wall running at all.")
	elif absf(a - b) < 5.0:
		print("CONTROL FAILED: the faceted wall travelled the same as the flat one, so")
		print("this gate is not seeing the facets and neither number means anything.")
	elif float(_rows[1]["worst"]) > FACET_DEG * 2.5:
		print("FAIL  the facing ends %.1f deg off the wall tangent, against %.1f deg of"
				% [float(_rows[1]["worst"]), FACET_DEG])
		print("      real geometry. Something is displacing the facing and nothing is")
		print("      putting it back. %.0f deg of yaw travelled against %.0f flat." % [b, a])
	else:
		print("PASS  the facing tracks the wall: %.1f deg off tangent against %.1f deg of"
				% [float(_rows[1]["worst"]), FACET_DEG])
		print("      real facet. %.0f deg travelled against %.0f flat." % [b, a])


func _model_yaw() -> float:
	if _player == null:
		return 0.0
	var m: Node3D = _player.get_node("Model") as Node3D
	var f: Vector3 = -m.global_transform.basis.z
	return atan2(f.x, -f.z)