diff --git a/CLAUDE.md b/CLAUDE.md index ee43bf4..7b49f47 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,10 @@ node index.js --max-resources --no-copy < save.txt Saves are LZString `compressToBase64` of a JSON object (not plain base64). Uses the `lz-string` npm package to match the game's own JS implementation. -Key top-level sections: `resource`, `race`, `tech`, `city`, `space`, `civic`, `genes`, `prestige`, `settings`, `arpa`. +Key top-level sections: `resource`, `race`, `tech`, `city`, `space`, `civic`, `genes`, `prestige`, `settings`, `arpa`, `pillars`. + +### Pillars (`pillars`) +Flat map of `: ` — only species with a pillar appear. Rank is the ascension `alevel()` (1–5, +1 per challenge gene active on that run), *not* the universe. 63 species total (all races except `protoplasm`); the race table lives in `RACES` in `index.js`, mirrored from the game's `src/races.js`. Pillar bonus: +1% production / +2% storage per pillar, with the currently-played species counting 4×. ### Prestige structure (`prestige.`) - `count`: current value diff --git a/README.md b/README.md index b789443..e0ccedf 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ Pass flags for non-interactive, scriptable usage. Reads save data from your **cl # List all resources and their current values node index.js --list +# Show which species already have a pillar in the Circle of Pillars +node index.js --pillars + +# Same, but also list the species still missing a pillar +node index.js --pillars-all + # Max all capped resources node index.js --max-resources @@ -61,6 +67,8 @@ node index.js --max-resources --no-copy | Flag | Description | |------|-------------| | `--list` | List all resources in the save, grouped by type (capped, crafted, special, locked) | +| `--pillars` | Show Circle of Pillars progress: pillared species grouped by genus, with their rank | +| `--pillars-all` | Same as `--pillars`, but also lists species that have no pillar yet | | `--max-resources` | Set all capped resources (`max > 0`) to their max | | `--set=key=N` | Set a specific resource or prestige currency to N (e.g. `--set=Plasmid=100`). Case-insensitive. Can be used multiple times. | | `--set-crafted=N` | Set all unlimited/crafted resources (`max == -1`) to N | diff --git a/index.js b/index.js index f79e934..21a450d 100644 --- a/index.js +++ b/index.js @@ -142,6 +142,151 @@ function setGeology(data) { 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(); @@ -226,7 +371,9 @@ function main() { } } - const hasAction = flags.has("--max-resources") || flags.has("--max-time") || flags.has("--max-geology") || flags.has("--max-soldiers") || flags.has("--list") || craftedValue !== null || setValues.length > 0; + 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] @@ -235,6 +382,8 @@ 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 @@ -262,9 +411,14 @@ Save string can be passed as argument, piped via stdin, or read from clipboard.` if (flags.has("--list")) { listResources(data); - if (!flags.has("--max-resources") && !flags.has("--max-soldiers") && !flags.has("--max-geology") && !flags.has("--max-time") && craftedValue === null) { - process.exit(0); - } + } + + 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")) { @@ -406,6 +560,7 @@ async function interactiveMode() { { 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" }, ], @@ -429,6 +584,12 @@ async function interactiveMode() { 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;