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:
+100
-29
@@ -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':
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
font: 12px/1.6 var(--font-mono); white-space: pre-wrap; overflow-wrap: anywhere;
|
||||
user-select: text;
|
||||
}
|
||||
#dev-output .dev-error { color: var(--danger); }
|
||||
#dev-form { display: flex; align-items: center; gap: 10px; }
|
||||
#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; }
|
||||
|
||||
+33
-4
@@ -1,4 +1,7 @@
|
||||
// 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() {
|
||||
$('dev-console').hidden = true;
|
||||
$('dev-input').blur();
|
||||
@@ -21,22 +24,48 @@ function toggleDevConsole() {
|
||||
}
|
||||
function devPrint(lines) {
|
||||
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();
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
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 = '';
|
||||
renderWallet();
|
||||
if (!$('m-notebook').hidden) renderNotebook();
|
||||
}
|
||||
$('dev-close').onclick = closeDevConsole;
|
||||
$('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 => {
|
||||
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 => {
|
||||
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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user