extends Node3D
## Does holding the ski key change the STANCE, not just the traction?
##
## Holding jump already drops traction to ski_hold_traction (player_procedural.gd
## :751, :1183) — the physics half of skiing has been there. Nothing tells the
## ANIMATOR, so a skier is posed exactly like a runner and the mechanic does not
## read on screen at all.
##
## WHAT A SKI STANCE IS, and so what this measures:
## WIDTH the feet separate laterally — a wider base of support, because the
## disturbing force is sideways and the base has to span it.
## CROUCH the pelvis drops — a lower centre of mass over that base.
## BANK the body leans INTO a turn. Not decoration: on skis the lean is what
## puts the edge into the surface, and it scales with how hard you are
## turning.
##
## THE CONTROL IS THE TABLE. Rows 1 and 2 are the same run and differ only in
## whether the key is held; so are 3 and 4. If a pair agrees, the gate is not
## seeing the feature and every number in it is worthless — which is the failure
## this whole session kept paying for.
##
## Run:
## "$HYPER_TPS_GODOT" --headless res://scenes/test/ski_stance_gate.tscn
const SETTLE := 70
const RUN := 120
const TURN_RATE := 1.2 ## rad/s of input rotation in the turning rows
var _plan: Array = []
var _idx := 0
var _f := 0
var _player: CharacterBody3D = null
var _ground: StaticBody3D = null
var _skel: Node = null
var _width_sum := 0.0
var _hip_sum := 0.0
var _n := 0
var _peak_tilt := 0.0
var _ski_seen := 0
var _rows: Array = []
var _done := false
func _ready() -> void:
process_priority = 100
for ski in [false, true]:
_plan.append({"ski": ski, "turn": false})
for ski in [false, true]:
_plan.append({"ski": ski, "turn": true})
_build()
func _build() -> void:
_ground = StaticBody3D.new()
var cs := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(400.0, 1.0, 400.0)
cs.shape = box
_ground.add_child(cs)
_ground.position = Vector3(0.0, -0.5, 0.0)
add_child(_ground)
var scene: PackedScene = load("res://scenes/player/player_procedural.tscn")
_player = scene.instantiate()
add_child(_player)
_player.global_position = Vector3(0.0, 0.6, 0.0)
_player.camera_head.global_transform = Transform3D(Basis.IDENTITY, Vector3.ZERO)
_skel = null
_f = 0
_width_sum = 0.0
_hip_sum = 0.0
_n = 0
_peak_tilt = 0.0
_ski_seen = 0
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)
var cfg: Dictionary = _plan[_idx]
if bool(cfg["turn"]):
var t: float = float(_f) / 60.0 * TURN_RATE
_player.input_sync.input_direction = Vector2(sin(t), -cos(t))
else:
_player.input_sync.input_direction = Vector2(0.0, -1.0)
# Hold-to-ski is Input-driven and needs is_on_floor(), so it cannot be forced
# by setting the flag: press the real action.
if bool(cfg["ski"]):
Input.action_press("jump")
else:
Input.action_release("jump")
_f += 1
if _f > SETTLE and _player.ski_holding:
_ski_seen += 1
if OS.has_environment("HTPS_SKI") and _f > SETTLE and _f % 10 == 0 and bool(cfg["ski"]) and not bool(cfg["turn"]):
print("[ski] f=%3d floor=%-5s state=%d ski=%-5s hold=%.3f fuel=%.2f y=%.2f vy=%+.2f"
% [_f, str(_player.is_on_floor()), _player.state, str(_player.ski_holding),
_player._jump_hold_time, _player.jump_fuel_remaining,
_player.global_position.y, _player.velocity.y])
func _on_skeleton_updated() -> void:
# SAMPLE ONLY A SETTLED, GROUNDED STANCE. Pressing the ski key fires one jump
# first -- pre_jump_delay 0.08 is shorter than ski_hold_delay 0.15, so the tap
# has already gone by the time the hold latches. Measured: the player leaves
# the ground at f=80, lands at f=130 and skis steadily from f=140. Averaging
# from a fixed frame put that whole arc in the numbers and read the pelvis at
# 2.12 m against a standing 0.69 -- an airborne player, not a stance.
if _done or _skel == null or _player == null or _f <= SETTLE:
return
if not _player.is_on_floor():
return
if bool(_plan[_idx]["ski"]) and not _player.ski_holding:
return
var l: int = _skel.find_bone("DEF-foot.L")
var r: int = _skel.find_bone("DEF-foot.R")
var h: int = _skel.find_bone("DEF-spine")
if l < 0 or r < 0 or h < 0:
return
var gx: Transform3D = _skel.global_transform
var pl: Vector3 = (gx * _skel.get_bone_global_pose(l)).origin
var pr: Vector3 = (gx * _skel.get_bone_global_pose(r)).origin
# Lateral separation is measured across the MODEL's right axis, not in world
# X: the player turns, and a world-axis measurement would read the turn.
var right: Vector3 = (_player.get_node("Model") as Node3D).global_transform.basis.x.normalized()
_width_sum += absf((pl - pr).dot(right))
_hip_sum += (gx * _skel.get_bone_global_pose(h)).origin.y
_n += 1
# LEAN IS MEASURED AT THE SPINE, NOT AT THE MODEL NODE. The acceleration tilt
# is applied as a rotation of the BONES about the COM (_apply_flip_rotation),
# so the Model's own basis never moves and a model-space measurement reads a
# flat 0.00 through a hard turn -- which is what this gate did first.
var mu: Vector3 = (gx * _skel.get_bone_global_pose(h)).basis.y.normalized()
var tilt: float = rad_to_deg(mu.angle_to(Vector3.UP))
if tilt > _peak_tilt:
_peak_tilt = tilt
func _process(_d: float) -> void:
if _done or _player == null or _f < SETTLE + RUN:
return
var cfg: Dictionary = _plan[_idx]
_rows.append({"ski": cfg["ski"], "turn": cfg["turn"],
"width": (_width_sum / float(_n)) if _n > 0 else 0.0,
"hip": (_hip_sum / float(_n)) if _n > 0 else 0.0,
"tilt": _peak_tilt,
"held": 100.0 * float(_ski_seen) / float(RUN), "n": _n})
_player.queue_free(); _player = null
_ground.queue_free(); _ground = null
_idx += 1
if _idx < _plan.size():
_build()
return
_report()
_done = true
get_tree().quit(0)
func _report() -> void:
print("")
print("run ski held samples stance width pelvis height peak lean")
print("-------------------------------------------------------------------")
for r in _rows:
print("%-12s %6.0f %% %5d %9.4f m %10.4f m %7.2f deg"
% ["turning" if r["turn"] else "straight", r["held"], int(r["n"]),
r["width"], r["hip"], r["tilt"]])
print("")
var straight_off: Dictionary = _rows[0]
var straight_on: Dictionary = _rows[1]
var turn_off: Dictionary = _rows[2]
var turn_on: Dictionary = _rows[3]
for r in _rows:
if int(r["n"]) < 20:
print("RIG FAILED: only %d settled samples in the %s/%s row — not enough of"
% [int(r["n"]), "ski" if r["ski"] else "no-ski", "turn" if r["turn"] else "straight"])
print("a grounded stance was ever observed to average.")
return
var dw: float = float(straight_on["width"]) - float(straight_off["width"])
var dh: float = float(straight_on["hip"]) - float(straight_off["hip"])
var dt: float = float(turn_on["tilt"]) - float(turn_off["tilt"])
print("holding the key changes: width %+.4f m pelvis %+.4f m lean %+.2f deg"
% [dw, dh, dt])
if absf(dw) < 0.01 and absf(dh) < 0.01 and absf(dt) < 1.0:
print("")
print("FAIL nothing about the stance responds to the ski key. The traction")
print(" changes and the body does not, so skiing is posed as running.")
return
var ok := true
if dw < 0.04:
print("FAIL stance does not widen (%+.4f m, want >= +0.04)" % dw)
ok = false
if dh > -0.02:
print("FAIL pelvis does not drop (%+.4f m, want <= -0.02)" % dh)
ok = false
if dt < 2.0:
print("FAIL no extra lean into the turn (%+.2f deg, want >= +2)" % dt)
ok = false
if ok:
print("")
print("PASS wider, lower, and it banks into the turn")
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