// Dev commands are pure simulation operations; the browser owns the panel and confirmation. let devWorld = false, devLog = [], devFree = false, devSpeed = 1, devPaused = false, devSteps = 0; function resetDevConsole() { devWorld = false; devLog = []; devFree = false; devSpeed = 1; devPaused = false; devSteps = 0; } function enableDevConsole() { devWorld = true; } const DEV_HELP = [ 'help — list commands', 'money | money set — add or set money; negative amounts allowed', 'debt | debt clear — set or clear claim debt', 'give [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 — 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', ]; // 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(); const ids = Object.entries(DEV_MATERIALS); const internal = ids.find(([key]) => key.toLowerCase() === 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.'; } 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 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.']; let result; switch (cmd) { case 'money': { const set = args[0] === 'set', value = Number(args[set ? 1 : 0]); if (args.length !== (set ? 2 : 1) || !number(args[set ? 1 : 0]) || !number(String(set ? value : money + value))) return usage(); money = set ? value : money + value; result = 'Money: ' + fmtMoney(money); break; } case 'debt': if (args.length !== 1 || (args[0] !== 'clear' && (!number(args[0]) || Number(args[0]) < 0))) return usage(); debt = args[0] === 'clear' ? 0 : Number(args[0]); result = debt ? 'Claim debt: ' + fmtMoney(debt) : 'Claim paid off.'; break; case 'give': { if (!args.length) return usage(); let n = 1000; 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.']; 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; } case 'bit': case 'bag': { const tiers = cmd === 'bit' ? BIT_TIERS : TANK_TIERS; if (!args.length) return tiers.map((t, i) => `${i}: ${t.name}${t.cap ? ' (' + t.cap + ' grains)' : ''}`); if (args.length !== 1 || !number(args[0]) || !Number.isInteger(Number(args[0]))) return usage(); const tier = Math.max(0, Math.min(tiers.length - 1, Number(args[0]))); player[cmd === 'bit' ? 'bitTier' : 'tankTier'] = tier; result = `${cmd}: ${tier} — ${tiers[tier].name}`; break; } case 'unlock': if (args.length) return usage(); seen.fill(1); player.lens = player.flask = true; for (const m of MILESTONES) ledger[m.id] = ledger[m.id] || tick || 1; result = 'All materials learned and progress rewards unlocked.'; break; 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 .']; 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; } case 'free': if (args.length !== 1 || !['on', 'off'].includes(args[0])) return usage(); devFree = args[0] === 'on'; result = 'Free building ' + args[0] + '.'; break; case 'speed': if (args.length !== 1 || !number(args[0])) return usage(); devSpeed = Math.max(0.1, Math.min(8, Number(args[0]))); result = 'Speed: ' + devSpeed + '×'; break; case 'pause': if (args.length) return usage(); devPaused = !devPaused; devSteps = 0; result = devPaused ? 'Dev paused. Use step or pause to resume.' : 'Dev pause off.'; break; 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.']; devSteps += n; result = `Queued ${n} ticks.`; break; } case 'weather': if (args.length !== 1 || !['clear', 'rain'].includes(args[0])) return usage(); weather.shower = args[0] === 'rain' ? { x0: Math.max(0, bot.x - 160), x1: Math.min(W, bot.x + 160), rate: 2, ramp: 1, until: tick + 3600 } : null; if (args[0] === 'clear') { weather.raining.fill(0); for (const f of flyers) if (f.rain) absorbSteam(f.x); flyers = flyers.filter(f => !f.rain); } weather.next = tick + rainGap(); result = args[0] === 'rain' ? 'A one-minute shower started near the bot.' : 'Current rain cleared. Stored clouds may rain again.'; break; default: return usage(); } devLog.push([tick, text]); if (devLog.length > 200) devLog.shift(); return ['> ' + text, result]; }