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>
114 lines
5.7 KiB
JavaScript
114 lines
5.7 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import { playerIsGrounded, playerPosition } from "../src/player.js";
|
|
|
|
// Befund A (Fix-Runde 3): main.js trieb Streaming, isReadyAt, Auffangnetz, lastSafe und
|
|
// Schattenwurf ausnahmslos mit player.playerCapsule.position an. Sitzt der Spieler in
|
|
// einem Fahrzeug, hängt PlayerController.syncMountedPlayer() die Kapsel als Kind an
|
|
// vehicle.vehicleGroup und überschreibt ihre .position mit dem lokalen Sitzplatz-Offset
|
|
// (driverSeatPosition * scale) - eine feste, kleine Zahl nahe dem Ursprung, NICHT die
|
|
// Weltposition. playerPosition() ist die eine Stelle, die "wo ist der Spieler gerade"
|
|
// entscheidet: Fahrzeugposition (vehicleGroup.position, Weltkoordinaten seit
|
|
// syncVehicleVisual() sie direkt aus dem Chassis-Rigidbody setzt), wenn eines aktiv ist,
|
|
// sonst die Kapsel.
|
|
function makePlayer({
|
|
activeVehicle = null,
|
|
// Default spiegelt den Normalfall (ein übergebenes Fahrzeug heißt "fährt gerade"); der
|
|
// Stale-Reference-Test unten übersteuert das gezielt auf 0.
|
|
controllerMode = activeVehicle ? 1 : 0,
|
|
capsulePosition = { x: 0, y: 0, z: 0 },
|
|
onGround = false,
|
|
} = {}) {
|
|
return {
|
|
getActiveVehicle: () => activeVehicle,
|
|
getControllerMode: () => controllerMode,
|
|
playerCapsule: { position: capsulePosition },
|
|
getIsOnGround: () => onGround,
|
|
};
|
|
}
|
|
|
|
function makeVehicle({ position, wheelsInContact = [] } = {}) {
|
|
return {
|
|
vehicleGroup: { position },
|
|
vehicleController: {
|
|
numWheels: () => wheelsInContact.length,
|
|
wheelIsInContact: (i) => wheelsInContact[i],
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("playerPosition", () => {
|
|
it("liefert die Kapselposition, wenn kein Fahrzeug aktiv ist (zu Fuß)", () => {
|
|
const capsulePosition = { x: 12, y: 3, z: -7 };
|
|
const player = makePlayer({ activeVehicle: null, capsulePosition });
|
|
expect(playerPosition(player)).toBe(capsulePosition);
|
|
});
|
|
|
|
it("liefert die Fahrzeug-Weltposition, wenn ein Fahrzeug aktiv ist, nicht den Sitzplatz-Offset der Kapsel", () => {
|
|
// capsulePosition steht hier bewusst auf dem winzigen, ortsfremden Sitzplatz-Offset
|
|
// (wie syncMountedPlayer ihn setzt), vehiclePosition auf der echten Weltposition weit
|
|
// entfernt davon - der Test muss fehlschlagen, wenn playerPosition() versehentlich
|
|
// weiterhin die Kapsel zurückgibt.
|
|
const vehiclePosition = { x: 1656, y: 1, z: -739 };
|
|
const player = makePlayer({
|
|
activeVehicle: makeVehicle({ position: vehiclePosition }),
|
|
capsulePosition: { x: -0.2, y: 0.5, z: 0.15 },
|
|
});
|
|
expect(playerPosition(player)).toBe(vehiclePosition);
|
|
});
|
|
|
|
// Im Browser nachgewiesen (s. Bericht): VehicleSystem löscht this.active beim Aussteigen
|
|
// NICHT — weder exit() noch das stopActive(), das reset() bei aktivem Fahrzeug aufruft,
|
|
// setzen es zurück; nur destroy() (kompletter Abbau aller Fahrzeuge) tut das. Nach einem
|
|
// ganz normalen Ausstieg (oder einem erzwungenen reset()/Teleport aus dem Fahrzeug heraus)
|
|
// liefert getActiveVehicle() also weiterhin das zuletzt benutzte, längst verlassene
|
|
// Fahrzeug — ohne diesen Test hätte playerPosition() nach dem Aussteigen für immer die
|
|
// Position des verlassenen Fahrzeugs zurückgegeben statt der (dann wieder gültigen)
|
|
// Kapselposition.
|
|
it("ignoriert ein verlassenes Fahrzeug: getControllerMode() ist die verlässliche Quelle, nicht nur getActiveVehicle()", () => {
|
|
const capsulePosition = { x: 1699, y: 3, z: -743 };
|
|
const player = makePlayer({
|
|
activeVehicle: makeVehicle({ position: { x: 1448, y: 0.67, z: -742 } }),
|
|
controllerMode: 0, // ausgestiegen — getActiveVehicle() bleibt trotzdem gesetzt
|
|
capsulePosition,
|
|
});
|
|
expect(playerPosition(player)).toBe(capsulePosition);
|
|
});
|
|
});
|
|
|
|
describe("playerIsGrounded", () => {
|
|
it("liefert getIsOnGround(), wenn kein Fahrzeug aktiv ist (zu Fuß, Ruling 52)", () => {
|
|
expect(playerIsGrounded(makePlayer({ onGround: true }))).toBe(true);
|
|
expect(playerIsGrounded(makePlayer({ onGround: false }))).toBe(false);
|
|
});
|
|
|
|
it("ignoriert das eingefrorene getIsOnGround() im Fahrzeug und prüft stattdessen Radkontakt", () => {
|
|
// setOnGround() läuft nur im Fußgänger-Codepfad (CharacterMovement), controllerMode 1
|
|
// (Fahrzeug) aktualisiert playerIsOnGround nie - es bliebe auf "true" von vor dem
|
|
// Einsteigen eingefroren. Ein Fahrzeug ganz ohne Radkontakt (frei fallend/über einer
|
|
// Klippe) muss trotzdem als nicht geerdet gelten, sonst überschreibt lastSafe sich
|
|
// jeden Frame blind, genau das Problem, das Ruling 52 ursprünglich verhindern sollte.
|
|
const player = makePlayer({
|
|
onGround: true,
|
|
activeVehicle: makeVehicle({ position: { x: 0, y: 0, z: 0 }, wheelsInContact: [false, false, false, false] }),
|
|
});
|
|
expect(playerIsGrounded(player)).toBe(false);
|
|
});
|
|
|
|
it("gilt im Fahrzeug als geerdet, sobald mindestens ein Rad Bodenkontakt hat", () => {
|
|
const player = makePlayer({
|
|
onGround: false,
|
|
activeVehicle: makeVehicle({ position: { x: 0, y: 0, z: 0 }, wheelsInContact: [false, true, false, false] }),
|
|
});
|
|
expect(playerIsGrounded(player)).toBe(true);
|
|
});
|
|
|
|
it("fällt nach dem Aussteigen auf getIsOnGround() zurück, auch wenn getActiveVehicle() noch das verlassene Fahrzeug liefert", () => {
|
|
const player = makePlayer({
|
|
onGround: true,
|
|
controllerMode: 0,
|
|
activeVehicle: makeVehicle({ position: { x: 0, y: 0, z: 0 }, wheelsInContact: [] }),
|
|
});
|
|
expect(playerIsGrounded(player)).toBe(true);
|
|
});
|
|
});
|