Files
minecraft-clone/js/player.js
T
minecraft-cloneandClaude f009789c42 feat: shadows, baked vertex AO, post-processing + central config.js
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>
2026-06-21 00:46:03 +02:00

144 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}
}
}
}
}
}