fix(world): leere Chunks, Bodensignal und Fall-Auffang-Schleife (Fix-Runde 1)

Ruling 51: #load() rief addCollider() auch für Chunks ohne jede Kollisions-
geometrie auf (offenes Wasser: 4_6, 4_10, 5_6, 5_10 — an den Rohdaten
nachgezählt). Das lieferte nur eine Fremdwarnung aus ColliderRegistry und
ein handle mit id:-1, das nie ready wird. addCollider läuft jetzt nur noch,
wenn die collide-Gruppe Kinder hat; #unload und isReadyAt behandeln einen
Eintrag ohne handle korrekt (kein removeCollider nötig, sofort "bereit").

Ruling 52: lastSafe hielt sich an der Höhe fest (p.y > -1), nicht am
tatsächlichen Bodenkontakt — der Scheitel eines Sprungs oder ein Dach
zählten als "sicher". Prüfung im Submodul ergab: playerIsOnGround (public,
via setOnGround() nur aus snapToGround() auf true gesetzt) mit passendem
Getter getIsOnGround(). lastSafe aktualisiert sich jetzt nur noch bei
echtem Bodenkontakt.

Ruling 53: setSkipCapsuleCollision(true) schaltet nur die horizontale
Kollision ab, applyGravity läuft unabhängig weiter — die "Schwebephase"
war freier Fall. Die alte 15s-Zeitschranke schaltete die Kollision ohne
vorhandenen Boden scharf, was bei dauerhaft fehlendem Chunk in eine
endlose Fall-Auffang-Schleife lief. Die Figur wird jetzt aktiv auf
Spawnhöhe gehalten, bis ihr Chunk bereit ist; nach 15s nur noch eine
einmalige Warnung, kein Verhaltenswechsel. Per künstlicher 8s-Verzögerung
gemessen: Höhe blieb über den gesamten (durch Worker-Pool-Stau auf über
14s verlängerten) Wartezeitraum innerhalb 0,004 Einheiten der Spawnhöhe.

Neue Tests in tests/streaming.test.js decken Ruling 51 ab (ChunkStreamer
mit gemocktem fetch/player/scene); Ruling 52/53 sind Physik-Verhalten des
Submoduls und über den Browser verifiziert (Bericht: task-8-report.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-12 21:18:23 +02:00
1 parent f7041dab91
commit 289bbbba1d
3 files changed
+102 -15

No files matched your search

+25 -8
View File
@@ -54,25 +54,42 @@ await player.init({
});
// Chunk-Collider treffen erst nach und nach ein (Fetch + asynchroner BVH-Bau, siehe
// streaming.js). Bis der Chunk unter dem Spawn bereit ist — oder spätestens nach 15s —
// bleibt die Kapselkollision aus, damit die Figur nicht an halb geladenen Gebäuden hängen
// bleibt; das Auffangnetz unten fängt jeden tieferen Durchfall ab (auch während dieser Zeit).
// streaming.js). setSkipCapsuleCollision(true) schaltet nur das horizontale Herausdrücken
// ab; applyGravity läuft im Controller unabhängig davon weiter (CharacterMovement.ts), die
// Kapsel würde also binnen Sekunden frei fallen, wenn man sie nur "schweben" ließe. Solange
// der Chunk unter dem Spawn nicht bereit ist, hält tick() sie deshalb aktiv auf Spawnhöhe
// fest (Ruling 53) statt sie fallen und vom Auffangnetz auffangen zu lassen — eine feste
// Zeitschranke, die danach die Kollision ohne vorhandenen Boden einschaltet, würde in eine
// endlose Fall-Auffang-Schleife laufen, sobald der Chunk dauerhaft ausbleibt. Nach 15s gibt
// es nur noch eine einmalige Warnung, kein Verhaltenswechsel.
const streamer = new ChunkStreamer({ scene, player, materials });
player.setSkipCapsuleCollision(true);
let settled = false;
let warnedSlowSpawn = false;
const startedAt = performance.now();
let lastSafe = { ...SPAWN };
function tick(delta) {
streamer.update(player.playerCapsule.position);
if (!settled && (streamer.isReadyAt(player.playerCapsule.position) || performance.now() - startedAt > 15000)) {
settled = true;
player.setSkipCapsuleCollision(false);
player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z));
if (!settled) {
if (streamer.isReadyAt(player.playerCapsule.position)) {
settled = true;
player.setSkipCapsuleCollision(false);
player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z));
} else {
player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z));
if (!warnedSlowSpawn && performance.now() - startedAt > 15000) {
warnedSlowSpawn = true;
console.warn(`Chunk am Spawn (${SPAWN.x}, ${SPAWN.z}) ist nach 15s immer noch nicht bereit.`);
}
}
return;
}
const p = player.playerCapsule.position;
// 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.
if (p.y < -30) player.reset(new Vector3(lastSafe.x, lastSafe.y, lastSafe.z));
else if (p.y > -1) lastSafe = { x: p.x, y: p.y + 1, z: p.z };
else if (player.getIsOnGround()) lastSafe = { x: p.x, y: p.y + 1, z: p.z };
}
const timer = new Timer();