Files
gta6/tests/dummies.test.js
knsoandClaude Opus 5 a0cd95cceb fix(showroom): Funde der Schlussdurchsicht auf der Testseite behoben
Die Spielfigur wirft jetzt einen Schatten (player.init durchlief applyCharacter
nie); drei stille Fehlerpfade meckern jetzt statt roh abzubrechen (fehlender
Clip fuer die Fingerhaltung, Waffe ohne Griff, mit neuem Test); die
Waffenauswahl im Panel zeigt Beschriftungen statt IDs; der Waffen-Umbau prueft
jetzt die Meshzahl je Datei; stageTransform ist aus main.js nach lineup.js
gezogen und mit dem im Browser gemessenen Fall getestet; slotPosition nimmt
DUMMIES als Argument statt es aus dem Modul zu lesen; dazu Kommentar- und
Entwurfskorrekturen (Docstring, Beckenspur-Test, Frustum-Culling, Port/Kamera/
VIEW_DISTANCE im Entwurf).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 03:19:42 +02:00

122 lines
5.4 KiB
JavaScript

import { describe, expect, it } from "vitest";
import { AnimationClip, Bone, BoxGeometry, Group, Mesh, MeshBasicMaterial, Object3D,
QuaternionKeyframeTrack, VectorKeyframeTrack } from "three";
import { applyGrip, createDummy, fingerPoseClip } from "../src/showroom/dummies.js";
const source = new AnimationClip("Pistol_Idle_Loop", 2, [
new QuaternionKeyframeTrack("index_01_r.quaternion", [0, 1], [0, 0, 0.3826834, 0.9238795, 0, 0, 0, 1]),
new QuaternionKeyframeTrack("thumb_03_l.quaternion", [0, 2], [0.1, 0, 0, 0.9949874, 0, 0, 0, 1]),
new QuaternionKeyframeTrack("index_04_leaf_r.quaternion", [0, 1], [0, 0, 0, 1, 0, 0, 0, 1]),
new QuaternionKeyframeTrack("hand_r.quaternion", [0, 1], [0, 0, 0, 1, 0, 0, 0, 1]),
new VectorKeyframeTrack("index_01_r.position", [0, 1], [0, 0, 0, 1, 1, 1]),
]);
describe("fingerPoseClip", () => {
// Bruch: Hand oder Blattknochen mitgenommen — die Fingerhaltung würde die Mocap-Bewegung überschreiben.
it("behält nur die Drehungen der Fingerknochen", () => {
expect(fingerPoseClip(source).tracks.map((track) => track.name).sort())
.toEqual(["index_01_r.quaternion", "thumb_03_l.quaternion"]);
});
it("hält den ersten Keyframe über die ganze Dauer", () => {
const clip = fingerPoseClip(source, "Finger_Waffe");
const track = clip.tracks.find((entry) => entry.name === "index_01_r.quaternion");
expect(clip.name).toBe("Finger_Waffe");
expect(clip.duration).toBe(1);
expect([...track.times]).toEqual([0, 1]);
const expected = Float32Array.from([0, 0, 0.3826834, 0.9238795, 0, 0, 0.3826834, 0.9238795]);
expect([...track.values]).toEqual([...expected]);
});
it("meldet einen Clip ohne Fingerspuren", () => {
const empty = new AnimationClip("Leer", 1, [
new QuaternionKeyframeTrack("hand_r.quaternion", [0, 1], [0, 0, 0, 1, 0, 0, 0, 1]),
]);
expect(() => fingerPoseClip(empty)).toThrow(/keine Fingerspuren/);
});
});
describe("applyGrip", () => {
it("setzt Position in Metern und Drehung in Grad", () => {
const object = new Object3D();
applyGrip(object, { position: [0.01, -0.02, 0.03], rotation: [90, 0, -45] });
expect(object.position.toArray()).toEqual([0.01, -0.02, 0.03]);
expect(object.rotation.x).toBeCloseTo(Math.PI / 2, 12);
expect(object.rotation.z).toBeCloseTo(-Math.PI / 4, 12);
expect(object.rotation.order).toBe("XYZ");
});
});
describe("createDummy", () => {
const WEAPON_GRIP = { position: [0, 0, 0], rotation: [0, 0, 0] };
function template() {
const root = new Group();
const pelvis = new Bone();
pelvis.name = "pelvis";
const right = new Bone();
right.name = "hand_r";
const left = new Bone();
left.name = "hand_l";
// Ziele der Fingerspuren aus `fingers` unten (index_01_r, thumb_03_l) — ohne sie meldet three.js beim
// Binden für jeden Testlauf "No target node found for track".
const indexR = new Bone();
indexR.name = "index_01_r";
const thumbL = new Bone();
thumbL.name = "thumb_03_l";
root.add(pelvis);
pelvis.add(right);
pelvis.add(left);
right.add(indexR);
left.add(thumbL);
return root;
}
const clip = new AnimationClip("take", 1, [
new QuaternionKeyframeTrack("pelvis.quaternion", [0, 1], [0, 0, 0, 1, 0, 0, 0, 1]),
]);
const fingers = fingerPoseClip(source, "Finger_Waffe");
const weapon = new Group();
weapon.add(new Mesh(new BoxGeometry(0.2, 0.05, 0.02), new MeshBasicMaterial()));
// Bruch: Waffe an der falschen Hand oder ohne Griff — sie steckte im Unterarm.
it("hängt eine eigene Kopie der Waffe mit Griff an die gewählte Hand", () => {
const dummy = createDummy({
template: template(), clip, fingers, weapon, hand: "l",
grip: { position: [0.1, 0, 0], rotation: [0, 90, 0] },
});
expect(dummy.weapon.parent.name).toBe("hand_l");
expect(dummy.weapon).not.toBe(weapon);
expect(dummy.weapon.position.x).toBeCloseTo(0.1, 12);
expect(dummy.weapon.rotation.y).toBeCloseTo(Math.PI / 2, 12);
});
it("spielt Clip und Fingerhaltung zugleich, lässt leere Hände leer und gibt den Clip zurück", () => {
const dummy = createDummy({ template: template(), clip, fingers });
expect(dummy.weapon).toBeNull();
expect(dummy.clip).toBe(clip);
expect(dummy.mixer.existingAction(clip).isRunning()).toBe(true);
expect(dummy.mixer.existingAction(fingers).isRunning()).toBe(true);
});
it("meldet eine fehlende Hand", () => {
const root = new Group();
const pelvis = new Bone();
pelvis.name = "pelvis";
const indexR = new Bone();
indexR.name = "index_01_r";
const thumbL = new Bone();
thumbL.name = "thumb_03_l";
root.add(pelvis);
pelvis.add(indexR);
pelvis.add(thumbL);
expect(() => createDummy({ template: root, clip, fingers, weapon, hand: "r", grip: WEAPON_GRIP }))
.toThrow(/Dummy ohne Knochen hand_r/);
});
// Bruch: Waffe ohne Griff — applyGrip bekäme kein Objekt und bräche mit einem rohen TypeError ab.
it("meldet eine Waffe ohne Griff", () => {
expect(() => createDummy({ template: template(), clip, fingers, weapon, hand: "r", grip: null }))
.toThrow(/Waffe ohne Griff/);
});
});