Name box in the New world panel (t10)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 01:19:25 -04:00
co-authored by Claude Opus 5.5
parent 36f82712ab
commit f3cedcacc8
8 changed files with 44 additions and 10 deletions
+3 -3
View File
@@ -370,9 +370,9 @@ The title screen art.
- functions: buildTitleCells, drawTitle - functions: buildTitleCells, drawTitle
- variables: FONT5, TITLE - variables: FONT5, TITLE
### src/ui/saves.js (177 lines) ### src/ui/saves.js (178 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, renameWorld, migrateWorlds, saveWorld, newestSave, b64FromBytes, bytesFromB64, gz, exportWorld, importWorldFile, libLoad, libSave, bpEncode, bpValid, bpDecode - functions: newWorldId, worldWrite, listWorlds, putWorldEntry, worldNewest, deleteWorld, cleanWorldName, renameWorld, migrateWorlds, saveWorld, newestSave, b64FromBytes, bytesFromB64, gz, exportWorld, importWorldFile, libLoad, libSave, bpEncode, bpValid, bpDecode
- variables: DB, worldIdTime, worldIdCounter, worldWrites - variables: DB, worldIdTime, worldIdCounter, worldWrites
### src/ui/session.js (125 lines) ### src/ui/session.js (125 lines)
@@ -431,7 +431,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 (119 lines) ### src/ui/loop.js (126 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
+11
View File
@@ -4,6 +4,17 @@ 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: t10 World name box for a new world (Claude Code, Codex)
- `src/shell.html`, `src/styles/style.css`: a Name box across the top of the New world panel.
- `src/ui/loop.js`: opening the panel fills it with the next `World N` and selects it; Enter in
Name or Seed generates. `src/ui/session.js`: `startNewWorld(seed, w, h, name)`.
- `src/ui/saves.js`: `cleanWorldName` (trim, 40 characters, else a fallback), shared with
`renameWorld`.
- `test/play.py`: a typed name reaches the Worlds list and the pause menu; the default and an
all-spaces name both give `World N`. Screenshot `shots/play/new-world.png` checked.
- Tests: `run_all.py --browser` all pass.
## 2026-10-24: t7 Option to turn off smooth pan following the mouse (Claude Code, Codex) ## 2026-10-24: t7 Option to turn off smooth pan following the mouse (Claude Code, Codex)
- New setting `cameraLead` (Display: "Camera looks toward the pointer", on by default): - New setting `cameraLead` (Display: "Camera looks toward the pointer", on by default):
+1
View File
@@ -50,6 +50,7 @@
<button class="btn" id="btn-worlds" hidden>Worlds <small>load · rename · delete</small></button> <button class="btn" id="btn-worlds" hidden>Worlds <small>load · rename · delete</small></button>
<button class="btn" id="btn-new">New world <small>generate</small></button> <button class="btn" id="btn-new">New world <small>generate</small></button>
<div class="plate newworld" id="newworld" hidden> <div class="plate newworld" id="newworld" hidden>
<label>Name <input id="nw-name" type="text" maxlength="40" autocomplete="off" spellcheck="false"></label>
<label>Seed <input id="nw-seed" type="text" inputmode="numeric" autocomplete="off"></label> <label>Seed <input id="nw-seed" type="text" inputmode="numeric" autocomplete="off"></label>
<label>Size <label>Size
<select id="nw-size"> <select id="nw-size">
+1
View File
@@ -293,6 +293,7 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border-
.newworld { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; padding: 10px; } .newworld { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; padding: 10px; }
.newworld label { display: flex; flex-direction: column; gap: 4px; font-size: 0.86em; color: var(--ash); } .newworld label { display: flex; flex-direction: column; gap: 4px; font-size: 0.86em; color: var(--ash); }
.newworld select, .newworld input { background: var(--soot-0); border: 1px solid var(--soot-4); border-radius: 4px; padding: 6px 8px; color: var(--bone); } .newworld select, .newworld input { background: var(--soot-0); border: 1px solid var(--soot-4); border-radius: 4px; padding: 6px 8px; color: var(--bone); }
.newworld label:first-child { grid-column: 1 / -1; }
.newworld .full { grid-column: 1 / -1; } .newworld .full { grid-column: 1 / -1; }
.tfoot { text-align: center; color: var(--ash-dim); font-size: 0.82em; font-family: var(--font-mono); } .tfoot { text-align: center; color: var(--ash-dim); font-size: 0.82em; font-family: var(--font-mono); }
#loading { position: fixed; inset: 0; z-index: 30; display: grid; place-items: center; background: rgba(11, 9, 8, 0.86); } #loading { position: fixed; inset: 0; z-index: 30; display: grid; place-items: center; background: rgba(11, 9, 8, 0.86); }
+9 -2
View File
@@ -97,11 +97,18 @@ function boot(hotData) {
buildTitleCells(); drawTitle(); buildTitleCells(); drawTitle();
$('nw-seed').value = String(Math.floor(Math.random() * 900000) + 100000); $('nw-seed').value = String(Math.floor(Math.random() * 900000) + 100000);
$('wallet').onclick = () => { playSfx('ui'); openNotebook('Market'); }; $('wallet').onclick = () => { playSfx('ui'); openNotebook('Market'); };
$('btn-new').onclick = () => { auInit(); playSfx('ui'); $('newworld').hidden = !$('newworld').hidden; if (!$('newworld').hidden) $('btn-generate').focus(); }; $('btn-new').onclick = async () => {
auInit(); playSfx('ui'); $('newworld').hidden = !$('newworld').hidden;
if ($('newworld').hidden) return;
try { $('nw-name').value = `World ${(await listWorlds()).length + 1}`; }
catch (e) { $('nw-name').value = 'World 1'; }
$('nw-name').focus(); $('nw-name').select();
};
for (const id of ['nw-name', 'nw-seed']) $(id).onkeydown = e => { if (e.key === 'Enter') $('btn-generate').click(); };
$('btn-generate').onclick = () => { $('btn-generate').onclick = () => {
const seed = parseInt(String($('nw-seed').value).replace(/\D/g, ''), 10) || Math.floor(Math.random() * 1e6); const seed = parseInt(String($('nw-seed').value).replace(/\D/g, ''), 10) || Math.floor(Math.random() * 1e6);
const [w, h] = $('nw-size').value.split('x').map(Number); const [w, h] = $('nw-size').value.split('x').map(Number);
startNewWorld(seed, w, h); startNewWorld(seed, w, h, $('nw-name').value);
}; };
$('btn-continue').onclick = async () => { auInit(); playSfx('ui'); const sv = await newestSave(); if (sv) startFromSave(sv); else toast('No saved world found.'); }; $('btn-continue').onclick = async () => { auInit(); playSfx('ui'); const sv = await newestSave(); if (sv) startFromSave(sv); else toast('No saved world found.'); };
$('btn-title-settings').onclick = () => { auInit(); playSfx('ui'); openSettings('Display'); }; $('btn-title-settings').onclick = () => { auInit(); playSfx('ui'); openSettings('Display'); };
+2 -1
View File
@@ -54,11 +54,12 @@ function deleteWorld(id) {
if (GAME.worldId === id) { GAME.worldId = ''; GAME.worldName = ''; } if (GAME.worldId === id) { GAME.worldId = ''; GAME.worldName = ''; }
}); });
} }
function cleanWorldName(name, fallback) { return String(name).trim().slice(0, 40) || fallback; }
function renameWorld(id, name) { function renameWorld(id, name) {
return worldWrite(async () => { return worldWrite(async () => {
const list = await listWorlds(), entry = list.find(w => w.id === id); const list = await listWorlds(), entry = list.find(w => w.id === id);
if (!entry) return; if (!entry) return;
entry.name = String(name).trim().slice(0, 40) || entry.name; entry.name = cleanWorldName(name, entry.name);
await DB.put('worlds', list); await DB.put('worlds', list);
if (GAME.worldId === id) GAME.worldName = entry.name; if (GAME.worldId === id) GAME.worldName = entry.name;
}); });
+2 -2
View File
@@ -15,13 +15,13 @@ 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();
} }
function startNewWorld(seed, w, h) { function startNewWorld(seed, w, h, name = '') {
auInit(); auInit();
showLoading(true, 0); showLoading(true, 0);
setTimeout(async () => { setTimeout(async () => {
try { try {
const worlds = await listWorlds(); const worlds = await listWorlds();
GAME.worldId = newWorldId(); GAME.worldName = `World ${worlds.length + 1}`; GAME.worldId = newWorldId(); GAME.worldName = cleanWorldName(name, `World ${worlds.length + 1}`);
await putWorldEntry({ id: GAME.worldId, name: GAME.worldName, seed, w, h, createdAt: Date.now(), savedAt: 0, playTime: 0 }); await putWorldEntry({ id: GAME.worldId, name: GAME.worldName, seed, w, h, createdAt: Date.now(), savedAt: 0, playTime: 0 });
generateWorld(seed, w, h, null); generateWorld(seed, w, h, null);
GAME.playTime = 0; GAME.clip = null; GAME.paste = false; GAME.selDrag = null; GAME.tool = 0; GAME.dropMat = 0; GAME.matChosen = false; 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.dropMat = 0; GAME.matChosen = false; GAME.wallMat = RSTONE; GAME.filterCfg = { m: [], inv: 0 }; GAME.pieceIdx = 0;
+15 -2
View File
@@ -41,11 +41,20 @@ with sync_playwright() as p:
pg.click('#btn-continue'); pg.wait_for_function('GAME.running') pg.click('#btn-continue'); pg.wait_for_function('GAME.running')
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('quitToTitle()') pg.evaluate('quitToTitle()')
names = {}
for seed in [222, 333]: for seed in [222, 333]:
if not pg.is_visible('#nw-seed'): pg.click('#btn-new') if pg.is_visible('#nw-seed'): pg.click('#btn-new')
pg.fill('#nw-seed', str(seed)); pg.click('#btn-generate') pg.click('#btn-new')
pg.wait_for_function("document.activeElement === $('nw-name')")
names[seed] = 'Copper Valley' if seed == 222 else pg.input_value('#nw-name')
check(f'new world {seed} offers the next default name', pg.input_value('#nw-name') == f'World {2 if seed == 222 else 3}')
if seed == 222:
pg.fill('#nw-name', names[seed])
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.wait_for_function('GAME.running', timeout=20000) pg.wait_for_function('GAME.running', timeout=20000)
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; }"))
pg.evaluate('quitToTitle()') pg.evaluate('quitToTitle()')
pg.wait_for_function("!$('title').hidden") pg.wait_for_function("!$('title').hidden")
pg.click('#btn-worlds'); pg.locator('#m-worlds').wait_for(state='visible') pg.click('#btn-worlds'); pg.locator('#m-worlds').wait_for(state='visible')
@@ -59,6 +68,7 @@ with sync_playwright() as p:
pg.set_viewport_size({'width': 1280, 'height': 800}); pg.evaluate("SET.look = 'pixel'") pg.set_viewport_size({'width': 1280, 'height': 800}); pg.evaluate("SET.look = 'pixel'")
ids = pg.evaluate("async () => Object.fromEntries((await listWorlds()).map(w => [w.seed, w.id]))") ids = pg.evaluate("async () => Object.fromEntries((await listWorlds()).map(w => [w.seed, w.id]))")
for seed in [222, 333]: for seed in [222, 333]:
check(f'Worlds list shows world {seed} name', pg.locator(f'[data-world="{ids[str(seed)]}"] strong').inner_text() == names[seed])
row = pg.locator(f'[data-world="{ids[str(seed)]}"]') row = pg.locator(f'[data-world="{ids[str(seed)]}"]')
row.get_by_role('button', name='Load', exact=True).click(); pg.wait_for_function('GAME.running') row.get_by_role('button', name='Load', exact=True).click(); pg.wait_for_function('GAME.running')
check(f'Load switches to seed {seed}', pg.evaluate('worldSeed') == seed and pg.evaluate('GAME.worldId') == ids[str(seed)]) check(f'Load switches to seed {seed}', pg.evaluate('worldSeed') == seed and pg.evaluate('GAME.worldId') == ids[str(seed)])
@@ -93,8 +103,11 @@ with sync_playwright() as p:
check('Continue chooses the most recently saved world', pg.evaluate('worldSeed') == 333) check('Continue chooses the most recently saved world', pg.evaluate('worldSeed') == 333)
pg.evaluate('quitToTitle()') pg.evaluate('quitToTitle()')
if not pg.is_visible('#nw-seed'): pg.click('#btn-new') if not pg.is_visible('#nw-seed'): pg.click('#btn-new')
blank_default = pg.evaluate("listWorlds().then(w => `World ${w.length + 1}`)")
pg.fill('#nw-name', ' ')
pg.fill('#nw-seed', '777'); pg.click('#btn-generate') pg.fill('#nw-seed', '777'); pg.click('#btn-generate')
pg.wait_for_function('GAME.running === true', timeout=20000) pg.wait_for_function('GAME.running === true', timeout=20000)
check('blank new world names use the default', pg.evaluate('GAME.worldName') == blank_default)
pg.wait_for_timeout(500) pg.wait_for_timeout(500)
ev = pg.evaluate ev = pg.evaluate
ev("window._maxStatus = ''; const _ss = setStatus; setStatus = function (s, k) { if (s && s.length > _maxStatus.length) _maxStatus = s; return _ss(s, k); }; 0") ev("window._maxStatus = ''; const _ss = setStatus; setStatus = function (s, k) { if (s && s.length > _maxStatus.length) _maxStatus = s; return _ss(s, k); }; 0")