Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
3.6 KiB
GDScript
77 lines
3.6 KiB
GDScript
extends Node
|
|
var checks: int = 0
|
|
var failures: Array[String] = []
|
|
func check(ok: bool, label: String) -> void:
|
|
checks += 1
|
|
if not ok:
|
|
failures.append(label)
|
|
printerr("RIG_FAIL: ", label)
|
|
func _ready() -> void:
|
|
call_deferred("run")
|
|
func run() -> void:
|
|
var a: Node3D = AssetLibrary.instantiate_fighter("worker")
|
|
var b: Node3D = AssetLibrary.instantiate_fighter("worker")
|
|
add_child(a)
|
|
add_child(b)
|
|
check(a.skeleton.get_bone_count() == 27, "27 imported bones")
|
|
check(a.player != b.player and a.skeleton != b.skeleton, "independent skeleton and playback")
|
|
for clip: String in ["idle", "start", "walk", "stop", "strike", "pickup", "deposit", "return", "hit", "death"]:
|
|
check(a.clips.has(clip), "imported clip " + clip)
|
|
check(a.clip_length(clip) > .25, "clip duration " + clip)
|
|
var hand: int = a.skeleton.find_bone("Hand.L")
|
|
var rest: Quaternion = b.skeleton.get_bone_pose_rotation(hand)
|
|
a.preview("strike", .80)
|
|
check(a.skeleton.get_bone_pose_rotation(hand).angle_to(rest) > .05, "strike deforms weighted arm")
|
|
a.preview("pickup", .40)
|
|
var spine: int = a.skeleton.find_bone("Spine")
|
|
check(a.skeleton.get_bone_pose_rotation(spine).angle_to(Quaternion.IDENTITY) > .05, "pickup crouches spine")
|
|
a.preview("deposit", .60)
|
|
check(a.current_clip == "deposit" or a.clips.has("deposit"), "deposit clip available")
|
|
a.preview("idle", 0)
|
|
check(a.skeleton.get_bone_pose_rotation(hand).is_equal_approx(b.skeleton.get_bone_pose_rotation(hand)), "idle restores hands after work")
|
|
# Work cycle advances through tool contact, gather, deposit and return.
|
|
var seen: Dictionary = {}
|
|
for i in range(400):
|
|
a.update_work(1.0 / 60, 0.0)
|
|
seen[a.current_clip] = true
|
|
check(seen.has("strike") and seen.has("pickup") and seen.has("deposit") and seen.has("return"), "full work cycle plays")
|
|
check(seen.has("idle"), "idle gap between work steps")
|
|
# Render cadence and worker phase offsets must not duplicate contact effects.
|
|
for fps: int in [30, 60, 144]:
|
|
for phase: float in [0.0, .83, 1.66]:
|
|
var worker: Node3D = AssetLibrary.instantiate_fighter("worker")
|
|
add_child(worker)
|
|
var contacts: int = 0
|
|
var extra_contact: bool = false
|
|
var elapsed: float = 0.0
|
|
var duration: float = worker.cycle_length() * 3.0
|
|
while elapsed < duration - .000001:
|
|
var step: float = minf(1.0 / fps, duration - elapsed)
|
|
worker.update_work(step, phase)
|
|
elapsed += step
|
|
if worker.consume_strike_contact():
|
|
contacts += 1
|
|
worker.update_work(0.0, phase)
|
|
extra_contact = extra_contact or worker.consume_strike_contact()
|
|
check(contacts == 3 and not extra_contact, "%d FPS / phase %.2f: one contact per cycle, none while paused (contacts=%d, extra=%s)" % [fps, phase, contacts, extra_contact])
|
|
worker.free()
|
|
# Contact frame is on the strike clip, before follow-through.
|
|
a.preview("strike", 0)
|
|
var contact: float = .62 * a.clip_length("strike")
|
|
a.preview("strike", contact)
|
|
check(absf(a.player.current_animation_position - contact) < .02, "strike contact time is authored")
|
|
var core: Node3D = AssetLibrary.instantiate_fighter("core_team0")
|
|
add_child(core)
|
|
check(core.has_method("set_core_health") and core.has_method("trigger_activate"), "core exposes activation and health")
|
|
core.trigger_activate()
|
|
core.set_core_health(.4)
|
|
core.pose(0.5, false, 0, 0, 0, 1.0 / 60)
|
|
check(true, "core pose accepts damage state")
|
|
core.free()
|
|
a.free()
|
|
b.free()
|
|
var f := FileAccess.open("res://docs/phase12-rig-verification.json", FileAccess.WRITE)
|
|
f.store_string(JSON.stringify({"checks": checks, "failures": failures}, "\t"))
|
|
f.close()
|
|
print("RIG_PASS: ", checks, " checks; ", failures.size(), " failures")
|
|
await Soundscape.quit_game(0 if failures.is_empty() else 1)
|