34528c1cd9
Lists which species have a pillar, grouped by genus, with their ascension rank and the resulting production/storage bonus. --pillars-all also shows the species still missing a pillar. Race table mirrored from the game's src/races.js (63 species). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
724 lines
22 KiB
JavaScript
724 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const LZString = require("lz-string");
|
|
const { execSync } = require("child_process");
|
|
|
|
function decodeSave(compressed) {
|
|
const json = LZString.decompressFromBase64(compressed);
|
|
if (!json) throw new Error("Failed to decompress save data");
|
|
return JSON.parse(json);
|
|
}
|
|
|
|
function encodeSave(data) {
|
|
return LZString.compressToBase64(JSON.stringify(data));
|
|
}
|
|
|
|
function matchesFilter(key, res, filter) {
|
|
if (!filter) return true;
|
|
const name = (res.name || key).toLowerCase();
|
|
return filter.has(key.toLowerCase()) || filter.has(name);
|
|
}
|
|
|
|
function maxResources(data, filter) {
|
|
const changed = [];
|
|
for (const [key, res] of Object.entries(data.resource || {})) {
|
|
if (!matchesFilter(key, res, filter)) continue;
|
|
const max = res.max ?? 0;
|
|
const amount = res.amount ?? 0;
|
|
if (max > 0 && amount < max) {
|
|
const old = amount;
|
|
res.amount = max;
|
|
changed.push(` ${res.name || key}: ${Math.floor(old)} -> ${max}`);
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
function setCraftedResources(data, value, filter) {
|
|
const changed = [];
|
|
for (const [key, res] of Object.entries(data.resource || {})) {
|
|
if (!matchesFilter(key, res, filter)) continue;
|
|
if (res.max === -1 && (res.amount ?? 0) < value) {
|
|
const old = res.amount ?? 0;
|
|
res.amount = value;
|
|
changed.push(` ${res.name || key}: ${Math.floor(old)} -> ${value}`);
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
function listResources(data) {
|
|
const resources = Object.entries(data.resource || {});
|
|
if (resources.length === 0) {
|
|
console.log("No resources found in save.");
|
|
return;
|
|
}
|
|
|
|
const capped = [];
|
|
const crafted = [];
|
|
const special = [];
|
|
const locked = [];
|
|
|
|
for (const [key, res] of resources) {
|
|
const name = res.name || key;
|
|
const amount = res.amount ?? 0;
|
|
const max = res.max ?? 0;
|
|
|
|
if (max > 0) {
|
|
capped.push(` ${name} (${key}): ${Math.floor(amount)} / ${max}`);
|
|
} else if (max === -1) {
|
|
crafted.push(` ${name} (${key}): ${Math.floor(amount)}`);
|
|
} else if (max === -2) {
|
|
special.push(` ${name} (${key}): ${Math.floor(amount)}`);
|
|
} else {
|
|
locked.push(` ${name} (${key})`);
|
|
}
|
|
}
|
|
|
|
if (capped.length > 0) {
|
|
console.log(`Capped resources (${capped.length}):`);
|
|
capped.forEach((l) => console.log(l));
|
|
}
|
|
if (crafted.length > 0) {
|
|
console.log(`Crafted/unlimited resources (${crafted.length}):`);
|
|
crafted.forEach((l) => console.log(l));
|
|
}
|
|
if (special.length > 0) {
|
|
console.log(`Special resources (${special.length}):`);
|
|
special.forEach((l) => console.log(l));
|
|
}
|
|
if (locked.length > 0) {
|
|
console.log(`Locked/unavailable resources (${locked.length}):`);
|
|
locked.forEach((l) => console.log(l));
|
|
}
|
|
}
|
|
|
|
function maxSoldiers(data) {
|
|
const garrison = data.civic?.garrison;
|
|
if (!garrison || !garrison.max) {
|
|
return "No garrison found in save.";
|
|
}
|
|
|
|
const lines = [];
|
|
const oldWorkers = garrison.workers ?? 0;
|
|
const max = garrison.max;
|
|
const oldWounded = garrison.wounded ?? 0;
|
|
|
|
if (oldWorkers < max) {
|
|
garrison.workers = max;
|
|
lines.push(` Soldiers: ${oldWorkers} -> ${max} (max)`);
|
|
} else {
|
|
lines.push(` Soldiers: already at max (${max})`);
|
|
}
|
|
|
|
if (oldWounded > 0) {
|
|
garrison.wounded = 0;
|
|
lines.push(` Wounded: ${oldWounded} -> 0 (healed)`);
|
|
}
|
|
|
|
if (lines.length === 0) return "Garrison already at full strength.";
|
|
return `Garrison:\n${lines.join("\n")}`;
|
|
}
|
|
|
|
function setGeology(data) {
|
|
// Max bonus depends on White Hole achievement: top = 30 + level * 5
|
|
const whLevel = data.stats?.achieve?.whitehole?.l ?? 0;
|
|
const top = 30 + whLevel * 5;
|
|
const maxBonus = (top - 10) / 100;
|
|
|
|
const geology = {
|
|
Copper: maxBonus,
|
|
Iron: maxBonus,
|
|
Aluminium: maxBonus,
|
|
Titanium: maxBonus,
|
|
};
|
|
|
|
data.city.geology = geology;
|
|
|
|
const entries = Object.entries(geology)
|
|
.map(([k, v]) => ` ${k}: +${(v * 100).toFixed(0)}%`)
|
|
.join("\n");
|
|
return `Set 4 geology bonuses (max +${(maxBonus * 100).toFixed(0)}% from White Hole level ${whLevel}):\n${entries}`;
|
|
}
|
|
|
|
// All pillar-able species (every race except protoplasm), in game order.
|
|
// Pillar count target for the Circle of Pillars is RACES.size (63).
|
|
const RACES = {
|
|
human: { name: "Human", genus: "humanoid" },
|
|
elven: { name: "Elf", genus: "humanoid" },
|
|
orc: { name: "Orc", genus: "humanoid" },
|
|
cath: { name: "Cath", genus: "carnivore" },
|
|
wolven: { name: "Wolven", genus: "carnivore" },
|
|
vulpine: { name: "Vulpine", genus: "carnivore" },
|
|
centaur: { name: "Centaur", genus: "herbivore" },
|
|
rhinotaur: { name: "Rhinotaur", genus: "herbivore" },
|
|
capybara: { name: "Capybara", genus: "herbivore" },
|
|
porkenari: { name: "Porkenari", genus: "omnivore" },
|
|
hedgeoken: { name: "Hedgeoken", genus: "omnivore" },
|
|
kobold: { name: "Kobold", genus: "small" },
|
|
goblin: { name: "Goblin", genus: "small" },
|
|
gnome: { name: "Gnome", genus: "small" },
|
|
ogre: { name: "Ogre", genus: "giant" },
|
|
cyclops: { name: "Cyclops", genus: "giant" },
|
|
troll: { name: "Troll", genus: "giant" },
|
|
tortoisan: { name: "Tortoisan", genus: "reptilian" },
|
|
gecko: { name: "Gecko", genus: "reptilian" },
|
|
slitheryn: { name: "Slitheryn", genus: "reptilian" },
|
|
arraak: { name: "Arraak", genus: "avian" },
|
|
pterodacti: { name: "Pterodacti", genus: "avian" },
|
|
dracnid: { name: "Dracnid", genus: "avian" },
|
|
entish: { name: "Ent", genus: "plant" },
|
|
cacti: { name: "Cacti", genus: "plant" },
|
|
pinguicula: { name: "Pinguicula", genus: "plant" },
|
|
sporgar: { name: "Sporgar", genus: "fungi" },
|
|
shroomi: { name: "Shroomi", genus: "fungi" },
|
|
moldling: { name: "Moldling", genus: "fungi" },
|
|
mantis: { name: "Mantis", genus: "insectoid" },
|
|
scorpid: { name: "Scorpid", genus: "insectoid" },
|
|
antid: { name: "Antid", genus: "insectoid" },
|
|
sharkin: { name: "Sharkin", genus: "aquatic" },
|
|
octigoran: { name: "Octigoran", genus: "aquatic" },
|
|
dryad: { name: "Dryad", genus: "fey" },
|
|
satyr: { name: "Satyr", genus: "fey" },
|
|
phoenix: { name: "Phoenix", genus: "heat" },
|
|
salamander: { name: "Salamander", genus: "heat" },
|
|
yeti: { name: "Yeti", genus: "polar" },
|
|
wendigo: { name: "Wendigo", genus: "polar" },
|
|
tuskin: { name: "Tuskin", genus: "sand" },
|
|
kamel: { name: "Kamel", genus: "sand" },
|
|
balorg: { name: "Balorg", genus: "demonic" },
|
|
imp: { name: "Imp", genus: "demonic" },
|
|
hellspawn: { name: "Hellspawn", genus: "demonic" },
|
|
seraph: { name: "Seraph", genus: "angelic" },
|
|
unicorn: { name: "Unicorn", genus: "angelic" },
|
|
synth: { name: "Synth", genus: "synthetic" },
|
|
nano: { name: "Nano", genus: "synthetic" },
|
|
ghast: { name: "Ghast", genus: "eldritch" },
|
|
shoggoth: { name: "Shoggoth", genus: "eldritch" },
|
|
dwarf: { name: "Dwarf", genus: "hybrid" },
|
|
raccoon: { name: "Racconar", genus: "hybrid" },
|
|
lichen: { name: "Lichen", genus: "hybrid" },
|
|
wyvern: { name: "Wyvern", genus: "hybrid" },
|
|
beholder: { name: "Eye-Spector", genus: "hybrid" },
|
|
djinn: { name: "Djinn", genus: "hybrid" },
|
|
narwhal: { name: "Narwhalus", genus: "hybrid" },
|
|
bombardier: { name: "Bombardier", genus: "hybrid" },
|
|
nephilim: { name: "Nephilim", genus: "hybrid" },
|
|
junker: { name: "Valdi", genus: "special" },
|
|
sludge: { name: "Sludge", genus: "special" },
|
|
ultra_sludge: { name: "Ultra Sludge", genus: "special" },
|
|
};
|
|
|
|
const GENUS_NAMES = {
|
|
humanoid: "Humanoid",
|
|
carnivore: "Carnivore",
|
|
herbivore: "Herbivore",
|
|
omnivore: "Omnivore",
|
|
small: "Small",
|
|
giant: "Giant",
|
|
reptilian: "Reptilian",
|
|
avian: "Avian",
|
|
plant: "Plant",
|
|
fungi: "Fungi",
|
|
insectoid: "Insectoid",
|
|
aquatic: "Aquatic",
|
|
fey: "Fey",
|
|
heat: "Heat",
|
|
polar: "Polar",
|
|
sand: "Sand",
|
|
demonic: "Demonic",
|
|
angelic: "Angelic",
|
|
synthetic: "Synthetic",
|
|
eldritch: "Eldritch",
|
|
hybrid: "Hybrid",
|
|
special: "Special",
|
|
};
|
|
|
|
function listPillars(data, showAll) {
|
|
const pillars = data.pillars || {};
|
|
const current = data.race?.species;
|
|
const total = Object.keys(RACES).length;
|
|
const owned = Object.keys(pillars).filter((k) => pillars[k]);
|
|
|
|
if (owned.length === 0 && !showAll) {
|
|
console.log("No pillars in this save (Circle of Pillars not started).");
|
|
return;
|
|
}
|
|
|
|
// Group races by genus, keeping game order within each genus
|
|
const byGenus = {};
|
|
for (const [key, race] of Object.entries(RACES)) {
|
|
(byGenus[race.genus] ||= []).push(key);
|
|
}
|
|
|
|
console.log(`Circle of Pillars: ${owned.length} / ${total} species pillared\n`);
|
|
|
|
for (const [genus, keys] of Object.entries(byGenus)) {
|
|
const done = keys.filter((k) => pillars[k]);
|
|
if (done.length === 0 && !showAll) continue;
|
|
|
|
const label = GENUS_NAMES[genus] || genus;
|
|
console.log(`${label} (${done.length}/${keys.length}):`);
|
|
for (const key of keys) {
|
|
const rank = pillars[key];
|
|
const name = RACES[key].name;
|
|
const marker = key === current ? " <- current species" : "";
|
|
if (rank) {
|
|
console.log(` [x] ${name} (${key}) - rank ${rank}${marker}`);
|
|
} else if (showAll) {
|
|
console.log(` [ ] ${name} (${key})${marker}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unknown keys (custom races or races added after this list was built)
|
|
const unknown = owned.filter((k) => !RACES[k]);
|
|
if (unknown.length > 0) {
|
|
console.log(`Unrecognized (custom?) species:`);
|
|
unknown.forEach((k) => console.log(` [x] ${k} - rank ${pillars[k]}`));
|
|
}
|
|
|
|
// Pillar bonus: +1% production / +2% storage per pillar, current species counts 4x
|
|
const active = owned.reduce((sum, k) => sum + (k === current ? 4 : 1), 0);
|
|
console.log(`\nPillar bonus: +${active}% production, +${active * 2}% storage`);
|
|
if (!showAll) {
|
|
console.log(`${total - owned.length} species still missing (use --pillars-all to list them).`);
|
|
}
|
|
}
|
|
|
|
function readFromClipboard() {
|
|
try {
|
|
return execSync("pbpaste", { encoding: "utf-8" }).trim();
|
|
} catch {
|
|
try {
|
|
return execSync("xclip -selection clipboard -o", { encoding: "utf-8" }).trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
function copyToClipboard(text) {
|
|
try {
|
|
execSync("pbcopy", { input: text });
|
|
return true;
|
|
} catch {
|
|
try {
|
|
execSync("xclip -selection clipboard", { input: text });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function setPrestige(data, key, value) {
|
|
for (const [k, entry] of Object.entries(data.prestige || {})) {
|
|
if (k.toLowerCase() === key.toLowerCase()) {
|
|
const old = entry.count ?? 0;
|
|
entry.count = value;
|
|
return ` ${k}: ${old} -> ${value}`;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function setResource(data, key, value) {
|
|
// Check prestige currencies first
|
|
const prestigeResult = setPrestige(data, key, value);
|
|
if (prestigeResult) return prestigeResult;
|
|
|
|
const res = (data.resource || {})[key];
|
|
if (!res) {
|
|
for (const [k, r] of Object.entries(data.resource || {})) {
|
|
if (k.toLowerCase() === key.toLowerCase() || (r.name || "").toLowerCase() === key.toLowerCase()) {
|
|
const old = r.amount ?? 0;
|
|
r.amount = value;
|
|
return ` ${r.name || k}: ${Math.floor(old)} -> ${value}`;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
const old = res.amount ?? 0;
|
|
res.amount = value;
|
|
return ` ${res.name || key}: ${Math.floor(old)} -> ${value}`;
|
|
}
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
const flags = new Set();
|
|
const positional = [];
|
|
let craftedValue = null;
|
|
let onlyFilter = null;
|
|
const setValues = []; // [{key, value}]
|
|
|
|
for (const arg of args) {
|
|
if (arg.startsWith("--set-crafted=")) {
|
|
craftedValue = Number(arg.split("=")[1]);
|
|
} else if (arg.startsWith("--set=")) {
|
|
const parts = arg.slice("--set=".length);
|
|
const eqIdx = parts.lastIndexOf("=");
|
|
if (eqIdx > 0) {
|
|
setValues.push({ key: parts.slice(0, eqIdx), value: Number(parts.slice(eqIdx + 1)) });
|
|
}
|
|
} else if (arg.startsWith("--only=")) {
|
|
onlyFilter = new Set(arg.slice("--only=".length).split(",").map(s => s.trim().toLowerCase()));
|
|
} else if (arg.startsWith("--")) {
|
|
flags.add(arg);
|
|
} else {
|
|
positional.push(arg);
|
|
}
|
|
}
|
|
|
|
const mutates = flags.has("--max-resources") || flags.has("--max-time") || flags.has("--max-geology") || flags.has("--max-soldiers") || craftedValue !== null || setValues.length > 0;
|
|
const inspects = flags.has("--list") || flags.has("--pillars") || flags.has("--pillars-all");
|
|
const hasAction = mutates || inspects;
|
|
|
|
if (flags.has("--help")) {
|
|
console.log(`Usage: node index.js [options] [save-string]
|
|
|
|
Running with no flags launches interactive mode.
|
|
|
|
Options:
|
|
--list List all resources in the save with current values
|
|
--pillars Show which species have a pillar in the Circle of Pillars
|
|
--pillars-all Same, but also list species without a pillar
|
|
--max-resources Set all capped resources to their max
|
|
--set=key=N Set a specific resource to N (e.g. --set=Plasmid=100)
|
|
--set-crafted=N Set all unlimited (crafted) resources to N
|
|
--only=a,b,c Only affect listed resources (comma-separated names or keys)
|
|
--max-soldiers Fill garrison to max and heal all wounded
|
|
--max-geology Set 4 geology resource bonuses at max values
|
|
--max-time Set accelerated time (AT) to 8 hours
|
|
--no-copy Print result instead of copying to clipboard
|
|
--help Show this help
|
|
|
|
Save string can be passed as argument, piped via stdin, or read from clipboard.`);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (!hasAction && process.stdin.isTTY) {
|
|
return interactiveMode();
|
|
}
|
|
|
|
if (!hasAction) {
|
|
console.error("Error: No action specified. Use --help for usage or run without flags for interactive mode.");
|
|
process.exit(1);
|
|
}
|
|
|
|
const { data, noCopy } = loadSave(positional, flags.has("--no-copy"));
|
|
|
|
if (flags.has("--list")) {
|
|
listResources(data);
|
|
}
|
|
|
|
if (flags.has("--pillars") || flags.has("--pillars-all")) {
|
|
listPillars(data, flags.has("--pillars-all"));
|
|
}
|
|
|
|
if (inspects && !mutates) {
|
|
process.exit(0);
|
|
}
|
|
|
|
if (flags.has("--max-resources")) {
|
|
const changed = maxResources(data, onlyFilter);
|
|
if (changed.length > 0) {
|
|
console.log(`Maxed ${changed.length} resources:`);
|
|
changed.forEach((line) => console.log(line));
|
|
} else {
|
|
console.log("No resources to max.");
|
|
}
|
|
}
|
|
|
|
if (flags.has("--max-soldiers")) {
|
|
console.log(maxSoldiers(data));
|
|
}
|
|
|
|
if (flags.has("--max-geology")) {
|
|
console.log(setGeology(data));
|
|
}
|
|
|
|
if (flags.has("--max-time")) {
|
|
const MAX_AT = 11520; // game adjusts down to 8 hours on load
|
|
const old = data.settings?.at ?? 0;
|
|
if (old < MAX_AT) {
|
|
data.settings.at = MAX_AT;
|
|
console.log(`Accelerated time: ${old} -> ${MAX_AT} (adjusts to 8 hours on load)`);
|
|
} else {
|
|
console.log(`Accelerated time already at ${old} (>= max).`);
|
|
}
|
|
}
|
|
|
|
if (setValues.length > 0) {
|
|
for (const { key, value } of setValues) {
|
|
if (isNaN(value)) {
|
|
console.error(`Invalid value for ${key}. Must be a number.`);
|
|
continue;
|
|
}
|
|
const result = setResource(data, key, value);
|
|
if (result) {
|
|
console.log(`Set resource:\n${result}`);
|
|
} else {
|
|
console.error(`Resource "${key}" not found in save.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (craftedValue !== null) {
|
|
const changed = setCraftedResources(data, craftedValue, onlyFilter);
|
|
if (changed.length > 0) {
|
|
console.log(`Set ${changed.length} crafted resources to ${craftedValue}:`);
|
|
changed.forEach((line) => console.log(line));
|
|
} else {
|
|
console.log("No crafted resources to set.");
|
|
}
|
|
}
|
|
|
|
outputResult(data, noCopy);
|
|
}
|
|
|
|
function loadSave(positional, noCopy) {
|
|
let input;
|
|
if (positional.length > 0) {
|
|
input = positional[0].trim();
|
|
} else if (!process.stdin.isTTY) {
|
|
try {
|
|
input = require("fs").readFileSync("/dev/stdin", "utf-8").trim();
|
|
} catch {
|
|
// stdin not readable (e.g. piped but empty), fall through to clipboard
|
|
}
|
|
}
|
|
|
|
if (!input) {
|
|
const clip = readFromClipboard();
|
|
if (clip) {
|
|
console.log("Reading save data from clipboard...");
|
|
input = clip;
|
|
} else {
|
|
console.error("Error: No save data provided. Pass as argument, pipe via stdin, or copy to clipboard.");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
return { data: decodeSave(input), noCopy };
|
|
}
|
|
|
|
function outputResult(data, noCopy) {
|
|
const result = encodeSave(data);
|
|
|
|
if (noCopy) {
|
|
console.log(result);
|
|
} else if (copyToClipboard(result)) {
|
|
console.log("Edited save copied to clipboard.");
|
|
} else {
|
|
console.error("Could not copy to clipboard. Output:");
|
|
console.log(result);
|
|
}
|
|
}
|
|
|
|
function getResourceChoices(data, type) {
|
|
const choices = [];
|
|
for (const [key, res] of Object.entries(data.resource || {})) {
|
|
const name = res.name || key;
|
|
const amount = res.amount ?? 0;
|
|
const max = res.max ?? 0;
|
|
|
|
if (type === "capped" && max > 0) {
|
|
const full = amount >= max;
|
|
choices.push({
|
|
value: key,
|
|
name: `${name} (${Math.floor(amount)} / ${max})${full ? " [full]" : ""}`,
|
|
checked: !full,
|
|
});
|
|
} else if (type === "crafted" && max === -1) {
|
|
choices.push({
|
|
value: key,
|
|
name: `${name} (${Math.floor(amount)})`,
|
|
checked: true,
|
|
});
|
|
}
|
|
}
|
|
return choices;
|
|
}
|
|
|
|
async function interactiveMode() {
|
|
const { select, checkbox, confirm, input } = require("@inquirer/prompts");
|
|
|
|
const { data } = loadSave([], false);
|
|
|
|
let modified = false;
|
|
|
|
while (true) {
|
|
const action = await select({
|
|
message: "What would you like to do?",
|
|
choices: [
|
|
{ value: "max-resources", name: "Max capped resources" },
|
|
{ value: "set-resource", name: "Set a specific resource" },
|
|
{ value: "set-crafted", name: "Set crafted resources" },
|
|
{ value: "max-soldiers", name: "Max soldiers & heal wounded" },
|
|
{ value: "max-geology", name: "Max geology bonuses" },
|
|
{ value: "max-time", name: "Max accelerated time" },
|
|
{ value: "list", name: "List all resources" },
|
|
{ value: "pillars", name: "Show Circle of Pillars progress" },
|
|
...(modified ? [{ value: "save", name: "Save & copy to clipboard" }] : []),
|
|
{ value: "exit", name: "Exit" },
|
|
],
|
|
});
|
|
|
|
if (action === "exit") {
|
|
if (modified) {
|
|
const discard = await confirm({ message: "Discard unsaved changes?", default: false });
|
|
if (!discard) continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (action === "save") {
|
|
outputResult(data, false);
|
|
break;
|
|
}
|
|
|
|
if (action === "list") {
|
|
listResources(data);
|
|
continue;
|
|
}
|
|
|
|
if (action === "pillars") {
|
|
const showAll = await confirm({ message: "Also show species without a pillar?", default: false });
|
|
listPillars(data, showAll);
|
|
continue;
|
|
}
|
|
|
|
if (action === "max-time") {
|
|
const MAX_AT = 11520;
|
|
const old = data.settings?.at ?? 0;
|
|
if (old < MAX_AT) {
|
|
data.settings.at = MAX_AT;
|
|
console.log(`Accelerated time: ${old} -> ${MAX_AT} (adjusts to 8 hours on load)`);
|
|
modified = true;
|
|
} else {
|
|
console.log(`Accelerated time already at ${old} (>= max).`);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (action === "max-soldiers") {
|
|
const result = maxSoldiers(data);
|
|
console.log(result);
|
|
if (!result.includes("No garrison") && !result.includes("already at")) {
|
|
modified = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (action === "max-geology") {
|
|
console.log(setGeology(data));
|
|
modified = true;
|
|
continue;
|
|
}
|
|
|
|
if (action === "max-resources") {
|
|
const choices = getResourceChoices(data, "capped");
|
|
if (choices.length === 0) {
|
|
console.log("No capped resources found.");
|
|
continue;
|
|
}
|
|
const selected = await checkbox({
|
|
message: "Select resources to max (space to toggle, enter to confirm)",
|
|
choices,
|
|
});
|
|
if (selected.length === 0) {
|
|
console.log("No resources selected.");
|
|
continue;
|
|
}
|
|
const filter = new Set(selected.map((s) => s.toLowerCase()));
|
|
const changed = maxResources(data, filter);
|
|
if (changed.length > 0) {
|
|
console.log(`Maxed ${changed.length} resources:`);
|
|
changed.forEach((line) => console.log(line));
|
|
modified = true;
|
|
} else {
|
|
console.log("Selected resources already at max.");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (action === "set-resource") {
|
|
const prestige = Object.entries(data.prestige || {})
|
|
.filter(([, p]) => (p.count ?? 0) > 0)
|
|
.map(([key, p]) => ({
|
|
value: `prestige:${key}`,
|
|
name: `${key} (${p.count}) [prestige]`,
|
|
}));
|
|
const resources = Object.entries(data.resource || {})
|
|
.filter(([, r]) => (r.amount ?? 0) > 0 || (r.max ?? 0) !== 0)
|
|
.map(([key, r]) => ({
|
|
value: `resource:${key}`,
|
|
name: `${r.name || key} (${Math.floor(r.amount ?? 0)}${r.max > 0 ? ` / ${r.max}` : ""})`,
|
|
}));
|
|
const choices = [...prestige, ...resources];
|
|
if (choices.length === 0) {
|
|
console.log("No resources found.");
|
|
continue;
|
|
}
|
|
const selected = await select({
|
|
message: "Which resource?",
|
|
choices,
|
|
});
|
|
const [type, resKey] = selected.split(":");
|
|
const label = type === "prestige" ? resKey : (data.resource[resKey].name || resKey);
|
|
const value = await input({
|
|
message: `Set ${label} to what value?`,
|
|
validate: (v) => {
|
|
const n = Number(v);
|
|
return !isNaN(n) && n >= 0 ? true : "Enter a non-negative number";
|
|
},
|
|
});
|
|
const result = type === "prestige"
|
|
? setPrestige(data, resKey, Number(value))
|
|
: setResource(data, resKey, Number(value));
|
|
if (result) {
|
|
console.log(`Set resource:\n${result}`);
|
|
modified = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (action === "set-crafted") {
|
|
const choices = getResourceChoices(data, "crafted");
|
|
if (choices.length === 0) {
|
|
console.log("No crafted resources found.");
|
|
continue;
|
|
}
|
|
const value = await input({
|
|
message: "Set crafted resources to what value?",
|
|
validate: (v) => {
|
|
const n = Number(v);
|
|
return !isNaN(n) && n >= 0 ? true : "Enter a non-negative number";
|
|
},
|
|
});
|
|
const selected = await checkbox({
|
|
message: "Select resources to set (space to toggle, enter to confirm)",
|
|
choices,
|
|
});
|
|
if (selected.length === 0) {
|
|
console.log("No resources selected.");
|
|
continue;
|
|
}
|
|
const filter = new Set(selected.map((s) => s.toLowerCase()));
|
|
const changed = setCraftedResources(data, Number(value), filter);
|
|
if (changed.length > 0) {
|
|
console.log(`Set ${changed.length} crafted resources to ${value}:`);
|
|
changed.forEach((line) => console.log(line));
|
|
modified = true;
|
|
} else {
|
|
console.log("Selected resources already at or above target.");
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
main();
|