Visuals:
- PCFSoftShadowMap (2048²), directional light follows player with a tight
shadow frustum (SHADOW_AREA) for high texel density; opak blocks cast+receive,
water receives.
- Baked vertex ambient occlusion ("smooth lighting"): per-vertex solid-neighbor
lookup (0fps algorithm) written into a color attribute, with flip-triangulation
to avoid brightness seams across quad diagonals.
- EffectComposer: RenderPass -> UnrealBloomPass (subtle) -> OutputPass with
ACES filmic tone-mapping + sRGB. HalfFloat render target (anti-banding) + MSAA.
Controls:
- Sprint moved from Ctrl to Shift (Ctrl is problematic in browsers). Shift in
fly mode still means "descend" (no conflict, sprint only applies when walking).
config.js:
- New central js/config.js for all tweakable values (world, performance,
renderer/tone-mapping, sky & fog, camera, lighting, shadows, bloom, AO,
player physics, mouse sensitivity, reach) with SHADOWS_ENABLED / BLOOM_ENABLED
feature toggles. Structural engine constants stay in constants.js.
- SEA_LEVEL / WORLD_SEED moved constants.js -> config.js (consumers rewired).
Co-Authored-By: Claude <noreply@anthropic.com>
144 lines
5.0 KiB
JavaScript
144 lines
5.0 KiB
JavaScript
// First-Person-Spieler: Bewegung, Gravitation, AABB-Kollision, Springen,
|
||
// Flug-Modus, Schwimmen. yaw/pitch steuern die Kamera (extern gesetzt).
|
||
import { isSolid, BLOCK } from './constants.js';
|
||
import { PLAYER, MOUSE_SENSITIVITY } from './config.js';
|
||
|
||
// Werte aus config.js (PLAYER). Lokal gebunden, damit die Verwendungen kurz bleiben.
|
||
const { HALF, HEIGHT, EYE, GRAVITY, JUMP_VEL, WALK, SPRINT, FLY_SPEED, MAX_FALL } = PLAYER;
|
||
|
||
export class Player {
|
||
constructor(world) {
|
||
this.world = world;
|
||
this.pos = new Float32Array(3); // x,y,z der Fuesse-Mittelpunkt
|
||
this.vel = new Float32Array(3);
|
||
this.onGround = false;
|
||
this.flying = false;
|
||
this.yaw = 0;
|
||
this.pitch = 0;
|
||
}
|
||
|
||
setSpawn(x, y, z) {
|
||
this.pos[0] = x; this.pos[1] = y; this.pos[2] = z;
|
||
this.vel[0] = this.vel[1] = this.vel[2] = 0;
|
||
}
|
||
|
||
eyePosition(out) {
|
||
out.set(this.pos[0], this.pos[1] + EYE, this.pos[2]);
|
||
return out;
|
||
}
|
||
|
||
inWater() {
|
||
// Pruefe den Block auf Augen- und Fuss-Hoehe
|
||
const ex = Math.floor(this.pos[0]);
|
||
const ey = Math.floor(this.pos[1] + EYE * 0.6);
|
||
const ez = Math.floor(this.pos[2]);
|
||
return this.world.getBlock(ex, ey, ez) === BLOCK.WATER;
|
||
}
|
||
|
||
update(input, dt) {
|
||
// --- Maus-Look ---
|
||
const sens = MOUSE_SENSITIVITY;
|
||
this.yaw -= input.mouseDX * sens;
|
||
this.pitch -= input.mouseDY * sens;
|
||
const lim = Math.PI / 2 - 0.001;
|
||
if (this.pitch > lim) this.pitch = lim;
|
||
if (this.pitch < -lim) this.pitch = -lim;
|
||
|
||
// --- Flug-Toggle ---
|
||
if (input.keys.has('KeyF') && !this._fHeld) {
|
||
this._fHeld = true;
|
||
this.flying = !this.flying;
|
||
this.vel[1] = 0;
|
||
}
|
||
if (!input.keys.has('KeyF')) this._fHeld = false;
|
||
|
||
// --- Wunschrichtung im XZ-Plane (yaw=0 -> Blickrichtung ist -Z) ---
|
||
const fwdX = -Math.sin(this.yaw);
|
||
const fwdZ = -Math.cos(this.yaw);
|
||
const rightX = -fwdZ; // = cos(yaw)
|
||
const rightZ = fwdX; // = -sin(yaw)
|
||
let mx = 0, mz = 0;
|
||
if (input.down('KeyW')) { mx += fwdX; mz += fwdZ; }
|
||
if (input.down('KeyS')) { mx -= fwdX; mz -= fwdZ; }
|
||
if (input.down('KeyD')) { mx += rightX; mz += rightZ; }
|
||
if (input.down('KeyA')) { mx -= rightX; mz -= rightZ; }
|
||
const len = Math.hypot(mx, mz);
|
||
if (len > 0) { mx /= len; mz /= len; }
|
||
|
||
// Sprint: Shift (Ctrl ist im Browser unbequem – Browser-Shortcuts).
|
||
// ShiftLeft/Right dient im Fly-Modus gleichzeitig als "Runter" – kein
|
||
// Konflikt, da Sprint nur beim Gehen greift (Fly nutzt konstante FLY_SPEED).
|
||
const sprint = input.down('ShiftLeft') || input.down('ShiftRight');
|
||
const speed = this.flying ? FLY_SPEED : (sprint ? SPRINT : WALK);
|
||
|
||
if (this.flying) {
|
||
this.vel[0] = mx * speed;
|
||
this.vel[2] = mz * speed;
|
||
let vy = 0;
|
||
if (input.down('Space')) vy += 1;
|
||
if (input.down('ShiftLeft')) vy -= 1;
|
||
this.vel[1] = vy * speed;
|
||
} else {
|
||
const water = this.inWater();
|
||
const move = water ? speed * 0.5 : speed;
|
||
this.vel[0] = mx * move;
|
||
this.vel[2] = mz * move;
|
||
// Gravitation / Schwimmen
|
||
if (water) {
|
||
this.vel[1] -= GRAVITY * 0.28 * dt;
|
||
if (this.vel[1] < -6) this.vel[1] = -6;
|
||
if (input.down('Space')) this.vel[1] = 4.0; // nach oben schwimmen
|
||
} else {
|
||
this.vel[1] -= GRAVITY * dt;
|
||
if (this.vel[1] < -MAX_FALL) this.vel[1] = -MAX_FALL;
|
||
if (input.down('Space') && this.onGround) {
|
||
this.vel[1] = JUMP_VEL;
|
||
this.onGround = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Bewegung mit per-Achsen-Kollision ---
|
||
this.onGround = false;
|
||
this._moveAxis(0, this.vel[0] * dt);
|
||
this._moveAxis(2, this.vel[2] * dt);
|
||
this._moveAxis(1, this.vel[1] * dt);
|
||
}
|
||
|
||
// axis: 0=x,1=y,2=z ; amount = Verschiebung auf dieser Achse
|
||
_moveAxis(axis, amount) {
|
||
this.pos[axis] += amount;
|
||
if (amount === 0) return;
|
||
|
||
const x = this.pos[0], y = this.pos[1], z = this.pos[2];
|
||
const minX = Math.floor(x - HALF), maxX = Math.floor(x + HALF);
|
||
const minY = Math.floor(y), maxY = Math.floor(y + HEIGHT - 0.001);
|
||
const minZ = Math.floor(z - HALF), maxZ = Math.floor(z + HALF);
|
||
|
||
for (let bx = minX; bx <= maxX; bx++) {
|
||
for (let by = minY; by <= maxY; by++) {
|
||
for (let bz = minZ; bz <= maxZ; bz++) {
|
||
if (!isSolid(this.world.getBlock(bx, by, bz))) continue;
|
||
if (axis === 0) {
|
||
if (amount > 0) this.pos[0] = Math.min(this.pos[0], bx - HALF - 1e-4);
|
||
else this.pos[0] = Math.max(this.pos[0], bx + 1 + HALF + 1e-4);
|
||
this.vel[0] = 0;
|
||
} else if (axis === 2) {
|
||
if (amount > 0) this.pos[2] = Math.min(this.pos[2], bz - HALF - 1e-4);
|
||
else this.pos[2] = Math.max(this.pos[2], bz + 1 + HALF + 1e-4);
|
||
this.vel[2] = 0;
|
||
} else {
|
||
if (amount > 0) { // nach oben gegen Decke
|
||
this.pos[1] = Math.min(this.pos[1], by - HEIGHT - 1e-4);
|
||
} else { // gelandet
|
||
this.pos[1] = Math.max(this.pos[1], by + 1 + 1e-4);
|
||
this.onGround = true;
|
||
}
|
||
this.vel[1] = 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|