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:
2026-10-28 23:35:42 -04:00
co-authored by Claude Opus 5.5
parent 780216b174
commit 1db265a018
8 changed files with 210 additions and 39 deletions
+100 -29
View File
@@ -4,41 +4,112 @@ function resetDevConsole() {
devWorld = false; devLog = []; devFree = false; devSpeed = 1; devPaused = false; devSteps = 0;
}
function enableDevConsole() { devWorld = true; }
const DEV_HELP = [
'help — list commands',
'money <n> | money set <n> — add or set money; negative amounts allowed',
'debt <n> | debt clear — set or clear claim debt',
'give <material> [n] — add grains or items (default 1000); display or internal name, unique prefix',
'bit [tier] — list or set drill tiers (numbered from 0)',
'bag [tier] — list or set bag tiers (numbered from 0)',
'unlock — learn all materials and unlock progress rewards',
'tp [x y] — teleport in grains; no coordinates uses the cursor',
'free on|off — build without spending money or materials',
'speed <x> — speed multiplier (0.25, 0.5, 1, 2, 4; clamped to 0.1–8)',
'pause — toggle dev pause',
'step [n] — queue 1–10000 ticks while dev paused (default 1)',
'weather clear|rain — stop current rain or start a one-minute shower',
];
// Each command defines its usage, example and completion keywords together.
const DEV_COMMANDS = {
help: ['help [command] — list commands or explain one', 'help give'],
materials: ['materials [text] — list giveable names and internal names', 'materials copper'],
clear: ['clear — empty console output', 'clear'],
money: ['money <n> | money set <n> — add or set money; negative amounts allowed', 'money set 500', ['set']],
debt: ['debt <n> | debt clear — set or clear claim debt', 'debt clear', ['clear']],
give: ['give <material> [n] — add grains or items (default 1000); display or internal name, unique prefix', 'give copper ore 500'],
bit: ['bit [tier] — list or set drill tiers (numbered from 0)', 'bit 1'],
bag: ['bag [tier] — list or set bag tiers (numbered from 0)', 'bag 1'],
unlock: ['unlock — learn all materials and unlock progress rewards', 'unlock'],
tp: ['tp [x y] — teleport in grains; no coordinates uses the cursor', 'tp 100 50'],
free: ['free on|off — build without spending money or materials', 'free on', ['on', 'off']],
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.
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) {
name = name.toLowerCase();
name = devName(name);
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];
const exact = ids.filter(([, id]) => MD[id].name.toLowerCase() === 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))];
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.';
const exact = ids.filter(([, id]) => devName(MD[id].name) === name);
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. Did you mean: ' + devSuggestions(name) + '? Use materials to list names.';
}
function devCommand(text, cursor) {
if (typeof text !== 'string' || !text.trim()) return ['Type help to list commands.'];
text = text.trim();
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));
if (cmd === 'help' && !args.length) return DEV_HELP.slice();
if (!DEV_HELP.some(s => s.startsWith(cmd + ' '))) return ['Unknown command. Type help to list commands.'];
if (!devWorld) return ['Open the dev console and accept the world warning first.'];
if (!Object.hasOwn(DEV_COMMANDS, cmd)) return devError('Unknown command. Type help to list commands.');
if (cmd === 'help') {
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;
switch (cmd) {
case 'money': {
@@ -56,9 +127,9 @@ function devCommand(text, cursor) {
if (number(args[args.length - 1])) n = Number(args.pop());
if (!args.length || !Number.isInteger(n) || n <= 0) return usage();
const m = devMaterial(args.join(' '));
if (typeof m === 'string') return [m];
if (m === EMPTY || PH[m] === PH_GAS || (PH[m] === PH_SOLID && !YLD[m])) return [`${MD[m].name} cannot go in the bag.`];
if (n > 2147483647 - bag[m]) return ['Too many: a material count cannot exceed 2147483647.'];
if (typeof m === 'string') return devError(m);
if (!devGiveable(m)) return devError(`${MD[m].name} cannot go in the bag.`);
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;
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': {
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;
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;
fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
result = `Bot: ${bot.x}, ${bot.y} grains`; break;
@@ -95,7 +166,7 @@ function devCommand(text, cursor) {
case 'step': {
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 (!devPaused) return ['Use pause before step.'];
if (!devPaused) return devError('Use pause before step.');
devSteps += n; result = `Queued ${n} ticks.`; break;
}
case 'weather':