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:
1 parent
f7041dab91
commit
289bbbba1d
3 files changed
+102
-15
No files matched your search
+62
-2
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { wantedChunks } from "../src/world/streaming.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ChunkStreamer, wantedChunks } from "../src/world/streaming.js";
|
||||
|
||||
describe("wantedChunks", () => {
|
||||
it("lädt die Umgebung des Spielers", () => {
|
||||
@@ -27,3 +27,63 @@ describe("wantedChunks", () => {
|
||||
expect(wantedChunks({ x: 0, z: -1000 }, { load: 400, unload: 600, active }).add).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChunkStreamer", () => {
|
||||
// Realer Chunk ohne jede Geometrie — offenes Wasser. Entspricht wörtlich chunk_4_6.json
|
||||
// (eines von vier: 4_6, 4_10, 5_6, 5_10 — per public/data/vice-a nachgezählt, Ruling 51).
|
||||
const emptyChunk = { chunk: [4, 6], origin: [0, -1000], land: [], surfaces: [], roads: [], buildings: [] };
|
||||
// Zentrum von Chunk 4_6 in three-Koordinaten: SLICE.x0 + 4.5*CHUNK_SIZE, -(SLICE.y1 - 6.5*CHUNK_SIZE).
|
||||
const emptyChunkCenter = { x: 125, z: -875 };
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function makeStreamer(chunkData) {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => chunkData })));
|
||||
const player = {
|
||||
addCollider: vi.fn(() => ({ id: 42 })),
|
||||
removeCollider: vi.fn(),
|
||||
// ready:false hier ist bewusst: ein echter (nicht-null) Handle wäre damit NIE bereit,
|
||||
// sodass "isReadyAt sofort true" nur bestehen kann, wenn Ruling 51 tatsächlich greift
|
||||
// und gar kein Handle erst registriert wird — sonst wäre der Test kein Regressionstest.
|
||||
collisionWorld: { get: vi.fn(() => ({ ready: false })) },
|
||||
};
|
||||
const scene = { add: vi.fn(), remove: vi.fn() };
|
||||
return { streamer: new ChunkStreamer({ scene, player, materials: {} }), player, scene };
|
||||
}
|
||||
|
||||
// #load() ist privat und läuft nebenläufig; zwei Mikrotask-Runden reichen, um
|
||||
// fetch() -> res.json() -> Geometrieaufbau vollständig abzuwarten (siehe #fetchChunk/#load).
|
||||
const flush = () => new Promise((r) => setTimeout(r, 0)).then(() => new Promise((r) => setTimeout(r, 0)));
|
||||
|
||||
it("registriert für einen Chunk ganz ohne Kollisionsgeometrie keinen Collider (Ruling 51)", async () => {
|
||||
const { streamer, player } = makeStreamer(emptyChunk);
|
||||
streamer.update(emptyChunkCenter); // Zentrum -> garantiert der nächstliegende, zuerst geladene Chunk.
|
||||
await flush();
|
||||
|
||||
expect(player.addCollider).not.toHaveBeenCalled();
|
||||
expect(streamer.active.get("4_6")).toMatchObject({ handle: null });
|
||||
});
|
||||
|
||||
it("meldet einen collider-losen Chunk sofort als bereit", async () => {
|
||||
const { streamer } = makeStreamer(emptyChunk);
|
||||
streamer.update(emptyChunkCenter);
|
||||
await flush();
|
||||
|
||||
expect(streamer.isReadyAt(emptyChunkCenter)).toBe(true);
|
||||
});
|
||||
|
||||
it("entlädt einen collider-losen Chunk, ohne removeCollider aufzurufen", async () => {
|
||||
const { streamer, player } = makeStreamer(emptyChunk);
|
||||
streamer.update(emptyChunkCenter);
|
||||
await flush();
|
||||
expect(streamer.active.has("4_6")).toBe(true);
|
||||
|
||||
// Weit weg -> jenseits des Entladeradius (600) -> #unload("4_6") im selben update().
|
||||
streamer.update({ x: emptyChunkCenter.x + 5000, z: emptyChunkCenter.z });
|
||||
|
||||
expect(player.removeCollider).not.toHaveBeenCalled();
|
||||
expect(streamer.active.has("4_6")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user