Files
rack/README.md

135 lines
6.6 KiB
Markdown
Raw Normal View History

# rack
A browser-based idle/incremental game about running a homelab. Start with a
single Raspberry Pi on a desk, incrementally expand into something that
vaguely justifies its electricity bill.
Dark terminal aesthetic, dry flavour text, numbers go up, occasionally
things go wrong.
## Running it
```bash
npm install
npm run dev # dev server on http://localhost:5677
npm run build # production build
npm run preview # preview the build on http://localhost:4587
npm test # vitest run - engine + balance tests
```
No backend. Game state lives entirely in `localStorage`, autosaved on every
render tick and on tab close/hide.
## How it works
- **Hardware** (`src/data/hardware.ts`) provides RAM slots and power budget,
enforced per host (not a shared pool). You can either unlock the next
tier, or buy another unit of a tier you already own as a cheaper bridge —
each extra unit of the same tier costs 40% more than the last
(`hardwareCost()` in `engine.ts`). The starter Pi 4 is a one-time freebie
and can't be rebought.
- **Services** (`src/data/services.ts`) run on hardware, consume RAM/power,
and produce points/sec. They age (production creeps up over time) and can
crash (probability rises with age, mitigated by the Cooling upgrade). The
deploy dropdown shows each service's RAM/power cost up front and disables
anything that wouldn't fit the currently selected host.
- **Upgrades** (`src/data/upgrades.ts`) are one-time persistent purchases:
UPS shortens outages, Backups tiers reduce/reverse crash losses,
Monitoring reveals crash risk, Redundancy runs a second instance of every
service, Documentation is a small passive multiplier.
- **Random events** (`src/engine.ts` `rollRandomEvent`) fire on a
Poisson-ish interval that tightens as the game clock advances: disk-full
crashes, power outages, kernel panics, fan-noise flavour-only events,
optional updates, and a rare `rm -rf` that deletes a service outright
(guarded once Backups I is bought).
- **Prestige**: once total lifetime earnings cross 50,000 pts, a banner
offers to decommission the rack — wipes hardware/services/upgrades but
grants a permanent production multiplier and carries forward.
- **Host nicknames** (`src/data/hostnames.ts`): every host is randomly
christened on purchase from a pool of sci-fi/mythology/sysadmin-humour
names (hal9000, shodan, the-basement, works-on-my-machine, ...), drawn
without replacement so they double as unique identifiers wherever a host
is named.
- **Quirks** (`src/data/quirks.ts`): each deploy has a 30% chance of rolling
a per-instance trait (Overclocked, Chatty, Legacy, Well-documented,
Flaky, Artisanal) that shifts that instance's production and crash risk.
Shown as a badge on the service row and in the rack view's tooltip.
- **Rack view**: an ASCII-art strip above the two main columns, one line per
owned host, showing which services (as 2-letter codes) are deployed where
and free capacity as dots — a compact answer to "what's actually running
on what."
- **Help & changelog**: click the `?` in the header, or press `?` anywhere
outside a text input, to open an in-app overlay with a quick "how this
works" list and the full version changelog (`src/data/version.ts`),
mirroring `../adventure`'s help-modal pattern.
## Project layout
```
src/
types.ts game state & definition types
state.ts new game, save/load (localStorage), id allocation
engine.ts tick loop: production, aging, crashes, events, prestige
ui.ts DOM rendering, one full re-render per render tick
help.ts the ?-triggered help/changelog overlay
data/ static definitions: hardware/services/upgrades/events/
quirks/version (VERSION + in-app CHANGELOG entries)
style.css terminal theme
```
The simulation tick runs on `requestAnimationFrame` and is time-delta based
(not frame-based), so offline/backgrounded tabs still catch up correctly
when they regain focus. The passive UI re-render runs on its own ~1s
interval, decoupled from the tick — user actions (buy, deploy, fix,
uninstall) re-render immediately via a direct callback instead of waiting
on that interval, and it's skipped entirely while a `<select>` has focus so
the service-picker dropdown doesn't get yanked shut mid-render.
## Deploying as a subdirectory (e.g. goonk.se/rack)
`npm run build` emits root-relative asset URLs (`/assets/...`), which only
works if rack is served from the domain root. To deploy it under a
subpath instead, use:
```bash
npm run build:subpath # vite build --base=/rack/
```
That rewrites `dist/index.html`'s asset URLs to `/rack/assets/...`. Drop
the resulting `dist/` contents into the host site wherever it serves
static files under `/rack/` — for the `goonk` Astro site specifically,
that's `goonk/public/rack/` (Astro copies `public/` verbatim into its own
`dist/` at build time, and its nginx config already does a
`try_files $uri $uri/ /index.html` fallback, so no nginx changes are
needed - existing `public/waste`, `public/pitwall` follow the same
one-subdir-per-project convention). `rack`'s own `localStorage` key
(`rack-save-v1`) is namespaced, so it won't collide with anything else
served from the same origin.
## Tests
`src/engine.test.ts` (Vitest) covers the core loop mechanics rather than UI:
deploy/capacity checks (including per-host isolation), hardware/upgrade
purchases and prerequisite gating, the fix-cooldown (per-service, not
global — see CHANGELOG for why that matters), uninstall refunds,
production/crash/outage ticking, and prestige. A separate `balance sanity`
block asserts properties of the static data in `src/data/` rather than
specific numbers — e.g. every service pays back its own deploy cost in
under 5 minutes at base rate, hardware cost/capacity strictly increases
with tier, and no service's crash risk has crept past the intended
ceiling. These are meant to catch a future data tweak that breaks the
early-game feel, not to lock the current numbers in stone — adjust the
thresholds deliberately if you retune the balance.
## Design decisions worth knowing
- **Vanilla TypeScript + DOM**, no framework — matches the rest of the
homelab-project family (`waste-go`, `flit`) and keeps this dependency-light
per `PLAN.md`.
- **Dev/preview ports are non-default** (`5677` / `4587` instead of Vite's
`5173` / `4173`) because the other sibling projects on this machine claim
the usual ports and everything within ±10 of them.
- **Starting balance is 15 pts**, not 0 — the cheapest service (Nginx)
costs 10, so a brand-new game can bootstrap its first deploy immediately
without an awkward zero-income deadlock.