mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 23:46:42 +02:00
- Move finance, mail, moodlit to apps-archived for later development - Rename games/voxel-lava to games/voxelava 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
608 lines
20 KiB
TypeScript
608 lines
20 KiB
TypeScript
/**
|
|
* PlayerController.ts
|
|
*
|
|
* Diese Klasse ist verantwortlich für die Verwaltung der Spielerphysik, Bewegung und Kollisionserkennung.
|
|
* Sie kapselt die gesamte Spielerlogik und macht den GameCanvas-Code übersichtlicher.
|
|
*/
|
|
|
|
import * as THREE from 'three';
|
|
import { BlockManager } from './BlockTypes';
|
|
import type { BlockType } from './BlockTypes';
|
|
|
|
export interface PlayerConfig {
|
|
height: number;
|
|
radius: number;
|
|
speed: number;
|
|
jumpVelocity: number;
|
|
gravity: number;
|
|
}
|
|
|
|
export interface PlayerPosition {
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
}
|
|
|
|
export class PlayerController {
|
|
// Spieler-Eigenschaften
|
|
private height: number;
|
|
private radius: number;
|
|
private speed: number;
|
|
private jumpVelocity: number;
|
|
private gravity: number;
|
|
|
|
// Spieler-Zustand
|
|
private cameraObject: THREE.PerspectiveCamera;
|
|
private blockManager: BlockManager;
|
|
private voxelSize: number;
|
|
private keyboardState: { [key: string]: boolean } = {};
|
|
private input: THREE.Vector3 = new THREE.Vector3();
|
|
private velocity: THREE.Vector3 = new THREE.Vector3();
|
|
private onGround: boolean = false;
|
|
private simulatedJump: boolean = false;
|
|
private isDying: boolean = false;
|
|
private deathTimer: number = 0;
|
|
private hasMovedSinceSpawn: boolean = false; // Flag, um zu verfolgen, ob der Spieler sich seit dem Spawn bewegt hat
|
|
private fragileTouchedBlocks: Map<string, { x: number; y: number; z: number; timer: number }> =
|
|
new Map();
|
|
|
|
// Callback-Funktionen
|
|
private onRespawn: (isInitialSpawn: boolean) => void;
|
|
private onGoalReached: (position: PlayerPosition) => void;
|
|
private onFirstMovement?: () => void; // Neue Callback-Funktion für die erste Bewegung (optional)
|
|
|
|
/**
|
|
* Konstruktor für den PlayerController
|
|
*
|
|
* @param config - Konfiguration für den Spieler (Höhe, Radius, Geschwindigkeit, etc.)
|
|
* @param camera - Die Kamera, die als Spielerobjekt dient
|
|
* @param blockManager - Der BlockManager für Kollisionsabfragen
|
|
* @param voxelSize - Die Größe eines Voxels in der Welt
|
|
* @param keyboardState - Der aktuelle Zustand der Tastatur
|
|
* @param onRespawn - Callback-Funktion, die beim Respawn aufgerufen wird
|
|
* @param onGoalReached - Callback-Funktion, die beim Erreichen eines Ziels aufgerufen wird
|
|
*/
|
|
constructor(
|
|
config: PlayerConfig,
|
|
camera: THREE.PerspectiveCamera,
|
|
blockManager: BlockManager,
|
|
voxelSize: number,
|
|
keyboardState: { [key: string]: boolean },
|
|
onRespawn: (isInitialSpawn?: boolean) => void,
|
|
onGoalReached: (position: PlayerPosition) => void,
|
|
onFirstMovement?: () => void // Neuer optionaler Parameter für die erste Bewegung
|
|
) {
|
|
this.height = config.height;
|
|
this.radius = config.radius;
|
|
this.speed = config.speed;
|
|
this.jumpVelocity = config.jumpVelocity;
|
|
this.gravity = config.gravity;
|
|
|
|
this.velocity = new THREE.Vector3();
|
|
this.input = new THREE.Vector3();
|
|
this.onGround = false;
|
|
this.cameraObject = camera;
|
|
|
|
this.blockManager = blockManager;
|
|
this.voxelSize = voxelSize;
|
|
this.keyboardState = keyboardState;
|
|
|
|
this.onRespawn = onRespawn;
|
|
this.onGoalReached = onGoalReached;
|
|
this.onFirstMovement = onFirstMovement; // Speichere den neuen Callback
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert den Spieler für den aktuellen Frame
|
|
*
|
|
* @param deltaTime - Die Zeit seit dem letzten Frame in Sekunden
|
|
* @param isLocked - Ob die Maussteuerung aktiv ist
|
|
* @param gameWon - Ob das Spiel gewonnen wurde
|
|
*/
|
|
public update(deltaTime: number, isLocked: boolean, gameWon: boolean): void {
|
|
if (gameWon) return;
|
|
|
|
// Wenn der Spieler stirbt (in Lava versinkt)
|
|
if (this.isDying) {
|
|
this.deathTimer += deltaTime;
|
|
|
|
// Schnell in die Lava sinken, direkt auf den Boden
|
|
// Beschleunigung des Sinkens mit der Zeit
|
|
const sinkSpeed = 2.0 * (1 + this.deathTimer * 2.0); // Schnellere Beschleunigung
|
|
|
|
// Berechne die aktuelle Y-Position des Blocks, in dem der Spieler sich befindet
|
|
const playerBlockY = Math.floor(this.cameraObject.position.y / this.voxelSize);
|
|
|
|
// Berechne die Position des Bodens des Lava-Blocks
|
|
const floorY = playerBlockY * this.voxelSize;
|
|
|
|
// Sinke zum Boden des Blocks
|
|
const newY = this.cameraObject.position.y - sinkSpeed * deltaTime;
|
|
this.cameraObject.position.y = Math.max(newY, floorY);
|
|
|
|
// Nach einer bestimmten Zeit respawnen (Respawn-Funktion kümmert sich um den Rest)
|
|
if (this.deathTimer >= 0.4) {
|
|
// Sehr kurze Zeit zum Respawnen (400ms)
|
|
this.isDying = false;
|
|
this.deathTimer = 0;
|
|
this.onRespawn(false); // Rufe den Respawn-Callback auf
|
|
return;
|
|
}
|
|
|
|
// Während des Sterbens keine weitere Bewegung
|
|
return;
|
|
}
|
|
|
|
this.handleKeyboardInput();
|
|
|
|
// Simulierten Sprung verarbeiten
|
|
if (this.simulatedJump) {
|
|
this.simulateJump();
|
|
this.simulatedJump = false;
|
|
}
|
|
|
|
// Schwerkraft anwenden
|
|
this.velocity.y -= this.gravity * deltaTime;
|
|
|
|
// Bewegung anwenden
|
|
this.applyMovement(deltaTime);
|
|
|
|
// Zerbrechliche Blöcke aktualisieren
|
|
this.updateFragileBlocks(deltaTime);
|
|
|
|
const playerWorldPos = this.cameraObject.position;
|
|
let newPos = playerWorldPos.clone().add(this.velocity.clone().multiplyScalar(deltaTime));
|
|
|
|
// Verfolge, welche zerbrechlichen Blöcke wir in diesem Frame berühren
|
|
const touchedBlocksInThisFrame = new Set<string>();
|
|
|
|
this.handleVerticalCollisions(newPos, playerWorldPos, touchedBlocksInThisFrame);
|
|
this.handleHorizontalCollisions(newPos, playerWorldPos);
|
|
|
|
// Aktualisiere die Liste der berührten Blöcke
|
|
this.updateTouchedBlocks(touchedBlocksInThisFrame);
|
|
|
|
// Aktualisiere die zerbrechlichen Blöcke
|
|
this.updateFragileBlocks(deltaTime);
|
|
}
|
|
|
|
/**
|
|
* Verarbeitet die Tastatureingaben und setzt den Eingabevektor
|
|
*/
|
|
private handleKeyboardInput(): void {
|
|
this.input.set(0, 0, 0);
|
|
if (this.keyboardState['KeyW'] || this.keyboardState['ArrowUp']) this.input.z = -1; // Vorwärts
|
|
if (this.keyboardState['KeyS'] || this.keyboardState['ArrowDown']) this.input.z = 1; // Rückwärts
|
|
if (this.keyboardState['KeyA'] || this.keyboardState['ArrowLeft']) this.input.x = -1; // Links
|
|
if (this.keyboardState['KeyD'] || this.keyboardState['ArrowRight']) this.input.x = 1; // Rechts
|
|
|
|
// Prüfe, ob der Spieler sich zum ersten Mal bewegt
|
|
if (!this.hasMovedSinceSpawn && (this.input.x !== 0 || this.input.z !== 0)) {
|
|
this.hasMovedSinceSpawn = true;
|
|
|
|
// Rufe den onFirstMovement-Callback auf, wenn er definiert ist
|
|
if (this.onFirstMovement) {
|
|
console.log('Erste Spielerbewegung erkannt, starte Timer...');
|
|
this.onFirstMovement();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wendet die Bewegung basierend auf dem Eingabevektor an
|
|
*
|
|
* @param deltaTime - Die Zeit seit dem letzten Frame in Sekunden
|
|
*/
|
|
private applyMovement(deltaTime: number): void {
|
|
const forward = new THREE.Vector3();
|
|
this.cameraObject.getWorldDirection(forward);
|
|
forward.y = 0;
|
|
forward.normalize();
|
|
|
|
const right = new THREE.Vector3();
|
|
right.crossVectors(this.cameraObject.up, forward).normalize();
|
|
|
|
// Ermittle den Block unter dem Spieler für die Reibung
|
|
const blockBelow = this.blockManager.getVoxelBlockType(
|
|
Math.floor(this.cameraObject.position.x / this.voxelSize),
|
|
Math.floor((this.cameraObject.position.y - this.height / 2 - 0.01) / this.voxelSize),
|
|
Math.floor(this.cameraObject.position.z / this.voxelSize)
|
|
);
|
|
|
|
// Standardreibung für Gras verwenden, wenn kein Block gefunden wurde
|
|
const friction = blockBelow ? blockBelow.frictionFactor : 0.7;
|
|
const isIceBlock = blockBelow && blockBelow.frictionFactor > 0.9; // Erkennen von Eis-Blöcken
|
|
|
|
// Wenn der Spieler auf Eis steht, behalte die Geschwindigkeit bei und wende nur leichte Reibung an
|
|
if (this.onGround && isIceBlock) {
|
|
// Wenn der Spieler aktiv Eingaben macht, setze die Zielgeschwindigkeit
|
|
if (this.input.z !== 0 || this.input.x !== 0) {
|
|
let targetVelocityX = 0;
|
|
let targetVelocityZ = 0;
|
|
|
|
// Vorwärts/Rückwärts Bewegung
|
|
if (this.input.z !== 0) {
|
|
targetVelocityX -= forward.x * this.input.z * this.speed;
|
|
targetVelocityZ -= forward.z * this.input.z * this.speed;
|
|
}
|
|
|
|
// Seitwärts Bewegung (Strafe)
|
|
if (this.input.x !== 0) {
|
|
targetVelocityX -= right.x * this.input.x * this.speed;
|
|
targetVelocityZ -= right.z * this.input.x * this.speed;
|
|
}
|
|
|
|
// Mische aktuelle Geschwindigkeit mit Zielgeschwindigkeit für Rutscheffekt
|
|
this.velocity.x = this.velocity.x * 0.8 + targetVelocityX * 0.2;
|
|
this.velocity.z = this.velocity.z * 0.8 + targetVelocityZ * 0.2;
|
|
} else {
|
|
// Wenn keine Eingabe, wende nur sehr geringe Reibung an (Gleiten)
|
|
this.velocity.x *= 0.99;
|
|
this.velocity.z *= 0.99;
|
|
}
|
|
} else {
|
|
// Normales Verhalten für andere Blöcke
|
|
let targetVelocityX = 0;
|
|
let targetVelocityZ = 0;
|
|
|
|
// Vorwärts/Rückwärts Bewegung
|
|
if (this.input.z !== 0) {
|
|
targetVelocityX -= forward.x * this.input.z * this.speed;
|
|
targetVelocityZ -= forward.z * this.input.z * this.speed;
|
|
}
|
|
|
|
// Seitwärts Bewegung (Strafe)
|
|
if (this.input.x !== 0) {
|
|
targetVelocityX -= right.x * this.input.x * this.speed;
|
|
targetVelocityZ -= right.z * this.input.x * this.speed;
|
|
}
|
|
|
|
this.velocity.x = targetVelocityX;
|
|
this.velocity.z = targetVelocityZ;
|
|
|
|
// Reibung anwenden, wenn der Spieler auf dem Boden steht und sich nicht bewegt
|
|
if (this.onGround && this.input.x === 0 && this.input.z === 0) {
|
|
this.velocity.x *= 1 - (1 - friction) * 15 * deltaTime;
|
|
this.velocity.z *= 1 - (1 - friction) * 15 * deltaTime;
|
|
}
|
|
}
|
|
|
|
// Kleine Werte auf 0 setzen, um Zittern zu vermeiden
|
|
if (Math.abs(this.velocity.x) < 0.01) this.velocity.x = 0;
|
|
if (Math.abs(this.velocity.z) < 0.01) this.velocity.z = 0;
|
|
|
|
// Schwerkraft anwenden
|
|
this.velocity.y -= this.gravity * deltaTime;
|
|
|
|
// Springen
|
|
if ((this.keyboardState['Space'] || this.simulatedJump) && this.onGround) {
|
|
this.velocity.y = this.jumpVelocity;
|
|
this.onGround = false;
|
|
if (this.simulatedJump) this.simulatedJump = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prüft auf Kollisionen mit einem Voxel
|
|
*
|
|
* @param playerBox - Die Bounding Box des Spielers
|
|
* @param voxelX - X-Koordinate des Voxels
|
|
* @param voxelY - Y-Koordinate des Voxels
|
|
* @param voxelZ - Z-Koordinate des Voxels
|
|
* @returns Der Blocktyp, wenn eine Kollision vorliegt, sonst null
|
|
*/
|
|
private checkCollisionWithVoxel(
|
|
playerBox: THREE.Box3,
|
|
voxelX: number,
|
|
voxelY: number,
|
|
voxelZ: number
|
|
): BlockType | null {
|
|
const blockType = this.blockManager.getVoxelBlockType(voxelX, voxelY, voxelZ);
|
|
if (blockType && blockType.solid) {
|
|
const voxelBox = new THREE.Box3(
|
|
new THREE.Vector3(
|
|
voxelX * this.voxelSize,
|
|
voxelY * this.voxelSize,
|
|
voxelZ * this.voxelSize
|
|
),
|
|
new THREE.Vector3(
|
|
(voxelX + 1) * this.voxelSize,
|
|
(voxelY + 1) * this.voxelSize,
|
|
(voxelZ + 1) * this.voxelSize
|
|
)
|
|
);
|
|
if (playerBox.intersectsBox(voxelBox)) {
|
|
return blockType;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Behandelt vertikale Kollisionen (Boden und Decke)
|
|
*
|
|
* @param newPos - Die neue Position des Spielers
|
|
* @param playerWorldPos - Die aktuelle Position des Spielers
|
|
* @param touchedBlocksInThisFrame - Set mit Schlüsseln der Blöcke, die in diesem Frame berührt wurden
|
|
* @returns true, wenn eine Kollision stattgefunden hat
|
|
*/
|
|
private handleVerticalCollisions(
|
|
newPos: THREE.Vector3,
|
|
playerWorldPos: THREE.Vector3,
|
|
touchedBlocksInThisFrame: Set<string>
|
|
): boolean {
|
|
this.onGround = false;
|
|
const playerFeetY = newPos.y - this.height / 2;
|
|
const playerHeadY = newPos.y + this.height / 2;
|
|
|
|
const checkMinX = Math.floor((newPos.x - this.radius) / this.voxelSize);
|
|
const checkMaxX = Math.floor((newPos.x + this.radius) / this.voxelSize);
|
|
const checkMinZ = Math.floor((newPos.z - this.radius) / this.voxelSize);
|
|
const checkMaxZ = Math.floor((newPos.z + this.radius) / this.voxelSize);
|
|
|
|
// Kollision mit dem Boden prüfen
|
|
if (this.velocity.y <= 0) {
|
|
const groundCheckY = Math.floor(playerFeetY / this.voxelSize);
|
|
for (let x = checkMinX; x <= checkMaxX; x++) {
|
|
for (let z = checkMinZ; z <= checkMaxZ; z++) {
|
|
const blockAtFeet = this.blockManager.getVoxelBlockType(x, groundCheckY, z);
|
|
if (blockAtFeet) {
|
|
if (blockAtFeet.isDeadly) {
|
|
// Starte den Sterbevorgang (in Lava versinken)
|
|
if (!this.isDying) {
|
|
this.isDying = true;
|
|
this.deathTimer = 0;
|
|
this.velocity.set(0, 0, 0); // Stoppe alle Bewegungen
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (blockAtFeet.isGoal) {
|
|
// Rufe den Callback auf, aber erlaube dem Spieler, auf dem Ziel-Block zu stehen
|
|
this.onGoalReached({ x, y: groundCheckY, z });
|
|
|
|
// Setze die Spielerposition auf die Oberkante des Blocks
|
|
playerWorldPos.y = (groundCheckY + 1) * this.voxelSize + this.height / 2;
|
|
this.velocity.y = 0;
|
|
this.onGround = true;
|
|
return true;
|
|
}
|
|
|
|
if (blockAtFeet.solid) {
|
|
// Prüfe, ob wir auf einem zerbrechlichen Block stehen
|
|
if (blockAtFeet.isFragile) {
|
|
// Erzeuge einen eindeutigen Schlüssel für diesen Block
|
|
const blockKey = `${x}_${groundCheckY}_${z}`;
|
|
touchedBlocksInThisFrame.add(blockKey);
|
|
|
|
// Wenn wir diesen Block noch nicht verfolgen, füge ihn hinzu
|
|
if (!this.fragileTouchedBlocks.has(blockKey)) {
|
|
this.fragileTouchedBlocks.set(blockKey, {
|
|
x,
|
|
y: groundCheckY,
|
|
z,
|
|
timer: blockAtFeet.breakTimer || 1.0,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Prüfe, ob wir auf einem Trampolinblock stehen
|
|
if (blockAtFeet.isTrampoline) {
|
|
// Setze die Spielerposition auf die Oberkante des Blocks
|
|
playerWorldPos.y = (groundCheckY + 1) * this.voxelSize + this.height / 2;
|
|
// Gib dem Spieler einen kräftigen Impuls nach oben
|
|
const trampolineForce = blockAtFeet.trampolineForce || 20.0;
|
|
this.velocity.y = trampolineForce;
|
|
this.onGround = false;
|
|
return true;
|
|
}
|
|
|
|
playerWorldPos.y = (groundCheckY + 1) * this.voxelSize + this.height / 2;
|
|
this.velocity.y = 0;
|
|
this.onGround = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Kollision mit der Decke prüfen
|
|
if (this.velocity.y > 0) {
|
|
const headCheckY = Math.floor(playerHeadY / this.voxelSize);
|
|
for (let x = checkMinX; x <= checkMaxX; x++) {
|
|
for (let z = checkMinZ; z <= checkMaxZ; z++) {
|
|
const blockAtHead = this.blockManager.getVoxelBlockType(x, headCheckY, z);
|
|
if (blockAtHead && blockAtHead.solid) {
|
|
playerWorldPos.y = headCheckY * this.voxelSize - this.height / 2 - 0.01;
|
|
this.velocity.y = 0;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
playerWorldPos.y = newPos.y;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Behandelt horizontale Kollisionen (Wände)
|
|
*
|
|
* @param newPos - Die neue Position des Spielers
|
|
* @param playerWorldPos - Die aktuelle Position des Spielers
|
|
*/
|
|
private handleHorizontalCollisions(newPos: THREE.Vector3, playerWorldPos: THREE.Vector3): void {
|
|
const R = this.radius;
|
|
const H_half = this.height / 2;
|
|
|
|
// X-Achsen-Kollision
|
|
let proposedXPos = newPos.x;
|
|
const playerBoxX = new THREE.Box3(
|
|
new THREE.Vector3(proposedXPos - R, playerWorldPos.y - H_half, playerWorldPos.z - R),
|
|
new THREE.Vector3(proposedXPos + R, playerWorldPos.y + H_half, playerWorldPos.z + R)
|
|
);
|
|
|
|
const minX_col = Math.floor(playerBoxX.min.x / this.voxelSize);
|
|
const maxX_col = Math.floor(playerBoxX.max.x / this.voxelSize);
|
|
const minY_col = Math.floor(playerBoxX.min.y / this.voxelSize);
|
|
const maxY_col = Math.floor(playerBoxX.max.y / this.voxelSize);
|
|
const minZ_col = Math.floor(playerBoxX.min.z / this.voxelSize);
|
|
const maxZ_col = Math.floor(playerBoxX.max.z / this.voxelSize);
|
|
|
|
let collisionX = false;
|
|
for (let ix = minX_col; ix <= maxX_col; ix++) {
|
|
for (let iy = minY_col; iy <= maxY_col; iy++) {
|
|
for (let iz = minZ_col; iz <= maxZ_col; iz++) {
|
|
const blockType = this.checkCollisionWithVoxel(playerBoxX, ix, iy, iz);
|
|
if (blockType) {
|
|
if (blockType.isDeadly) {
|
|
this.onRespawn(false);
|
|
return;
|
|
}
|
|
|
|
if (blockType.isGoal) {
|
|
this.onGoalReached({ x: ix, y: iy, z: iz });
|
|
return;
|
|
}
|
|
|
|
collisionX = true;
|
|
this.velocity.x = 0;
|
|
break;
|
|
}
|
|
}
|
|
if (collisionX) break;
|
|
}
|
|
if (collisionX) break;
|
|
}
|
|
|
|
if (!collisionX) {
|
|
playerWorldPos.x = proposedXPos;
|
|
}
|
|
|
|
// Z-Achsen-Kollision
|
|
let proposedZPos = newPos.z;
|
|
const playerBoxZ = new THREE.Box3(
|
|
new THREE.Vector3(playerWorldPos.x - R, playerWorldPos.y - H_half, proposedZPos - R),
|
|
new THREE.Vector3(playerWorldPos.x + R, playerWorldPos.y + H_half, proposedZPos + R)
|
|
);
|
|
|
|
const minX_colZ = Math.floor(playerBoxZ.min.x / this.voxelSize);
|
|
const maxX_colZ = Math.floor(playerBoxZ.max.x / this.voxelSize);
|
|
const minY_colZ = Math.floor(playerBoxZ.min.y / this.voxelSize);
|
|
const maxY_colZ = Math.floor(playerBoxZ.max.y / this.voxelSize);
|
|
const minZ_colZ = Math.floor(playerBoxZ.min.z / this.voxelSize);
|
|
const maxZ_colZ = Math.floor(playerBoxZ.max.z / this.voxelSize);
|
|
|
|
let collisionZ = false;
|
|
for (let ix = minX_colZ; ix <= maxX_colZ; ix++) {
|
|
for (let iy = minY_colZ; iy <= maxY_colZ; iy++) {
|
|
for (let iz = minZ_colZ; iz <= maxZ_colZ; iz++) {
|
|
const blockType = this.checkCollisionWithVoxel(playerBoxZ, ix, iy, iz);
|
|
if (blockType) {
|
|
if (blockType.isDeadly) {
|
|
this.onRespawn(false);
|
|
return;
|
|
}
|
|
|
|
if (blockType.isGoal) {
|
|
this.onGoalReached({ x: ix, y: iy, z: iz });
|
|
return;
|
|
}
|
|
|
|
collisionZ = true;
|
|
this.velocity.z = 0;
|
|
break;
|
|
}
|
|
}
|
|
if (collisionZ) break;
|
|
}
|
|
if (collisionZ) break;
|
|
}
|
|
|
|
if (!collisionZ) {
|
|
playerWorldPos.z = proposedZPos;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert die Timer für zerbrechliche Blöcke und zerstört sie, wenn der Timer abläuft
|
|
* oder wenn der Spieler den Block verlassen hat
|
|
*
|
|
* @param deltaTime - Die Zeit seit dem letzten Frame in Sekunden
|
|
*/
|
|
private updateFragileBlocks(deltaTime: number): void {
|
|
// Iteriere über alle verfolgten zerbrechlichen Blöcke
|
|
for (const [blockKey, blockInfo] of this.fragileTouchedBlocks.entries()) {
|
|
// Reduziere den Timer
|
|
blockInfo.timer -= deltaTime;
|
|
|
|
// Wenn der Timer abgelaufen ist, zerstöre den Block
|
|
if (blockInfo.timer <= 0) {
|
|
// Entferne den Block aus der Welt
|
|
this.blockManager.removeVoxel(blockInfo.x, blockInfo.y, blockInfo.z, true);
|
|
|
|
// Entferne den Block aus der Map
|
|
this.fragileTouchedBlocks.delete(blockKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert die Liste der berührten zerbrechlichen Blöcke
|
|
* Blöcke, die nicht mehr berührt werden, werden markiert, um später zerstört zu werden
|
|
*
|
|
* @param touchedBlocksInThisFrame - Set mit Schlüsseln der Blöcke, die in diesem Frame berührt wurden
|
|
*/
|
|
private updateTouchedBlocks(touchedBlocksInThisFrame: Set<string>): void {
|
|
// Finde Blöcke, die nicht mehr berührt werden
|
|
for (const [blockKey, blockInfo] of this.fragileTouchedBlocks.entries()) {
|
|
if (!touchedBlocksInThisFrame.has(blockKey)) {
|
|
// Setze den Timer auf 0, damit der Block im nächsten Frame zerstört wird
|
|
blockInfo.timer = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gibt die aktuelle Geschwindigkeit des Spielers zurück
|
|
*/
|
|
public getVelocity(): THREE.Vector3 {
|
|
return this.velocity.clone();
|
|
}
|
|
|
|
/**
|
|
* Gibt die aktuelle Position des Spielers zurück
|
|
*/
|
|
public getPosition(): THREE.Vector3 {
|
|
return this.cameraObject.position.clone();
|
|
}
|
|
|
|
/**
|
|
* Setzt die Position des Spielers
|
|
*
|
|
* @param position - Die neue Position des Spielers
|
|
*/
|
|
public setPosition(position: PlayerPosition): void {
|
|
this.cameraObject.position.set(position.x, position.y, position.z);
|
|
|
|
// Setze das Flag zurück, damit der Timer erst wieder startet, wenn der Spieler sich bewegt
|
|
this.hasMovedSinceSpawn = false;
|
|
}
|
|
|
|
/**
|
|
* Setzt die Geschwindigkeit des Spielers
|
|
*
|
|
* @param velocity - Die neue Geschwindigkeit des Spielers
|
|
*/
|
|
public setVelocity(velocity: THREE.Vector3): void {
|
|
this.velocity.copy(velocity);
|
|
}
|
|
|
|
/**
|
|
* Simuliert einen Sprung des Spielers
|
|
*/
|
|
public simulateJump(): void {
|
|
this.simulatedJump = true;
|
|
}
|
|
}
|