OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_flip_roll.gd

149 lines · gd ref:
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