fix(world): Chunk-Streaming, Auffangnetz und Schatten folgen dem Fahrzeug

playerCapsule.position ist beim Fahren nicht die Weltposition, sondern der lokale
Sitzplatz-Offset (syncMountedPlayer hängt die Kapsel an vehicleGroup und überschreibt
ihre .position mit driverSeatPosition * scale). main.js trieb Streaming, isReadyAt,
das Auffangnetz, lastSafe und den Schattenwurf trotzdem ausnahmslos damit an — die
Welt hörte beim Fahren auf nachzuladen.

src/player.js bündelt "wo ist der Spieler gerade" in playerPosition()/
playerIsGrounded(): Fahrzeugposition (vehicleGroup.position) und Radkontakt, wenn
eines aktiv ist, sonst Kapsel und das vorhandene getIsOnGround(). Zusätzlich über
getControllerMode() abgesichert, nicht nur getActiveVehicle() — Letzteres wird beim
Aussteigen vom Controller nie zurückgesetzt (nur destroy() tut das), im Browser
nachgewiesen: nach einem erzwungenen Ausstieg lieferte getActiveVehicle() weiterhin
das verlassene Fahrzeug, obwohl controllerMode schon wieder 0 war.

Im Browser mit von Hand angetriebenem tick()/player.update() verifiziert (die
automatisierte Vorschau drosselt requestAnimationFrame auf ~2 Hz): Fahrzeug 227
Einheiten bewegt, playerPosition() folgte exakt, vier neue Chunks luden nach
(Netzwerk-Log bestätigt), sichtbar andere Umgebung nach der Fahrt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-13 05:13:54 +02:00
1 parent e179794967
commit b273bb09cc
3 files changed
+193 -6

No files matched your search

+29 -6
View File
@@ -9,6 +9,7 @@ import { createMaterials } from "./world/materials.js";
import { PropLayer } from "./world/props.js";
import { chunkForWorld, threeToWorld } from "./world/coords.js";
import { createCharacterModule } from "./characters/index.js";
import { playerIsGrounded, playerPosition } from "./player.js";
import { createPanel } from "./ui/panel.js";
import { buildAttribution, mountHud } from "./ui/hud.js";
@@ -214,9 +215,16 @@ let lastSafe = { ...SPAWN };
let vehiclesSpawned = false;
function tick(delta) {
streamer.update(player.playerCapsule.position);
// Befund A (Fix-Runde 3): playerPosition() statt player.playerCapsule.position an jeder
// der sechs Stellen, die "wo ist der Spieler" brauchen — sitzt er in einem Fahrzeug, hängt
// die Kapsel als Kind an dessen vehicleGroup und ihre .position ist nur noch der lokale
// Sitzplatz-Offset, nicht die Weltposition (Begründung in src/player.js). Ohne diese
// Umstellung hört das Streaming beim Fahren auf nachzuladen.
streamer.update(playerPosition(player));
if (!settled) {
if (streamer.isReadyAt(player.playerCapsule.position)) {
// Vehicles existieren hier noch nicht (vehiclesSpawned kommt erst nach settled) —
// playerPosition() liefert also zwangsläufig die Kapsel, wie zuvor.
if (streamer.isReadyAt(playerPosition(player))) {
settled = true;
player.setSkipCapsuleCollision(false);
player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z));
@@ -238,8 +246,12 @@ function tick(delta) {
}
// Sprungziel wird gehalten, bis sein Chunk bereit ist (s. Kommentar bei requestTeleport).
// requestTeleport() ruft player.reset() synchron auf, noch bevor dieser tick() läuft, und
// reset() steigt bei aktivem Fahrzeug zuerst aus (controllerMode -> 0, s. PlayerController.
// reset()) — hier kann also nie ein Fahrzeug aktiv sein. playerPosition() ändert an diesem
// Aufruf inhaltlich nichts, macht die Stelle aber konsistent mit den übrigen fünf.
if (pendingTeleport) {
if (streamer.isReadyAt(player.playerCapsule.position)) {
if (streamer.isReadyAt(playerPosition(player))) {
player.setSkipCapsuleCollision(false);
pendingTeleport = null;
} else {
@@ -254,11 +266,20 @@ function tick(delta) {
}
}
const p = player.playerCapsule.position;
// Befund A: p muss die Fahrzeugposition sein, sobald eines aktiv ist — sonst sieht das
// Auffangnetz ein abstürzendes Fahrzeug nie fallen (die Kapsel hängt ja am bedeutungslosen
// Sitzplatz-Offset fest, dessen y sich nie in die Nähe von -30 bewegt), und lastSafe würde
// mit genau dieser bedeutungslosen Zahl überschrieben.
const p = playerPosition(player);
// Ruling 52: an ein echtes Bodensignal binden statt an die Höhe — sonst würde auch der
// Scheitelpunkt eines Sprungs, ein Dach oder ein Moment im freien Fall als "sicher" gelten.
// playerIsGrounded() zieht dasselbe Prinzip fahrzeugbewusst durch: player.getIsOnGround()
// friert beim Fahren ein (controllerMode 1 durchläuft nie den Fußgänger-Codepfad, der ihn
// setzt, bliebe also auf "true" von kurz vor dem Einsteigen stehen) und würde lastSafe
// sonst jeden Frame blind fortschreiben, selbst wenn das Fahrzeug gerade über einer Klippe
// oder einer noch ungeladenen Lücke schwebt — exakt das, was Ruling 52 verhindern sollte.
if (p.y < -30) player.reset(new Vector3(lastSafe.x, lastSafe.y, lastSafe.z));
else if (player.getIsOnGround()) lastSafe = { x: p.x, y: p.y + 1, z: p.z };
else if (playerIsGrounded(player)) lastSafe = { x: p.x, y: p.y + 1, z: p.z };
}
const timer = new Timer();
@@ -268,7 +289,9 @@ renderer.setAnimationLoop((t) => {
const delta = Math.min(timer.getDelta(), 1 / 40);
player.update(delta);
tick(delta);
updateShadow(player.playerCapsule.position);
// Befund A: Schatten folgen der aktuellen Position (Fahrzeug beim Fahren, sonst Kapsel) —
// sonst bliebe die Schattenkamera beim Fahren auf dem Einstiegspunkt stehen.
updateShadow(playerPosition(player));
materials.water.normalMap.offset.x += delta * 0.01;
renderer.render(scene, camera);
});