feat: live settings panel (≈45 Werte, Live/Reload, Persistenz, AO live)

- panel.js: Drawer-Panel (P / ⚙), schema-getrieben, ●live/⟳reload-Badges
- Werte direkt im Panel einstellbar statt in config.js grabben
- Live-Apply über mutateNested (nur Plain-Objekte, nie Modul-Namespace)
  + registerApply-Callbacks für Three.js-Objekte (FOV, Belichtung, Fog, Licht, Schatten-Frustum, Bloom)
- PLAYER.*/CONTROLS.* live (player.js liest jeden Frame neu)
- AO jetzt live: debounced world.markAllDirty() remesht alle Chunks ohne F5
- Reload-Pflichtige Werte persistiert + blinkender Neu-Starten-Button
  (Seed, Meereshöhe, Spawn, MSAA, Tone-Mapping, Schatten an/Größe, AO-Level)
- Persistenz: localStorage (debounced), config.js lädt beim Start
- HUD/Overlay ausgeblendet, solange das Panel offen ist (Welt frei sichtbar)
- Buttons: Neu starten, Auf Standard zurücksetzen, Werte kopieren (JSON)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
minecraft-cloneandClaude committed 2026-06-21 16:31:25 +02:00
1 parent d6f6774815
commit 6e8e207afa
7 files changed
+775 -78

No files matched your search

