Fog of war: underground is black until the bot has seen it (t8)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+17
-10
@@ -75,6 +75,7 @@ dist/, shots/ build output and screenshots (not committed)
|
|||||||
only those are simulated. `setCell` wakes what it touches; after writing arrays directly, call
|
only those are simulated. `setCell` wakes what it touches; after writing arrays directly, call
|
||||||
`wakeXY` / `wakeHeatXY`.
|
`wakeXY` / `wakeHeatXY`.
|
||||||
- **Other state:**
|
- **Other state:**
|
||||||
|
- `fog`: exploration at one cell per 2×2 grains; see [fog](systems/fog.md);
|
||||||
- `machines`: index = machine id; removed ones are `null`. Each is
|
- `machines`: index = machine id; removed ones are `null`. Each is
|
||||||
`{ type, x, y, w, h, dir, on, ... }`. Its footprint is hull cells (solid) and void cells
|
`{ type, x, y, w, h, dir, on, ... }`. Its footprint is hull cells (solid) and void cells
|
||||||
(open inside).
|
(open inside).
|
||||||
@@ -215,11 +216,16 @@ Starter furnace designs and the milestone list shown in the notebook.
|
|||||||
- functions: smelterBlueprint
|
- functions: smelterBlueprint
|
||||||
- variables: STARTER_DESIGNS, MILESTONES
|
- variables: STARTER_DESIGNS, MILESTONES
|
||||||
|
|
||||||
### src/sim/world.js (96 lines)
|
### src/sim/world.js (97 lines)
|
||||||
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
|
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
|
||||||
- functions: emit, bagCap, discover, alloc, depthU, depthY, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
|
- functions: emit, bagCap, discover, alloc, depthU, depthY, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
|
||||||
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, awake, hot, hotLvl, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, tubeLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
|
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, awake, hot, hotLvl, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, tubeLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
|
||||||
|
|
||||||
|
### src/sim/fog.js (35 lines)
|
||||||
|
Exploration only: never changes grains or the random stream.
|
||||||
|
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
|
||||||
|
- variables: FOG, fog, FW, FH
|
||||||
|
|
||||||
### src/sim/cells.js (176 lines)
|
### src/sim/cells.js (176 lines)
|
||||||
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
||||||
- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, simMove
|
- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, simMove
|
||||||
@@ -314,16 +320,16 @@ Weather: clouds from steam, rain and snow, and the world water count.
|
|||||||
- functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick
|
- functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick
|
||||||
- variables: WBAND, WS, weather
|
- variables: WBAND, WS, weather
|
||||||
|
|
||||||
### src/sim/tick.js (25 lines)
|
### src/sim/tick.js (26 lines)
|
||||||
simTick: one simulation step, in order.
|
simTick: one simulation step, in order.
|
||||||
- functions: simTick
|
- functions: simTick
|
||||||
|
|
||||||
### src/sim/worldgen.js (315 lines)
|
### src/sim/worldgen.js (316 lines)
|
||||||
World generation: noise, layers, ores, caves, water, trees, and clearing tiny hard specks.
|
World generation: noise, layers, ores, caves, water, trees, and clearing tiny hard specks.
|
||||||
- functions: makeNoise, fbm, clearHardSpecks, generateWorld
|
- functions: makeNoise, fbm, clearHardSpecks, generateWorld
|
||||||
- variables: SPECK_MIN
|
- variables: SPECK_MIN
|
||||||
|
|
||||||
### src/sim/save.js (112 lines)
|
### src/sim/save.js (120 lines)
|
||||||
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
|
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
|
||||||
- functions: serializeWorld, deserializeWorld
|
- functions: serializeWorld, deserializeWorld
|
||||||
- variables: SAVE_VERSION, TOFF
|
- variables: SAVE_VERSION, TOFF
|
||||||
@@ -337,16 +343,16 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
|
|||||||
Propagated coloured light from glowing and burning cells.
|
Propagated coloured light from glowing and burning cells.
|
||||||
- functions: computeLight
|
- functions: computeLight
|
||||||
|
|
||||||
### src/render/world.js (277 lines)
|
### src/render/world.js (297 lines)
|
||||||
drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
|
Scale colour without changing the smooth-look class.
|
||||||
- functions: drawWorld
|
- functions: fogColor, drawWorld
|
||||||
- variables: TUBE_CLASS
|
- variables: TUBE_CLASS
|
||||||
|
|
||||||
### src/render/machines.js (197 lines)
|
### src/render/machines.js (197 lines)
|
||||||
Machine overlays: fans, engines, refiner, cutter, silo and the rest, drawn on top of the world.
|
Machine overlays: fans, engines, refiner, cutter, silo and the rest, drawn on top of the world.
|
||||||
- functions: cellRect, drawHazardRims, drawMachines, drawSilo, drawSiloPixel
|
- functions: cellRect, drawHazardRims, drawMachines, drawSilo, drawSiloPixel
|
||||||
|
|
||||||
### src/render/overlays.js (228 lines)
|
### src/render/overlays.js (229 lines)
|
||||||
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
|
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
|
||||||
- functions: spawnFloater, drawRain, drawFlowArrows, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
|
- functions: spawnFloater, drawRain, drawFlowArrows, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
|
||||||
|
|
||||||
@@ -379,7 +385,7 @@ Saves in IndexedDB, world files (export / import) and the blueprint library.
|
|||||||
Starting, loading and quitting a game; modal dialogs; the pause menu.
|
Starting, loading and quitting a game; modal dialogs; the pause menu.
|
||||||
- functions: showLoading, enterGame, startNewWorld, startFromSave, quitToTitle, refreshContinue, anyModal, openModal, closeModal, closeAllModals, topModal, dialog, openPause, renderWorlds
|
- functions: showLoading, enterGame, startNewWorld, startFromSave, quitToTitle, refreshContinue, anyModal, openModal, closeModal, closeAllModals, topModal, dialog, openPause, renderWorlds
|
||||||
|
|
||||||
### src/ui/settings.js (123 lines)
|
### src/ui/settings.js (124 lines)
|
||||||
The settings screen.
|
The settings screen.
|
||||||
- functions: openSettings, renderSettings, applySettings, toggleFullscreen, confirmWipe
|
- functions: openSettings, renderSettings, applySettings, toggleFullscreen, confirmWipe
|
||||||
- variables: SET_TABS, setTab
|
- variables: SET_TABS, setTab
|
||||||
@@ -403,7 +409,7 @@ The selected tool and piece: R / Q / E, drill width, filter picker, the selectio
|
|||||||
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleCollect, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
|
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleCollect, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
|
||||||
- variables: lastBagKey
|
- variables: lastBagKey
|
||||||
|
|
||||||
### src/ui/info.js (170 lines)
|
### src/ui/info.js (171 lines)
|
||||||
Discovery cards, hover info (I) and the cursor label.
|
Discovery cards, hover info (I) and the cursor label.
|
||||||
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel
|
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel
|
||||||
|
|
||||||
@@ -447,6 +453,7 @@ The frame loop and boot.
|
|||||||
- `test/t_conveyors.js`: Screw tubes that never carry liquid (it flows through), tube and belt throughput, filters.
|
- `test/t_conveyors.js`: Screw tubes that never carry liquid (it flows through), tube and belt throughput, filters.
|
||||||
- `test/t_drill.js`: The resonance drill: pulses from the bot shake rock until it shatters. Widths trade area for speed, tuning shatters only one material and drains it out of the wall, built blocks are left alone, and the drill says why when nothing breaks.
|
- `test/t_drill.js`: The resonance drill: pulses from the bot shake rock until it shatters. Widths trade area for speed, tuning shatters only one material and drains it out of the wall, built blocks are left alone, and the drill says why when nothing breaks.
|
||||||
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
|
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
|
||||||
|
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
||||||
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
||||||
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
||||||
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Refiner grinds waste rock for trace ore.
|
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Refiner grinds waste rock for trace ore.
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ What agents have done, newest first. Add an entry for every piece of work: the d
|
|||||||
note or request it was for, what changed, the files touched, the tests run, and anything left
|
note or request it was for, what changed, the files touched, the tests run, and anything left
|
||||||
open or not verified. Keep each entry short; the git history has the details.
|
open or not verified. Keep each entry short; the git history has the details.
|
||||||
|
|
||||||
|
## 2026-10-24: t8 Fog of war for unexplored areas (Claude Code, Codex)
|
||||||
|
|
||||||
|
- Decided with the owner: revealed by line of sight; unseen is black with a soft edge.
|
||||||
|
- New `src/sim/fog.js` (2×2-grain cells, 0-255, only ever rises): `fogInit` (sky and surface
|
||||||
|
seen), `fogLook` (256 rays, 48 grains, through air, gas and liquid, 4 grains into rock), called
|
||||||
|
every 4 ticks from `simTick`. Never read by the simulation.
|
||||||
|
- `src/sim/save.js`: fog saved; saves without it rebuild it (surface, 12 grains around built
|
||||||
|
things, the bot's view). `src/sim/world.js`, `worldgen.js`: allocate and initialize.
|
||||||
|
- `src/render/world.js`: grains scaled by a bilinear fog value; fully unseen grains drawn like
|
||||||
|
out-of-world grains; no glow from fog; thermal lens fogged too. `overlays.js`: rain and
|
||||||
|
particles hidden in fog. `src/ui/info.js`: "Unexplored". Setting `fog` (Game tab).
|
||||||
|
- Browser tests and tools turn fog off (`play.py`, `t_smooth.py`, `shot.py`, `perf.py`,
|
||||||
|
`playthrough.py`) except the fog checks. New `test/t_fog.js`; `docs/systems/fog.md`.
|
||||||
|
- Tests: all 12 headless suites, `play.py` (185 checks), `t_smooth` pass. Screenshots checked in
|
||||||
|
both looks. `tools/perf.py`: drawWorld 0.34 → 0.43 ms with fog on (no change with light off).
|
||||||
|
|
||||||
## 2026-10-24: t5 Material selection menu (Claude Code, Codex)
|
## 2026-10-24: t5 Material selection menu (Claude Code, Codex)
|
||||||
|
|
||||||
- Decided with the owner: F opens it for walls and molds, R keeps cycling.
|
- Decided with the owner: F opens it for walls and molds, R keeps cycling.
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Fog of war
|
||||||
|
|
||||||
|
Unexplored ground is black with a soft edge. The sky and the first few grains of the
|
||||||
|
surface start seen. Exploration never changes the simulation.
|
||||||
|
|
||||||
|
## Data and revealing
|
||||||
|
|
||||||
|
`src/sim/fog.js` keeps a `Uint8Array` at half the grain resolution (`FOG = 2`).
|
||||||
|
Values run from 0 (unseen) to 255 (seen) and only increase. `alloc` allocates it with
|
||||||
|
the other world arrays. `fogInit` reveals above `surfY` and fades eight grains below it.
|
||||||
|
|
||||||
|
Every four ticks, 256 rays from the bot's centre reveal up to 48 grains. The last eight
|
||||||
|
grains fade out. Solid and powder stop sight after a four-grain fade into their face;
|
||||||
|
air, gas and liquid pass sight. Bars and machine voids do not block it.
|
||||||
|
|
||||||
|
## Saving
|
||||||
|
|
||||||
|
The fog buffer is saved and exported with the other typed arrays, without a save version
|
||||||
|
change. Older saves rebuild the surface, reveal a radius of 12 grains around built blocks,
|
||||||
|
machine footprints and molds, then look from the bot. Loading and generating also look
|
||||||
|
immediately from the bot.
|
||||||
|
|
||||||
|
## Drawing and setting
|
||||||
|
|
||||||
|
Both looks and the thermal lens sample fog bilinearly. Seen grains use the usual drawing;
|
||||||
|
unseen grains use the out-of-world colour and smooth-look buffers, hiding their shapes.
|
||||||
|
Partial values dim foreground, background and trees. Bloom sources need at least 128;
|
||||||
|
bars, flyers, rain and particles need at least 64. Hover info and the cursor label say
|
||||||
|
only “Unexplored” below 64.
|
||||||
|
|
||||||
|
Game settings → Fog of war is on by default. Turning it off shows the whole world while
|
||||||
|
exploration continues to be recorded and saved. Browser scenes and performance tools turn
|
||||||
|
it off in their shared setup; dedicated fog checks enable it explicitly.
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"sim/economy.js",
|
"sim/economy.js",
|
||||||
"sim/progress.js",
|
"sim/progress.js",
|
||||||
"sim/world.js",
|
"sim/world.js",
|
||||||
|
"sim/fog.js",
|
||||||
"sim/cells.js",
|
"sim/cells.js",
|
||||||
"sim/heat.js",
|
"sim/heat.js",
|
||||||
"sim/machines.js",
|
"sim/machines.js",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function drawRain() {
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
let snow = false;
|
let snow = false;
|
||||||
for (const f of flyers) {
|
for (const f of flyers) {
|
||||||
if (!f.rain || f.x < x0 || f.x > x1 || f.y < y0 || f.y > y1) continue;
|
if ((SET.fog && fogSeen(f.x, f.y) < 64) || !f.rain || f.x < x0 || f.x > x1 || f.y < y0 || f.y > y1) continue;
|
||||||
if (f.m !== WATER) { snow = true; continue; }
|
if (f.m !== WATER) { snow = true; continue; }
|
||||||
const a = worldToScreenPx(f.x, f.y), len = Math.max(1.5, Math.hypot(f.vx, f.vy) * 3.2);
|
const a = worldToScreenPx(f.x, f.y), len = Math.max(1.5, Math.hypot(f.vx, f.vy) * 3.2);
|
||||||
ctx.moveTo(a.x, a.y); ctx.lineTo(a.x - f.vx / Math.max(0.2, f.vy) * len * zp * 0.5, a.y - len * zp);
|
ctx.moveTo(a.x, a.y); ctx.lineTo(a.x - f.vx / Math.max(0.2, f.vy) * len * zp * 0.5, a.y - len * zp);
|
||||||
@@ -20,7 +20,7 @@ function drawRain() {
|
|||||||
if (snow) {
|
if (snow) {
|
||||||
ctx.fillStyle = 'rgba(244,247,252,0.9)';
|
ctx.fillStyle = 'rgba(244,247,252,0.9)';
|
||||||
for (const f of flyers) {
|
for (const f of flyers) {
|
||||||
if (!f.rain || f.m === WATER || f.x < x0 || f.x > x1 || f.y < y0 || f.y > y1) continue;
|
if ((SET.fog && fogSeen(f.x, f.y) < 64) || !f.rain || f.m === WATER || f.x < x0 || f.x > x1 || f.y < y0 || f.y > y1) continue;
|
||||||
const a = worldToScreenPx(f.x + Math.sin(f.age * 0.07 + f.x) * 0.6, f.y), r = Math.max(1, zp * 0.42);
|
const a = worldToScreenPx(f.x + Math.sin(f.age * 0.07 + f.x) * 0.6, f.y), r = Math.max(1, zp * 0.42);
|
||||||
ctx.beginPath(); ctx.arc(a.x, a.y, r, 0, Math.PI * 2); ctx.fill();
|
ctx.beginPath(); ctx.arc(a.x, a.y, r, 0, Math.PI * 2); ctx.fill();
|
||||||
}
|
}
|
||||||
@@ -220,6 +220,7 @@ function drawParticles() {
|
|||||||
p.x += p.vx; p.y += p.vy; p.vy += 0.04; p.life--;
|
p.x += p.vx; p.y += p.vy; p.vy += 0.04; p.life--;
|
||||||
if (p.life <= 0) { ps.splice(k, 1); continue; }
|
if (p.life <= 0) { ps.splice(k, 1); continue; }
|
||||||
}
|
}
|
||||||
|
if (SET.fog && fogSeen(p.x, p.y) < 64) continue;
|
||||||
const s = worldToScreenPx(p.x, p.y);
|
const s = worldToScreenPx(p.x, p.y);
|
||||||
ctx.fillStyle = `rgba(${p.r | 0},${p.g | 0},${p.b | 0},${Math.min(1, p.life / 10)})`;
|
ctx.fillStyle = `rgba(${p.r | 0},${p.g | 0},${p.b | 0},${Math.min(1, p.life / 10)})`;
|
||||||
const sz = p.spark ? Math.max(1, zp * 0.5) : p.suck ? zp : Math.max(1, zp * 0.7);
|
const sz = p.spark ? Math.max(1, zp * 0.5) : p.suck ? zp : Math.max(1, zp * 0.7);
|
||||||
|
|||||||
+22
-2
@@ -1,3 +1,5 @@
|
|||||||
|
// Scale colour without changing the smooth-look class.
|
||||||
|
function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 16) | (((c >>> 8 & 255) * k) << 8) | ((c & 255) * k); }
|
||||||
// drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
|
// drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
|
||||||
// ---------- World pixels ----------
|
// ---------- World pixels ----------
|
||||||
// smooth-look class of a screw tube cell by direction: 9 up, 10 right, 11 down, 12 left
|
// smooth-look class of a screw tube cell by direction: 9 up, 10 right, 11 down, 12 left
|
||||||
@@ -20,6 +22,7 @@ function drawWorld() {
|
|||||||
for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) { const sx = (i % W) - vx0, sy = ((i / W) | 0) - vy0; if (sx >= 0 && sy >= 0 && sx < bw && sy < bh) mmask[sy * bw + sx] = 1; }
|
for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) { const sx = (i % W) - vx0, sy = ((i / W) | 0) - vy0; if (sx >= 0 && sy >= 0 && sx < bw && sy < bh) mmask[sy * bw + sx] = 1; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const useFog = SET.fog;
|
||||||
const OUT = sm ? 0x040a0a0c : 0xff0a0a0c;
|
const OUT = sm ? 0x040a0a0c : 0xff0a0a0c;
|
||||||
const lighting = SET.lighting !== 'off' && !R.lens;
|
const lighting = SET.lighting !== 'off' && !R.lens;
|
||||||
if (lighting && (R.frame % SET.lightEvery === 0 || !R.lw)) computeLight();
|
if (lighting && (R.frame % SET.lightEvery === 0 || !R.lw)) computeLight();
|
||||||
@@ -33,12 +36,20 @@ function drawWorld() {
|
|||||||
const y = vy0 + by;
|
const y = vy0 + by;
|
||||||
const row = by * bw;
|
const row = by * bw;
|
||||||
if (y < 0 || y >= H) { for (let bx = 0; bx < bw; bx++) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } } continue; }
|
if (y < 0 || y >= H) { for (let bx = 0; bx < bw; bx++) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } } continue; }
|
||||||
|
const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW;
|
||||||
const yw = y * W;
|
const yw = y * W;
|
||||||
let skr = SKY_R[y], skg = SKY_G[y], skb = SKY_B[y];
|
let skr = SKY_R[y], skg = SKY_G[y], skb = SKY_B[y];
|
||||||
if (rk > 0.01) { const gy = (skr + skg + skb) / 3 * 0.82; skr += (gy - skr) * rk * 0.7; skg += (gy - skg) * rk * 0.7; skb += (gy - skb) * rk * 0.65; }
|
if (rk > 0.01) { const gy = (skr + skg + skb) / 3 * 0.82; skr += (gy - skr) * rk * 0.7; skg += (gy - skg) * rk * 0.7; skb += (gy - skb) * rk * 0.65; }
|
||||||
for (let bx = 0; bx < bw; bx++) {
|
for (let bx = 0; bx < bw; bx++) {
|
||||||
const x = vx0 + bx;
|
const x = vx0 + bx;
|
||||||
if (x < 0 || x >= W) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
if (x < 0 || x >= W) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
||||||
|
let fv = 255;
|
||||||
|
if (useFog) {
|
||||||
|
const fx = x / FOG, j = Math.floor(fx), t = fx - j, j1 = Math.min(FW - 1, j + 1);
|
||||||
|
const a = fog[fr + j], b = fog[fr + j1], c = fog[fn + j], d = fog[fn + j1];
|
||||||
|
if ((a & b & c & d) !== 255) fv = (a + (b - a) * t) * (1 - ft) + (c + (d - c) * t) * ft;
|
||||||
|
if (!fv) { px[row + bx] = OUT; if (sm) { bpx[row + bx] = 0x000a0a0c; tpx[row + bx] = 0; fpx[row + bx] = 0xff; } continue; }
|
||||||
|
}
|
||||||
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
||||||
const T = temp[i];
|
const T = temp[i];
|
||||||
let r, g, b;
|
let r, g, b;
|
||||||
@@ -48,6 +59,7 @@ function drawWorld() {
|
|||||||
else { const v = (COLR[m] + COLG[m] + COLB[m]) / 3 * 0.25; r = tl[ti] * 0.78 + v; g = tl[ti + 1] * 0.78 + v; b = tl[ti + 2] * 0.78 + v; }
|
else { const v = (COLR[m] + COLG[m] + COLB[m]) / 3 * 0.25; r = tl[ti] * 0.78 + v; g = tl[ti + 1] * 0.78 + v; b = tl[ti + 2] * 0.78 + v; }
|
||||||
const p = part[i];
|
const p = part[i];
|
||||||
if (p && ((x + y) & 1)) { r = r * 0.7 + 40; g = g * 0.7 + 40; b = b * 0.7 + 40; }
|
if (p && ((x + y) & 1)) { r = r * 0.7 + 40; g = g * 0.7 + 40; b = b * 0.7 + 40; }
|
||||||
|
if (fv < 255) { r *= fv / 255; g *= fv / 255; b *= fv / 255; }
|
||||||
px[row + bx] = 0xff000000 | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
px[row + bx] = 0xff000000 | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -167,7 +179,7 @@ function drawWorld() {
|
|||||||
if (T > 450 && m !== EMPTY && incand) {
|
if (T > 450 && m !== EMPTY && incand) {
|
||||||
const gi = glowIndex(PH[m] === PH_SOLID ? T + nz * 0.9 : m === MAGMA ? T + Math.sin(x * 0.19 + y * 0.11 + anim * 0.03 + nz * 0.04) * 55 - 30 : T), w = GLOW_W[gi];
|
const gi = glowIndex(PH[m] === PH_SOLID ? T + nz * 0.9 : m === MAGMA ? T + Math.sin(x * 0.19 + y * 0.11 + anim * 0.03 + nz * 0.04) * 55 - 30 : T), w = GLOW_W[gi];
|
||||||
r = r * (1 - w) + GLOW_R[gi] * w; g = g * (1 - w) + GLOW_G[gi] * w; b = b * (1 - w) + GLOW_B[gi] * w;
|
r = r * (1 - w) + GLOW_R[gi] * w; g = g * (1 - w) + GLOW_G[gi] * w; b = b * (1 - w) + GLOW_B[gi] * w;
|
||||||
if (glowOn && w > 0.3 && ((bx | by) & 1) === 0) {
|
if (glowOn && fv >= 128 && w > 0.3 && ((bx | by) & 1) === 0) {
|
||||||
const gk = (by >> 1) * gw + (bx >> 1), s = (w - 0.3) * 1.4;
|
const gk = (by >> 1) * gw + (bx >> 1), s = (w - 0.3) * 1.4;
|
||||||
gpx[gk] = 0xff000000 | (Math.min(255, GLOW_B[gi] * s) << 16) | (Math.min(255, GLOW_G[gi] * s) << 8) | Math.min(255, GLOW_R[gi] * s);
|
gpx[gk] = 0xff000000 | (Math.min(255, GLOW_B[gi] * s) << 16) | (Math.min(255, GLOW_G[gi] * s) << 8) | Math.min(255, GLOW_R[gi] * s);
|
||||||
}
|
}
|
||||||
@@ -198,11 +210,19 @@ function drawWorld() {
|
|||||||
top = cl << 24;
|
top = cl << 24;
|
||||||
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 || cl === 4 || cl === 6 ? 0xff : 0);
|
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 || cl === 4 || cl === 6 ? 0xff : 0);
|
||||||
}
|
}
|
||||||
|
if (fv < 255) {
|
||||||
|
const k = fv / 255; r *= k; g *= k; b *= k;
|
||||||
|
if (sm) {
|
||||||
|
const c = bpx[row + bx], t = tpx[row + bx];
|
||||||
|
bpx[row + bx] = fogColor(c, k); tpx[row + bx] = fogColor(t, k);
|
||||||
|
}
|
||||||
|
}
|
||||||
px[row + bx] = top | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
px[row + bx] = top | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// bars and other loose objects
|
// bars and other loose objects
|
||||||
if (!lens) for (const e of ents) {
|
if (!lens) for (const e of ents) {
|
||||||
|
if (useFog && fogSeen(e.ix, e.iy) < 64) continue;
|
||||||
const ex = e.ix - vx0, ey = e.iy - vy0;
|
const ex = e.ix - vx0, ey = e.iy - vy0;
|
||||||
if (ex + BAR_W < 0 || ey + BAR_H < 0 || ex >= bw || ey >= bh) continue;
|
if (ex + BAR_W < 0 || ey + BAR_H < 0 || ex >= bw || ey >= bh) continue;
|
||||||
const m = e.m;
|
const m = e.m;
|
||||||
@@ -219,7 +239,7 @@ function drawWorld() {
|
|||||||
}
|
}
|
||||||
// grains in flight from the vacuum nozzle, a tube or a belt (rain and snow are drawn over the top, see drawRain)
|
// grains in flight from the vacuum nozzle, a tube or a belt (rain and snow are drawn over the top, see drawRain)
|
||||||
if (!lens) for (const f of flyers) {
|
if (!lens) for (const f of flyers) {
|
||||||
if (f.rain) continue;
|
if (f.rain || (useFog && fogSeen(f.x, f.y) < 64)) continue;
|
||||||
const sx = Math.floor(f.x) - vx0, sy = Math.floor(f.y) - vy0;
|
const sx = Math.floor(f.x) - vx0, sy = Math.floor(f.y) - vy0;
|
||||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||||
const m = f.m, v = ((sx * 7 + sy * 13) & 15) - 8;
|
const m = f.m, v = ((sx * 7 + sy * 13) & 15) - 8;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Exploration only: never changes grains or the random stream.
|
||||||
|
const FOG = 2;
|
||||||
|
let fog, FW, FH;
|
||||||
|
function fogAlloc() { FW = Math.ceil(W / FOG); FH = Math.ceil(H / FOG); fog = new Uint8Array(FW * FH); }
|
||||||
|
function fogMark(x, y, v) {
|
||||||
|
if (x < 0 || y < 0 || x >= W || y >= H) return;
|
||||||
|
const i = Math.floor(y / FOG) * FW + Math.floor(x / FOG);
|
||||||
|
if (v > fog[i]) fog[i] = v;
|
||||||
|
}
|
||||||
|
function fogSeen(x, y) {
|
||||||
|
if (x < 0 || y < 0 || x >= W || y >= H) return 255;
|
||||||
|
return fog[Math.floor(y / FOG) * FW + Math.floor(x / FOG)];
|
||||||
|
}
|
||||||
|
function fogInit() {
|
||||||
|
for (let x = 0; x < W; x++) for (let y = 0; y < Math.min(H, surfY[x] + 8); y++) fogMark(x, y, y <= surfY[x] ? 255 : (surfY[x] + 8 - y) * 255 / 8);
|
||||||
|
}
|
||||||
|
function fogLook(cx, cy) {
|
||||||
|
for (let a = 0; a < 256; a++) {
|
||||||
|
const dx = Math.cos(a * Math.PI / 128), dy = Math.sin(a * Math.PI / 128);
|
||||||
|
let hit = -1;
|
||||||
|
for (let d = 0; d < 48; d++) {
|
||||||
|
const x = Math.floor(cx + dx * d), y = Math.floor(cy + dy * d);
|
||||||
|
if (x < 0 || y < 0 || x >= W || y >= H) break;
|
||||||
|
const ph = PH[mat[y * W + x]];
|
||||||
|
if (hit < 0 && (ph === PH_SOLID || ph === PH_POWDER)) hit = d;
|
||||||
|
const v = Math.min(255, (48 - d) * 255 / 8, hit < 0 ? 255 : (4 - d + hit) * 255 / 4);
|
||||||
|
if (v <= 0) break;
|
||||||
|
fogMark(x, y, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function fogRevealAround(x, y, r) {
|
||||||
|
for (let yy = Math.max(0, Math.floor(y - r)); yy <= Math.min(H - 1, y + r); yy++) for (let xx = Math.max(0, Math.floor(x - r)); xx <= Math.min(W - 1, x + r); xx++) if ((xx - x) ** 2 + (yy - y) ** 2 <= r * r) fogMark(xx, yy, 255);
|
||||||
|
}
|
||||||
|
function fogRevealAll() { fog.fill(255); }
|
||||||
+9
-1
@@ -25,7 +25,7 @@ function serializeWorld() {
|
|||||||
pipes: (() => { if (pipesDirty) rebuildPipes(); return pipeNets.filter(pn => pn.n > 0).map(pn => [pn.blocks[0], pn.m, pn.n, Math.round(pn.T)]); })(),
|
pipes: (() => { if (pipesDirty) rebuildPipes(); return pipeNets.filter(pn => pn.n > 0).map(pn => [pn.blocks[0], pn.m, pn.n, Math.round(pn.T)]); })(),
|
||||||
cols: { surfY: Array.from(surfY), dirtY: Array.from(dirtY), graniteY: Array.from(graniteY), basaltY: Array.from(basaltY), magmaY: Array.from(magmaY) },
|
cols: { surfY: Array.from(surfY), dirtY: Array.from(dirtY), graniteY: Array.from(graniteY), basaltY: Array.from(basaltY), magmaY: Array.from(magmaY) },
|
||||||
};
|
};
|
||||||
return { meta, buffers: { mat: mat.slice(), temp: t16, life: life.slice(), flags: flags.slice(), part: part.slice(), blockP: blockP.slice(), blockV: blockV.slice(), bgm: bgm.slice() } };
|
return { meta, buffers: { fog: fog.slice(), mat: mat.slice(), temp: t16, life: life.slice(), flags: flags.slice(), part: part.slice(), blockP: blockP.slice(), blockV: blockV.slice(), bgm: bgm.slice() } };
|
||||||
}
|
}
|
||||||
function deserializeWorld(meta, buffers) {
|
function deserializeWorld(meta, buffers) {
|
||||||
if (meta.v !== SAVE_VERSION) throw new Error('This world was saved by an older version of the game and cannot be loaded.');
|
if (meta.v !== SAVE_VERSION) throw new Error('This world was saved by an older version of the game and cannot be loaded.');
|
||||||
@@ -108,5 +108,13 @@ function deserializeWorld(meta, buffers) {
|
|||||||
if (!meta.specks) clearHardSpecks(true); // older saves: tidy away tiny specks of too-hard rock once
|
if (!meta.specks) clearHardSpecks(true); // older saves: tidy away tiny specks of too-hard rock once
|
||||||
rebuildPipes();
|
rebuildPipes();
|
||||||
for (const [b, m, n, T] of meta.pipes || []) { const id = pipeNetAt[b]; if (id >= 0) { const pn = pipeNets[id]; pn.m = m; pn.n = Math.min(n, pn.cap); pn.T = T; } }
|
for (const [b, m, n, T] of meta.pipes || []) { const id = pipeNetAt[b]; if (id >= 0) { const pn = pipeNets[id]; pn.m = m; pn.n = Math.min(n, pn.cap); pn.T = T; } }
|
||||||
|
if (buffers.fog && buffers.fog.length === fog.length) fog.set(buffers.fog);
|
||||||
|
else {
|
||||||
|
fogInit();
|
||||||
|
for (let b = 0; b < NB; b++) if (blockP[b]) fogRevealAround((b % BW) * BS + BS / 2, ((b / BW) | 0) * BS + BS / 2, 12);
|
||||||
|
for (const m of machines) if (m) for (let y = m.y; y < m.y + m.h; y += BS) for (let x = m.x; x < m.x + m.w; x += BS) fogRevealAround(x, y, 12);
|
||||||
|
for (const mo of molds) for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) fogRevealAround(i % W, (i / W) | 0, 12);
|
||||||
|
}
|
||||||
|
fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
||||||
EV.length = 0;
|
EV.length = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ function simTick() {
|
|||||||
if (tick % 60 === 17) { marketTick(); ordersTick(); }
|
if (tick % 60 === 17) { marketTick(); ordersTick(); }
|
||||||
if (tick % 30 === 11 && saplings.length) saplingTick();
|
if (tick % 30 === 11 && saplings.length) saplingTick();
|
||||||
ghostTick();
|
ghostTick();
|
||||||
|
if (tick % 4 === 0) fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function discover(m, x, y, from, how, T) {
|
|||||||
|
|
||||||
function alloc(w, h) {
|
function alloc(w, h) {
|
||||||
W = w; H = h; N = w * h;
|
W = w; H = h; N = w * h;
|
||||||
|
fogAlloc();
|
||||||
CW = Math.ceil(w / 32); CH = Math.ceil(h / 32); NCH = CW * CH;
|
CW = Math.ceil(w / 32); CH = Math.ceil(h / 32); NCH = CW * CH;
|
||||||
mat = new Uint8Array(N); temp = new Float32Array(N); life = new Uint8Array(N); flags = new Uint8Array(N);
|
mat = new Uint8Array(N); temp = new Float32Array(N); life = new Uint8Array(N); flags = new Uint8Array(N);
|
||||||
stamp = new Uint8Array(N); part = new Uint8Array(N); mach = new Uint16Array(N); digp = new Uint8Array(N);
|
stamp = new Uint8Array(N); part = new Uint8Array(N); mach = new Uint16Array(N); digp = new Uint8Array(N);
|
||||||
|
|||||||
@@ -311,5 +311,6 @@ function generateWorld(seedIn, w, h, onProgress) {
|
|||||||
seen = new Uint8Array(NMAT); ledger = {};
|
seen = new Uint8Array(NMAT); ledger = {};
|
||||||
stats.dug = stats.moved = stats.placed = stats.ignitions = stats.cast = stats.bars = 0;
|
stats.dug = stats.moved = stats.placed = stats.ignitions = stats.cast = stats.bars = 0;
|
||||||
EV.length = 0;
|
EV.length = 0;
|
||||||
|
fogInit(); fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
||||||
if (onProgress) onProgress(1);
|
if (onProgress) onProgress(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ function reactionsOf(m, forNotebook) {
|
|||||||
// What is under the cursor: t1 is the name, t2 the details, warn a short problem
|
// What is under the cursor: t1 is the name, t2 the details, warn a short problem
|
||||||
// (too hard, out of reach) for the label next to the cursor.
|
// (too hard, out of reach) for the label next to the cursor.
|
||||||
function hoverInfo(ax, ay) {
|
function hoverInfo(ax, ay) {
|
||||||
|
if (SET.fog && fogSeen(ax, ay) < 64) return { t1: 'Unexplored', t2: [], t3: [] };
|
||||||
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return { t1: '', t2: ['Outside the world'], warn: '', air: true };
|
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return { t1: '', t2: ['Outside the world'], warn: '', air: true };
|
||||||
const i = ay * W + ax, m = mat[i];
|
const i = ay * W + ax, m = mat[i];
|
||||||
let warn = '';
|
let warn = '';
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const SET_TABS = {
|
|||||||
{ html: () => controlsHtml() },
|
{ html: () => controlsHtml() },
|
||||||
],
|
],
|
||||||
Game: [
|
Game: [
|
||||||
|
{ k: 'fog', label: 'Fog of war', d: 'Places you have not seen yet are black. Off shows the whole world.', type: 'toggle' },
|
||||||
{ k: 'cardSeconds', label: 'Discovery cards', d: 'How long a card stays when you find or make something new. Everything is also in the notebook.', type: 'select', opts: [[0, 'Off'], [5, '5 seconds'], [9, '9 seconds'], [15, '15 seconds'], [-1, 'Until I close them']] },
|
{ k: 'cardSeconds', label: 'Discovery cards', d: 'How long a card stays when you find or make something new. Everything is also in the notebook.', type: 'select', opts: [[0, 'Off'], [5, '5 seconds'], [9, '9 seconds'], [15, '15 seconds'], [-1, 'Until I close them']] },
|
||||||
{ k: 'autosave', label: 'Autosave', type: 'select', opts: [[0, 'Off'], [1, 'Every minute'], [3, 'Every 3 minutes'], [5, 'Every 5 minutes'], [10, 'Every 10 minutes']] },
|
{ k: 'autosave', label: 'Autosave', type: 'select', opts: [[0, 'Off'], [1, 'Every minute'], [3, 'Every 3 minutes'], [5, 'Every 5 minutes'], [10, 'Every 10 minutes']] },
|
||||||
{ k: 'save', label: 'Save', d: 'Saves to this browser.', type: 'button', text: 'Save now', fn: () => saveWorld('main'), ingame: true },
|
{ k: 'save', label: 'Save', d: 'Saves to this browser.', type: 'button', text: 'Save now', fn: () => saveWorld('main'), ingame: true },
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
const SET_DEFAULT = {
|
const SET_DEFAULT = {
|
||||||
zoom: 12, integerScale: true, renderScale: 1, uiScale: 1, fpsCap: 0, showPerf: false, shake: true, cameraSmooth: 0.7, cameraLead: true,
|
zoom: 12, integerScale: true, renderScale: 1, uiScale: 1, fpsCap: 0, showPerf: false, shake: true, cameraSmooth: 0.7, cameraLead: true,
|
||||||
look: 'smooth', lighting: 'full', lightEvery: 1, ambient: 0.12, daylight: 1, glow: 'low', grain: 1, incandescence: true, particles: true,
|
look: 'smooth', lighting: 'full', lightEvery: 1, ambient: 0.12, daylight: 1, glow: 'low', grain: 1, incandescence: true, particles: true,
|
||||||
thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3,
|
fog: true, thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3,
|
||||||
volMaster: 0.8, volMusic: 0.5, volSfx: 0.8, muted: false, muteUnfocused: true,
|
volMaster: 0.8, volMusic: 0.5, volSfx: 0.8, muted: false, muteUnfocused: true,
|
||||||
deadzone: 0.18, aimSpeed: 1, autosave: 3, cardSeconds: 9, cursorLabel: true, sv: 2,
|
deadzone: 0.18, aimSpeed: 1, autosave: 3, cardSeconds: 9, cursorLabel: true, sv: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
+49
-3
@@ -20,7 +20,7 @@ with sync_playwright() as p:
|
|||||||
logs = []
|
logs = []
|
||||||
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
||||||
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type == 'error' else None)
|
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type == 'error' else None)
|
||||||
pg.goto(url); pg.wait_for_timeout(300)
|
pg.goto(url); pg.evaluate("SET.fog = false"); pg.wait_for_timeout(300)
|
||||||
check('storage transactions roll back when a write throws', pg.evaluate("""async () => {
|
check('storage transactions roll back when a write throws', pg.evaluate("""async () => {
|
||||||
try { await DB.op('readwrite', st => { st.put('partial', 'abort-test'); throw new Error('test abort'); }); }
|
try { await DB.op('readwrite', st => { st.put('partial', 'abort-test'); throw new Error('test abort'); }); }
|
||||||
catch (e) { return await DB.get('abort-test') === undefined; }
|
catch (e) { return await DB.get('abort-test') === undefined; }
|
||||||
@@ -669,10 +669,12 @@ with sync_playwright() as p:
|
|||||||
dl = dl_info.value
|
dl = dl_info.value
|
||||||
wf = json.loads(pathlib.Path(dl.path()).read_text())
|
wf = json.loads(pathlib.Path(dl.path()).read_text())
|
||||||
check('Export world file downloads a world file', wf.get('format') == 'deepgrain-world' and wf['meta']['W'] == ev('W'), dl.suggested_filename)
|
check('Export world file downloads a world file', wf.get('format') == 'deepgrain-world' and wf['meta']['W'] == ev('W'), dl.suggested_filename)
|
||||||
|
fog_want = ev('Array.from(fog)')
|
||||||
|
check('world export includes fog bytes', wf['buffers']['fog']['type'] == 'Uint8Array')
|
||||||
want = ev('JSON.stringify([W, H, Math.round(money), Math.round(debt), bagTotal])')
|
want = ev('JSON.stringify([W, H, Math.round(money), Math.round(debt), bagTotal])')
|
||||||
pg3 = ctx.new_page()
|
pg3 = ctx.new_page()
|
||||||
pg3.on('pageerror', lambda e: logs.append(f'PAGEERROR(import): {e}'))
|
pg3.on('pageerror', lambda e: logs.append(f'PAGEERROR(import): {e}'))
|
||||||
pg3.goto(url); pg3.wait_for_timeout(300)
|
pg3.goto(url); pg3.evaluate('SET.fog = false'); pg3.wait_for_timeout(300)
|
||||||
worlds_before_import = ev('listWorlds().then(w => w.length)')
|
worlds_before_import = ev('listWorlds().then(w => w.length)')
|
||||||
pg3.set_input_files('#importfile', dl.path())
|
pg3.set_input_files('#importfile', dl.path())
|
||||||
pg3.wait_for_function('GAME.running === true', timeout=60000)
|
pg3.wait_for_function('GAME.running === true', timeout=60000)
|
||||||
@@ -680,13 +682,57 @@ with sync_playwright() as p:
|
|||||||
check('importing it on the title screen brings the same world back', got == want, f'{want} -> {got}')
|
check('importing it on the title screen brings the same world back', got == want, f'{want} -> {got}')
|
||||||
pg3.wait_for_function("async () => (await listWorlds()).length === " + str(worlds_before_import + 1))
|
pg3.wait_for_function("async () => (await listWorlds()).length === " + str(worlds_before_import + 1))
|
||||||
check('import adds an independent saved world', pg3.evaluate('GAME.worldId') != ev('GAME.worldId') and pg3.evaluate('listWorlds().then(w => w.length)') == worlds_before_import + 1)
|
check('import adds an independent saved world', pg3.evaluate('GAME.worldId') != ev('GAME.worldId') and pg3.evaluate('listWorlds().then(w => w.length)') == worlds_before_import + 1)
|
||||||
|
check('world file keeps fog bytes', pg3.evaluate('Array.from(fog)') == fog_want)
|
||||||
pg3.close()
|
pg3.close()
|
||||||
pg.keyboard.press('Escape'); settle(150)
|
pg.keyboard.press('Escape'); settle(150)
|
||||||
|
|
||||||
|
# Fog has its own paused scene; existing play checks keep the whole world visible.
|
||||||
|
fb = p.chromium.launch(args=['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'])
|
||||||
|
fp = fb.new_page(viewport={'width': 1280, 'height': 800})
|
||||||
|
fp.on('pageerror', lambda e: logs.append(f'PAGEERROR(fog): {e}'))
|
||||||
|
fp.add_init_script('window.__allowSoftwareGL = true')
|
||||||
|
fp.goto(url)
|
||||||
|
fp.evaluate('startNewWorld(4242, 512, 384)')
|
||||||
|
fp.wait_for_function('GAME.running')
|
||||||
|
fp.evaluate("""() => {
|
||||||
|
GAME.paused = true; SET.fog = true; SET.look = 'pixel'; SET.zoom = 6; SET.lighting = 'off';
|
||||||
|
SET.cameraLead = false; SET.cameraSmooth = 0; resizeRender();
|
||||||
|
for (let y = 130; y < 280; y++) for (let x = 160; x < 350; x++) setCell(y * W + x, STONE, 20, F_NAT);
|
||||||
|
for (let y = 140; y < 205; y++) for (let x = 240; x < 252; x++) setCell(y * W + x, EMPTY, 20, 0);
|
||||||
|
for (let y = 192; y < 205; y++) for (let x = 240; x < 280; x++) setCell(y * W + x, EMPTY, 20, 0);
|
||||||
|
bot.x = 243; bot.y = 192; fog.fill(0); fogInit(); fogLook(bot.x + 2.5, bot.y + 2.5);
|
||||||
|
snapCamera(); GAME.hidePreviews = true; $('cards').replaceChildren();
|
||||||
|
}""")
|
||||||
|
fp.wait_for_timeout(300)
|
||||||
|
sample = "(() => { drawWorld(); const k = (235 - R.vy0) * R.bw + 300 - R.vx0; return R.px[k] & 0xffffff; })()"
|
||||||
|
check('unexplored pixel is out-of-world black', fp.evaluate(sample) == 0x0a0a0c)
|
||||||
|
check('sky starts seen and draws colour', fp.evaluate("(() => { const cy=R.camY; R.camY=0; drawWorld(); const c=R.px[20*R.bw+20]&0xffffff; R.camY=cy; drawWorld(); return fogSeen(300,0)===255 && c!==0x0a0a0c; })()"))
|
||||||
|
check('unexplored canvas pixel is black', fp.evaluate("(() => {const s=worldToScreenPx(300.5,235.5);return Array.from(R.ctx.getImageData(s.x,s.y,1,1).data).slice(0,3).every(v=>v<=12)})()"))
|
||||||
|
check('hover and cursor share only Unexplored', fp.evaluate("JSON.stringify(hoverInfo(300,235)) === JSON.stringify({t1:'Unexplored',t2:[],t3:[]})"))
|
||||||
|
fp.evaluate("GAME.aim = {x:300,y:235}; renderInfo()")
|
||||||
|
check('info box says only Unexplored', fp.locator('#info').inner_text() == 'Unexplored')
|
||||||
|
check('cursor draws only Unexplored', fp.evaluate("""() => {
|
||||||
|
const old=R.ctx.fillText, inside=GAME.mouse.in, texts=[];
|
||||||
|
R.ctx.fillText=function(t) { texts.push(t); }; GAME.mouse.in=true;
|
||||||
|
drawCursorLabel(500,400,1); R.ctx.fillText=old; GAME.mouse.in=inside;
|
||||||
|
return texts.length===1 && texts[0]==='Unexplored';
|
||||||
|
}"""))
|
||||||
|
fp.screenshot(path=str(out / 'fog-pixel.png'))
|
||||||
|
fp.evaluate('R.lens = true')
|
||||||
|
check('thermal lens hides unexplored grains', fp.evaluate(sample) == 0x0a0a0c)
|
||||||
|
fp.evaluate('R.lens = false; SET.fog = false')
|
||||||
|
check('fog toggle shows the same rock', fp.evaluate(sample) != 0x0a0a0c)
|
||||||
|
fp.evaluate("SET.fog = true; SET.look = 'smooth'; SM.t0 = performance.now() + 100000")
|
||||||
|
fp.wait_for_timeout(500)
|
||||||
|
check('fog draws in the smooth look', fp.evaluate('R.smoothOn && SM.ok'))
|
||||||
|
check('unseen smooth grains have no terrain shape', fp.evaluate("(() => {const k=(235-R.vy0)*R.bw+300-R.vx0;return R.px[k]===0x040a0a0c && R.backPx[k]===0x000a0a0c && R.treePx[k]===0 && R.fieldPx[k]===255})()"))
|
||||||
|
fp.screenshot(path=str(out / 'fog-smooth.png'))
|
||||||
|
fb.close()
|
||||||
|
|
||||||
# 11. the largest world from the title screen
|
# 11. the largest world from the title screen
|
||||||
pg2 = ctx.new_page()
|
pg2 = ctx.new_page()
|
||||||
pg2.on('pageerror', lambda e: logs.append(f'PAGEERROR(large): {e}'))
|
pg2.on('pageerror', lambda e: logs.append(f'PAGEERROR(large): {e}'))
|
||||||
pg2.goto(url); pg2.wait_for_timeout(300)
|
pg2.goto(url); pg2.evaluate('SET.fog = false'); pg2.wait_for_timeout(300)
|
||||||
check('the size menu offers small, medium and large, and no deep world', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')") == '2048x1024,3072x1280,4096x1536', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')"))
|
check('the size menu offers small, medium and large, and no deep world', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')") == '2048x1024,3072x1280,4096x1536', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')"))
|
||||||
pg2.click('#btn-new'); pg2.fill('#nw-seed', '4242'); pg2.select_option('#nw-size', '4096x1536'); pg2.click('#btn-generate')
|
pg2.click('#btn-new'); pg2.fill('#nw-seed', '4242'); pg2.select_option('#nw-size', '4096x1536'); pg2.click('#btn-generate')
|
||||||
pg2.wait_for_function('GAME.running === true', timeout=90000)
|
pg2.wait_for_function('GAME.running === true', timeout=90000)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Fog: surface, sight, range, saves and simulation independence.
|
||||||
|
const { load, suite, flatWorld, rect, ticks } = require('./harness');
|
||||||
|
const { run } = load(), { check, done } = suite();
|
||||||
|
run('generateWorld(4242, 512, 384, null)');
|
||||||
|
check('sky and surface start seen', run('fogSeen(20, 0) >= 200 && fogSeen(20, surfY[20]) >= 200'));
|
||||||
|
check('deep ground starts unseen', run('fogSeen(20, surfY[20] + 60) === 0'));
|
||||||
|
flatWorld(run, 256, 256, 40);
|
||||||
|
rect(run, 40, 100, 140, 110, run('EMPTY'));
|
||||||
|
rect(run, 40, 125, 140, 135, run('EMPTY'));
|
||||||
|
run('bot.x = 60; bot.y = 102; fogLook(62.5, 104.5)');
|
||||||
|
check('open pocket is visible', run('fogSeen(82, 104) >= 200'));
|
||||||
|
check('rock hides separate pocket', run('fogSeen(62, 130) === 0'));
|
||||||
|
check('one look has limited range', run('fogSeen(124, 104) === 0'));
|
||||||
|
check('fog only increases', run('(()=>{const f = fog.slice(); fogLook(90,104); return f.every((v,i)=>fog[i]>=v)})()'));
|
||||||
|
check('save/load keeps fog', run('(()=>{fogLook(bot.x+bot.w/2,bot.y+bot.h/2);const s=serializeWorld(), f=fog.slice();deserializeWorld(s.meta,s.buffers);return f.every((v,i)=>fog[i]===v)})()'));
|
||||||
|
rect(run, 200, 160, 203, 163, run('EMPTY'));
|
||||||
|
run('placeBlock(BP_WALL, RSTONE, 50, 40, true)');
|
||||||
|
check('old save rebuilds exploration', run('(()=>{const s=serializeWorld();delete s.buffers.fog;deserializeWorld(s.meta,s.buffers);return fogSeen(62,104)>=200 && fogSeen(202,162)===255 && fogSeen(20,220)===0})()'));
|
||||||
|
check('wrong length fog rebuilds exploration', run('(()=>{const s=serializeWorld();s.buffers.fog=new Uint8Array(1);deserializeWorld(s.meta,s.buffers);return fogSeen(202,162)===255})()'));
|
||||||
|
const hash = () => run('mat.reduce((h,v)=>Math.imul(h^v,16777619)>>>0,2166136261)');
|
||||||
|
run('generateWorld(42, 256, 256, null)'); ticks(run, 300); const h = hash();
|
||||||
|
run('generateWorld(42, 256, 256, null); fogLook = function () {}'); ticks(run, 300);
|
||||||
|
check('fog leaves simulation unchanged', hash() === h);
|
||||||
|
done();
|
||||||
+2
-2
@@ -16,7 +16,7 @@ with sync_playwright() as p:
|
|||||||
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
||||||
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type in ('error', 'warning') else None)
|
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type in ('error', 'warning') else None)
|
||||||
pg.add_init_script('window.__allowSoftwareGL = true; localStorage.clear();')
|
pg.add_init_script('window.__allowSoftwareGL = true; localStorage.clear();')
|
||||||
pg.goto((root / 'dist' / 'index.html').as_uri()); pg.wait_for_timeout(300)
|
pg.goto((root / 'dist' / 'index.html').as_uri()); pg.evaluate('SET.fog = false'); pg.wait_for_timeout(300)
|
||||||
pg.click('#btn-new'); pg.fill('#nw-seed', '777'); pg.click('#btn-generate')
|
pg.click('#btn-new'); pg.fill('#nw-seed', '777'); pg.click('#btn-generate')
|
||||||
pg.wait_for_function('GAME.running === true', timeout=20000); pg.wait_for_timeout(1500)
|
pg.wait_for_function('GAME.running === true', timeout=20000); pg.wait_for_timeout(1500)
|
||||||
ev = pg.evaluate
|
ev = pg.evaluate
|
||||||
@@ -38,7 +38,7 @@ with sync_playwright() as p:
|
|||||||
# a browser without hardware drawing gets the pixel look and a note
|
# a browser without hardware drawing gets the pixel look and a note
|
||||||
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
|
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
|
||||||
pg2.add_init_script('localStorage.clear();')
|
pg2.add_init_script('localStorage.clear();')
|
||||||
pg2.goto((root / 'dist' / 'index.html').as_uri()); pg2.wait_for_timeout(300)
|
pg2.goto((root / 'dist' / 'index.html').as_uri()); pg2.evaluate('SET.fog = false'); pg2.wait_for_timeout(300)
|
||||||
pg2.click('#btn-new'); pg2.fill('#nw-seed', '5'); pg2.click('#btn-generate')
|
pg2.click('#btn-new'); pg2.fill('#nw-seed', '5'); pg2.click('#btn-generate')
|
||||||
pg2.wait_for_function('GAME.running === true', timeout=20000); pg2.wait_for_timeout(1200)
|
pg2.wait_for_function('GAME.running === true', timeout=20000); pg2.wait_for_timeout(1200)
|
||||||
check('software rendering falls back to pixels with a note', pg2.evaluate("SET.look === 'pixel' && !R.smoothOn && SM.why === 'software'") and pg2.locator('.toast', has_text='graphics card').count() >= 1, pg2.evaluate('[SET.look, SM.why]'))
|
check('software rendering falls back to pixels with a note', pg2.evaluate("SET.look === 'pixel' && !R.smoothOn && SM.why === 'software'") and pg2.locator('.toast', has_text='graphics card').count() >= 1, pg2.evaluate('[SET.look, SM.why]'))
|
||||||
|
|||||||
+3
-1
@@ -12,7 +12,7 @@ url = (root / 'dist' / 'index.html').as_uri()
|
|||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
b = p.chromium.launch(args=['--enable-gpu-rasterization','--ignore-gpu-blocklist'])
|
b = p.chromium.launch(args=['--enable-gpu-rasterization','--ignore-gpu-blocklist'])
|
||||||
pg = b.new_page(viewport={'width': 1440, 'height': 860})
|
pg = b.new_page(viewport={'width': 1440, 'height': 860})
|
||||||
pg.goto(url); pg.wait_for_timeout(300)
|
pg.goto(url); pg.evaluate("SET.fog = false"); pg.wait_for_timeout(300)
|
||||||
pg.evaluate("startNewWorld(424242, 1024, 640)")
|
pg.evaluate("startNewWorld(424242, 1024, 640)")
|
||||||
pg.wait_for_function('GAME.running === true', timeout=20000)
|
pg.wait_for_function('GAME.running === true', timeout=20000)
|
||||||
pg.wait_for_timeout(500)
|
pg.wait_for_timeout(500)
|
||||||
@@ -23,6 +23,8 @@ with sync_playwright() as p:
|
|||||||
out.drawWorld = t(() => drawWorld(), 20);
|
out.drawWorld = t(() => drawWorld(), 20);
|
||||||
SET.lighting = 'off'; out.drawWorldNoLight = t(() => drawWorld(), 20); SET.lighting='full';
|
SET.lighting = 'off'; out.drawWorldNoLight = t(() => drawWorld(), 20); SET.lighting='full';
|
||||||
SET.glow = 'off'; out.drawWorldNoGlow = t(() => drawWorld(), 20); SET.glow='low';
|
SET.glow = 'off'; out.drawWorldNoGlow = t(() => drawWorld(), 20); SET.glow='low';
|
||||||
|
SET.fog = true; out.drawWorldFog = t(() => drawWorld(), 100);
|
||||||
|
SET.lighting = 'off'; out.drawWorldFogNoLight = t(() => drawWorld(), 100); SET.lighting = 'full'; SET.fog = false;
|
||||||
out.put = t(() => { R.bctx.putImageData(R.img,0,0); }, 20);
|
out.put = t(() => { R.bctx.putImageData(R.img,0,0); }, 20);
|
||||||
out.blit = t(() => { R.ctx.drawImage(R.buf, 0, 0, R.bw, R.bh, 0, 0, R.bw*R.zp, R.bh*R.zp); }, 20);
|
out.blit = t(() => { R.ctx.drawImage(R.buf, 0, 0, R.bw, R.bh, 0, 0, R.bw*R.zp, R.bh*R.zp); }, 20);
|
||||||
out.sim = t(() => simTick(), 60);
|
out.sim = t(() => simTick(), 60);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ with sync_playwright() as p:
|
|||||||
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}'))
|
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}'))
|
||||||
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
||||||
pg.goto(url)
|
pg.goto(url)
|
||||||
|
pg.evaluate("SET.fog = false")
|
||||||
pg.wait_for_timeout(800)
|
pg.wait_for_timeout(800)
|
||||||
pg.screenshot(path=str(out / '01-title.png'))
|
pg.screenshot(path=str(out / '01-title.png'))
|
||||||
pg.click('#btn-new')
|
pg.click('#btn-new')
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ def main():
|
|||||||
pg.on('console', lambda m: errors.append('console: ' + m.text) if m.type == 'error' else None)
|
pg.on('console', lambda m: errors.append('console: ' + m.text) if m.type == 'error' else None)
|
||||||
pg.add_init_script('window.__allowSoftwareGL = true;' + (FREEZE if a.freeze else ''))
|
pg.add_init_script('window.__allowSoftwareGL = true;' + (FREEZE if a.freeze else ''))
|
||||||
pg.goto(pathlib.Path(a.page).resolve().as_uri())
|
pg.goto(pathlib.Path(a.page).resolve().as_uri())
|
||||||
|
pg.evaluate("SET.fog = false")
|
||||||
pg.wait_for_timeout(300)
|
pg.wait_for_timeout(300)
|
||||||
# pause the moment the world exists, so the shot depends only on the seed, the setup
|
# pause the moment the world exists, so the shot depends only on the seed, the setup
|
||||||
# code and --ticks (simTick is deterministic), not on how fast the browser ran
|
# code and --ticks (simTick is deterministic), not on how fast the browser ran
|
||||||
|
|||||||
Reference in New Issue
Block a user