Dev console: name suggestions, materials list, history, Tab completion, help, clear (t94)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -482,6 +482,11 @@ above, including:
|
|||||||
doesn't need to fill a rectangle for belts, conveyors and the like. Belts, filters, screw
|
doesn't need to fill a rectangle for belts, conveyors and the like. Belts, filters, screw
|
||||||
conveyors, shafts, pipes, pumps and outlets (`lineOnly()`) always draw a line, Shift or not, and
|
conveyors, shafts, pipes, pumps and outlets (`lineOnly()`) always draw a line, Shift or not, and
|
||||||
a diagonal belt drag stays sloped. Walls, grates and gates still Shift-fill.
|
a diagonal belt drag stays sloped. Walls, grates and gates still Shift-fill.
|
||||||
|
- **Dev console name discovery and keys (t94, 2026-10-28).** The owner chose close-name
|
||||||
|
suggestions and a filtered material list because they had not known material names could
|
||||||
|
contain spaces. Match names with spaces ignored; keep display and internal names, with no
|
||||||
|
hover hint. Add Tab completion, persistent 50-command history, command-specific help,
|
||||||
|
output clearing and red errors for easier use.
|
||||||
- **The dev console ships in every build (t66, 2026-10-27).** The owner is happy for players to
|
- **The dev console ships in every build (t66, 2026-10-27).** The owner is happy for players to
|
||||||
experiment and play their own way, so the console isn't hidden behind a dev build. Turning it
|
experiment and play their own way, so the console isn't hidden behind a dev build. Turning it
|
||||||
on warns first that the save will be marked, and the mark stays with the world; any future
|
on warns first that the save will be marked, and the mark stays with the world; any future
|
||||||
|
|||||||
+5
-4
@@ -387,10 +387,10 @@ World generation helpers: noise and removal of tiny hard rock specks.
|
|||||||
World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
|
World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
|
||||||
- functions: generateWorld, generateWorldAsync
|
- functions: generateWorld, generateWorldAsync
|
||||||
|
|
||||||
### src/sim/devconsole.js (114 lines)
|
### src/sim/devconsole.js (185 lines)
|
||||||
Dev commands are pure simulation operations; the browser owns the panel and confirmation.
|
Dev commands are pure simulation operations; the browser owns the panel and confirmation.
|
||||||
- functions: resetDevConsole, enableDevConsole, devMaterial, devCommand
|
- functions: resetDevConsole, enableDevConsole, devError, devName, devGiveable, devMaterialList, devDistance, devSuggestions, devHistory, devRemember, devRecall, devComplete, devMaterial, devCommand
|
||||||
- variables: devWorld, devLog, devFree, devSpeed, devPaused, devSteps, DEV_HELP, DEV_MATERIALS
|
- variables: devWorld, devLog, devFree, devSpeed, devPaused, devSteps, DEV_COMMANDS, DEV_MATERIALS
|
||||||
|
|
||||||
### src/sim/save.js (185 lines)
|
### src/sim/save.js (185 lines)
|
||||||
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
|
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
|
||||||
@@ -572,9 +572,10 @@ Hover work is shared by the cursor label and the vein outline.
|
|||||||
Discovery cards, hover info (I) and the cursor label.
|
Discovery cards, hover info (I) and the cursor label.
|
||||||
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
|
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
|
||||||
|
|
||||||
### src/ui/devconsole.js (42 lines)
|
### src/ui/devconsole.js (71 lines)
|
||||||
Bottom panel; command dispatch and state live in sim/devconsole.js.
|
Bottom panel; command dispatch and state live in sim/devconsole.js.
|
||||||
- functions: closeDevConsole, showDevConsole, toggleDevConsole, devPrint, runDevCommand
|
- functions: closeDevConsole, showDevConsole, toggleDevConsole, devPrint, runDevCommand
|
||||||
|
- variables: devInputHistory, devCompletion
|
||||||
|
|
||||||
### src/ui/input.js (92 lines)
|
### src/ui/input.js (92 lines)
|
||||||
Keyboard and mouse listeners.
|
Keyboard and mouse listeners.
|
||||||
|
|||||||
+20
-1
@@ -6,6 +6,26 @@ open or not verified. Keep each entry short; the git history has the details.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-10-28: t94 dev console discovery and keyboard ease of use (Codex)
|
||||||
|
|
||||||
|
- Added case/space-insensitive material lookup, three close giveable suggestions and
|
||||||
|
materials filtering with internal names. Command definitions hold help usage/examples
|
||||||
|
and keyword candidates. Pure helpers handle cycling completion and capped 50-command
|
||||||
|
history; UI owns storage (all accesses guarded), keys, output clearing and red errors.
|
||||||
|
Tab, arrows and Ctrl+L stop before game controls. Simulation tick behaviour unchanged.
|
||||||
|
- Tests added first: name/filter/help checks failed before implementation. Corrected test
|
||||||
|
assumptions for the separate giveable Copper ore vein and for the latest submitted
|
||||||
|
history command. Full python3 test/run_all.py --browser: ALL PASSED (34 headless,
|
||||||
|
9 browser suites); console checks: 77 Node, 27 browser. Log: /tmp/t94-all-browser.log.
|
||||||
|
Browser checks cycling Tab, Up/Down, Ctrl+L, exact bot/control isolation, colours,
|
||||||
|
clear and history across page reload. Saved and inspected shots/t94-console.png.
|
||||||
|
- Files: src/sim/devconsole.js, src/ui/devconsole.js, src/styles/devconsole.css,
|
||||||
|
test/t_dev_console.js, test/t_dev_console.py, docs/DESIGN.md, docs/PROJECT_MAP.md,
|
||||||
|
this log and only the t94 task (Review). No commit; wrapper owns committing.
|
||||||
|
- Environment: docs/kandown.md is absent; read kandown work rules. Sandboxed browser
|
||||||
|
timed out starting a world; the complete passing run used host GPU access.
|
||||||
|
Owner playtesting remains open.
|
||||||
|
|
||||||
|
|
||||||
## 2026-10-28: t93 round 3 review and tidy (Claude)
|
## 2026-10-28: t93 round 3 review and tidy (Claude)
|
||||||
|
|
||||||
@@ -62,7 +82,6 @@ open or not verified. Keep each entry short; the git history has the details.
|
|||||||
/tmp/t93c-red.log, /tmp/t93c-browser-red.log, /tmp/t93c-browser-final.log,
|
/tmp/t93c-red.log, /tmp/t93c-browser-red.log, /tmp/t93c-browser-final.log,
|
||||||
/tmp/t93c-final-headless2.log.
|
/tmp/t93c-final-headless2.log.
|
||||||
|
|
||||||
|
|
||||||
## 2026-10-28: t93 reopened, vacuum-cut faces drain in columns (Codex)
|
## 2026-10-28: t93 reopened, vacuum-cut faces drain in columns (Codex)
|
||||||
|
|
||||||
- Read AGENTS, the t93 task/log and 807217b; inspected both new owner screenshots.
|
- Read AGENTS, the t93 task/log and 807217b; inspected both new owner screenshots.
|
||||||
|
|||||||
+100
-29
@@ -4,41 +4,112 @@ function resetDevConsole() {
|
|||||||
devWorld = false; devLog = []; devFree = false; devSpeed = 1; devPaused = false; devSteps = 0;
|
devWorld = false; devLog = []; devFree = false; devSpeed = 1; devPaused = false; devSteps = 0;
|
||||||
}
|
}
|
||||||
function enableDevConsole() { devWorld = true; }
|
function enableDevConsole() { devWorld = true; }
|
||||||
const DEV_HELP = [
|
// Each command defines its usage, example and completion keywords together.
|
||||||
'help — list commands',
|
const DEV_COMMANDS = {
|
||||||
'money <n> | money set <n> — add or set money; negative amounts allowed',
|
help: ['help [command] — list commands or explain one', 'help give'],
|
||||||
'debt <n> | debt clear — set or clear claim debt',
|
materials: ['materials [text] — list giveable names and internal names', 'materials copper'],
|
||||||
'give <material> [n] — add grains or items (default 1000); display or internal name, unique prefix',
|
clear: ['clear — empty console output', 'clear'],
|
||||||
'bit [tier] — list or set drill tiers (numbered from 0)',
|
money: ['money <n> | money set <n> — add or set money; negative amounts allowed', 'money set 500', ['set']],
|
||||||
'bag [tier] — list or set bag tiers (numbered from 0)',
|
debt: ['debt <n> | debt clear — set or clear claim debt', 'debt clear', ['clear']],
|
||||||
'unlock — learn all materials and unlock progress rewards',
|
give: ['give <material> [n] — add grains or items (default 1000); display or internal name, unique prefix', 'give copper ore 500'],
|
||||||
'tp [x y] — teleport in grains; no coordinates uses the cursor',
|
bit: ['bit [tier] — list or set drill tiers (numbered from 0)', 'bit 1'],
|
||||||
'free on|off — build without spending money or materials',
|
bag: ['bag [tier] — list or set bag tiers (numbered from 0)', 'bag 1'],
|
||||||
'speed <x> — speed multiplier (0.25, 0.5, 1, 2, 4; clamped to 0.1–8)',
|
unlock: ['unlock — learn all materials and unlock progress rewards', 'unlock'],
|
||||||
'pause — toggle dev pause',
|
tp: ['tp [x y] — teleport in grains; no coordinates uses the cursor', 'tp 100 50'],
|
||||||
'step [n] — queue 1–10000 ticks while dev paused (default 1)',
|
free: ['free on|off — build without spending money or materials', 'free on', ['on', 'off']],
|
||||||
'weather clear|rain — stop current rain or start a one-minute shower',
|
speed: ['speed <x> — speed multiplier (0.25, 0.5, 1, 2, 4; clamped to 0.1–8)', 'speed 2'],
|
||||||
];
|
pause: ['pause — toggle dev pause', 'pause'],
|
||||||
|
step: ['step [n] — queue 1–10000 ticks while dev paused (default 1)', 'step 10'],
|
||||||
|
weather: ['weather clear|rain — stop current rain or start a one-minute shower', 'weather rain', ['clear', 'rain']],
|
||||||
|
};
|
||||||
|
function devError(text) { return Object.assign([text], { error: true }); }
|
||||||
|
function devName(text) { return text.toLowerCase().replace(/\s+/g, ''); }
|
||||||
|
function devGiveable(id) { return id !== EMPTY && PH[id] !== PH_GAS && (PH[id] !== PH_SOLID || !!YLD[id]); }
|
||||||
|
function devMaterialList(text = '') {
|
||||||
|
return Object.entries(DEV_MATERIALS).filter(([key, id]) => devGiveable(id) && (devName(key).includes(devName(text)) || devName(MD[id].name).includes(devName(text))));
|
||||||
|
}
|
||||||
|
// Edit distance is only used on a console error, never in the simulation tick.
|
||||||
|
function devDistance(a, b) {
|
||||||
|
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
||||||
|
for (let i = 1; i <= a.length; i++) {
|
||||||
|
let diagonal = row[0]; row[0] = i;
|
||||||
|
for (let j = 1; j <= b.length; j++) {
|
||||||
|
const old = row[j];
|
||||||
|
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] !== b[j - 1]));
|
||||||
|
diagonal = old;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return row[b.length];
|
||||||
|
}
|
||||||
|
function devSuggestions(name) {
|
||||||
|
return devMaterialList().map(([key, id]) => ({ id, distance: Math.min(devDistance(name, devName(key)), devDistance(name, devName(MD[id].name))) }))
|
||||||
|
.sort((a, b) => a.distance - b.distance).slice(0, 3).map(({ id }) => MD[id].name).join(', ');
|
||||||
|
}
|
||||||
|
function devHistory(items) {
|
||||||
|
items = Array.isArray(items) ? items.filter(s => typeof s === 'string' && s.trim()).slice(-50) : [];
|
||||||
|
return { items, index: items.length };
|
||||||
|
}
|
||||||
|
function devRemember(history, text) {
|
||||||
|
if (text.trim()) { history.items.push(text.trim()); if (history.items.length > 50) history.items.shift(); }
|
||||||
|
history.index = history.items.length;
|
||||||
|
}
|
||||||
|
function devRecall(history, direction) {
|
||||||
|
history.index = Math.max(0, Math.min(history.items.length, history.index + direction));
|
||||||
|
return history.items[history.index] || '';
|
||||||
|
}
|
||||||
|
function devComplete(text, previous) {
|
||||||
|
if (previous && previous.text === text && previous.matches.length) {
|
||||||
|
const index = (previous.index + 1) % previous.matches.length;
|
||||||
|
return { matches: previous.matches, index, text: previous.matches[index] };
|
||||||
|
}
|
||||||
|
const input = text.trimStart(), split = input.indexOf(' ');
|
||||||
|
let matches;
|
||||||
|
if (split < 0) matches = Object.keys(DEV_COMMANDS).filter(k => k.startsWith(input.toLowerCase()));
|
||||||
|
else {
|
||||||
|
const cmd = input.slice(0, split).toLowerCase(), arg = input.slice(split + 1);
|
||||||
|
if (cmd === 'give') {
|
||||||
|
const amount = arg.match(/\s+[-+]?\d+(?:\.\d+)?$/), suffix = amount ? amount[0] : '';
|
||||||
|
const name = devName(suffix ? arg.slice(0, -suffix.length) : arg);
|
||||||
|
matches = [...new Set(devMaterialList().filter(([key, id]) => devName(key).startsWith(name) || devName(MD[id].name).startsWith(name))
|
||||||
|
.map(([, id]) => 'give ' + MD[id].name + suffix))];
|
||||||
|
} else {
|
||||||
|
const words = cmd === 'help' ? Object.keys(DEV_COMMANDS) : Object.hasOwn(DEV_COMMANDS, cmd) ? DEV_COMMANDS[cmd][2] || [] : [];
|
||||||
|
matches = words.filter(k => k.startsWith(arg.toLowerCase())).map(k => cmd + ' ' + k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { matches, index: 0, text: matches[0] || text };
|
||||||
|
}
|
||||||
// Exact internal names win when a display name names a different material.
|
// Exact internal names win when a display name names a different material.
|
||||||
const DEV_MATERIALS = { EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, WOOD, LEAVES, COALV, COAL, CUV, CUORE, FEV, FEORE, ASH, SLAG, BRICK, GLASS, COPPER, IRON, STEEL, WATER, MAGMA, MCU, MFE, MST, MGL, STEAM, SMOKE, FIRE, BELTL, BELTR, SHAFT, GATE, HULL, SPRING, RSTONE, RGRAN, RBAS, LCLAY, LWOOD, RBRICK, CULLET, SCRCU, SCRFE, SCRST, BARCU, BARFE, BARST, ACORN, CHARCOAL, SNOW, ICE, SNV, SNORE, TIN, MSN, SCRSN, BARSN, ZNV, ZNORE, ZINC, MZN, SCRZN, BARZN, AGV, AGORE, SILVER, MAG, SCRAG, BARAG, AUV, AUORE, GOLD, MAU, SCRAU, BARAU, BRONZE, MBZ, SCRBZ, BARBZ, BRASS, MBS, SCRBS, BARBS, DOCKW, MSLAG, PIPE, BARGL, CUTST, BARSB, GRINDER, SAWB, PLANK, PRESSB, GREENB, BARBR, RACKB };
|
const DEV_MATERIALS = { EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, WOOD, LEAVES, COALV, COAL, CUV, CUORE, FEV, FEORE, ASH, SLAG, BRICK, GLASS, COPPER, IRON, STEEL, WATER, MAGMA, MCU, MFE, MST, MGL, STEAM, SMOKE, FIRE, BELTL, BELTR, SHAFT, GATE, HULL, SPRING, RSTONE, RGRAN, RBAS, LCLAY, LWOOD, RBRICK, CULLET, SCRCU, SCRFE, SCRST, BARCU, BARFE, BARST, ACORN, CHARCOAL, SNOW, ICE, SNV, SNORE, TIN, MSN, SCRSN, BARSN, ZNV, ZNORE, ZINC, MZN, SCRZN, BARZN, AGV, AGORE, SILVER, MAG, SCRAG, BARAG, AUV, AUORE, GOLD, MAU, SCRAU, BARAU, BRONZE, MBZ, SCRBZ, BARBZ, BRASS, MBS, SCRBS, BARBS, DOCKW, MSLAG, PIPE, BARGL, CUTST, BARSB, GRINDER, SAWB, PLANK, PRESSB, GREENB, BARBR, RACKB };
|
||||||
function devMaterial(name) {
|
function devMaterial(name) {
|
||||||
name = name.toLowerCase();
|
name = devName(name);
|
||||||
const ids = Object.entries(DEV_MATERIALS);
|
const ids = Object.entries(DEV_MATERIALS);
|
||||||
const internal = ids.find(([key]) => key.toLowerCase() === name);
|
const internal = ids.find(([key]) => devName(key) === name);
|
||||||
if (internal) return internal[1];
|
if (internal) return internal[1];
|
||||||
const exact = ids.filter(([, id]) => MD[id].name.toLowerCase() === name);
|
const exact = ids.filter(([, id]) => devName(MD[id].name) === name);
|
||||||
const matches = [...new Set((exact.length ? exact : ids.filter(([key, id]) => key.toLowerCase().startsWith(name) || MD[id].name.toLowerCase().startsWith(name))).map(([, id]) => id))];
|
const matches = [...new Set((exact.length ? exact : ids.filter(([key, id]) => devName(key).startsWith(name) || devName(MD[id].name).startsWith(name))).map(([, id]) => id))];
|
||||||
return matches.length === 1 ? matches[0] : matches.length ? `Ambiguous material: ${matches.map(id => MD[id].name + ' (' + ids.find(([, m]) => m === id)[0] + ')').join(', ')}.` : 'Unknown material. Use a display name or internal name, such as copper ore or CUORE.';
|
return matches.length === 1 ? matches[0] : matches.length ? `Ambiguous material: ${matches.map(id => MD[id].name + ' (' + ids.find(([, m]) => m === id)[0] + ')').join(', ')}.` : 'Unknown material. Did you mean: ' + devSuggestions(name) + '? Use materials to list names.';
|
||||||
}
|
}
|
||||||
function devCommand(text, cursor) {
|
function devCommand(text, cursor) {
|
||||||
if (typeof text !== 'string' || !text.trim()) return ['Type help to list commands.'];
|
if (typeof text !== 'string' || !text.trim()) return ['Type help to list commands.'];
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
const [cmd, ...args] = text.toLowerCase().split(/\s+/);
|
const [cmd, ...args] = text.toLowerCase().split(/\s+/);
|
||||||
const usage = () => ['Usage: ' + DEV_HELP.find(s => s.startsWith(cmd + ' '))];
|
const usage = () => devError('Usage: ' + DEV_COMMANDS[cmd][0]);
|
||||||
const number = s => s !== undefined && Number.isFinite(Number(s));
|
const number = s => s !== undefined && Number.isFinite(Number(s));
|
||||||
if (cmd === 'help' && !args.length) return DEV_HELP.slice();
|
if (!Object.hasOwn(DEV_COMMANDS, cmd)) return devError('Unknown command. Type help to list commands.');
|
||||||
if (!DEV_HELP.some(s => s.startsWith(cmd + ' '))) return ['Unknown command. Type help to list commands.'];
|
if (cmd === 'help') {
|
||||||
if (!devWorld) return ['Open the dev console and accept the world warning first.'];
|
if (!args.length) return Object.values(DEV_COMMANDS).map(def => def[0]);
|
||||||
|
if (args.length !== 1) return usage();
|
||||||
|
if (!Object.hasOwn(DEV_COMMANDS, args[0])) return devError('Unknown command. Type help to list commands.');
|
||||||
|
const def = DEV_COMMANDS[args[0]];
|
||||||
|
return ['Usage: ' + def[0], 'Example: ' + def[1]];
|
||||||
|
}
|
||||||
|
if (cmd === 'materials') {
|
||||||
|
const lines = devMaterialList(args.join(' ')).map(([key, id]) => MD[id].name + ' (' + key + ')');
|
||||||
|
return lines.length ? lines : ['No matching materials.'];
|
||||||
|
}
|
||||||
|
if (cmd === 'clear') return args.length ? usage() : Object.assign([], { clear: true });
|
||||||
|
if (!devWorld) return devError('Open the dev console and accept the world warning first.');
|
||||||
let result;
|
let result;
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case 'money': {
|
case 'money': {
|
||||||
@@ -56,9 +127,9 @@ function devCommand(text, cursor) {
|
|||||||
if (number(args[args.length - 1])) n = Number(args.pop());
|
if (number(args[args.length - 1])) n = Number(args.pop());
|
||||||
if (!args.length || !Number.isInteger(n) || n <= 0) return usage();
|
if (!args.length || !Number.isInteger(n) || n <= 0) return usage();
|
||||||
const m = devMaterial(args.join(' '));
|
const m = devMaterial(args.join(' '));
|
||||||
if (typeof m === 'string') return [m];
|
if (typeof m === 'string') return devError(m);
|
||||||
if (m === EMPTY || PH[m] === PH_GAS || (PH[m] === PH_SOLID && !YLD[m])) return [`${MD[m].name} cannot go in the bag.`];
|
if (!devGiveable(m)) return devError(`${MD[m].name} cannot go in the bag.`);
|
||||||
if (n > 2147483647 - bag[m]) return ['Too many: a material count cannot exceed 2147483647.'];
|
if (n > 2147483647 - bag[m]) return devError('Too many: a material count cannot exceed 2147483647.');
|
||||||
const into = PH[m] === PH_SOLID && YLD[m] ? YLD[m] : m;
|
const into = PH[m] === PH_SOLID && YLD[m] ? YLD[m] : m;
|
||||||
bagAdd(m, n, true); result = `Added ${n} ${MD[into].name}${into !== m ? ` (as ${MD[m].name.toLowerCase()} breaks up)` : ''}.`; break;
|
bagAdd(m, n, true); result = `Added ${n} ${MD[into].name}${into !== m ? ` (as ${MD[m].name.toLowerCase()} breaks up)` : ''}.`; break;
|
||||||
}
|
}
|
||||||
@@ -78,7 +149,7 @@ function devCommand(text, cursor) {
|
|||||||
case 'tp': {
|
case 'tp': {
|
||||||
if (args.length !== 0 && (args.length !== 2 || !args.every(number))) return usage();
|
if (args.length !== 0 && (args.length !== 2 || !args.every(number))) return usage();
|
||||||
const x = args.length ? Number(args[0]) : cursor && cursor.x, y = args.length ? Number(args[1]) : cursor && cursor.y;
|
const x = args.length ? Number(args[0]) : cursor && cursor.x, y = args.length ? Number(args[1]) : cursor && cursor.y;
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return ['No cursor position. Use tp <x> <y>.'];
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return devError('No cursor position. Use tp <x> <y>.');
|
||||||
bot.x = Math.max(0, Math.min(W - bot.w, x)); bot.y = Math.max(0, Math.min(H - bot.h, y)); bot.vx = bot.vy = 0;
|
bot.x = Math.max(0, Math.min(W - bot.w, x)); bot.y = Math.max(0, Math.min(H - bot.h, y)); bot.vx = bot.vy = 0;
|
||||||
fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
||||||
result = `Bot: ${bot.x}, ${bot.y} grains`; break;
|
result = `Bot: ${bot.x}, ${bot.y} grains`; break;
|
||||||
@@ -95,7 +166,7 @@ function devCommand(text, cursor) {
|
|||||||
case 'step': {
|
case 'step': {
|
||||||
const n = args.length ? Number(args[0]) : 1;
|
const n = args.length ? Number(args[0]) : 1;
|
||||||
if (args.length > 1 || !Number.isInteger(n) || n < 1 || n > 10000 || devSteps + n > 10000) return usage();
|
if (args.length > 1 || !Number.isInteger(n) || n < 1 || n > 10000 || devSteps + n > 10000) return usage();
|
||||||
if (!devPaused) return ['Use pause before step.'];
|
if (!devPaused) return devError('Use pause before step.');
|
||||||
devSteps += n; result = `Queued ${n} ticks.`; break;
|
devSteps += n; result = `Queued ${n} ticks.`; break;
|
||||||
}
|
}
|
||||||
case 'weather':
|
case 'weather':
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
font: 12px/1.6 var(--font-mono); white-space: pre-wrap; overflow-wrap: anywhere;
|
font: 12px/1.6 var(--font-mono); white-space: pre-wrap; overflow-wrap: anywhere;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
#dev-output .dev-error { color: var(--danger); }
|
||||||
#dev-form { display: flex; align-items: center; gap: 10px; }
|
#dev-form { display: flex; align-items: center; gap: 10px; }
|
||||||
#dev-input { flex: 1; min-width: 0; font-family: var(--font-mono); }
|
#dev-input { flex: 1; min-width: 0; font-family: var(--font-mono); }
|
||||||
.dev-badge { display: inline-block; padding: 1px 6px; border: 1px solid var(--patina-dim); border-radius: 4px; color: var(--patina); font-size: 11px; }
|
.dev-badge { display: inline-block; padding: 1px 6px; border: 1px solid var(--patina-dim); border-radius: 4px; color: var(--patina); font-size: 11px; }
|
||||||
|
|||||||
+33
-4
@@ -1,4 +1,7 @@
|
|||||||
// Bottom panel; command dispatch and state live in sim/devconsole.js.
|
// Bottom panel; command dispatch and state live in sim/devconsole.js.
|
||||||
|
let devInputHistory = devHistory([]), devCompletion = null;
|
||||||
|
try { devInputHistory = devHistory(JSON.parse(localStorage.getItem('deepgrain.consoleHistory'))); } catch {}
|
||||||
|
|
||||||
function closeDevConsole() {
|
function closeDevConsole() {
|
||||||
$('dev-console').hidden = true;
|
$('dev-console').hidden = true;
|
||||||
$('dev-input').blur();
|
$('dev-input').blur();
|
||||||
@@ -21,22 +24,48 @@ function toggleDevConsole() {
|
|||||||
}
|
}
|
||||||
function devPrint(lines) {
|
function devPrint(lines) {
|
||||||
const log = $('dev-output');
|
const log = $('dev-output');
|
||||||
for (const line of lines) { const row = document.createElement('div'); row.textContent = line; log.appendChild(row); }
|
for (const line of lines) {
|
||||||
|
const row = document.createElement('div'); row.textContent = line;
|
||||||
|
if (lines.error) row.className = 'dev-error';
|
||||||
|
log.appendChild(row);
|
||||||
|
}
|
||||||
while (log.children.length > 400) log.firstChild.remove();
|
while (log.children.length > 400) log.firstChild.remove();
|
||||||
log.scrollTop = log.scrollHeight;
|
log.scrollTop = log.scrollHeight;
|
||||||
}
|
}
|
||||||
function runDevCommand(text) {
|
function runDevCommand(text) {
|
||||||
devPrint(devCommand(text, GAME.aim));
|
const result = devCommand(text, GAME.aim);
|
||||||
|
if (result.clear) $('dev-output').replaceChildren();
|
||||||
|
else devPrint(result);
|
||||||
hudDirty = true; lastBagKey = ''; lastBuildKey = '';
|
hudDirty = true; lastBagKey = ''; lastBuildKey = '';
|
||||||
renderWallet();
|
renderWallet();
|
||||||
if (!$('m-notebook').hidden) renderNotebook();
|
if (!$('m-notebook').hidden) renderNotebook();
|
||||||
}
|
}
|
||||||
$('dev-close').onclick = closeDevConsole;
|
$('dev-close').onclick = closeDevConsole;
|
||||||
$('dev-input').onfocus = () => { keys.clear(); GAME.lmb = GAME.rmb = false; GAME.drag = null; GAME.replace = false; };
|
$('dev-input').onfocus = () => { keys.clear(); GAME.lmb = GAME.rmb = false; GAME.drag = null; GAME.replace = false; };
|
||||||
|
$('dev-input').oninput = () => { devCompletion = null; };
|
||||||
$('dev-input').onkeydown = e => {
|
$('dev-input').onkeydown = e => {
|
||||||
if (e.key === 'Escape' || e.key === '`') { e.preventDefault(); e.stopPropagation(); closeDevConsole(); }
|
e.stopPropagation();
|
||||||
|
const input = $('dev-input');
|
||||||
|
if (e.key === 'Escape' || e.key === '`') { e.preventDefault(); closeDevConsole(); }
|
||||||
|
else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault(); devCompletion = null;
|
||||||
|
input.value = devRecall(devInputHistory, e.key === 'ArrowUp' ? -1 : 1);
|
||||||
|
input.setSelectionRange(input.value.length, input.value.length);
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
const cycling = devCompletion && devCompletion.text === input.value;
|
||||||
|
devCompletion = devComplete(input.value, devCompletion);
|
||||||
|
input.value = devCompletion.text;
|
||||||
|
if (!cycling && devCompletion.matches.length > 1) devPrint(['Matches: ' + devCompletion.matches.join(', ')]);
|
||||||
|
} else if (e.ctrlKey && e.key.toLowerCase() === 'l') {
|
||||||
|
e.preventDefault(); $('dev-output').replaceChildren();
|
||||||
|
} else if (!['Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) devCompletion = null;
|
||||||
};
|
};
|
||||||
$('dev-form').onsubmit = e => {
|
$('dev-form').onsubmit = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const input = $('dev-input'); runDevCommand(input.value); input.value = ''; input.focus();
|
const input = $('dev-input');
|
||||||
|
devRemember(devInputHistory, input.value);
|
||||||
|
try { localStorage.setItem('deepgrain.consoleHistory', JSON.stringify(devInputHistory.items)); } catch {}
|
||||||
|
devCompletion = null;
|
||||||
|
runDevCommand(input.value); input.value = ''; input.focus();
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-1
@@ -44,7 +44,7 @@ for (const text of ['money', 'money set', 'money Infinity', 'money 1e309', 'mone
|
|||||||
check('bad arguments produce a helpful error: ' + text, output.length > 0 && !output[0].startsWith('>'));
|
check('bad arguments produce a helpful error: ' + text, output.length > 0 && !output[0].startsWith('>'));
|
||||||
}
|
}
|
||||||
check('invalid commands do not mutate or log', beforeBad === run('JSON.stringify([money, debt, bagTotal, player, devLog, devSpeed, devSteps])'));
|
check('invalid commands do not mutate or log', beforeBad === run('JSON.stringify([money, debt, bagTotal, player, devLog, devSpeed, devSteps])'));
|
||||||
check('help lists every command', command('help').length === 13);
|
check('help lists every command', command('help').length === 15);
|
||||||
run('money = 17; bag.fill(0); bagTotal = 0; bagAdd(RSTONE, 32, true); bagAdd(LCLAY, 100, true);');
|
run('money = 17; bag.fill(0); bagTotal = 0; bagAdd(RSTONE, 32, true); bagAdd(LCLAY, 100, true);');
|
||||||
check('normal build costs are checked', run('!canAfford([[CASH, 100]], 1)'));
|
check('normal build costs are checked', run('!canAfford([[CASH, 100]], 1)'));
|
||||||
command('free on');
|
command('free on');
|
||||||
@@ -72,4 +72,21 @@ run('alloc(240,160)');
|
|||||||
check('a new world starts unmarked without a dev log', run('!devWorld && devLog.length === 0'));
|
check('a new world starts unmarked without a dev log', run('!devWorld && devLog.length === 0'));
|
||||||
check('give turns rock into its rubble and says so', run(`(() => { enableDevConsole(); const before = bag[RBAS]; const out = devCommand('give basalt 50').join(' '); return bag[RBAS] === before + 50 && bag[BASALT] === 0 && /Basalt rubble/.test(out); })()`));
|
check('give turns rock into its rubble and says so', run(`(() => { enableDevConsole(); const before = bag[RBAS]; const out = devCommand('give basalt 50').join(' '); return bag[RBAS] === before + 50 && bag[BASALT] === 0 && /Basalt rubble/.test(out); })()`));
|
||||||
check('give refuses things that cannot be in a bag', run(`(() => { enableDevConsole(); const out = devCommand('give steam 5').join(' '); return /cannot go in the bag/.test(out) && bag[STEAM] === 0; })()`));
|
check('give refuses things that cannot be in a bag', run(`(() => { enableDevConsole(); const out = devCommand('give steam 5').join(' '); return /cannot go in the bag/.test(out) && bag[STEAM] === 0; })()`));
|
||||||
|
|
||||||
|
check('material matching ignores spaces and case', run("devMaterial('CoPpErOrE') === CUORE && devMaterial('copper bar') === BARCU"));
|
||||||
|
check('unknown material suggests close giveable names', /Did you mean:.*Copper ore/i.test(command('give coperore')[0]));
|
||||||
|
check('materials lists giveable names with internal names', command('materials').some(s => /Copper ore.*CUORE/.test(s)) && !command('materials').some(s => /STEAM/.test(s)));
|
||||||
|
check('materials filtering ignores spaces and case', command('materials CoPpErOrE').every(s => /Copper ore/i.test(s)) && command('materials copperore').some(s => /CUORE/.test(s)));
|
||||||
|
check('help for a command has usage and example', command('help give').some(s => s.startsWith('Usage: give')) && command('help give').some(s => s.startsWith('Example: give copper ore')));
|
||||||
|
check('all commands have detailed help', run("Object.keys(DEV_COMMANDS).every(k => devCommand('help ' + k).some(s => s.startsWith('Example:')))"));
|
||||||
|
check('errors carry explicit status while results do not', command('give no-such-material').error === true && !command('materials').error && command('money nope').error === true);
|
||||||
|
check('clear requests output clearing without changing world', command('clear').clear === true);
|
||||||
|
check('command completion cycles and wraps', run("(() => { const a = devComplete('s'), b = devComplete(a.text, a), c = devComplete(b.text, b); return a.matches.length === 2 && a.text === 'speed' && b.text === 'step' && c.text === a.text; })()"));
|
||||||
|
check('completion keeps spaced materials and amount', run("devComplete('give copperore 25').matches.includes('give Copper ore 25')"));
|
||||||
|
check('completion supports internal names', run("devComplete('give cuor').text === 'give Copper ore'"));
|
||||||
|
check('completion cycles ambiguous materials', run("(() => { const a = devComplete('give copper'), b = devComplete(a.text, a); return a.matches.length > 1 && a.text !== b.text; })()"));
|
||||||
|
check('completion supports keywords and help commands', run("devComplete('free o').matches.length === 2 && devComplete('money s').text === 'money set' && devComplete('debt c').text === 'debt clear' && devComplete('weather r').text === 'weather rain' && devComplete('help gi').text === 'help give'"));
|
||||||
|
check('completion leaves unmatched input intact', run("devComplete('give zzzzz').text === 'give zzzzz'"));
|
||||||
|
check('history validates stored data', run("devHistory(null).items.length === 0 && devHistory(['ok', 5, '', ' ']).items.join() === 'ok'"));
|
||||||
|
check('history caps at 50 and navigates to empty after newest', run("(() => { const h = devHistory([]); for (let i=0; i<55; i++) devRemember(h, 'money ' + i); devRemember(h, ' '); const newest = devRecall(h, -1); for (let i=0;i<60;i++) devRecall(h,-1); const oldest = devRecall(h,-1); for(let i=0;i<60;i++) devRecall(h,1); return h.items.length === 50 && newest === 'money 54' && oldest === 'money 5' && devRecall(h,1) === ''; })()"));
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -59,6 +59,31 @@ with launch() as (browser, page, url):
|
|||||||
tick = page.evaluate('tick')
|
tick = page.evaluate('tick')
|
||||||
page.wait_for_timeout(150)
|
page.wait_for_timeout(150)
|
||||||
check('dev pause freezes simulation ticks', page.evaluate('tick') == tick)
|
check('dev pause freezes simulation ticks', page.evaluate('tick') == tick)
|
||||||
|
position = page.evaluate("[bot.x, bot.y, bot.lamp, GAME.tool, $('bagpanel').hidden]")
|
||||||
|
page.fill('#dev-input', 'give copper')
|
||||||
|
page.keyboard.press('Tab')
|
||||||
|
first = page.input_value('#dev-input')
|
||||||
|
page.keyboard.press('Tab')
|
||||||
|
check('Tab completes spaced material names and cycles matches', first.startswith('give Copper') and first != page.input_value('#dev-input') and 'Matches:' in page.inner_text('#dev-output'))
|
||||||
|
page.keyboard.press('ArrowUp')
|
||||||
|
check('Up recalls the newest submitted command', page.input_value('#dev-input') == 'pause')
|
||||||
|
page.keyboard.press('ArrowDown')
|
||||||
|
check('Down beyond newest returns an empty input', page.input_value('#dev-input') == '')
|
||||||
|
page.keyboard.press('Control+l')
|
||||||
|
check('Ctrl+L clears output and keeps console focus', page.evaluate("$('dev-output').children.length === 0 && document.activeElement === $('dev-input')"))
|
||||||
|
page.wait_for_timeout(100)
|
||||||
|
check('Tab, arrows and Ctrl+L leave bot and game controls alone', page.evaluate("[bot.x, bot.y, bot.lamp, GAME.tool, $('bagpanel').hidden]") == position and page.evaluate('keys.size === 0 && !GAME.ctrl'))
|
||||||
|
command('materials copperore')
|
||||||
|
command('help give')
|
||||||
|
command('give coperore')
|
||||||
|
check('errors are red while results use normal colour', page.evaluate('''() => {
|
||||||
|
const rows = $('dev-output').children, error = rows[rows.length - 1];
|
||||||
|
return error.classList.contains('dev-error') && getComputedStyle(error).color === 'rgb(239, 106, 85)' && getComputedStyle(error).color !== getComputedStyle(rows[0]).color;
|
||||||
|
}'''))
|
||||||
|
page.screenshot(path=str(SHOTS / 't94-console.png'))
|
||||||
|
command('clear')
|
||||||
|
check('clear command empties output', page.evaluate("$('dev-output').children.length === 0"))
|
||||||
|
check('submitted history is stored across sessions', page.evaluate("JSON.parse(localStorage.getItem('deepgrain.consoleHistory')).slice(-1)[0] === 'clear'"))
|
||||||
command('step 5')
|
command('step 5')
|
||||||
page.wait_for_function(f'tick === {tick + 5} && devSteps === 0')
|
page.wait_for_function(f'tick === {tick + 5} && devSteps === 0')
|
||||||
page.wait_for_timeout(150)
|
page.wait_for_timeout(150)
|
||||||
@@ -93,6 +118,9 @@ with launch() as (browser, page, url):
|
|||||||
await startFromSave(sv);
|
await startFromSave(sv);
|
||||||
return devWorld && devLog.length === count && devSpeed === 1 && !devPaused && !devFree && $('dev-console').hidden;
|
return devWorld && devLog.length === count && devSpeed === 1 && !devPaused && !devFree && $('dev-console').hidden;
|
||||||
}'''))
|
}'''))
|
||||||
|
page.reload()
|
||||||
|
page.wait_for_function("!$('title').hidden")
|
||||||
|
check('history is restored on a fresh page', page.evaluate("devInputHistory.items.slice(-1)[0] === 'step 5' && devRecall(devInputHistory, -1) === 'step 5'"))
|
||||||
check('no browser errors', not errors)
|
check('no browser errors', not errors)
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user