fix(ui): Höhe-Regler nachführen, Teleport-Warnfrist, SUV-Abstimmung
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
182db238c6
commit
0ece90e2ec
3 files changed
+105
-4
No files matched your search
+39
-2
@@ -146,6 +146,12 @@ async function spawnVehicles() {
|
|||||||
if (!child.isMesh) return;
|
if (!child.isMesh) return;
|
||||||
child.castShadow = true;
|
child.castShadow = true;
|
||||||
child.receiveShadow = 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
|
// 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
|
// 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.
|
// 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;
|
let pendingTeleport = null;
|
||||||
function requestTeleport(position) {
|
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.setSkipCapsuleCollision(true);
|
||||||
player.reset(new Vector3(position.x, position.y, position.z));
|
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.
|
// Meeresboden: man watet hinein, statt endlos zu fallen. Einmalig, nicht je Chunk.
|
||||||
const seafloor = new Mesh(new BoxGeometry(4000, 1, 4000), materials.seafloor);
|
const seafloor = new Mesh(new BoxGeometry(4000, 1, 4000), materials.seafloor);
|
||||||
@@ -234,6 +265,12 @@ function tick(delta) {
|
|||||||
pendingTeleport = null;
|
pendingTeleport = null;
|
||||||
} else {
|
} else {
|
||||||
player.reset(new Vector3(pendingTeleport.x, pendingTeleport.y, pendingTeleport.z));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-2
@@ -139,6 +139,22 @@ async function buildMapMesh(manifest, baseUrl) {
|
|||||||
return mesh;
|
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,
|
* 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
|
* 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 gui = new GUI({ title: "Debug" });
|
||||||
|
|
||||||
const characterFolder = gui.addFolder("Figur");
|
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")
|
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")
|
characterFolder.add(params, "hair", [HAIR_NONE, ...HAIRSTYLES]).name("Frisur")
|
||||||
.onChange((value) => { void characterModule.switchHair(value); });
|
.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));
|
.onChange((value) => characterModule.applyHeight(value));
|
||||||
characterFolder.add(params, "showHitbox").name("Hitbox anzeigen")
|
characterFolder.add(params, "showHitbox").name("Hitbox anzeigen")
|
||||||
.onChange((value) => player.setPlayerCapsuleDebug?.(value));
|
.onChange((value) => player.setPlayerCapsuleDebug?.(value));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user