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); }); });