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

+15 -5
View File
@@ -72,9 +72,15 @@ export class ChunkStreamer {
group.add(collide);
this.scene.add(group);
group.updateMatrixWorld(true);
const handle = this.player.addCollider({
motion: "static", useWorker: true, shape: { kind: "mesh", source: collide },
});
// Ruling 51: ein Chunk ganz ohne Kollisionsgeometrie (offenes Wasser, z.B. 4_6/4_10/
// 5_6/5_10 — per public/data/vice-a nachgezählt) braucht keinen Collider. addCollider
// auf einer leeren Gruppe liefert nur eine Fremdwarnung und ein handle mit id:-1, das
// nie ready wird — der Chunk bleibt trotzdem in active, nur ohne Collider-Handle.
const handle = collide.children.length > 0
? this.player.addCollider({
motion: "static", useWorker: true, shape: { kind: "mesh", source: collide },
})
: null;
this.active.set(k, { group, handle, chunk });
} catch (err) {
console.warn(`Chunk ${k} konnte nicht geladen werden:`, err);
@@ -97,7 +103,8 @@ export class ChunkStreamer {
#unload(k) {
const entry = this.active.get(k);
if (!entry) return;
this.player.removeCollider(entry.handle);
// Ruling 51: collider-lose Chunks (offenes Wasser) haben kein handle abzumelden.
if (entry.handle) this.player.removeCollider(entry.handle);
this.scene.remove(entry.group);
entry.group.traverse((o) => o.geometry?.dispose());
this.active.delete(k);
@@ -108,6 +115,9 @@ export class ChunkStreamer {
const i = Math.floor((position.x - SLICE.x0) / CHUNK_SIZE);
const j = Math.floor((SLICE.y1 + position.z) / CHUNK_SIZE);
const entry = this.active.get(key(i, j));
return Boolean(entry && this.player.collisionWorld.get(entry.handle.id)?.ready);
if (!entry) return false;
// Ruling 51: kein Collider nötig (leerer Chunk) -> nichts, worauf noch zu warten wäre.
if (!entry.handle) return true;
return Boolean(this.player.collisionWorld.get(entry.handle.id)?.ready);
}
}