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 40d9647b73
commit edd9cdf940
9 changed files with 52 additions and 12 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;
+8 -2
View File
@@ -1,9 +1,15 @@
--- ---
id: t10 id: t10
title: World name box for a new world title: World name box for a new world
status: Todo status: Review
created: 2026-10-24 created: 2026-10-24
updated: 2026-10-24T04:41:46Z updated: 2026-10-24T05:19:25Z
order: 4
report: |
(agent) The New world panel now has a Name box above Seed. It starts filled with the next
"World N" and selected, so you can type over it; leave it empty to get "World N". Enter in
the Name or Seed box starts the world. The name shows in the Worlds list and the pause menu.
To check: title > New world, type a name, Enter. Pause (Esc) shows the name at the top.
--- ---
## Context ## Context
+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")