Progress bar and steps while generating, loading, opening claims and travelling; saving chip (t30)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -252,6 +252,13 @@ above, including:
|
|||||||
career and come along; buildings stay on their claim and wait (they don't run) until you
|
career and come along; buildings stay on their claim and wait (they don't run) until you
|
||||||
travel back from the notebook's Claims page.
|
travel back from the notebook's Claims page.
|
||||||
|
|
||||||
|
- **Long waits show what's happening (t30, 2026-10-24).** Generating, loading, opening a claim
|
||||||
|
and travelling show the loading overlay with a title, a moving bar and the current step
|
||||||
|
("Laying rock and ore", "Unpacking", "Building the world"...). Generation runs as steps that
|
||||||
|
give the browser a frame every 30 ms, producing exactly the same world as before. A small
|
||||||
|
"Saving…" chip shows bottom right only when a save takes longer than 150 ms. The owner found big
|
||||||
|
worlds froze on a 0% bar with no sign of progress.
|
||||||
|
|
||||||
- **Clay bricks come from a Brick press (t28, 2026-10-24).** A hopper block like the Saw: every
|
- **Clay bricks come from a Brick press (t28, 2026-10-24).** A hopper block like the Saw: every
|
||||||
8 clay lumps on top become a Green brick below, and a Green brick fires into a Brick above about
|
8 clay lumps on top become a Green brick below, and a Green brick fires into a Brick above about
|
||||||
900°C (any fire). 2 Bricks build a brick wall block; builders order them. The owner found it odd
|
900°C (any fire). 2 Bricks build a brick wall block; builders order them. The owner found it odd
|
||||||
|
|||||||
+8
-8
@@ -324,9 +324,9 @@ Weather: clouds from steam, rain and snow, and the world water count.
|
|||||||
simTick: one simulation step, in order.
|
simTick: one simulation step, in order.
|
||||||
- functions: simTick
|
- functions: simTick
|
||||||
|
|
||||||
### src/sim/worldgen.js (326 lines)
|
### src/sim/worldgen.js (344 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, generateWorldAsync
|
||||||
- variables: SPECK_MIN
|
- variables: SPECK_MIN
|
||||||
|
|
||||||
### src/sim/save.js (126 lines)
|
### src/sim/save.js (126 lines)
|
||||||
@@ -376,19 +376,19 @@ The title screen art.
|
|||||||
- functions: buildTitleCells, drawTitle
|
- functions: buildTitleCells, drawTitle
|
||||||
- variables: FONT5, TITLE
|
- variables: FONT5, TITLE
|
||||||
|
|
||||||
### src/ui/saves.js (212 lines)
|
### src/ui/saves.js (224 lines)
|
||||||
Saves in IndexedDB, world files (export / import) and the blueprint library.
|
Saves in IndexedDB, world files (export / import) and the blueprint library.
|
||||||
- functions: newWorldId, worldWrite, listWorlds, putWorldEntry, worldNewest, deleteWorld, cleanWorldName, renameWorld, migrateWorlds, saveWorld, newestSave, b64FromBytes, bytesFromB64, gz, packBuffers, unpackBuffers, exportWorld, importWorldFile, libLoad, libSave, bpEncode, bpValid, bpDecode
|
- functions: newWorldId, worldWrite, listWorlds, putWorldEntry, worldNewest, deleteWorld, cleanWorldName, renameWorld, migrateWorlds, saveWorld, newestSave, b64FromBytes, bytesFromB64, gz, packBuffers, unpackBuffers, exportWorld, importWorldFile, libLoad, libSave, bpEncode, bpValid, bpDecode
|
||||||
- variables: DB, worldIdTime, worldIdCounter, worldWrites
|
- variables: DB, worldIdTime, worldIdCounter, worldWrites, savingCount
|
||||||
|
|
||||||
### src/ui/claims.js (141 lines)
|
### src/ui/claims.js (145 lines)
|
||||||
Shared player state; the world save remains a complete standalone fallback.
|
Shared player state; the world save remains a complete standalone fallback.
|
||||||
- functions: careerState, validCareer, applyCareerState, loadCareer, claimName, openNewClaim, travel, renderClaimsTab
|
- functions: careerState, validCareer, applyCareerState, loadCareer, claimName, openNewClaim, travel, renderClaimsTab
|
||||||
- variables: career, claimBusy
|
- variables: career, claimBusy
|
||||||
|
|
||||||
### src/ui/session.js (133 lines)
|
### src/ui/session.js (157 lines)
|
||||||
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, setupNewWorld, startNewWorld, startFromSave, quitToTitle, refreshContinue, anyModal, openModal, closeModal, closeAllModals, topModal, dialog, openPause, renderWorlds
|
- functions: showLoading, enterGame, setupNewWorld, startNewWorld, startFromSave, loadSavedWorld, quitToTitle, refreshContinue, anyModal, openModal, closeModal, closeAllModals, topModal, dialog, openPause, renderWorlds
|
||||||
|
|
||||||
### src/ui/settings.js (124 lines)
|
### src/ui/settings.js (124 lines)
|
||||||
The settings screen.
|
The settings screen.
|
||||||
@@ -442,7 +442,7 @@ Previews drawn under the cursor: build ghosts, drill aim and pulses, selections,
|
|||||||
Events from the simulation (emit) turned into sounds, toasts and cards.
|
Events from the simulation (emit) turned into sounds, toasts and cards.
|
||||||
- functions: drainEvents
|
- functions: drainEvents
|
||||||
|
|
||||||
### src/ui/loop.js (126 lines)
|
### src/ui/loop.js (127 lines)
|
||||||
The frame loop and boot.
|
The frame loop and boot.
|
||||||
- functions: scanVisible, frame, boot
|
- functions: scanVisible, frame, boot
|
||||||
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
|
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
|
||||||
|
|||||||
@@ -4,6 +4,19 @@ 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: t30: progress while generating, loading and saving (Claude Code, Codex)
|
||||||
|
|
||||||
|
- `worldgen.js`: `worldSteps` generator yields `[fraction, label]`; `generateWorld` (sync, tests)
|
||||||
|
and `generateWorldAsync` (yields every 30 ms) run it. Small and Medium mat/temp/flags hashes
|
||||||
|
match the old generator exactly.
|
||||||
|
- `session.js` `showLoading(on, p, label, title)` with `#loadstep`; `GAME.loading` stops the
|
||||||
|
frame loop's sim and drawing while worlds swap. `loadSavedWorld` for Continue and the world
|
||||||
|
list; `worldNewest`/`unpackBuffers`/`startFromSave` report progress; travel and new claims too.
|
||||||
|
`#saving` chip after 150 ms of saving.
|
||||||
|
- Tests: t_world progress labels and identical worlds; play.py overlay, responsiveness during
|
||||||
|
async generation, Continue labels, saving chip. All pass except t_smooth half-resolution
|
||||||
|
(fails under load, as before). Screenshots `shots/t30-loading.png`, `shots/t30-saving.png`.
|
||||||
|
|
||||||
## 2026-10-24: t3 follow-up: claim names; t30 added (Claude Code, Codex)
|
## 2026-10-24: t3 follow-up: claim names; t30 added (Claude Code, Codex)
|
||||||
|
|
||||||
- New claims get a camp name from their seed ("Cinder Gulch"; `claimName` in `src/ui/claims.js`,
|
- New claims get a camp name from their seed ("Cinder Gulch"; `claimName` in `src/ui/claims.js`,
|
||||||
|
|||||||
+2
-1
@@ -41,6 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="toasts" aria-live="polite"></div>
|
<div id="toasts" aria-live="polite"></div>
|
||||||
<div id="perf"></div>
|
<div id="perf"></div>
|
||||||
|
<div id="saving" hidden role="status"><i></i>Saving…</div>
|
||||||
|
|
||||||
<div id="title">
|
<div id="title">
|
||||||
<div class="titlewrap">
|
<div class="titlewrap">
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
<div class="tfoot">Prototype v0.1 · keyboard + mouse or gamepad · all graphics and sound are generated in code</div>
|
<div class="tfoot">Prototype v0.1 · keyboard + mouse or gamepad · all graphics and sound are generated in code</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading" hidden><div class="plate"><span class="label">Generating world</span><div class="bar"><i id="loadbar" style="width:0%"></i></div><span id="loadtext" class="num" style="color:var(--ash)">0%</span></div></div>
|
<div id="loading" hidden><div class="plate"><span class="label">Generating world</span><div class="bar"><i id="loadbar" style="width:0%"></i></div><span id="loadstep" role="status"></span><span id="loadtext" class="num" style="color:var(--ash)">0%</span></div></div>
|
||||||
|
|
||||||
<div id="m-pause" class="modal small" hidden>
|
<div id="m-pause" class="modal small" hidden>
|
||||||
<div class="plate">
|
<div class="plate">
|
||||||
|
|||||||
+23
-5
@@ -60,7 +60,8 @@ function clearHardSpecks(live) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
// ---------- World generation ----------
|
// ---------- World generation ----------
|
||||||
function generateWorld(seedIn, w, h, onProgress) {
|
function* worldSteps(seedIn, w, h) {
|
||||||
|
yield [0, 'Shaping the land'];
|
||||||
worldSeed = seedIn >>> 0;
|
worldSeed = seedIn >>> 0;
|
||||||
alloc(w, h);
|
alloc(w, h);
|
||||||
RS = (worldSeed ^ 0x9E3779B9) | 1;
|
RS = (worldSeed ^ 0x9E3779B9) | 1;
|
||||||
@@ -146,6 +147,7 @@ function generateWorld(seedIn, w, h, onProgress) {
|
|||||||
for (let x = Math.max(0, f.x - dw); x <= Math.min(w - 1, f.x + dw); x++) if (Math.abs(x - f.x) < dw - Math.abs(nDetail(x / 20, 3.3)) * 10) desAt[x] = 1;
|
for (let x = Math.max(0, f.x - dw); x <= Math.min(w - 1, f.x + dw); x++) if (Math.abs(x - f.x) < dw - Math.abs(nDetail(x / 20, 3.3)) * 10) desAt[x] = 1;
|
||||||
}
|
}
|
||||||
computeAmbient();
|
computeAmbient();
|
||||||
|
yield [0, 'Laying rock and ore'];
|
||||||
for (let y = 0; y < h; y++) {
|
for (let y = 0; y < h; y++) {
|
||||||
const fy = depthU(y); // fraction of the underground depth (negative in the sky)
|
const fy = depthU(y); // fraction of the underground depth (negative in the sky)
|
||||||
for (let x = 0; x < w; x++) {
|
for (let x = 0; x < w; x++) {
|
||||||
@@ -231,9 +233,11 @@ function generateWorld(seedIn, w, h, onProgress) {
|
|||||||
temp[i] = m === MAGMA ? Math.max(AMB[y], 1180) : AMB[y];
|
temp[i] = m === MAGMA ? Math.max(AMB[y], 1180) : AMB[y];
|
||||||
if (PH[m] === PH_LIQUID && FRZ[m]) life[i] = 255;
|
if (PH[m] === PH_LIQUID && FRZ[m]) life[i] = 255;
|
||||||
}
|
}
|
||||||
if (onProgress && (y & 63) === 0) onProgress(y / h * 0.7);
|
if ((y & 63) === 0) yield [y / h * 0.7, 'Laying rock and ore'];
|
||||||
}
|
}
|
||||||
|
yield [0.7, 'Carving caves'];
|
||||||
clearHardSpecks(false);
|
clearHardSpecks(false);
|
||||||
|
yield [0.71, 'Snow and trees'];
|
||||||
// (tree bases are collected after the trees below)
|
// (tree bases are collected after the trees below)
|
||||||
// snow lies on the high ground wherever the air is below freezing
|
// snow lies on the high ground wherever the air is below freezing
|
||||||
for (let x = 1; x < w - 1; x++) {
|
for (let x = 1; x < w - 1; x++) {
|
||||||
@@ -244,6 +248,7 @@ function generateWorld(seedIn, w, h, onProgress) {
|
|||||||
const depth = Math.abs(surfY[x + 1] - surfY[x - 1]) <= 3 ? 3 : 1;
|
const depth = Math.abs(surfY[x + 1] - surfY[x - 1]) <= 3 ? 3 : 1;
|
||||||
for (let k = 1; k <= depth; k++) { const i = (sy - k) * w + x; if (mat[i] === EMPTY) { mat[i] = SNOW; flags[i] = F_NAT; temp[i] = AMB[sy - k]; } }
|
for (let k = 1; k <= depth; k++) { const i = (sy - k) * w + x; if (mat[i] === EMPTY) { mat[i] = SNOW; flags[i] = F_NAT; temp[i] = AMB[sy - k]; } }
|
||||||
}
|
}
|
||||||
|
yield [0.72, 'Snow and trees'];
|
||||||
// trees
|
// trees
|
||||||
for (let x = 12; x < w - 12;) {
|
for (let x = 12; x < w - 12;) {
|
||||||
const sy = surfY[x];
|
const sy = surfY[x];
|
||||||
@@ -267,6 +272,7 @@ function generateWorld(seedIn, w, h, onProgress) {
|
|||||||
x += 16 + Math.floor(rng() * 20);
|
x += 16 + Math.floor(rng() * 20);
|
||||||
} else x += 4;
|
} else x += 4;
|
||||||
}
|
}
|
||||||
|
yield [0.73, 'Springs and water'];
|
||||||
// springs: in cave side walls just above the cave floor
|
// springs: in cave side walls just above the cave floor
|
||||||
{
|
{
|
||||||
const cands = [];
|
const cands = [];
|
||||||
@@ -296,7 +302,7 @@ function generateWorld(seedIn, w, h, onProgress) {
|
|||||||
void picks;
|
void picks;
|
||||||
}
|
}
|
||||||
fillNoise();
|
fillNoise();
|
||||||
if (onProgress) onProgress(0.75);
|
yield [0.75, 'Settling loose ground'];
|
||||||
// settle loose material
|
// settle loose material
|
||||||
awake.fill(3);
|
awake.fill(3);
|
||||||
for (let k = 0; k < 260; k++) {
|
for (let k = 0; k < 260; k++) {
|
||||||
@@ -304,8 +310,9 @@ function generateWorld(seedIn, w, h, onProgress) {
|
|||||||
simMove();
|
simMove();
|
||||||
let any = 0; for (let c = 0; c < NCH; c++) if (awake[c]) { awake[c]--; any++; }
|
let any = 0; for (let c = 0; c < NCH; c++) if (awake[c]) { awake[c]--; any++; }
|
||||||
if (!any) break;
|
if (!any) break;
|
||||||
if (onProgress && (k & 31) === 0) onProgress(0.75 + 0.25 * k / 260);
|
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
|
||||||
}
|
}
|
||||||
|
yield [0.99, 'Finishing'];
|
||||||
// re-ambient after settling, then start clean
|
// re-ambient after settling, then start clean
|
||||||
for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; }
|
for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; }
|
||||||
tick = 0;
|
tick = 0;
|
||||||
@@ -322,5 +329,16 @@ function generateWorld(seedIn, w, h, onProgress) {
|
|||||||
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);
|
fogInit(); fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
||||||
if (onProgress) onProgress(1);
|
yield [1, 'Finishing'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateWorld(seedIn, w, h, onProgress) {
|
||||||
|
for (const [f, label] of worldSteps(seedIn, w, h)) if (onProgress) onProgress(f, label);
|
||||||
|
}
|
||||||
|
async function generateWorldAsync(seedIn, w, h, onProgress) {
|
||||||
|
let last = Date.now();
|
||||||
|
for (const [f, label] of worldSteps(seedIn, w, h)) {
|
||||||
|
if (onProgress) onProgress(f, label);
|
||||||
|
if (Date.now() - last >= 30) { await new Promise(r => setTimeout(r, 0)); last = Date.now(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,3 +369,10 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border-
|
|||||||
#continue-meta { max-width: 72%; overflow-wrap: anywhere; text-align: right; }
|
#continue-meta { max-width: 72%; overflow-wrap: anywhere; text-align: right; }
|
||||||
|
|
||||||
.vacchoice { display: flex; align-items: center; gap: 4px; text-align: left; min-width: 0; background: transparent; border: 0; padding: 0; cursor: pointer; }
|
.vacchoice { display: flex; align-items: center; gap: 4px; text-align: left; min-width: 0; background: transparent; border: 0; padding: 0; cursor: pointer; }
|
||||||
|
|
||||||
|
#saving { position: fixed; right: max(16px, env(safe-area-inset-right, 0px)); bottom: 54px; z-index: 10; padding: 5px 9px; border-radius: 12px; background: rgba(11, 9, 8, 0.86); color: var(--ash); font-size: 12px; pointer-events: none; }
|
||||||
|
#saving i { display: inline-block; width: 6px; height: 6px; margin-right: 6px; border-radius: 50%; background: currentColor; animation: saving-pulse 1.5s ease-in-out infinite alternate; }
|
||||||
|
@keyframes saving-pulse { to { opacity: 0.3; } }
|
||||||
|
@media (prefers-reduced-motion: reduce) { #saving i { animation: none; } }
|
||||||
|
#loadstep { color: var(--ash); font-size: 13px; }
|
||||||
|
#hud:has(~ #saving:not([hidden])) .br { padding-bottom: 72px; }
|
||||||
|
|||||||
+8
-4
@@ -82,11 +82,13 @@ async function openNewClaim() {
|
|||||||
carried.money -= 5000;
|
carried.money -= 5000;
|
||||||
let seed; do { seed = Math.floor(Math.random() * 900000) + 100000; } while (seed === worldSeed);
|
let seed; do { seed = Math.floor(Math.random() * 900000) + 100000; } while (seed === worldSeed);
|
||||||
const name = claimName(seed, (await listWorlds()).map(w => w.name));
|
const name = claimName(seed, (await listWorlds()).map(w => w.name));
|
||||||
await setupNewWorld(seed, w, h, name, carried);
|
await setupNewWorld(seed, w, h, name, carried, 'Opening a new claim');
|
||||||
debt = CLAIM_DEBT * 2 ** (n - 1);
|
debt = CLAIM_DEBT * 2 ** (n - 1);
|
||||||
career = { ...career, claims: [...career.claims, GAME.worldId], claimNo: n };
|
career = { ...career, claims: [...career.claims, GAME.worldId], claimNo: n };
|
||||||
GAME.claimNo = n;
|
GAME.claimNo = n;
|
||||||
|
showLoading(true, 0.9, 'Saving');
|
||||||
if (!await saveWorld('auto', true)) throw new Error('Save failed');
|
if (!await saveWorld('auto', true)) throw new Error('Save failed');
|
||||||
|
showLoading(true, 1, 'Saving');
|
||||||
showLoading(false); closeAllModals(); enterGame();
|
showLoading(false); closeAllModals(); enterGame();
|
||||||
showCard({ kind: 'New claim', title: name, body: [`Claim ${n}. You owe ${fmtMoney(debt)} on this claim. Your money, bag, upgrades and what you’ve learned came along. Buildings stayed on the old claim.`], hold: 1.8 });
|
showCard({ kind: 'New claim', title: name, body: [`Claim ${n}. You owe ${fmtMoney(debt)} on this claim. Your money, bag, upgrades and what you’ve learned came along. Buildings stayed on the old claim.`], hold: 1.8 });
|
||||||
return true;
|
return true;
|
||||||
@@ -105,12 +107,14 @@ async function travel(worldId) {
|
|||||||
try {
|
try {
|
||||||
const entry = (await listWorlds()).find(w => w.id === worldId);
|
const entry = (await listWorlds()).find(w => w.id === worldId);
|
||||||
if (!entry || entry.career !== GAME.careerId) return false;
|
if (!entry || entry.career !== GAME.careerId) return false;
|
||||||
|
const progress = (f, s) => showLoading(true, f, s, 'Travelling to ' + entry.name);
|
||||||
|
progress(0, 'Saving');
|
||||||
if (!await saveWorld('main', true)) throw new Error('Save failed');
|
if (!await saveWorld('main', true)) throw new Error('Save failed');
|
||||||
const sv = await worldNewest(worldId);
|
const sv = await worldNewest(worldId, progress);
|
||||||
if (!sv || !await startFromSave(sv)) return false;
|
if (!sv || !await startFromSave(sv, progress)) return false;
|
||||||
closeAllModals(); arrived = true; return true;
|
closeAllModals(); arrived = true; return true;
|
||||||
} catch (e) { toast('Could not travel. Check browser storage and try again.'); return false; }
|
} catch (e) { toast('Could not travel. Check browser storage and try again.'); return false; }
|
||||||
finally { claimBusy = false; if (!arrived) GAME.paused = paused; }
|
finally { showLoading(false); claimBusy = false; if (!arrived) GAME.paused = paused; }
|
||||||
}
|
}
|
||||||
async function renderClaimsTab(body) {
|
async function renderClaimsTab(body) {
|
||||||
body.innerHTML = '<p class=note>Loading claims…</p>';
|
body.innerHTML = '<p class=note>Loading claims…</p>';
|
||||||
|
|||||||
+3
-2
@@ -16,6 +16,7 @@ function frame(now) {
|
|||||||
const dtReal = Math.min(0.25, (now - lastT) / 1000); lastT = now;
|
const dtReal = Math.min(0.25, (now - lastT) / 1000); lastT = now;
|
||||||
if (SET.fpsCap && now - lastRender < 1000 / SET.fpsCap - 1) return;
|
if (SET.fpsCap && now - lastRender < 1000 / SET.fpsCap - 1) return;
|
||||||
const dt = Math.min(0.1, (now - (lastRender || now)) / 1000) || 1 / 60; lastRender = now;
|
const dt = Math.min(0.1, (now - (lastRender || now)) / 1000) || 1 / 60; lastRender = now;
|
||||||
|
if (GAME.loading) { acc = 0; return; }
|
||||||
if (!GAME.running) { pollPad(); return; }
|
if (!GAME.running) { pollPad(); return; }
|
||||||
const t0 = performance.now();
|
const t0 = performance.now();
|
||||||
const pad = pollPad();
|
const pad = pollPad();
|
||||||
@@ -110,7 +111,7 @@ function boot(hotData) {
|
|||||||
const [w, h] = $('nw-size').value.split('x').map(Number);
|
const [w, h] = $('nw-size').value.split('x').map(Number);
|
||||||
startNewWorld(seed, w, h, $('nw-name').value);
|
startNewWorld(seed, w, h, $('nw-name').value);
|
||||||
};
|
};
|
||||||
$('btn-continue').onclick = async () => { auInit(); playSfx('ui'); const sv = await newestSave(); if (sv) await startFromSave(sv); else toast('No saved world found.'); };
|
$('btn-continue').onclick = async () => { auInit(); playSfx('ui'); await loadSavedWorld(); };
|
||||||
$('btn-title-settings').onclick = () => { auInit(); playSfx('ui'); openSettings('Display'); };
|
$('btn-title-settings').onclick = () => { auInit(); playSfx('ui'); openSettings('Display'); };
|
||||||
$('btn-title-help').onclick = () => { auInit(); playSfx('ui'); openNotebook('Controls'); };
|
$('btn-title-help').onclick = () => { auInit(); playSfx('ui'); openNotebook('Controls'); };
|
||||||
const touchOnly = matchMedia('(pointer: coarse)').matches && !matchMedia('(any-pointer: fine)').matches;
|
const touchOnly = matchMedia('(pointer: coarse)').matches && !matchMedia('(any-pointer: fine)').matches;
|
||||||
@@ -118,7 +119,7 @@ function boot(hotData) {
|
|||||||
$('title').hidden = true;
|
$('title').hidden = true;
|
||||||
migrateWorlds().then(() => refreshContinue()).then(async () => {
|
migrateWorlds().then(() => refreshContinue()).then(async () => {
|
||||||
$('title').hidden = false; if (!TITLE.raf) drawTitle();
|
$('title').hidden = false; if (!TITLE.raf) drawTitle();
|
||||||
if (hotData && hotData.resume) { const sv = await newestSave(); if (sv) await startFromSave(sv); }
|
if (hotData && hotData.resume) { await loadSavedWorld(); }
|
||||||
}).catch(() => { $('title').hidden = false; if (!TITLE.raf) drawTitle(); });
|
}).catch(() => { $('title').hidden = false; if (!TITLE.raf) drawTitle(); });
|
||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-7
@@ -40,12 +40,13 @@ function putWorldEntry(entry) {
|
|||||||
await DB.put('worlds', list);
|
await DB.put('worlds', list);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function worldNewest(id) {
|
async function worldNewest(id, onProgress) {
|
||||||
|
if (onProgress) onProgress(0.05, 'Reading the save');
|
||||||
const a = await DB.get(`${id}:main`), b = await DB.get(`${id}:auto`);
|
const a = await DB.get(`${id}:main`), b = await DB.get(`${id}:auto`);
|
||||||
const sv = a && b ? (a.savedAt >= b.savedAt ? a : b) : a || b;
|
const sv = a && b ? (a.savedAt >= b.savedAt ? a : b) : a || b;
|
||||||
if (!sv) return null;
|
if (!sv) return null;
|
||||||
const entry = (await listWorlds()).find(w => w.id === id);
|
const entry = (await listWorlds()).find(w => w.id === id);
|
||||||
return { ...sv, buffers: await unpackBuffers(sv.buffers), worldId: id, worldName: entry ? entry.name : 'World' };
|
return { ...sv, buffers: await unpackBuffers(sv.buffers, onProgress), worldId: id, worldName: entry ? entry.name : 'World' };
|
||||||
}
|
}
|
||||||
function deleteWorld(id) {
|
function deleteWorld(id) {
|
||||||
return worldWrite(async () => {
|
return worldWrite(async () => {
|
||||||
@@ -98,7 +99,10 @@ async function migrateWorlds() {
|
|||||||
});
|
});
|
||||||
} catch (e) { toast('Could not move the old save. It has been kept. Reload to try again.'); throw e; }
|
} catch (e) { toast('Could not move the old save. It has been kept. Reload to try again.'); throw e; }
|
||||||
}
|
}
|
||||||
|
let savingCount = 0;
|
||||||
async function saveWorld(slot, quiet) {
|
async function saveWorld(slot, quiet) {
|
||||||
|
savingCount++;
|
||||||
|
const indicator = setTimeout(() => { if (!GAME.loading) $('saving').hidden = false; }, 150);
|
||||||
try {
|
try {
|
||||||
const s = serializeWorld(), savedAt = Date.now(), playTime = GAME.playTime;
|
const s = serializeWorld(), savedAt = Date.now(), playTime = GAME.playTime;
|
||||||
if (!GAME.worldId) { GAME.worldId = newWorldId(); GAME.worldName = `World ${(await listWorlds()).length + 1}`; }
|
if (!GAME.worldId) { GAME.worldId = newWorldId(); GAME.worldName = `World ${(await listWorlds()).length + 1}`; }
|
||||||
@@ -116,10 +120,10 @@ async function saveWorld(slot, quiet) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!quiet) toast('Could not save. Browser storage is unavailable in this view. Use Export world file instead.');
|
if (!quiet) toast('Could not save. Browser storage is unavailable in this view. Use Export world file instead.');
|
||||||
return false;
|
return false;
|
||||||
|
} finally { clearTimeout(indicator); if (!--savingCount) $('saving').hidden = true; }
|
||||||
}
|
}
|
||||||
}
|
async function newestSave(onProgress) {
|
||||||
async function newestSave() {
|
try { for (const entry of await listWorlds()) { const sv = await worldNewest(entry.id, onProgress); if (sv) return sv; } return null; }
|
||||||
try { for (const entry of await listWorlds()) { const sv = await worldNewest(entry.id); if (sv) return sv; } return null; }
|
|
||||||
catch (e) { return null; }
|
catch (e) { return null; }
|
||||||
}
|
}
|
||||||
function b64FromBytes(u8) { let s = ''; const CH = 0x8000; for (let i = 0; i < u8.length; i += CH) s += String.fromCharCode.apply(null, u8.subarray(i, i + CH)); return btoa(s); }
|
function b64FromBytes(u8) { let s = ''; const CH = 0x8000; for (let i = 0; i < u8.length; i += CH) s += String.fromCharCode.apply(null, u8.subarray(i, i + CH)); return btoa(s); }
|
||||||
@@ -137,9 +141,11 @@ async function packBuffers(buffers) {
|
|||||||
}
|
}
|
||||||
return packed;
|
return packed;
|
||||||
}
|
}
|
||||||
async function unpackBuffers(buffers) {
|
async function unpackBuffers(buffers, onProgress) {
|
||||||
|
let n = 0; const total = Object.keys(buffers).length;
|
||||||
const raw = {}, types = { Uint8Array, Uint8ClampedArray, Int8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array };
|
const raw = {}, types = { Uint8Array, Uint8ClampedArray, Int8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array };
|
||||||
for (const k in buffers) {
|
for (const k in buffers) {
|
||||||
|
if (onProgress) onProgress(0.1 + 0.6 * n++ / total, 'Unpacking');
|
||||||
const b = buffers[k];
|
const b = buffers[k];
|
||||||
if (ArrayBuffer.isView(b)) { raw[k] = b; continue; }
|
if (ArrayBuffer.isView(b)) { raw[k] = b; continue; }
|
||||||
if (!b || b.gz !== true) throw new Error('Invalid saved buffer: ' + k);
|
if (!b || b.gz !== true) throw new Error('Invalid saved buffer: ' + k);
|
||||||
@@ -148,6 +154,7 @@ async function unpackBuffers(buffers) {
|
|||||||
if (!bytes) throw new Error('This browser cannot decompress saved worlds.');
|
if (!bytes) throw new Error('This browser cannot decompress saved worlds.');
|
||||||
raw[k] = new C(bytes.buffer, bytes.byteOffset, bytes.byteLength / C.BYTES_PER_ELEMENT);
|
raw[k] = new C(bytes.buffer, bytes.byteOffset, bytes.byteLength / C.BYTES_PER_ELEMENT);
|
||||||
}
|
}
|
||||||
|
if (onProgress) onProgress(0.7, 'Unpacking');
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
async function exportWorld() {
|
async function exportWorld() {
|
||||||
@@ -177,22 +184,27 @@ async function exportWorld() {
|
|||||||
catch (e) { if (e && e.code !== 'declined') toast('The world file was not saved.'); }
|
catch (e) { if (e && e.code !== 'declined') toast('The world file was not saved.'); }
|
||||||
}
|
}
|
||||||
async function importWorldFile(file) {
|
async function importWorldFile(file) {
|
||||||
|
const progress = (f, s) => showLoading(true, f, s, 'Loading world');
|
||||||
|
progress(0.05, 'Reading the save');
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(await file.text());
|
const obj = JSON.parse(await file.text());
|
||||||
if (obj.format !== 'deepgrain-world') throw new Error('Not a Deepgrain world file.');
|
if (obj.format !== 'deepgrain-world') throw new Error('Not a Deepgrain world file.');
|
||||||
const bufs = {};
|
const bufs = {};
|
||||||
const T = { Uint8Array, Uint16Array, Int16Array, Float32Array, Int32Array };
|
const T = { Uint8Array, Uint16Array, Int16Array, Float32Array, Int32Array };
|
||||||
|
let n = 0; const total = Object.keys(obj.buffers).length;
|
||||||
for (const k in obj.buffers) {
|
for (const k in obj.buffers) {
|
||||||
|
progress(0.1 + 0.6 * n++ / total, 'Unpacking');
|
||||||
const b = obj.buffers[k]; let bytes = bytesFromB64(b.b64);
|
const b = obj.buffers[k]; let bytes = bytesFromB64(b.b64);
|
||||||
if (b.gz) bytes = await gz(bytes, true);
|
if (b.gz) bytes = await gz(bytes, true);
|
||||||
const C = T[b.type] || Uint8Array;
|
const C = T[b.type] || Uint8Array;
|
||||||
bufs[k] = new C(bytes.buffer, bytes.byteOffset, bytes.byteLength / C.BYTES_PER_ELEMENT);
|
bufs[k] = new C(bytes.buffer, bytes.byteOffset, bytes.byteLength / C.BYTES_PER_ELEMENT);
|
||||||
}
|
}
|
||||||
const id = newWorldId(), name = `Imported ${obj.meta.seed}`.slice(0, 40);
|
const id = newWorldId(), name = `Imported ${obj.meta.seed}`.slice(0, 40);
|
||||||
if (!await startFromSave({ meta: obj.meta, buffers: bufs, worldId: id, worldName: name })) return;
|
if (!await startFromSave({ meta: obj.meta, buffers: bufs, worldId: id, worldName: name }, progress)) return;
|
||||||
if (!await saveWorld('auto')) return;
|
if (!await saveWorld('auto')) return;
|
||||||
toast('World imported.');
|
toast('World imported.');
|
||||||
} catch (e) { toast('That file could not be loaded: ' + (e.message || 'unknown error')); }
|
} catch (e) { toast('That file could not be loaded: ' + (e.message || 'unknown error')); }
|
||||||
|
finally { showLoading(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Blueprint library ----------
|
// ---------- Blueprint library ----------
|
||||||
|
|||||||
+30
-6
@@ -1,6 +1,12 @@
|
|||||||
// Starting, loading and quitting a game; modal dialogs; the pause menu.
|
// Starting, loading and quitting a game; modal dialogs; the pause menu.
|
||||||
// ---------- Start / stop ----------
|
// ---------- Start / stop ----------
|
||||||
function showLoading(on, p) {
|
function showLoading(on, p, label, title) {
|
||||||
|
GAME.loading = on;
|
||||||
|
if (on) {
|
||||||
|
$('saving').hidden = true;
|
||||||
|
if (title) $('loading').querySelector('.label').textContent = title;
|
||||||
|
$('loadstep').textContent = label || '';
|
||||||
|
}
|
||||||
$('loading').hidden = !on;
|
$('loading').hidden = !on;
|
||||||
if (on) { $('loadbar').style.width = Math.round((p || 0) * 100) + '%'; $('loadtext').textContent = Math.round((p || 0) * 100) + '%'; }
|
if (on) { $('loadbar').style.width = Math.round((p || 0) * 100) + '%'; $('loadtext').textContent = Math.round((p || 0) * 100) + '%'; }
|
||||||
}
|
}
|
||||||
@@ -15,11 +21,11 @@ function enterGame() {
|
|||||||
GAME.drag = null; GAME.lmb = GAME.rmb = false; lastBagKey = '';
|
GAME.drag = null; GAME.lmb = GAME.rmb = false; lastBagKey = '';
|
||||||
hudDirty = true; renderHotbar();
|
hudDirty = true; renderHotbar();
|
||||||
}
|
}
|
||||||
async function setupNewWorld(seed, w, h, name, carried) {
|
async function setupNewWorld(seed, w, h, name, carried, title = 'Generating world') {
|
||||||
auInit(); showLoading(true, 0);
|
auInit(); showLoading(true, 0, 'Shaping the land', title);
|
||||||
await new Promise(resolve => setTimeout(resolve, 30));
|
await new Promise(resolve => setTimeout(resolve, 30));
|
||||||
for (const k of Object.keys(stats)) stats[k] = 0;
|
for (const k of Object.keys(stats)) stats[k] = 0;
|
||||||
generateWorld(seed, w, h, null);
|
await generateWorldAsync(seed, w, h, (f, s) => showLoading(true, f * 0.9, s, title));
|
||||||
GAME.worldId = newWorldId(); GAME.worldName = name;
|
GAME.worldId = newWorldId(); GAME.worldName = name;
|
||||||
GAME.playTime = 0; GAME.clip = null; GAME.paste = false; GAME.selDrag = null; GAME.tool = 0; GAME.blow.clear(); GAME.wallMat = RSTONE; GAME.filterCfg = { m: [], inv: 0 }; GAME.pieceIdx = 0;
|
GAME.playTime = 0; GAME.clip = null; GAME.paste = false; GAME.selDrag = null; GAME.tool = 0; GAME.blow.clear(); GAME.wallMat = RSTONE; GAME.filterCfg = { m: [], inv: 0 }; GAME.pieceIdx = 0;
|
||||||
if (carried) applyCareerState(carried);
|
if (carried) applyCareerState(carried);
|
||||||
@@ -30,30 +36,48 @@ async function startNewWorld(seed, w, h, name = '') {
|
|||||||
await setupNewWorld(seed, w, h, cleanWorldName(name, `World ${worlds.length + 1}`));
|
await setupNewWorld(seed, w, h, cleanWorldName(name, `World ${worlds.length + 1}`));
|
||||||
career = { id: newWorldId(), claims: [GAME.worldId], claimNo: 1 };
|
career = { id: newWorldId(), claims: [GAME.worldId], claimNo: 1 };
|
||||||
GAME.careerId = career.id; GAME.claimNo = 1;
|
GAME.careerId = career.id; GAME.claimNo = 1;
|
||||||
|
showLoading(true, 0.9, 'Saving');
|
||||||
await saveWorld('auto');
|
await saveWorld('auto');
|
||||||
|
showLoading(true, 1, 'Saving');
|
||||||
showLoading(false); closeAllModals(); enterGame();
|
showLoading(false); closeAllModals(); enterGame();
|
||||||
showCard({ kind: 'Welcome', title: 'The dig site', body: ['Fly with WASD. The Drill (1) breaks rock loose. The Pneumatic nozzle (2) picks up and blows the selection. Click or Q/E in the bag selects; Ctrl+click mixes. Hold Ctrl to see everything found. Clear returns to picking what you click. Shift: everything.', 'You owe $20,000 on this claim. Sell what you dig and make at the trade dock next to you: anything that drops through its grate is paid for on the spot, and buyers post orders that pay far more. Every tip lives in the notebook (N).'], hold: 1.8 });
|
showCard({ kind: 'Welcome', title: 'The dig site', body: ['Fly with WASD. The Drill (1) breaks rock loose. The Pneumatic nozzle (2) picks up and blows the selection. Click or Q/E in the bag selects; Ctrl+click mixes. Hold Ctrl to see everything found. Clear returns to picking what you click. Shift: everything.', 'You owe $20,000 on this claim. Sell what you dig and make at the trade dock next to you: anything that drops through its grate is paid for on the spot, and buyers post orders that pay far more. Every tip lives in the notebook (N).'], hold: 1.8 });
|
||||||
} catch (e) { showLoading(false); toast('Could not create a world. Browser storage is unavailable.'); }
|
} catch (e) { showLoading(false); toast('Could not create a world. Browser storage is unavailable.'); }
|
||||||
}
|
}
|
||||||
async function startFromSave(sv) {
|
async function startFromSave(sv, onProgress) {
|
||||||
|
onProgress = onProgress || ((f, s) => showLoading(true, f, s, 'Loading world'));
|
||||||
auInit();
|
auInit();
|
||||||
GAME.paused = true;
|
GAME.paused = true;
|
||||||
try {
|
try {
|
||||||
for (const k of Object.keys(stats)) stats[k] = 0;
|
for (const k of Object.keys(stats)) stats[k] = 0;
|
||||||
|
onProgress(0.75, 'Building the world');
|
||||||
|
await new Promise(r => setTimeout(r, 30));
|
||||||
deserializeWorld(sv.meta, sv.buffers);
|
deserializeWorld(sv.meta, sv.buffers);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
toast(e && e.message ? e.message : 'That save could not be loaded.', { big: true, title: 'Save not loaded' });
|
toast(e && e.message ? e.message : 'That save could not be loaded.', { big: true, title: 'Save not loaded' });
|
||||||
|
showLoading(false);
|
||||||
GAME.running = false; $('hud').hidden = true; $('title').hidden = false; if (!TITLE.raf) drawTitle();
|
GAME.running = false; $('hud').hidden = true; $('title').hidden = false; if (!TITLE.raf) drawTitle();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
GAME.worldId = sv.worldId || ''; GAME.worldName = sv.worldName || '';
|
GAME.worldId = sv.worldId || ''; GAME.worldName = sv.worldName || '';
|
||||||
GAME.playTime = sv.playTime || 0;
|
GAME.playTime = sv.playTime || 0;
|
||||||
GAME.drag = null; GAME.blow.clear();
|
GAME.drag = null; GAME.blow.clear();
|
||||||
|
onProgress(0.95, 'Your gear and notebook');
|
||||||
await loadCareer();
|
await loadCareer();
|
||||||
|
showLoading(false);
|
||||||
enterGame();
|
enterGame();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
async function loadSavedWorld(id) {
|
||||||
|
const progress = (f, s) => showLoading(true, f, s, 'Loading world');
|
||||||
|
progress(0, 'Reading the save');
|
||||||
|
try {
|
||||||
|
const sv = id ? await worldNewest(id, progress) : await newestSave(progress);
|
||||||
|
if (sv) return await startFromSave(sv, progress);
|
||||||
|
toast('No saved world found.'); return false;
|
||||||
|
} catch (e) { toast('Could not reach browser storage.'); return false; }
|
||||||
|
finally { showLoading(false); }
|
||||||
|
}
|
||||||
async function quitToTitle() {
|
async function quitToTitle() {
|
||||||
await saveWorld('main', true);
|
await saveWorld('main', true);
|
||||||
GAME.running = false; closeAllModals();
|
GAME.running = false; closeAllModals();
|
||||||
@@ -119,7 +143,7 @@ async function renderWorlds(editId) {
|
|||||||
button('Save', async () => { await renameWorld(w.id, input.value); await renderWorlds(); await refreshContinue(); });
|
button('Save', async () => { await renameWorld(w.id, input.value); await renderWorlds(); await refreshContinue(); });
|
||||||
button('Cancel', () => renderWorlds());
|
button('Cancel', () => renderWorlds());
|
||||||
} else {
|
} else {
|
||||||
button('Load', async () => { const sv = await worldNewest(w.id); if (sv && await startFromSave(sv)) closeAllModals(); else if (!sv) toast('No saved world found.'); });
|
button('Load', async () => { const ok = await loadSavedWorld(w.id); if (ok) closeAllModals(); });
|
||||||
button('Rename', () => renderWorlds(w.id));
|
button('Rename', () => renderWorlds(w.id));
|
||||||
button('Delete', () => dialog('Delete world?', `<p class="note">Delete ${esc(w.name)} and its autosave? Exported world files are not affected.</p>`, [
|
button('Delete', () => dialog('Delete world?', `<p class="note">Delete ${esc(w.name)} and its autosave? Exported world files are not affected.</p>`, [
|
||||||
{ label: 'Cancel', cls: 'ghost' },
|
{ label: 'Cancel', cls: 'ghost' },
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ const BAG_ORDER = [BARAU, BARAG, BARBZ, BARBS, BARST, BARFE, BARCU, BARSN, BARZN
|
|||||||
SCRCU, SCRFE, SCRST, SCRSN, SCRZN, SCRAG, SCRAU, SCRBZ, SCRBS, ASH, SLAG, SNOW, WATER, MAGMA, MCU, MFE, MST, MGL, MSN, MZN, MAG, MAU, MBZ, MBS];
|
SCRCU, SCRFE, SCRST, SCRSN, SCRZN, SCRAG, SCRAU, SCRBZ, SCRBS, ASH, SLAG, SNOW, WATER, MAGMA, MCU, MFE, MST, MGL, MSN, MZN, MAG, MAU, MBZ, MBS];
|
||||||
const GAME = {
|
const GAME = {
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
running: false, paused: false, tool: 0, blow: new Set(), wallMat: RSTONE, filterCfg: { m: [], inv: 0 }, pieceIdx: 0, pieceDir: {},
|
running: false, loading: false, paused: false, tool: 0, blow: new Set(), wallMat: RSTONE, filterCfg: { m: [], inv: 0 }, pieceIdx: 0, pieceDir: {},
|
||||||
brush: [3, 2, 0, 0, 0], tipsOpen: new Set(), cards: [], vacFilter: 0, rmbConsumed: false, rmbSeen: new Set(), lookHold: { x: 0, y: 0 },
|
brush: [3, 2, 0, 0, 0], tipsOpen: new Set(), cards: [], vacFilter: 0, rmbConsumed: false, rmbSeen: new Set(), lookHold: { x: 0, y: 0 },
|
||||||
paste: false, selDrag: null, rmbRec: null, dragDir: -1, replaceShown: false, clip: null, drag: null, aim: { x: 0, y: 0 }, mouse: { x: 0, y: 0, in: false },
|
paste: false, selDrag: null, rmbRec: null, dragDir: -1, replaceShown: false, clip: null, drag: null, aim: { x: 0, y: 0 }, mouse: { x: 0, y: 0, in: false },
|
||||||
lmb: false, rmb: false, drillW: 1, drillTune: 0, lastDug: 0, padRun: 0, status: '', statusKind: 'err', statusT: 0, usingPad: false, padAim: { x: 12, y: 0 },
|
lmb: false, rmb: false, drillW: 1, drillTune: 0, lastDug: 0, padRun: 0, status: '', statusKind: 'err', statusT: 0, usingPad: false, padAim: { x: 12, y: 0 },
|
||||||
|
|||||||
@@ -26,6 +26,24 @@ with sync_playwright() as p:
|
|||||||
catch (e) { return await DB.get('abort-test') === undefined; }
|
catch (e) { return await DB.get('abort-test') === undefined; }
|
||||||
return false;
|
return false;
|
||||||
}"""))
|
}"""))
|
||||||
|
check('async generation preserves the world and yields frames with labels', pg.evaluate("""async () => {
|
||||||
|
const hash = a => { let h = 2166136261; for (const v of a) h = Math.imul(h ^ v, 16777619); return h; };
|
||||||
|
generateWorld(424242, 2048, 1024); const want = hash(mat);
|
||||||
|
let frames = 0, active = true, labels = [];
|
||||||
|
const frameCount = () => { if (active) { frames++; requestAnimationFrame(frameCount); } };
|
||||||
|
requestAnimationFrame(frameCount);
|
||||||
|
showLoading(true, 0, '', 'Generating world');
|
||||||
|
try {
|
||||||
|
await generateWorldAsync(424242, 2048, 1024, (f, s) => { showLoading(true, f, s); labels.push($('loadstep').textContent); });
|
||||||
|
return hash(mat) === want && frames > 1 && labels.includes('Settling loose ground') && labels.every(Boolean);
|
||||||
|
} finally { active = false; showLoading(false); }
|
||||||
|
}"""))
|
||||||
|
# Record progress at the browser's own frame boundaries, without slowing the operation.
|
||||||
|
pg.add_init_script("""
|
||||||
|
window.__loads = [];
|
||||||
|
const watch = () => { if (typeof GAME !== 'undefined' && GAME.loading) __loads.push([$('loading').querySelector('.label').textContent, $('loadstep').textContent, $('loadbar').style.width]); requestAnimationFrame(watch); };
|
||||||
|
requestAnimationFrame(watch);
|
||||||
|
""")
|
||||||
# Migrate a real legacy save before starting the play-through.
|
# Migrate a real legacy save before starting the play-through.
|
||||||
pg.evaluate("generateWorld(12345, 512, 384, null)")
|
pg.evaluate("generateWorld(12345, 512, 384, null)")
|
||||||
pg.evaluate("async () => { const s = serializeWorld(); await DB.put('main', { ...s, savedAt: Date.now(), playTime: 3661 }); }")
|
pg.evaluate("async () => { const s = serializeWorld(); await DB.put('main', { ...s, savedAt: Date.now(), playTime: 3661 }); }")
|
||||||
@@ -39,7 +57,18 @@ with sync_playwright() as p:
|
|||||||
pg.reload(); pg.wait_for_function("!$('title').hidden && !$('btn-continue').hidden")
|
pg.reload(); pg.wait_for_function("!$('title').hidden && !$('btn-continue').hidden")
|
||||||
check('legacy save becomes World 1 and retires its old key', pg.evaluate("async () => { const w = await listWorlds(); return w.length === 1 && w[0].name === 'World 1' && await DB.get('main') === undefined; }"))
|
check('legacy save becomes World 1 and retires its old key', pg.evaluate("async () => { const w = await listWorlds(); return w.length === 1 && w[0].name === 'World 1' && await DB.get('main') === undefined; }"))
|
||||||
pg.click('#btn-continue'); pg.wait_for_function('GAME.running')
|
pg.click('#btn-continue'); pg.wait_for_function('GAME.running')
|
||||||
|
check('Continue paints loading steps', pg.evaluate("__loads.some(([t, s]) => t === 'Loading world' && s === 'Building the world') && !GAME.loading && $('loading').hidden"))
|
||||||
check('Continue loads the migrated seed and playtime', pg.evaluate('worldSeed === 12345 && GAME.playTime >= 3661 && !!GAME.worldId'))
|
check('Continue loads the migrated seed and playtime', pg.evaluate('worldSeed === 12345 && GAME.playTime >= 3661 && !!GAME.worldId'))
|
||||||
|
pg.evaluate("""() => {
|
||||||
|
window.__pack = packBuffers;
|
||||||
|
packBuffers = async b => { await new Promise(r => setTimeout(r, 600)); return __pack(b); };
|
||||||
|
window.__saving = saveWorld('main', true);
|
||||||
|
}""")
|
||||||
|
pg.locator('#saving').wait_for(state='visible')
|
||||||
|
pg.screenshot(path=str(out / 'saving-progress.png'))
|
||||||
|
check('slow save shows the saving chip', pg.is_visible('#saving'))
|
||||||
|
pg.evaluate('async () => { await __saving; packBuffers = __pack; }')
|
||||||
|
check('saving chip hides when the save ends', not pg.is_visible('#saving'))
|
||||||
pg.evaluate('quitToTitle()')
|
pg.evaluate('quitToTitle()')
|
||||||
names = {}
|
names = {}
|
||||||
for seed in [222, 333]:
|
for seed in [222, 333]:
|
||||||
@@ -53,6 +82,7 @@ with sync_playwright() as p:
|
|||||||
pg.screenshot(path=str(out / 'new-world.png'))
|
pg.screenshot(path=str(out / 'new-world.png'))
|
||||||
pg.fill('#nw-seed', str(seed)); pg.press('#nw-name' if seed == 222 else '#nw-seed', 'Enter')
|
pg.fill('#nw-seed', str(seed)); pg.press('#nw-name' if seed == 222 else '#nw-seed', 'Enter')
|
||||||
pg.wait_for_function('GAME.running', timeout=20000)
|
pg.wait_for_function('GAME.running', timeout=20000)
|
||||||
|
check(f'new world {seed} paints a moving generation bar', pg.evaluate("__loads.some(([t, s, w]) => t === 'Generating world' && parseFloat(w) > 0 && parseFloat(w) < 90) && !GAME.loading"))
|
||||||
check(f'new world {seed} shows the Welcome card', pg.evaluate("[...$('cards').children].some(c => c.textContent.includes('Welcome'))"))
|
check(f'new world {seed} shows the Welcome card', pg.evaluate("[...$('cards').children].some(c => c.textContent.includes('Welcome'))"))
|
||||||
check(f'pause menu shows world {seed} name', names[seed] in pg.evaluate("() => { openPause(); return $('pause-meta').textContent; }"))
|
check(f'pause menu shows world {seed} name', names[seed] in pg.evaluate("() => { openPause(); return $('pause-meta').textContent; }"))
|
||||||
pg.evaluate('quitToTitle()')
|
pg.evaluate('quitToTitle()')
|
||||||
@@ -1058,6 +1088,8 @@ with sync_playwright() as p:
|
|||||||
check('the size menu offers small, medium, large and huge', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')") == '2048x1024,3072x1280,4096x1536,6144x2048', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')"))
|
check('the size menu offers small, medium, large and huge', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')") == '2048x1024,3072x1280,4096x1536,6144x2048', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')"))
|
||||||
check('Medium remains the default size', pg2.input_value('#nw-size') == '3072x1280')
|
check('Medium remains the default size', pg2.input_value('#nw-size') == '3072x1280')
|
||||||
pg2.click('#btn-new'); pg2.fill('#nw-seed', '4242'); pg2.select_option('#nw-size', '6144x2048'); pg2.click('#btn-generate')
|
pg2.click('#btn-new'); pg2.fill('#nw-seed', '4242'); pg2.select_option('#nw-size', '6144x2048'); pg2.click('#btn-generate')
|
||||||
|
pg2.wait_for_function("GAME.loading && parseFloat($('loadbar').style.width) > 10 && parseFloat($('loadbar').style.width) < 85", timeout=30000)
|
||||||
|
pg2.screenshot(path=str(out / 'generation-progress.png'))
|
||||||
pg2.wait_for_function('GAME.running === true', timeout=90000)
|
pg2.wait_for_function('GAME.running === true', timeout=90000)
|
||||||
check('huge world generates', pg2.evaluate('W === 6144 && H === 2048'), pg2.evaluate('[W, H]'))
|
check('huge world generates', pg2.evaluate('W === 6144 && H === 2048'), pg2.evaluate('[W, H]'))
|
||||||
pg2.evaluate("bot.x = 2000; bot.y = magmaY[2000] - 60; snapCamera();"); pg2.wait_for_timeout(800)
|
pg2.evaluate("bot.x = 2000; bot.y = magmaY[2000] - 60; snapCamera();"); pg2.wait_for_timeout(800)
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ const { check, done } = suite();
|
|||||||
const fresh = () => { flatWorld(run, 240, 160, 100); run('player.tankTier = 3; saplings = []; stats.grown = 0;'); };
|
const fresh = () => { flatWorld(run, 240, 160, 100); run('player.tankTier = 3; saplings = []; stats.grown = 0;'); };
|
||||||
const box = (x0, y0, x1, y1, m) => rect(run, x0, y0, x1, y1, m);
|
const box = (x0, y0, x1, y1, m) => rect(run, x0, y0, x1, y1, m);
|
||||||
|
|
||||||
|
const { createHash } = require('node:crypto');
|
||||||
|
const worldHash = () => { const hash = createHash('sha256'); for (const k of ['mat', 'temp', 'flags']) { const a = run(k); hash.update(new Uint8Array(a.buffer, a.byteOffset, a.byteLength)); } return hash.digest('hex'); };
|
||||||
|
run('generateWorld(4242, 512, 384)');
|
||||||
|
const generatedHash = worldHash();
|
||||||
|
const progress = [];
|
||||||
|
run('generateWorld')(4242, 512, 384, (f, label) => progress.push([f, label]));
|
||||||
|
check('generation progress rises to 1 with named steps', progress.length > 10 && progress[0][0] === 0 && progress.at(-1)[0] === 1 && progress.every(([f, label], i) => f >= 0 && f <= 1 && (!i || f >= progress[i - 1][0]) && typeof label === 'string' && label.length > 0));
|
||||||
|
check('generation callback keeps the same world', worldHash() === generatedHash);
|
||||||
|
|
||||||
// 1. acorns sprout on soil under open sky and grow into a tree
|
// 1. acorns sprout on soil under open sky and grow into a tree
|
||||||
fresh();
|
fresh();
|
||||||
box(100, 100, 140, 103, run('DIRT'));
|
box(100, 100, 140, 103, run('DIRT'));
|
||||||
|
|||||||
Reference in New Issue
Block a user