New claims get camp names from their seed, renameable from the Claims tab; add t30 (t3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -381,9 +381,9 @@ 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
|
||||||
|
|
||||||
### src/ui/claims.js (115 lines)
|
### src/ui/claims.js (141 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, 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 (133 lines)
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ 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: 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`,
|
||||||
|
skipping names already in the world list) instead of "Claim N". The Claims tab shows "Claim N"
|
||||||
|
beside the name and has a Rename button on every row (reuses `renameWorld`).
|
||||||
|
- Owner's request added to Todo as t30: progress meters while generating, loading and saving.
|
||||||
|
- Tests: play.py claims checks extended; t_smooth half-resolution still fails under load.
|
||||||
|
|
||||||
## 2026-10-24: t3 fix: deep ore burned and melted after a reload (Claude Code)
|
## 2026-10-24: t3 fix: deep ore burned and melted after a reload (Claude Code)
|
||||||
|
|
||||||
- Owner: a Huge world ran at about 10 fps after travelling between claims, even after a refresh.
|
- Owner: a Huge world ran at about 10 fps after travelling between claims, even after a refresh.
|
||||||
|
|||||||
+29
-3
@@ -56,6 +56,21 @@ async function loadCareer() {
|
|||||||
GAME.careerId = career.id;
|
GAME.careerId = career.id;
|
||||||
} catch (e) { career = null; toast('Career could not be loaded. Using this world’s own money and bag.'); }
|
} catch (e) { career = null; toast('Career could not be loaded. Using this world’s own money and bag.'); }
|
||||||
}
|
}
|
||||||
|
// Seeded camp names stay the same unless an existing world already uses one.
|
||||||
|
function claimName(seed, taken = []) {
|
||||||
|
const first = ['Copper', 'Iron', 'Red', 'Black', 'Grey', 'Silver', 'Deep', 'Cinder', 'Ash', 'Granite', 'Basalt', 'Slate', 'Rust', 'Ember', 'Flint', 'Cold', 'Hollow', 'Lone', 'Twin', 'Broken'];
|
||||||
|
const last = ['Hollow', 'Ridge', 'Flats', 'Gulch', 'Mesa', 'Seam', 'Reach', 'Bluff', 'Basin', 'Crag', 'Shelf', 'Draw', 'Cut', 'Rise', 'Pit', 'Vein', 'Wash', 'Fork', 'Run', 'Bench'];
|
||||||
|
const count = first.length * last.length, start = Math.floor(mulberry32(seed)() * count);
|
||||||
|
for (let offset = 0; offset < count; offset++) {
|
||||||
|
const i = (start + offset) % count, a = first[Math.floor(i / last.length)], b = last[i % last.length];
|
||||||
|
const name = a + ' ' + b;
|
||||||
|
if (a !== b && !taken.includes(name)) return name;
|
||||||
|
}
|
||||||
|
// every camp name is taken: fall back to numbered names
|
||||||
|
let n = 1;
|
||||||
|
while (taken.includes('Mining Camp ' + n)) n++;
|
||||||
|
return 'Mining Camp ' + n;
|
||||||
|
}
|
||||||
async function openNewClaim() {
|
async function openNewClaim() {
|
||||||
if (claimBusy || !career || debt > 0 || money < 5000) return false;
|
if (claimBusy || !career || debt > 0 || money < 5000) return false;
|
||||||
claimBusy = true; const paused = GAME.paused; GAME.paused = true;
|
claimBusy = true; const paused = GAME.paused; GAME.paused = true;
|
||||||
@@ -66,13 +81,14 @@ async function openNewClaim() {
|
|||||||
const carried = careerState(), n = career.claimNo + 1, w = W, h = H;
|
const carried = careerState(), n = career.claimNo + 1, w = W, h = H;
|
||||||
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);
|
||||||
await setupNewWorld(seed, w, h, `Claim ${n}`, carried);
|
const name = claimName(seed, (await listWorlds()).map(w => w.name));
|
||||||
|
await setupNewWorld(seed, w, h, name, carried);
|
||||||
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;
|
||||||
if (!await saveWorld('auto', true)) throw new Error('Save failed');
|
if (!await saveWorld('auto', true)) throw new Error('Save failed');
|
||||||
showLoading(false); closeAllModals(); enterGame();
|
showLoading(false); closeAllModals(); enterGame();
|
||||||
showCard({ kind: 'New claim', title: `Claim ${n}`, body: [`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;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (previous) {
|
if (previous) {
|
||||||
@@ -101,9 +117,19 @@ async function renderClaimsTab(body) {
|
|||||||
try {
|
try {
|
||||||
const list = (await listWorlds()).filter(w => career && w.career === GAME.careerId && career.claims.includes(w.id));
|
const list = (await listWorlds()).filter(w => career && w.career === GAME.careerId && career.claims.includes(w.id));
|
||||||
if (nbTab !== 'Claims') return;
|
if (nbTab !== 'Claims') return;
|
||||||
body.innerHTML = list.map(w => `<div class="world-row"><div class="world-info"><strong>${esc(w.name)}${w.id === GAME.worldId ? ' · here' : ''}</strong><div class="note">${esc(w.w)}×${esc(w.h)} grains · ${(w.id === GAME.worldId ? debt : w.debt) <= 0 ? 'paid off ✓' : fmtMoney(w.id === GAME.worldId ? debt : w.debt || 0) + ' owed'} · Last played: ${w.savedAt ? esc(new Date(w.savedAt).toLocaleString()) : 'Just opened'}</div></div>${w.id === GAME.worldId ? '' : `<button class="btn" data-travel="${esc(w.id)}">Travel</button>`}</div>`).join('') +
|
body.innerHTML = list.map(w => `<div class="world-row"><div class="world-info"><strong>${esc(w.name)}${w.id === GAME.worldId ? ' · here' : ''}</strong> <small class="note">Claim ${esc(w.claimNo)}</small><div class="note">${esc(w.w)}×${esc(w.h)} grains · ${(w.id === GAME.worldId ? debt : w.debt) <= 0 ? 'paid off ✓' : fmtMoney(w.id === GAME.worldId ? debt : w.debt || 0) + ' owed'} · Last played: ${w.savedAt ? esc(new Date(w.savedAt).toLocaleString()) : 'Just opened'}</div></div><button class="btn" data-rename="${esc(w.id)}">Rename</button>${w.id === GAME.worldId ? '' : `<button class="btn" data-travel="${esc(w.id)}">Travel</button>`}</div>`).join('') +
|
||||||
(debt <= 0 ? `<button class="btn primary" id="claim-open" ${money < 5000 || !career ? 'disabled' : ''}>Open a new claim: $5,000 down, then ${fmtMoney(CLAIM_DEBT * 2 ** (career ? career.claimNo : 1))} owed</button>${money < 5000 ? '<p class=note>You need $5,000 for the down payment.</p>' : ''}` : '<p class=note>A new claim opens once this one is paid off.</p>') +
|
(debt <= 0 ? `<button class="btn primary" id="claim-open" ${money < 5000 || !career ? 'disabled' : ''}>Open a new claim: $5,000 down, then ${fmtMoney(CLAIM_DEBT * 2 ** (career ? career.claimNo : 1))} owed</button>${money < 5000 ? '<p class=note>You need $5,000 for the down payment.</p>' : ''}` : '<p class=note>A new claim opens once this one is paid off.</p>') +
|
||||||
'<p class=note>Machines on other claims wait until you come back.</p>';
|
'<p class=note>Machines on other claims wait until you come back.</p>';
|
||||||
|
body.querySelectorAll('[data-rename]').forEach(b => b.onclick = () => {
|
||||||
|
const w = list.find(w => w.id === b.dataset.rename);
|
||||||
|
dialog('Rename claim', `<input class="text" type="text" id="claim-name" aria-label="Claim name" maxlength="40" value="${esc(w.name)}">`, [
|
||||||
|
{ label: 'Cancel', cls: 'ghost' }, { label: 'Save', cls: 'primary', fn: async () => {
|
||||||
|
const name = cleanWorldName($('claim-name').value, w.name);
|
||||||
|
try { await renameWorld(w.id, name); closeModal('m-dialog'); renderClaimsTab(body); }
|
||||||
|
catch (e) { toast('Could not rename this claim. Try again.'); }
|
||||||
|
} },
|
||||||
|
]);
|
||||||
|
});
|
||||||
const button = $('claim-open');
|
const button = $('claim-open');
|
||||||
if (button) button.onclick = () => dialog('Open a new claim?', `<p class="note">Pay $5,000 down and owe ${fmtMoney(CLAIM_DEBT * 2 ** career.claimNo)} on fresh land. Your buildings stay here.</p>`, [
|
if (button) button.onclick = () => dialog('Open a new claim?', `<p class="note">Pay $5,000 down and owe ${fmtMoney(CLAIM_DEBT * 2 ** career.claimNo)} on fresh land. Your buildings stay here.</p>`, [
|
||||||
{ label: 'Cancel', cls: 'ghost' }, { label: 'Open claim', cls: 'primary', fn: () => { closeModal('m-dialog'); openNewClaim(); } },
|
{ label: 'Cancel', cls: 'ghost' }, { label: 'Open claim', cls: 'primary', fn: () => { closeModal('m-dialog'); openNewClaim(); } },
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
id: t30
|
||||||
|
title: Progress meters while generating, loading and saving
|
||||||
|
status: Todo
|
||||||
|
created: 2026-10-24
|
||||||
|
updated: 2026-10-24T22:36:27Z
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- Making a world, opening a new claim, travelling between claims and loading a save can take several seconds on big worlds (a Huge world takes about 6 s to generate), and the screen gives little sign of what is happening. Show a progress bar with a short message for each step. Saving is quick today, but a small indicator would help there too, especially for Huge worlds.
|
||||||
|
|
||||||
|
## Subtasks
|
||||||
|
|
||||||
|
- [ ] Generating a world shows a progress bar that moves and names the step (terrain, ores, caves, water, trees…)
|
||||||
|
- [ ] Loading a save, opening a new claim and travelling show progress (reading, unpacking, building the world)
|
||||||
|
- [ ] Saving shows a small, unobtrusive indicator while it runs
|
||||||
+19
-3
@@ -850,11 +850,27 @@ with sync_playwright() as p:
|
|||||||
ev('GAME.paused = true')
|
ev('GAME.paused = true')
|
||||||
check('new claim carries money, bag, upgrades and knowledge', ev("money === 5000 && earned === 12345 && JSON.stringify(Array.from(bag)) === JSON.stringify(claimTest.bag) && player.bitTier === 2 && seen[GOLD] === 1 && ledger.dig === 42 && stats.dug === 321"))
|
check('new claim carries money, bag, upgrades and knowledge', ev("money === 5000 && earned === 12345 && JSON.stringify(Array.from(bag)) === JSON.stringify(claimTest.bag) && player.bitTier === 2 && seen[GOLD] === 1 && ledger.dig === 42 && stats.dug === 321"))
|
||||||
check('claim 2 has doubled debt, a new seed and the same dimensions', ev('debt === 40000 && worldSeed !== claimTest.seed && W === 512 && H === 384'))
|
check('claim 2 has doubled debt, a new seed and the same dimensions', ev('debt === 40000 && worldSeed !== claimTest.seed && W === 512 && H === 384'))
|
||||||
check('claim 2 is stored with its career and name', ev("async () => { const c = await DB.get('career:' + GAME.careerId); const w = (await listWorlds()).find(w => w.id === GAME.worldId); return c.claims.length === 2 && c.claimNo === 2 && w.name === 'Claim 2' && w.career === c.id && w.claimNo === 2; }"))
|
check('claim 2 is stored with its career and name', ev("async () => { const c = await DB.get('career:' + GAME.careerId); const w = (await listWorlds()).find(w => w.id === GAME.worldId); return c.claims.length === 2 && c.claimNo === 2 && w.name !== 'Claim 2' && /^[A-Z][a-z]+ [A-Z][a-z]+$/.test(w.name) && w.name === GAME.worldName && w.career === c.id && w.claimNo === 2; }"))
|
||||||
check('new claim shows its arrival card', ev("$('cards').textContent.includes('New claim') && $('cards').textContent.includes('$40,000')"))
|
check('new claim shows its arrival card', ev("$('cards').textContent.includes(GAME.worldName) && $('cards').textContent.includes('Claim 2.') && $('cards').textContent.includes('$40,000')"))
|
||||||
ev("openNotebook('Claims')")
|
ev("openNotebook('Claims')")
|
||||||
cp.wait_for_function("$('nb-body').querySelectorAll('[data-travel]').length === 1")
|
cp.wait_for_function("$('nb-body').querySelectorAll('[data-travel]').length === 1")
|
||||||
check('Claims lists both claims with here, size and paid off', ev("$('nb-body').textContent.includes('Claim 2 · here') && $('nb-body').textContent.includes('512×384') && $('nb-body').textContent.includes('paid off ✓')"))
|
check('Claims lists both claims with here, size and paid off', ev("$('nb-body').textContent.includes(GAME.worldName + ' · here') && $('nb-body').textContent.includes('Claim 2') && $('nb-body').textContent.includes('512×384') && $('nb-body').textContent.includes('paid off ✓')"))
|
||||||
|
check('camp names are stable and varied', ev("claimName(424242) === claimName(424242) && new Set(Array.from({length: 20}, (_, i) => claimName(i))).size >= 16"))
|
||||||
|
check('camp names skip taken names and repeated words', ev("""() => {
|
||||||
|
const names = Array.from({length: 1000}, (_, i) => claimName(i));
|
||||||
|
return names.every(n => n.split(' ')[0] !== n.split(' ')[1]) && claimName(42, [claimName(42)]) !== claimName(42);
|
||||||
|
}"""))
|
||||||
|
check('every claim has a rename button', cp.locator('[data-rename]').count() == 2)
|
||||||
|
cp.locator('[data-rename="' + ev('GAME.worldId') + '"]').click()
|
||||||
|
check('rename is prefilled with the current name', cp.locator('#claim-name').input_value() == ev('GAME.worldName'))
|
||||||
|
cp.fill('#claim-name', 'Silver Camp')
|
||||||
|
cp.locator('#dlg-foot').get_by_role('button', name='Cancel', exact=True).click()
|
||||||
|
check('cancel keeps the claim name', ev("GAME.worldName !== 'Silver Camp'"))
|
||||||
|
cp.locator('[data-rename="' + ev('GAME.worldId') + '"]').click()
|
||||||
|
cp.fill('#claim-name', ' Silver Camp ')
|
||||||
|
cp.locator('#dlg-foot').get_by_role('button', name='Save', exact=True).click()
|
||||||
|
cp.wait_for_function("$('nb-body').textContent.includes('Silver Camp · here')")
|
||||||
|
check('renaming the current claim updates storage and game', ev("async () => GAME.worldName === 'Silver Camp' && (await listWorlds()).find(w => w.id === GAME.worldId).name === 'Silver Camp'"))
|
||||||
cp.screenshot(path=str(out / '09-claims.png'))
|
cp.screenshot(path=str(out / '09-claims.png'))
|
||||||
cp.click('[data-travel]'); cp.locator('#dlg-foot').get_by_role('button', name='Cancel', exact=True).click()
|
cp.click('[data-travel]'); cp.locator('#dlg-foot').get_by_role('button', name='Cancel', exact=True).click()
|
||||||
check('travelling asks for confirmation', ev('GAME.worldId !== claimTest.id'))
|
check('travelling asks for confirmation', ev('GAME.worldId !== claimTest.id'))
|
||||||
|
|||||||
Reference in New Issue
Block a user