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:
1 parent
d6f6774815
commit
6e8e207afa
7 files changed
+775
-78
No files matched your search
+60
-5
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user