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>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-18 03:19:42 +02:00
1 parent c8f1832eae
commit a0cd95cceb
9 files changed
+97 -18

No files matched your search

+4 -1
View File
@@ -37,7 +37,9 @@ export function createDummy({ template, clip, fingers, weapon = null, hand = "r"
if (!object.isMesh) return;
object.castShadow = true;
object.receiveShadow = true;
// Der Bogenschütze wandert bis 3 m von seinem Platz; die Hülle der Ruhepose schnitte ihn am Bildrand weg.
// Für ein Skinned Mesh ist die Hülle der Ruhepose ohnehin die falsche Grundlage, sobald Knochen die
// Figur bewegen — besonders sichtbar beim Bogenschützen, der bis 3 m von seinem Platz wandert. Deshalb
// für alle Dummies aus, nicht nur für ihn.
object.frustumCulled = false;
});
const mixer = new AnimationMixer(root);
@@ -46,6 +48,7 @@ export function createDummy({ template, clip, fingers, weapon = null, hand = "r"
let held = null;
if (weapon) {
if (!grip) throw new Error("Dummy mit Waffe ohne Griff");
const boneName = hand === "l" ? "hand_l" : "hand_r";
const bone = root.getObjectByName(boneName);
if (!bone) throw new Error(`Dummy ohne Knochen ${boneName}`);
+14 -2
View File
@@ -47,12 +47,24 @@ export const DUMMIES = [
];
/** Platz eines Dummys in Mannequin-Metern. Der Bogenschütze steht abseits, er wandert bis 3,13 m. */
export function slotPosition(index) {
export function slotPosition(index, dummies = DUMMIES) {
if (index < 8) return { x: FIRST_SLOT_X + ROW_SPACING * index, z: 0 };
if (DUMMIES[index]?.clip.startsWith("hunter_bowArrow")) return { x: BOW_X, z: -ROW_DEPTH };
if (dummies[index]?.clip.startsWith("hunter_bowArrow")) return { x: BOW_X, z: -ROW_DEPTH };
return { x: FIRST_SLOT_X + ROW_SPACING * (index - 8), z: -ROW_DEPTH };
}
/**
* Lage der Aufstellung: VIEW_DISTANCE Mannequin-Meter vor der Figur in Blickrichtung, +Z zurück zu ihr gedreht.
* position ist die Figurenposition (x, z genügen), view die bereits normalisierte waagrechte Blickrichtung.
*/
export function stageTransform(position, view, scale) {
return {
x: position.x + view.x * VIEW_DISTANCE * scale,
z: position.z + view.z * VIEW_DISTANCE * scale,
rotationY: Math.atan2(-view.x, -view.z),
};
}
/** Text des Schilds: Clip oben, Waffe darunter. */
export function labelLines(dummy) {
return [dummy.label, dummy.weapon ? WEAPONS[dummy.weapon].label : "ohne Waffe"];
+21 -7
View File
@@ -14,7 +14,7 @@ import { createSky } from "../world/sky.js";
import { createSunShadows } from "../world/shadows.js";
import { createDummy, fingerPoseClip } from "./dummies.js";
import { createLabel } from "./labels.js";
import { DUMMIES, labelLines, LABEL_HEIGHT, MOTIONS_URL, slotPosition, VIEW_DISTANCE, WEAPONS } from "./lineup.js";
import { DUMMIES, labelLines, LABEL_HEIGHT, MOTIONS_URL, slotPosition, stageTransform, WEAPONS } from "./lineup.js";
import { createShowroomPanel } from "./panel.js";
// Dieselben Werte wie in src/main.js (dort lokale Konstanten): Sonne in gemessener Stärke, Himmelslicht und
@@ -81,6 +81,14 @@ await player.init({
playerModelConfig: { ...PLAYER_MODEL_BASE, model: ual.scene, animations: ual.animations },
colliders: [{ motion: "static", shape: { kind: "mesh", source: ground } }],
});
// Wie in createCharacterModule (src/characters/index.js): Der Controller setzt beim Laden selbst keinen
// Schatten, und dieses Startmodell durchläuft applyCharacter nie — ohne diese Schleife bliebe die Spielfigur
// als einzige Figur der Seite ohne Schatten.
player.playerModel.traverse((object) => {
if (!object.isMesh) return;
object.castShadow = true;
object.receiveShadow = true;
});
// Der Controller skaliert das Modell selbst (playerCapsuleHeight × scale). Ausgelesen statt nachgerechnet:
// daran hängen Dummies, Waffen, Schilder und alle Abstände.
const figureScale = player.playerModel.getWorldScale(new Vector3()).x;
@@ -91,8 +99,15 @@ stage.scale.setScalar(figureScale);
scene.add(stage);
const clips = new Map(motions.animations.map((clip) => [clip.name, clip]));
const gripFingers = fingerPoseClip(AnimationClip.findByName(ual.animations, "Pistol_Idle_Loop"), "Finger_Waffe");
const looseFingers = fingerPoseClip(AnimationClip.findByName(ual.animations, "Idle_Loop"), "Finger_locker");
// Wie beim Clip-Lookup der Dummies unten: findByName gibt bei einem fehlenden Clip still null zurück, das
// bräche erst tief in fingerPoseClip mit einem unklaren TypeError ab.
function requireAnimation(name) {
const clip = AnimationClip.findByName(ual.animations, name);
if (!clip) throw new Error(`ual.glb ohne Clip ${name}`);
return clip;
}
const gripFingers = fingerPoseClip(requireAnimation("Pistol_Idle_Loop"), "Finger_Waffe");
const looseFingers = fingerPoseClip(requireAnimation("Idle_Loop"), "Finger_locker");
const weaponScenes = new Map(weaponIds.map((id, index) => [id, weaponFiles[index].scene]));
const weaponObjects = new Map();
const dummies = DUMMIES.map((entry, index) => {
@@ -130,10 +145,9 @@ function placeStage() {
view.y = 0;
if (view.lengthSq() < 1e-6) return false;
view.normalize();
const position = player.playerCapsule.position;
stage.position.set(
position.x + view.x * VIEW_DISTANCE * figureScale, 0, position.z + view.z * VIEW_DISTANCE * figureScale);
stage.rotation.y = Math.atan2(-view.x, -view.z);
const { x, z, rotationY } = stageTransform(player.playerCapsule.position, view, figureScale);
stage.position.set(x, 0, z);
stage.rotation.y = rotationY;
return true;
}
+3 -1
View File
@@ -15,6 +15,8 @@ export function createShowroomPanel({ playback, weaponObjects }) {
const folder = gui.addFolder("Griff");
const ids = Object.keys(WEAPONS);
// lil-gui zeigt bei einem Objekt { Beschriftung: Wert } die Schlüssel statt der IDs im Dropdown an.
const weaponOptions = Object.fromEntries(ids.map((id) => [WEAPONS[id].label, id]));
const edit = { weapon: ids[0], x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0 };
const sliders = [];
const load = () => {
@@ -29,7 +31,7 @@ export function createShowroomPanel({ playback, weaponObjects }) {
grip.rotation = [edit.rx, edit.ry, edit.rz];
for (const object of weaponObjects.get(edit.weapon) ?? []) applyGrip(object, grip);
};
folder.add(edit, "weapon", ids).name("Waffe").onChange(load);
folder.add(edit, "weapon", weaponOptions).name("Waffe").onChange(load);
for (const axis of ["x", "y", "z"]) {
sliders.push(folder.add(edit, axis, -0.3, 0.3, 0.001).name(`Position ${axis} (m)`).onChange(apply));
}