Docs
TC-Bot looks like “a Discord bot” from the outside; underneath it is a straight-line boot sequence, recursive command discovery, optional Google Sheets caching, and a mopup scheduler that keeps two channels honest about timers. This page sketches that shape, the sheet dependency for troop-heavy commands, and the sharp edges that show up when credentials or channel IDs drift.
Repository map
At the repository root, package.json drives a single app: compile with tsc, run dist/tc-bot.js. Source layout is intentionally flat:
tc-bot/
src/
commands/
aow/ # Ark of War: gearcheck, healtroop, its, mopup
utility/ # help, ping, metrics, reboot
events/ # interaction handlers, ready, messageCreate
helper/ # constants, sheets cache, mopup math, metrics DB, idempotency
env/ # dotenv loading
dist/ # compiled output (after build)If CI only runs pnpm install and pnpm run validate at the root, you are aligned with how the project expects to ship. If someone tries to run TypeScript files by hand without building, send them to pnpm run build with kindness.
Boot flow (high level)

Slash commands vs. deploy
Commands are implemented as Discord slash command builders plus an executehandler. pnpm run deploy walks src/commands in TypeScript and pushes definitions to the application (guild commands when GUILD_ID is set - check src/deployCommands.ts for the exact REST routes). The running bot loads compiled .js from dist/commands after build. Change a command name or option? Rebuild and redeploy; Discord does not guess.
Google Sheets: why the credentials file matters
Healing (/healtroop) and iTS (/its) pull troop rows through getSheetRowsCached, backed by the Sheets API and GOOGLE_SPREADSHEET_ID / GOOGLE_SHEET_ID. Boot validates those IDs and prefetches rows to warm the cache. If client_secret.json is missing or the service account cannot read the sheet, initialization fails fast - there is no silent fallback to fictional troop stats.
Gear check (/gearcheck) uses fixed level breakpoints and multipliers from helper/constants.ts, so it remains usable even when sheet access is misconfigured - once the bot starts at all.
Mopup: channels, embeds, and time windows
Mopup timing is computed from a deterministic schedule (helper/mopup.ts): alternating daily windows expressed relative to epoch-aligned “days,” surfaced as embeds with status and countdown-style timestamps. The bot also schedules periodic updates to the configured status and timer channels (CHANNEL_ID1, CHANNEL_ID2) so members see live countdowns without typing commands.
Optional legacy !tcmu message commands require ENABLE_LEGACY_MESSAGE_COMMANDS and extra intents (GuildMessages, MessageContent). Idempotency guards and duplicate-reply cleanup exist because Discord can retry events; the bot tries not to spam the same answer twice.
Metrics and PM2
Usage and operational metrics land in SQLite (SQLITE_DB_PATH) with configurable retention and flush batching. PM2 IO counters in helper/metrics.ts expose command counts, error counts, Discord latency, and sheet cache stats for operators who want dashboards without reading logs.

Testing and CI
Vitest covers logic that must not drift silently - validators, formatters, and command math. Treat failing tests as regressions in public-facing numbers: alliances remember the old answer. pnpm run validate is the “would CI merge this?” button; run it before tagging a release.
Building and validating like you mean it
pnpm run build- TypeScript todist/, the artifactnodeactually runs.pnpm run validate- format, lint, typecheck, tests per project script.
Using both before deploy saves everyone the emotional cost of a Sunday rollback because /healtroop suddenly throws on a mistyped tier.