+60 -5
View File
@@ -15,7 +15,7 @@ import {
HEMISPHERE, SUN, SUN_DIRECTION,
SHADOWS_ENABLED, SHADOW_MAP_SIZE, SHADOW_AREA, SHADOW_BIAS, SHADOW_NORMAL_BIAS,
BLOOM_ENABLED, BLOOM,
PLAYER, SPAWN, REACH,
PLAYER, SPAWN, CONTROLS,
} from './config.js';
import { buildAtlasTexture } from './textures.js';
import { World } from './world.js';
@@ -23,6 +23,7 @@ import { Player } from './player.js';
import { Input } from './input.js';
import { raycastVoxel } from './raycast.js';
import { getBiome } from './worldgen.js';
import { initPanel, registerApply } from './panel.js';
const BIOME_NAME = { [BIOME.PLAINS]: 'Plains', [BIOME.FOREST]: 'Wald', [BIOME.DESERT]: 'Wüste' };
@@ -98,7 +99,10 @@ const bloom = new UnrealBloomPass(
BLOOM.radius,
BLOOM.threshold,
);
if (BLOOM_ENABLED) composer.addPass(bloom);
// Bloom-Pass immer anhängen, nur aktivieren/deaktivieren – so lässt er sich
// über das Settings-Panel live ein-/ausschalten (pass.enabled).
composer.addPass(bloom);
bloom.enabled = BLOOM_ENABLED;
composer.addPass(new OutputPass());
// ---------- Welt & Spieler ----------
@@ -158,7 +162,12 @@ overlay.addEventListener('click', () => {
input.requestLock();
});
document.addEventListener('pointerlockchange', () => {
if (!document.pointerLockElement) overlay.classList.remove('hidden');
// Start-Overlay nur zeigen, wenn der Lock verloren ging UND nicht das
// Settings-Panel geöffnet ist – das Panel gibt den Pointer-Lock selbst
// frei (open()), darf dabei aber nicht die Welt verdecken.
if (!document.pointerLockElement && !document.body.classList.contains('panel-open')) {
overlay.classList.remove('hidden');
}
});
// ---------- Resize ----------
@@ -194,6 +203,52 @@ function wouldIntersectPlayer(bx, by, bz) {
const _dir = new THREE.Vector3();
// ---------- Settings-Panel: Live-Apply-Callbacks ----------
// Performance-Werte werden in der Loop gelesen → lokal halten und per
// Apply-Callback mutieren (ESM-lets lassen sich von außen nicht schreiben).
let renderDistance = RENDER_DISTANCE;
let maxOpsPerFrame = MAX_OPS_PER_FRAME;
registerApply('RENDER_DISTANCE', (v) => { renderDistance = v; });
registerApply('MAX_OPS_PER_FRAME', (v) => { maxOpsPerFrame = v; });
registerApply('MAX_PIXEL_RATIO', (v) => renderer.setPixelRatio(Math.min(window.devicePixelRatio, v)));
registerApply('TONE_MAPPING_EXPOSURE', (v) => { renderer.toneMappingExposure = v; });
registerApply('SKY_COLOR', (v) => { scene.background.setHex(v); scene.fog.color.setHex(v); });
registerApply('FOG_NEAR', (v) => { scene.fog.near = v; });
registerApply('FOG_FAR', (v) => { scene.fog.far = v; });
registerApply('CAMERA_FOV', (v) => { camera.fov = v; camera.updateProjectionMatrix(); });
registerApply('CAMERA_NEAR', (v) => { camera.near = v; camera.updateProjectionMatrix(); });
registerApply('CAMERA_FAR', (v) => { camera.far = v; camera.updateProjectionMatrix(); });
registerApply('HEMISPHERE.intensity', (v) => { hemi.intensity = v; });
registerApply('HEMISPHERE.sky', (v) => hemi.color.setHex(v));
registerApply('HEMISPHERE.ground', (v) => hemi.groundColor.setHex(v));
registerApply('SUN.intensity', (v) => { sun.intensity = v; });
registerApply('SUN.color', (v) => sun.color.setHex(v));
registerApply('SHADOW_AREA', (v) => {
sun.shadow.camera.left = -v; sun.shadow.camera.right = v;
sun.shadow.camera.top = v; sun.shadow.camera.bottom = -v;
sun.shadow.camera.updateProjectionMatrix();
});
registerApply('SHADOW_BIAS', (v) => { sun.shadow.bias = v; });
registerApply('SHADOW_NORMAL_BIAS', (v) => { sun.shadow.normalBias = v; });
registerApply('BLOOM_ENABLED', (v) => { bloom.enabled = v; });
registerApply('BLOOM.strength', (v) => { bloom.strength = v; });
registerApply('BLOOM.radius', (v) => { bloom.radius = v; });
registerApply('BLOOM.threshold', (v) => { bloom.threshold = v; });
// AO live: AO_LEVEL ist pro Vertex in die Chunk-Meshes gebacken. Eine Änderung
// → debounced Neubau aller geladenen Chunks (panel.js mutiert AO_LEVEL im
// Config-Array; das Streaming verteilt den Neubau über mehrere Frames).
let aoRemeshTimer = 0;
function scheduleAoRemesh() {
if (aoRemeshTimer) clearTimeout(aoRemeshTimer);
aoRemeshTimer = setTimeout(() => { aoRemeshTimer = 0; world.markAllDirty(); }, 150);
}
for (const p of ['AO_LEVEL.0', 'AO_LEVEL.1', 'AO_LEVEL.2', 'AO_LEVEL.3']) {
registerApply(p, scheduleAoRemesh);
}
// PLAYER.* und CONTROLS.* braucht keinen Callback: player.js/raycast lesen
// die Config-Objekte live; panel.js mutiert sie direkt.
initPanel({ onRequestLock: () => input.requestLock() });
// ---------- Game-Loop ----------
let last = performance.now();
function frame(now) {
@@ -213,7 +268,7 @@ function frame(now) {
const hit = raycastVoxel(
(x, y, z) => world.getBlock(x, y, z),
camera.position.x, camera.position.y, camera.position.z,
_dir.x, _dir.y, _dir.z, REACH
_dir.x, _dir.y, _dir.z, CONTROLS.REACH
);
if (hit) {
hlEdges.visible = true;
@@ -240,7 +295,7 @@ function frame(now) {
input.endFrame();
// Streaming (immer, auch im Pause-Menue, damit die Welt laedt)
world.updateStreaming(player.pos[0], player.pos[2], RENDER_DISTANCE, MAX_OPS_PER_FRAME);
world.updateStreaming(player.pos[0], player.pos[2], renderDistance, maxOpsPerFrame);
// Sonne folgt dem Spieler -> Shadow-Frustum bleibt um den Spieler zentriert.
sun.position.set(