diff --git a/index.html b/index.html index 02f6812..bd3d8f6 100644 --- a/index.html +++ b/index.html @@ -67,7 +67,7 @@

Settings

- +
diff --git a/src/engine/Renderer.ts b/src/engine/Renderer.ts index 339ee60..de2bc97 100644 --- a/src/engine/Renderer.ts +++ b/src/engine/Renderer.ts @@ -16,11 +16,11 @@ export class Renderer { this.engine = new Engine( this.canvas, - true, // antialias + false, // antialias; FXAA is controlled by graphics settings when requested { preserveDrawingBuffer: true, // needed for canvas.toDataURL screenshots powerPreference: "high-performance", - stencil: true, + stencil: false, }, false, // adaptToDeviceRatio — we cap manually below ); diff --git a/src/game/Game.ts b/src/game/Game.ts index 107dac3..0db131a 100644 --- a/src/game/Game.ts +++ b/src/game/Game.ts @@ -11,6 +11,7 @@ import { Vector3, } from "@babylonjs/core"; import { + CHUNK_SIZE, PLAYER_HALF_WIDTH, PLAYER_HEIGHT, SEA_LEVEL, @@ -925,13 +926,13 @@ export class Game { this.scene.fogEnd = 1e6; return; } - const far = this.settings.viewDistance * 16 * 2.0; + const far = (this.settings.viewDistance + 1.25) * CHUNK_SIZE; // Higher presets get a later fog start → clearer mid-distance terrain and // less of the washed-out haze. Low keeps an earlier start to hide the // pop-in that comes with its shorter render distance. const startFrac = - this.settings.graphics.preset === "high" ? 0.6 : - this.settings.graphics.preset === "low" ? 0.42 : 0.5; + this.settings.graphics.preset === "high" ? 0.78 : + this.settings.graphics.preset === "low" ? 0.58 : 0.7; this.scene.fogEnd = far; this.scene.fogStart = far * startFrac; // Tell the underwater renderer the new above-water baseline so its blend diff --git a/src/game/World.ts b/src/game/World.ts index 8a630df..0a8ba3c 100644 --- a/src/game/World.ts +++ b/src/game/World.ts @@ -194,9 +194,11 @@ export class World { const cached = this.spiralCache.get(radius); if (cached) return cached; const list: Array<{ dx: number; dz: number; d: number }> = []; + const radiusSq = radius * radius; for (let dx = -radius; dx <= radius; dx++) { for (let dz = -radius; dz <= radius; dz++) { - list.push({ dx, dz, d: dx * dx + dz * dz }); + const d = dx * dx + dz * dz; + if (d <= radiusSq) list.push({ dx, dz, d }); } } list.sort((a, b) => a.d - b.d); @@ -514,7 +516,7 @@ export class World { /** Set the foliage (cutout) render-distance tier. Draw-time only. */ setFoliageDensity(density: FoliageDensity): void { - this.foliageCutoutDistance = density === "low" ? 48 : density === "medium" ? 96 : Infinity; + this.foliageCutoutDistance = density === "low" ? 48 : density === "medium" ? 96 : 160; } /** diff --git a/src/game/graphics/GraphicsController.ts b/src/game/graphics/GraphicsController.ts index bfda87e..aa93fea 100644 --- a/src/game/graphics/GraphicsController.ts +++ b/src/game/graphics/GraphicsController.ts @@ -41,7 +41,7 @@ export function shadowConfigForQuality(quality: ShadowQuality): { } /** Maximum supported render distance (chunk radius), capped for browser safety. */ -export const MAX_RENDER_DISTANCE = 12; +export const MAX_RENDER_DISTANCE = 20; /** Minimum render distance — below this the world feels empty. */ export const MIN_RENDER_DISTANCE = 2; @@ -133,9 +133,9 @@ export class GraphicsController { } this.pipeline.fxaaEnabled = true; } else if (this.pipeline) { - // AA off → tear down the pipeline so the scene renders straight to the - // default framebuffer (no fullscreen pass). Engine-level MSAA (set at - // context creation) remains as the baseline edge AA. + // AA off: tear down the pipeline so the scene renders straight to the + // default framebuffer. Engine-level antialiasing is also disabled at + // context creation, so low/custom presets render with no AA. this.pipeline.dispose(); this.pipeline = null; } @@ -179,9 +179,9 @@ export class GraphicsController { export function presetRenderDistance(preset: GraphicsSettings["preset"]): number { switch (preset) { case "low": return 4; - case "high": return 8; + case "high": return 16; case "medium": - default: return 6; + default: return 10; } } diff --git a/src/game/graphics/GraphicsSettings.ts b/src/game/graphics/GraphicsSettings.ts index 2625d5a..c52fadf 100644 --- a/src/game/graphics/GraphicsSettings.ts +++ b/src/game/graphics/GraphicsSettings.ts @@ -60,7 +60,7 @@ export const GRAPHICS_PRESETS: Record, Graphic renderScale: 1.0, dprCap: 2, antiAliasing: false, - shadows: "low", + shadows: "off", water: "medium", foliage: "medium", clouds: "fancy", @@ -71,7 +71,7 @@ export const GRAPHICS_PRESETS: Record, Graphic renderScale: 1.0, dprCap: 2, antiAliasing: true, - shadows: "medium", + shadows: "off", water: "high", foliage: "high", clouds: "fancy", @@ -88,7 +88,7 @@ export function defaultGraphicsSettings(): GraphicsSettings { /** A sensible default render distance (chunks) matching the device class. */ export function defaultRenderDistance(): number { - return detectLowEndDevice() ? 4 : 6; + return detectLowEndDevice() ? 4 : 10; } /** @@ -129,12 +129,16 @@ export function migrateGraphics(parsed: Partial | undefined): if (clouds === undefined && typeof legacy.clouds === "boolean") { clouds = legacy.clouds ? "fancy" : "off"; } + const preset = parsed.preset ?? base.preset; + const oldPresetShadow = + (preset === "medium" && parsed.shadows === "low") || + (preset === "high" && parsed.shadows === "medium"); return { - preset: parsed.preset ?? base.preset, + preset, renderScale: clamp(parsed.renderScale ?? base.renderScale, 0.5, 1), dprCap: clamp(parsed.dprCap ?? base.dprCap, 1.5, 2), antiAliasing: parsed.antiAliasing ?? base.antiAliasing, - shadows: parsed.shadows ?? base.shadows, + shadows: oldPresetShadow ? "off" : parsed.shadows ?? base.shadows, water: parsed.water ?? base.water, foliage: parsed.foliage ?? base.foliage, clouds: clouds ?? base.clouds, diff --git a/src/game/lighting/WaterMaterial.ts b/src/game/lighting/WaterMaterial.ts index b616931..8e86eee 100644 --- a/src/game/lighting/WaterMaterial.ts +++ b/src/game/lighting/WaterMaterial.ts @@ -43,7 +43,7 @@ export class WaterMaterial { private alpha = 0.82; /** Base diffuse/emissive colours (the subtle animation oscillates around these). */ private readonly baseDiffuse = Color3.FromHexString("#1f86d8"); - private readonly baseEmissive = Color3.FromHexString("#0b3a6b"); + private readonly baseEmissive = Color3.Black(); private readonly shimmerColor = Color3.FromHexString("#2aa0e8"); private animTime = 0; private animationEnabled = true; @@ -160,9 +160,7 @@ export class WaterMaterial { m.diffuseColor.r = this.baseDiffuse.r + (this.shimmerColor.r - this.baseDiffuse.r) * amp * w; m.diffuseColor.g = this.baseDiffuse.g + (this.shimmerColor.g - this.baseDiffuse.g) * amp * w; m.diffuseColor.b = this.baseDiffuse.b + (this.shimmerColor.b - this.baseDiffuse.b) * amp * w; - m.emissiveColor.r = this.baseEmissive.r * (1 + amp * 0.5 * w); - m.emissiveColor.g = this.baseEmissive.g * (1 + amp * 0.5 * w); - m.emissiveColor.b = this.baseEmissive.b * (1 + amp * 0.5 * w); + m.emissiveColor.copyFrom(this.baseEmissive); } /** Debug: enable/disable the surface scroll + shimmer (texture stays put). */ diff --git a/src/game/liquid/LiquidSimulator.ts b/src/game/liquid/LiquidSimulator.ts index b9a5a9e..bc5bd34 100644 --- a/src/game/liquid/LiquidSimulator.ts +++ b/src/game/liquid/LiquidSimulator.ts @@ -452,10 +452,9 @@ export class LiquidSimulator { /** * True if the liquid cell at (x,y,z) may spread SIDEWARDS — i.e. it is a - * source, or a flowing cell resting on SOLID terrain. A flowing cell with - * water/air below is part of a suspended/falling column and must NOT spread - * sideways (otherwise a waterfall grows into a diverging 3D plume). Only the - * cell that actually lands on the floor spreads, which keeps flow bounded. + * source, or a flowing cell resting on SOLID terrain. This mirrors Luanti's + * `LIQUID_FLOW_DOWN_MASK` behavior: source nodes still feed same-level + * neighbours, but same-level falling flowing nodes cannot feed sideways. */ private isSupported(access: LiquidAccess, x: number, y: number, z: number): boolean { const id = access.getBlock(x, y, z); diff --git a/src/game/liquid/LiquidUpdateQueue.ts b/src/game/liquid/LiquidUpdateQueue.ts index d651ce9..4505cbd 100644 --- a/src/game/liquid/LiquidUpdateQueue.ts +++ b/src/game/liquid/LiquidUpdateQueue.ts @@ -25,9 +25,11 @@ export class LiquidUpdateQueue { /** Normal lane (flow propagation + seeding). */ private readonly pending = new Set(); private fifo: QueuedCell[] = []; + private fifoHead = 0; /** Priority lane (player edits) — drained before the normal lane. */ private readonly priorityPending = new Set(); private priorityFifo: QueuedCell[] = []; + private priorityFifoHead = 0; /** High-water mark (both lanes) since creation, for the debug overlay. */ peakSize = 0; @@ -84,17 +86,25 @@ export class LiquidUpdateQueue { */ dequeue(): QueuedCell | null { // Priority first. - while (this.priorityFifo.length > 0) { - const cell = this.priorityFifo.shift()!; + while (this.priorityFifoHead < this.priorityFifo.length) { + const cell = this.priorityFifo[this.priorityFifoHead++]!; const k = this.key(cell.x, cell.y, cell.z); - if (this.priorityPending.delete(k)) return cell; + if (this.priorityPending.delete(k)) { + this.compactPriority(); + return cell; + } } + this.compactPriority(true); // Then the normal backlog. - while (this.fifo.length > 0) { - const cell = this.fifo.shift()!; + while (this.fifoHead < this.fifo.length) { + const cell = this.fifo[this.fifoHead++]!; const k = this.key(cell.x, cell.y, cell.z); - if (this.pending.delete(k)) return cell; + if (this.pending.delete(k)) { + this.compactNormal(); + return cell; + } } + this.compactNormal(true); return null; } @@ -104,11 +114,15 @@ export class LiquidUpdateQueue { * scheduled tick). Returns null when the priority lane is empty. */ pullPriority(): QueuedCell | null { - while (this.priorityFifo.length > 0) { - const cell = this.priorityFifo.shift()!; + while (this.priorityFifoHead < this.priorityFifo.length) { + const cell = this.priorityFifo[this.priorityFifoHead++]!; const k = this.key(cell.x, cell.y, cell.z); - if (this.priorityPending.delete(k)) return cell; + if (this.priorityPending.delete(k)) { + this.compactPriority(); + return cell; + } } + this.compactPriority(true); return null; } @@ -116,12 +130,28 @@ export class LiquidUpdateQueue { clear(): void { this.pending.clear(); this.fifo.length = 0; + this.fifoHead = 0; this.priorityPending.clear(); this.priorityFifo.length = 0; + this.priorityFifoHead = 0; } /** Debug snapshot of queued positions (read-only; does not mutate). */ snapshot(): readonly QueuedCell[] { - return this.fifo; + return this.fifo.slice(this.fifoHead); + } + + private compactNormal(force = false): void { + if (this.fifoHead === 0) return; + if (!force && this.fifoHead < 1024 && this.fifoHead * 2 < this.fifo.length) return; + this.fifo = this.fifo.slice(this.fifoHead); + this.fifoHead = 0; + } + + private compactPriority(force = false): void { + if (this.priorityFifoHead === 0) return; + if (!force && this.priorityFifoHead < 1024 && this.priorityFifoHead * 2 < this.priorityFifo.length) return; + this.priorityFifo = this.priorityFifo.slice(this.priorityFifoHead); + this.priorityFifoHead = 0; } } diff --git a/src/state/Settings.ts b/src/state/Settings.ts index e1c763b..000127b 100644 --- a/src/state/Settings.ts +++ b/src/state/Settings.ts @@ -36,7 +36,8 @@ export function loadSettings(): Settings { if (legacyClouds !== undefined && savedGraphics?.clouds === undefined) { graphics.clouds = legacyClouds ? "fancy" : "off"; } - return { ...DEFAULT_SETTINGS, ...rest, mode, graphics }; + const viewDistance = migratePresetViewDistance(rest.viewDistance, graphics.preset); + return { ...DEFAULT_SETTINGS, ...rest, viewDistance, mode, graphics }; } catch { return { ...DEFAULT_SETTINGS }; } @@ -50,4 +51,11 @@ export function saveSettings(settings: Settings): void { } } +function migratePresetViewDistance(viewDistance: number | undefined, preset: GraphicsSettings["preset"]): number { + if (viewDistance === undefined) return DEFAULT_SETTINGS.viewDistance; + if (preset === "medium" && (viewDistance === 6 || viewDistance === 8)) return 10; + if (preset === "high" && (viewDistance === 8 || viewDistance === 12)) return 16; + return viewDistance; +} + export type { GraphicsSettings }; diff --git a/tests/liquid.test.ts b/tests/liquid.test.ts index 778f730..be6c96a 100644 --- a/tests/liquid.test.ts +++ b/tests/liquid.test.ts @@ -66,10 +66,24 @@ console.log("\n[Test 1] Source above air pours straight down (waterfall), stops assert(w.getBlock(0, 4, 0) === WATER_FLOWING_BLOCK, "flowing at y=4"); assert(w.getBlock(0, 3, 0) === WATER_FLOWING_BLOCK, "flowing at y=3"); assert(w.getLevel(0, 4, 0) === 7 && w.getLevel(0, 1, 0) === 7, "falling column is full level (7)"); + assert(w.getBlock(1, 5, 0) === WATER_FLOWING_BLOCK && w.getBlock(-1, 5, 0) === WATER_FLOWING_BLOCK, "source feeds same-level neighbours like Luanti"); assert(w.getBlock(0, 0, 0) === 3, "floor intact"); assert(sim.queueSize === 0, "queue drained (no endless loop)"); } +console.log("\n[Test 1b] Falling flowing water does not feed sideways until it lands."); +{ + const w = new FakeWorld(); + w.floor(-14, 14, -14, 14, 0); + w.set(0, 5, 0, WATER_FLOWING_BLOCK, 7); + w.set(0, 4, 0, WATER_FLOWING_BLOCK, 7); + const sim = new LiquidSimulator(); + sim.enqueue(1, 4, 0); + settle(sim, w); + assert(w.getBlock(1, 4, 0) === AIR_BLOCK, "same-level falling flowing node does not feed sideways"); + assert(sim.queueSize === 0, "queue drained"); +} + console.log("\n[Test 2] Source on a solid floor spreads horizontally and stops at range."); { const w = new FakeWorld();