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)