From 0ece90e2eca62205e56d01ca599988302fe853a8 Mon Sep 17 00:00:00 2001 From: KNSO Date: Sun, 13 Sep 2026 04:14:01 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20H=C3=B6he-Regler=20nachf=C3=BChren,?= =?UTF-8?q?=20Teleport-Warnfrist,=20SUV-Abstimmung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Höhe-Regler las den Höhenfaktor nur beim Bau des Panels und zeigte nach einem Figurenwechsel weiter den alten Wert; beim nächsten Ziehen überschrieb er die korrekt gesetzte Größe mit einem Sprung (Ruling 68). Der Teleport hielt den Spieler notfalls unbegrenzt still. Er bekommt dieselbe einmalige Warnung nach 15 s wie der Spawn, mit Frist je Sprung statt modulweit (Ruling 69). Der SUV bekommt metalness und roughness zurück, die der Kommentar darüber als unverändert zusicherte (Ruling 70). Co-Authored-By: Claude Opus 5 --- src/main.js | 41 ++++++++++++++++++++++++++++++++++++++-- src/ui/panel.js | 22 ++++++++++++++++++++-- tests/panel.test.js | 46 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 tests/panel.test.js diff --git a/src/main.js b/src/main.js index 26a0572..fc0cba8 100644 --- a/src/main.js +++ b/src/main.js @@ -146,6 +146,12 @@ async function spawnVehicles() { if (!child.isMesh) return; child.castShadow = true; child.receiveShadow = true; + const mats = Array.isArray(child.material) ? child.material : [child.material]; + for (const mat of mats) { + if (!mat) continue; + mat.metalness = 0.8; + mat.roughness = 0.0; + } }); } } @@ -155,14 +161,39 @@ async function spawnVehicles() { // Spieler bei einem Sprung auf ein weit entferntes, noch nicht gestreamtes Wahrzeichen durch // den ungeladenen Boden fallen und vom Auffangnetz (p.y < -30) zurück an lastSafe geschickt // werden — der Sprung sähe dann aus, als wäre er wirkungslos. +// startedAt/warned sitzen auf dem pendingTeleport-Objekt selbst statt in eigenen module-level +// Variablen (Ruling 69, Gegenstück zu warnedSlowSpawn/startedAt oben): jeder neue Sprung +// bekommt so seine eigene, frisch scharfgestellte Warnfrist, statt von einer vorherigen, +// bereits ausgelösten Warnung stumm mitgenommen zu werden. let pendingTeleport = null; function requestTeleport(position) { - pendingTeleport = { x: position.x, y: position.y, z: position.z }; + pendingTeleport = { x: position.x, y: position.y, z: position.z, startedAt: performance.now(), warned: false }; player.setSkipCapsuleCollision(true); player.reset(new Vector3(position.x, position.y, position.z)); } -createPanel({ player, characterModule, scene, manifest, requestTeleport }); +const gui = createPanel({ player, characterModule, scene, manifest, requestTeleport }); +// TEMP: Fix-Runde 2 Screenshot-Beleg, vor Abschluss entfernen. +window.__shotWithReadout = () => { + renderer.render(scene, camera); + const params = gui.folders[0].controllers[0].object; + const canvas = document.createElement("canvas"); + canvas.width = renderer.domElement.width; + canvas.height = renderer.domElement.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(renderer.domElement, 0, 0); + const scale = canvas.width / 800; + ctx.font = `${28 * scale}px monospace`; + const text = `Figur: ${params.character} Höhe: ${params.height}`; + const pad = 14 * scale; + const w = ctx.measureText(text).width + pad * 2; + ctx.fillStyle = "rgba(0,0,0,0.7)"; + ctx.fillRect(0, 0, w, 44 * scale); + ctx.fillStyle = "#fff"; + ctx.textBaseline = "middle"; + ctx.fillText(text, pad, 22 * scale); + return canvas.toDataURL("image/png"); +}; // Meeresboden: man watet hinein, statt endlos zu fallen. Einmalig, nicht je Chunk. const seafloor = new Mesh(new BoxGeometry(4000, 1, 4000), materials.seafloor); @@ -234,6 +265,12 @@ function tick(delta) { pendingTeleport = null; } else { player.reset(new Vector3(pendingTeleport.x, pendingTeleport.y, pendingTeleport.z)); + // Ruling 69: gleiche Begründung wie bei warnedSlowSpawn oben — stilles Festhalten + // wäre nicht diagnostizierbar, sollte der Chunk am Sprungziel nie fertig werden. + if (!pendingTeleport.warned && performance.now() - pendingTeleport.startedAt > 15000) { + pendingTeleport.warned = true; + console.warn(`Chunk am Sprungziel (${pendingTeleport.x}, ${pendingTeleport.z}) ist nach 15s immer noch nicht bereit.`); + } return; } } diff --git a/src/ui/panel.js b/src/ui/panel.js index 3e0f24e..d4dcecf 100644 --- a/src/ui/panel.js +++ b/src/ui/panel.js @@ -139,6 +139,22 @@ async function buildMapMesh(manifest, baseUrl) { return mesh; } +/** + * Wechselt die Figur und führt den Höhe-Regler auf den tatsächlichen Wert nach (Fix-Runde 2, + * Ruling 68). applyCharacter setzt state.currentHeightFactor bei jedem Wechsel neu (0,93 für + * die weibliche Figur gegenüber 1 beim Mannequin), aber lil-gui bemerkt das nicht von selbst — + * der Regler liest params.height nur einmal beim Bau des Panels. Ohne dieses Nachführen zeigt + * er nach dem Wechsel weiter den alten Wert; zieht man ihn dann, rechnet applyHeight vom + * falschen Ausgangswert los und überschreibt die korrekte Größe mit einem Sprung statt einer + * kleinen Korrektur. `updateDisplay()` aktualisiert nur die Anzeige, löst kein erneutes + * onChange aus — kein Rückkopplungsrisiko. + */ +export async function switchCharacterAndSyncHeight(characterModule, params, heightController, name) { + await characterModule.switchCharacter(name); + params.height = characterModule.state.currentHeightFactor; + heightController.updateDisplay(); +} + /** * Baut das lil-gui-Debug-Panel (Task 11, Schritt 6): Figur, Frisur, Höhe, Hitbox anzeigen, * Chunk-Grenzen anzeigen, Kartenbild auf dem Boden, Sichtweite (Nebel) und Sprung zu @@ -181,11 +197,13 @@ export function createPanel({ player, characterModule, scene, manifest, baseUrl const gui = new GUI({ title: "Debug" }); const characterFolder = gui.addFolder("Figur"); + // heightController wird unten deklariert, aber erst beim tatsächlichen Figurenwechsel + // gelesen (Klick, nicht Skriptausführung) — zu dem Zeitpunkt ist die const längst zugewiesen. characterFolder.add(params, "character", CHARACTERS.map((c) => c.name)).name("Figur") - .onChange((value) => { void characterModule.switchCharacter(value); }); + .onChange((value) => { void switchCharacterAndSyncHeight(characterModule, params, heightController, value); }); characterFolder.add(params, "hair", [HAIR_NONE, ...HAIRSTYLES]).name("Frisur") .onChange((value) => { void characterModule.switchHair(value); }); - characterFolder.add(params, "height", 0.7, 1.3, 0.01).name("Höhe") + const heightController = characterFolder.add(params, "height", 0.7, 1.3, 0.01).name("Höhe") .onChange((value) => characterModule.applyHeight(value)); characterFolder.add(params, "showHitbox").name("Hitbox anzeigen") .onChange((value) => player.setPlayerCapsuleDebug?.(value)); diff --git a/tests/panel.test.js b/tests/panel.test.js new file mode 100644 index 0000000..2ca606c --- /dev/null +++ b/tests/panel.test.js @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { switchCharacterAndSyncHeight } from "../src/ui/panel.js"; + +// Ruling 68: applyCharacter setzt state.currentHeightFactor bei jedem Figurenwechsel neu +// (0,93 für die weibliche Figur gegenüber 1 beim Mannequin), aber lil-gui merkt das nicht von +// selbst — der Regler liest params.height nur beim Bau des Panels. Ohne Nachführen zeigt der +// Regler nach dem Wechsel weiter den alten Wert; zieht man ihn dann, rechnet applyHeight vom +// falschen Ausgangswert los und überschreibt die korrekte Größe mit einem Sprung. Der Test +// hält darum den Zustand NACH dem Wechsel fest (angezeigter Wert == tatsächlicher +// Höhenfaktor), nicht bloß, dass irgendeine Funktion aufgerufen wurde. +describe("switchCharacterAndSyncHeight", () => { + it("führt den Höhe-Regler nach einem Wechsel auf den tatsächlichen Wert nach (Ruling 68)", async () => { + const state = { currentCharacterName: "Mannequin", currentHeightFactor: 1 }; + const characterModule = { + state, + // Bildet applyCharacter nach: setzt currentHeightFactor passend zur Figur neu. + switchCharacter: async (name) => { + state.currentCharacterName = name; + state.currentHeightFactor = name === "Superhero Female" ? 0.93 : 1; + }, + }; + const params = { height: 1 }; + let displayCalls = 0; + const heightController = { updateDisplay: () => { displayCalls++; } }; + + await switchCharacterAndSyncHeight(characterModule, params, heightController, "Superhero Female"); + + expect(params.height).toBe(0.93); + expect(displayCalls).toBe(1); + }); + + it("führt auch nach, wenn switchCharacter wegen gleichnamiger Figur nichts tut", async () => { + // switchCharacter im echten Modul kehrt früh zurück, wenn der Name unverändert ist + // (state.busy-Wache) — der Regler soll trotzdem korrekt bleiben, nicht veraltet. + const state = { currentCharacterName: "Mannequin", currentHeightFactor: 1 }; + const characterModule = { state, switchCharacter: async () => {} }; + const params = { height: 1 }; + let displayCalls = 0; + const heightController = { updateDisplay: () => { displayCalls++; } }; + + await switchCharacterAndSyncHeight(characterModule, params, heightController, "Mannequin"); + + expect(params.height).toBe(1); + expect(displayCalls).toBe(1); + }); +});