Files
devplacepy/devplacepy/static/js/GameCommands.js
T
2026-10-19 21:32:45 +02:00

128 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// retoor <retoor@molodetz.nl>
/** Declarative coverage of the existing game API; no command runs system code. */
export class GameCommands {
static API = Object.freeze([
{"command":"home","method":"GET","path":"/game","args":[],"description":"Read your own farm overview."},
{"command":"state","method":"GET","path":"/game/state","args":[],"description":"Read every farm field as JSON."},
{"command":"leaderboard","method":"GET","path":"/game/leaderboard","args":["board"],"defaults":{"board":"score"},"params":{"board":"board"},"description":"Show a leaderboard; defaults to score."},
{"command":"view","method":"GET","path":"/game/farm/{username}","args":["username"],"description":"Inspect another player's rack."},
{"command":"plant","method":"POST","path":"/game/plant","args":["server","crop"],"params":{"slot":"server","crop":"crop"},"action":"plant","description":"Spin up a workload on an idle server."},
{"command":"harvest","method":"POST","path":"/game/harvest","args":["server"],"params":{"slot":"server"},"action":"harvest","bulk":true,"description":"Harvest a server, or harvest all ready servers."},
{"command":"stop","method":"POST","path":"/game/plot/stop","args":["server"],"params":{"slot":"server"},"fixed":{"stop":"true"},"action":"plot-stop","description":"Stop auto-replant after this server's next harvest."},
{"command":"resume","method":"POST","path":"/game/plot/stop","args":["server"],"params":{"slot":"server"},"fixed":{"stop":"false"},"action":"plot-stop","description":"Resume this server's auto-replant."},
{"command":"buy server","method":"POST","path":"/game/buy-plot","args":[],"action":"buy-plot","description":"Install the next server at its current price."},
{"command":"ci upgrade","method":"POST","path":"/game/upgrade","args":[],"action":"upgrade","description":"Upgrade your CI tier."},
{"command":"fertilize","method":"POST","path":"/game/fertilize","args":["server"],"params":{"slot":"server"},"action":"fertilize","description":"Pay to speed up a growing build."},
{"command":"daily","method":"POST","path":"/game/daily","args":[],"action":"daily","description":"Claim the daily bonus."},
{"command":"grant","method":"POST","path":"/game/grant","args":[],"action":"grant","description":"Claim your community grant if eligible."},
{"command":"perk","method":"POST","path":"/game/perk","args":["key"],"catalog":"perks","params":{"perk":"key"},"action":"perk","description":"Upgrade a perk. Run perks to see keys and costs."},
{"command":"prestige","method":"POST","path":"/game/prestige","args":[],"action":"prestige","confirm":true,"description":"Refactor your farm after confirming the live fee and reset."},
{"command":"legacy","method":"POST","path":"/game/legacy","args":["key"],"catalog":"legacy","params":{"key":"key"},"action":"legacy","description":"Buy a permanent Legacy upgrade with Stars."},
{"command":"mastery","method":"POST","path":"/game/mastery","args":["key"],"catalog":"mastery","params":{"key":"key"},"action":"mastery","description":"Spend Mastery points on an upgrade."},
{"command":"claim","method":"POST","path":"/game/quests/claim","args":["quest","scope"],"defaults":{"scope":"daily"},"params":{"quest":"quest","scope":"scope"},"action":"claim-quest","description":"Claim a daily quest or weekly contract."},
{"command":"defense upgrade","method":"POST","path":"/game/defense/upgrade","args":[],"action":"defense","description":"Upgrade defense and its daily upkeep."},
{"command":"defense downgrade","method":"POST","path":"/game/defense/downgrade","args":[],"action":"defense-downgrade","confirm":true,"description":"Reduce defense and upkeep after confirmation."},
{"command":"infra","method":"POST","path":"/game/infrastructure/buy","args":["key"],"catalog":"infrastructure","params":{"key":"key"},"action":"infrastructure","confirm":true,"description":"Buy infrastructure at its current coin price."},
{"command":"cosmetic buy","method":"POST","path":"/game/cosmetics/buy","args":["key"],"catalog":"cosmetics","params":{"key":"key"},"action":"cosmetic-buy","confirm":true,"description":"Buy a cosmetic after confirming its price."},
{"command":"cosmetic equip","method":"POST","path":"/game/cosmetics/equip","args":["key"],"catalog":"cosmetics","params":{"key":"key"},"action":"cosmetic-equip","description":"Equip an owned title."},
{"command":"water","method":"POST","path":"/game/farm/{username}/water","args":["username","server"],"params":{"slot":"server"},"action":"water","social":true,"description":"Water another player's growing server."},
{"command":"steal","method":"POST","path":"/game/farm/{username}/steal","args":["username","server"],"params":{"slot":"server"},"action":"steal","social":true,"confirm":true,"description":"Raid another player's ready server after confirmation."}
]);
static BOARDS = ["score", "prestige", "harvests", "raids", "time_to_kernel", "fair_play", "era"];
static ALIASES = { spinup: "plant", boost: "fertilize", refactor: "prestige", ls: "servers" };
static LOCAL = {
help: "Show commands and examples.", status: "Show the current rack's status.", servers: "List current servers and countdowns.",
server: "Select a server: server 3.", crops: "List workloads, prices, rewards, locks and market state.",
perks: "List perk keys, levels and costs.", legacy: "List permanent Legacy upgrades.", mastery: "List Mastery upgrades.",
infra: "List infrastructure and ownership.", defense: "Show defense and upkeep.", cosmetics: "List cosmetics and titles.",
quests: "List daily quests and weekly contracts.", ci: "Show your CI tier.", stats: "Show lifetime, weekly and Era statistics.",
api: "Show every game API route and its command.", clear: "Clear the terminal output."
};
static usage(spec) {
return [spec.command, ...spec.args.map((arg) => Object.hasOwn(spec.defaults || {}, arg) ? `[${arg}]` : `<${arg}>`)].join(" ");
}
static server(value, farm, remote = false) {
if (!/^\d+$/.test(value || "")) throw new Error("Use a server number, for example 1.");
const slot = Number(value) - 1;
if (slot < 0 || slot >= farm.max_plots || (!remote && !farm.plots.some((plot) => plot.slot === slot))) {
throw new Error(`Server ${value} is not available. Run servers to see your installed servers.`);
}
return slot;
}
static parse(line, farm) {
if (!farm) throw new Error("The rack is still loading. Try again in a moment.");
if (line.length > 256) throw new Error("Commands must be 256 characters or fewer.");
const tokens = line.trim().split(/\s+/);
tokens[0] = tokens[0].toLowerCase();
if (Object.hasOwn(GameCommands.ALIASES, tokens[0])) tokens[0] = GameCommands.ALIASES[tokens[0]];
const matching = GameCommands.API.filter((spec) => spec.command.split(" ").every((word, index) => tokens[index]?.toLowerCase() === word));
const spec = matching.find((item) => {
const count = tokens.length - item.command.split(" ").length;
return count >= item.args.filter((arg) => !Object.hasOwn(item.defaults || {}, arg)).length && count <= item.args.length;
});
if (!spec) {
if (Object.hasOwn(GameCommands.LOCAL, tokens[0]) && (tokens.length === 1 || tokens[0] === "server" && tokens.length === 2)) {
return { local: tokens[0], slot: tokens[0] === "server" ? GameCommands.server(tokens[1], farm) : undefined };
}
if (matching.length) throw new Error(`Usage: ${GameCommands.usage(matching[0])}`);
throw new Error("Unknown command. Type help for commands and examples.");
}
if (spec.method === "POST" && !spec.social && !farm.is_owner) throw new Error("Open your own rack at /game to use this command.");
const raw = tokens.slice(spec.command.split(" ").length);
const values = { ...spec.defaults };
spec.args.forEach((arg, index) => { if (raw[index] !== undefined) values[arg] = raw[index]; });
for (const [arg, value] of Object.entries(values)) {
if (arg === "server") {
values[arg] = spec.bulk && value.toLowerCase() === "all" ? "all" : GameCommands.server(value, farm, spec.social);
} else if (arg === "username") {
if (!/^[a-zA-Z0-9_-]{3,32}$/.test(value)) throw new Error("Use a valid username (3–32 letters, numbers, hyphens or underscores).");
} else {
values[arg] = value.toLowerCase();
if (!/^[a-z][a-z0-9_]{0,39}$/.test(values[arg])) throw new Error(`Invalid ${arg}. Type help for the accepted syntax.`);
if (arg === "board" && !GameCommands.BOARDS.includes(values[arg])) throw new Error(`Boards: ${GameCommands.BOARDS.join(", ")}`);
if (arg === "scope" && !["daily", "weekly"].includes(values[arg])) throw new Error("Quest scope must be daily or weekly.");
const catalog = arg === "crop" ? farm.crops : arg === "key" ? farm[spec.catalog] : null;
if (catalog && !catalog.some((entry) => entry.key === values[arg])) throw new Error(`Unknown ${arg}: ${value}. Run ${arg === "crop" ? "crops" : spec.catalog} to see available keys.`);
}
}
const params = { ...spec.fixed };
for (const [field, arg] of Object.entries(spec.params || {})) params[field] = values[arg];
let url = spec.path.replace("{username}", encodeURIComponent(values.username || ""));
if (spec.method === "GET" && Object.keys(params).length) url += `?${new URLSearchParams(params)}`;
return { spec, url, params, values, bulk: values.server === "all" };
}
static completions(line, farm) {
const tokens = line.trimStart().split(/\s+/);
const first = tokens[0]?.toLowerCase();
const command = Object.hasOwn(GameCommands.ALIASES, first) ? GameCommands.ALIASES[first] : first;
const index = tokens.length - 1;
const prefix = tokens[index].toLowerCase();
let choices = [];
if (index === 0) choices = [...Object.keys(GameCommands.LOCAL), ...GameCommands.API.map((spec) => spec.command.split(" ")[0]), ...Object.keys(GameCommands.ALIASES)];
else if (index === 1) {
if (["plant", "harvest", "fertilize", "stop", "resume", "server"].includes(command)) choices = (farm.plots || []).map((plot) => String(plot.slot + 1));
if (command === "harvest") choices.push("all");
const collections = { perk: "perks", legacy: "legacy", mastery: "mastery", infra: "infrastructure" };
if (collections[command]) choices = (farm[collections[command]] || []).map((entry) => entry.key);
if (command === "leaderboard") choices = GameCommands.BOARDS;
if (command === "buy") choices = ["server"];
if (command === "ci") choices = ["upgrade"];
if (command === "defense") choices = ["upgrade", "downgrade"];
if (command === "cosmetic") choices = ["buy", "equip"];
if (command === "claim") choices = [...new Set((farm.quests || []).map((quest) => quest.kind))];
} else if (index === 2) {
if (command === "plant") choices = (farm.crops || []).map((crop) => crop.key);
if (command === "cosmetic") choices = (farm.cosmetics || []).map((entry) => entry.key);
if (["water", "steal"].includes(command)) choices = Array.from({ length: farm.max_plots }, (_, slot) => String(slot + 1));
if (command === "claim") choices = ["daily", "weekly"];
}
return [...new Set(choices)].filter((choice) => choice.startsWith(prefix)).sort();
}
}