extends SceneTree
## GATE: the ground ROLL (crouch while moving) and the air FLIP (double jump)
## rotate the body all the way round, smoothly.
##
## DISPLAY=:0 "$G" --path . --rendering-driver vulkan --script res://scenes/test/gate_flip_roll.gd
##
## Reported 2026-09-01, the day after Phase 12 (the pelvis-deviation clamp) landed
## and the same session Jolt went in: "only thing i can see broken so far is the
## tumble roll on crouch and double jump" -- parked, never closed.
##
## FlipRoll (Phase 10) rotates every root bone a full turn about the COM. Sampled on
## Skeleton3D.skeleton_updated (after the modifiers), per frame: the angle between
## the HIP bone's up and the body's up. A full turn has to pass through ~180 deg; a
## smooth one has no frame-to-frame jump beyond what the turn rate allows.
## peak -- largest hip-up deviation during the action (must reach MIN_PEAK)
## step -- largest change of that deviation between two frames (MAX_STEP)
## Each action runs with max_pelvis_deviation_deg at the shipping value; set
## FLIP_CLAMP=0 in the environment to run with the Phase 12 clamp off (A/B).
const CAPS := [40, 60]
const MIN_PEAK := 150.0
const MAX_STEP := 40.0 # a 0.4-0.5 s turn at 40 fps is ~18-23 deg/frame
const ACTION_S := 1.2
var _player: CharacterBody3D = null
var _anim: Node = null
var _skel: Skeleton3D = null
var _cases: Array = []
var _ci := -1
var _rec := false
var _peak := 0.0
var _step := 0.0
var _prev := -1.0
var _frames := 0
var _fails := 0
var _ran := false
class StubGame extends Node3D:
var current_map: Node = null
func _process(_d: float) -> bool:
if not _ran:
_ran = true
_build()
return false
func _build() -> void:
get_multiplayer().multiplayer_peer = OfflineMultiplayerPeer.new()
var game := StubGame.new()
game.name = "Game"
root.add_child(game)
var cam := Camera3D.new()
game.add_child(cam)
cam.position = Vector3(0, 3, 8)
var fb := StaticBody3D.new()
var fc := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(2000, 2, 2000)
fc.shape = box
fb.add_child(fc)
fb.position = Vector3(0, -1, 0)
game.add_child(fb)
_player = (load("res://scenes/player/player_procedural.tscn") as PackedScene).instantiate()
game.add_child(_player)
_player.global_position = Vector3(0, 0.3, 0)
_anim = _find(_player, "ProceduralAnimator")
_skel = _find_skel(_player)
if _anim == null or _skel == null:
print("GATE flip_roll: no animator/skeleton"); quit(1); return
var clamp_env := OS.get_environment("FLIP_CLAMP")
if not clamp_env.is_empty():
_anim.set("max_pelvis_deviation_deg", float(clamp_env))
print("[FLIP] max_pelvis_deviation_deg = %.1f" % float(_anim.get("max_pelvis_deviation_deg")))
for a in ["roll", "flip"]:
for cap in CAPS:
_cases.append([a, cap])
_skel.skeleton_updated.connect(_sample)
create_timer(1.5).timeout.connect(_next)
func _find(n: Node, nm: String) -> Node:
if n.name == nm:
return n
for c in n.get_children():
var r := _find(c, nm)
if r != null:
return r
return null
func _find_skel(n: Node) -> Skeleton3D:
if n is Skeleton3D:
return n
for c in n.get_children():
var r := _find_skel(c)
if r != null:
return r
return null
func _next() -> void:
_ci += 1
if _ci >= _cases.size():
print("RESULT: %s" % ("PASS" if _fails == 0 else "FAIL"))
quit(0 if _fails == 0 else 1)
return
var action: String = _cases[_ci][0]
Engine.max_fps = _cases[_ci][1]
_player.global_position = Vector3(0, 0.3, 0)
_player.velocity = Vector3.ZERO
await create_timer(0.8).timeout
_peak = 0.0
_step = 0.0
_prev = -1.0
_frames = 0
var fwd: Vector3 = -(_player.get_node("Model") as Node3D).global_transform.basis.z
fwd.y = 0.0
fwd = fwd.normalized()
if action == "roll":
_anim.call("start_roll", fwd)
else:
_player.velocity = Vector3(0, 12, 0) # airborne, as a double jump is
await create_timer(0.1).timeout
_anim.call("start_flip", fwd)
_rec = true
await create_timer(ACTION_S).timeout
_rec = false
var ok: bool = _peak >= MIN_PEAK and _step <= MAX_STEP
if not ok:
_fails += 1
print("[FLIP] %s %s cap %d (%d frames): peak hip deviation %.1f deg (need >= %.0f), largest frame step %.1f deg (max %.0f)" % [
"PASS" if ok else "FAIL", action, _cases[_ci][1], _frames, _peak, MIN_PEAK, _step, MAX_STEP])
_next()
func _sample() -> void:
if not _rec:
return
var i := _skel.find_bone("DEF-spine")
var hip_up: Vector3 = (_skel.global_transform * _skel.get_bone_global_pose(i)).basis.y.normalized()
var body_up: Vector3 = _skel.global_transform.basis.y.normalized()
var dev: float = rad_to_deg(hip_up.angle_to(body_up))
_peak = maxf(_peak, dev)
if _prev >= 0.0:
_step = maxf(_step, absf(dev - _prev))
_prev = dev
_frames += 1