Skip to content

Repository files navigation

Anamnesia Idle

Play it at game.onyxleeds.co.uk. A name and a password make an account; the save lives on the hill.

A browser idle RPG (Melvor-shaped) with a generated item and icon pipeline. See BRIEF.md for the project brief and DECISIONS.md for the why behind non-obvious choices.

Phase 0.5 is in: exactly one tab runs the simulation (Web Locks leader election), followers mirror it over a BroadcastChannel, every save is guarded by a compare-and-swap on saveCounter in IndexedDB, and offline progress is derived from timestamps, capped (4 h bare; the trader's lamps and the hall's watchtower lengthen the night), and idempotent.

Phase 1 is in: a real save shape (player, skills, inventory, equipment, bank, action queue), a swappable XP curve, content as validated JSON, a single action primitive (duration in ticks, success roll, outcome), and mining end to end with tests that pin exact XP and drop results from fixed seeds.

Phase 2 is in: an item's icon is derived, not authored. Design tokens come from the Claude Design screens in design/claude-design/; material palettes and rarities are content; a renderer composes inline SVG from icon geometry + material gradient + rarity treatment + corner badges; swords are generated from seeded parts whose stats match their shape; rendered markup is cached by its inputs. /dev/items.html is the contact sheet.

Phase 3 is in: the content. Setting and tone are the hill — plain material words, one dry line each (see DECISIONS.md). Seven veins, six trees, 65 smithing recipes across a six-tier equipment ladder, five combat zones with 21 monsters (data only until the combat loop), 110 items, four rarities. Mining, woodcutting and smithing are playable end to end; tools equip into per-skill slots and shorten actions; containers open. A content audit test checks that everything is obtainable, the economy never destroys value, and names and descriptions keep the house style.

Phase 4 is in: the UI, built from the Claude Design screens. Sidebar (or top bar + bottom tabs under 700px), a skill screen per gathering skill with the active action, its progress bar, xp pops, the node list and the drop feed; smithing on the same skeleton; the bank with filters, search, selection, sell, open, equip and purchasable slots; first-run naming; the level-up moment, the rare-drop toast, the offline recap (honest about the cap) and the calm full-page states for other-tab / catching-up / stale / save-error. Save v4 adds coins, bank slots, an event log and action counters to carry it. Combat is still data only.

Phase 5 is in: the rest of Screen D's onboarding (choose a god, the ready card), four gods whose boons are data, three more skills — fishing, firemaking, cooking — that feed and gate each other (smithing makes rods from bars and logs; cooking wants a hotter fire; fires eat logs), a "quick" method in every gathering skill that out-earns its tier and banks nothing worth keeping, ten first steps checked by the sim with small rewards, and a progression model (src/sim/progression.ts) pinned by a test so every skill takes 27–45 hours of its own idle time to 99 and the mean sits near 36. Save v5 adds the sworn god, lifetime counters, first-steps progress and the rod slot.

Phase 6 is in: combat, from the design's Screen E. A fight is an action whose cycle is the hero's swing, with the monster on its own clock; one combat skill plus an unlisted hitpoints skill; xp paid per point of damage; food chosen from the bank and eaten automatically below a threshold; death destroys one random worn body item (never a tool) and nothing else. The combat screen (fight card, food row, zones and monsters, kill log) and an equipment screen (worn grid, toolbelt, selected item, equip from the bank). The progression model gained a combat climb in ladder gear, and scripts/tune-combat.ts retunes monster hp/xp against it. Save v6 adds the combat state and kill/death counters.

Phase 7 is in: the gods fight too. A fourth gathering skill, Foraging (by hand, no tool), gathers offerings over patches; each god has a combat boon as content — Tharok's Stone Skin, Vessith's Green Return, Maren's Still Hand, Ashkar's Ember — that runs on favour, which burns one a second in a fight and is bought back by burning the chosen offering from the bank, the same shape as food. First steps gains a combat step, a foraging step and an offering. The progression model measures every boon (scripts/tune-boons.ts prints the comparison) and a test pins them to about the same worth. Save v7 adds the offering, favour and a burnt counter.

Phase 8 fills the empty slots. Gauntlets are smithed like the rest of the set; capes carry an xp boost as the design pictured — one per skill, found rarely while working it, plus beast capes with combat stats from the monsters that wore the hide; javelins go in the ammo slot, twenty to the bar, one thrown from the bank with every swing that lands; rings and necks run from copper to aether. scripts/tune-gear.ts prints what javelins buy and cost. Save v8 adds a thrown counter.

Phase 9 is in: highscores for every skill, total level and wealth — the hero's standing on every board, and the chosen board best first. Until Phase 11 the other names were curves; now they are players.

Phase 11 is in: the hill has a register. A name and a password make an account (server/, node's own HTTP server and SQLite, no new dependencies); the save lives on the server and travels between browsers and devices through the same compare-and-swap the tabs already used; the highscores rank every account's last save. In dev the API mounts inside Vite, so npm run dev is still the whole game; in production npm run build && npm start serves the game and the API from one port. The NPC roster is gone.

Phase 10 is the coin sink. The ferryman: when the hero falls, an obol in the bank settles the crossing, else twice the lost item's worth in coins if the hero pays him (on by default), else the item goes as before — the one price that tracks the gear. The trader, a new screen, sells what the hill will take coins for: a lamp ladder that lengthens the offline cap (16, 20, 24 h), a second look that doubles finds, and release from the oath from 100,000 gp doubling. src/sim/progression.ts gained coinsPerHour; scripts/tune-trader.ts prints income by level, each ware as hours of work and the ferryman's fee per tier. Save v9 adds purchases, the ferryman setting and spent/ferried counters.

Phase 12 is the hall: clans that raise a place on the hill. A name founds a hall, invites names or answers those asking at the door, and the members give items and coins to raise six rooms through three tiers — the Hearth, Storehouse, Larder, Strongroom, Watchtower and Pyre, each a few per cent for everyone in it (src/content/halls.json). The hall lives in the register (server/hall.ts); the sim only carries what the register last said and a cart of gifts, which ride inside the save and are answered with the write (src/sim/hall.ts). hoursToMake in progression.ts prices a room in hours of one name's work — gathered, made, or taken from a monster — and every tier reaches across the skills: a first tier is an evening of one name's work, a second a couple of days, a third a week or more, which a hall shares. scripts/tune-hall.ts prints it. Save v10 adds the hall.

Phase 13 is the fire: talk. A room every name on the hill can hear, and a word with one name at a time; a name can turn away from another (their words go unheard and unsent) and turn back. Words live in the register (server/chat.ts), never in the save; a tab gets them by long poll — one open question to /api/chat/poll that the register answers when there is a word or after 25 s — which is plain HTTP through Caddy and Cloudflare. useChat holds the one poll for the tab; ChatPanel is one talk on its own, so another screen can seat a room (the wheel's table talk is already a room id). Rooms keep a month; 20 words a minute a name.

Phase 14 is the wheel: one roulette table for the whole hill, turned by the register's clock. A round is thirty seconds — bets for twenty-four, then the pocket, drawn on the server the moment the bets close, never by the sim's dice — and every name at the table sees the same spin, the same chips, the same strip of last pockets. Coins reach the table the way gifts reach the hall: buy in takes them from the purse onto a cart the save carries, the register credits chips and answers the save; bets (/api/wheel/bet) debit the chips and are final; cash out becomes a payout the next save takes home, stamped into the stored record so no reload can take it twice or lose it. An American wheel, 0 and 00 — every bet carries the same edge, two pockets in thirty-eight. The table has a room of its own in the talk. scripts/tune-wheel.ts prints the edge and what a chip costs in hours. Save v11 adds the cart.

Phase 15 is the looks: a name paints a likeness, and a hall's founder paints the mark over its door, in the same brush (src/look/, LookEditor). Sixteen cells by sixteen in the design's own colours — the chrome's greys, the accent greens, the gold, the hurt red, the material tiers — painted by hand (paint, fill, erase, mirror, undo) over a stack of plain shapes (disc, box, triangle, diamond, line; turn, lower, raise, recolour, press into the paint, remove) on a backdrop. Shapes stay vectors, so a look is crisp at 22 px and at 52. A look lives on the register (users.look, halls.look), never in the save; the hill reads them by name in batches (/api/looks) through one small cache (src/ui/looks.ts) and <Face> shows the look or falls back to the first letter — beside every word at the fire, on every board, in the hall, at the wheel, in the sidebar. The brush is under the name at the bottom of the sidebar (or Settings → Likeness); the hall's is "Paint the mark" on its door.

Phase 16 is Sorcery: a ninth listed skill that is also the other way to fight. At the bench it inscribes marks (the tier's ore pressed with ash — firemaking's ash finally has a buyer) and staffs (the tier's log shod with its bar); in the zones a staff casts on the Sorcery level and pays Sorcery, burning one mark from the ammo slot per landed cast the way a javelin is thrown — except that without marks the staff is a stick and the fight will not start. The worn weapon decides the fight, not a toggle; ammo of the other style is carried and nothing more. Every monster is weak to one style and takes a quarter more from it, so each zone has work for a sword and a staff; scripts/tune-combat.ts now sets hp against the style a monster is for and scripts/tune-sorcery.ts prints both climbs, the marks an hour of casting burns and what inscribing to 99 eats. Save v12 counts marks cast.

Phase 17 is the slab: fish come out of the water in sizes, and the biggest of each kind stays. Every raw fish carries a weight band in grams (items.json), and a catch is drawn on a cubed curve between the band's floor and what the hero can actually reach — half the band at the water's own level, all of it thirty levels above it — so a big minnow is the work of someone who long ago left the Rain Pool. Beating your own best pays the water's xp again, scaled by how far into the band the fish sits; it is a moment and not a rate, because the nth catch only beats every one before it about one time in n. Near the top of each band is a trophy line, and the first fish of a kind over it is one the trader will buy the story of, for coins priced at about four hours of fishing at the level that line opens (scripts/tune-slab.ts prints all of it). The lines open across the whole climb — the minnow's at 28, the pale fish's at 98, the char's not until 99 — so the slab is something to go back down the hill for rather than a chore saved for the end. It is a card under the drop feed on the Fishing screen, one row per kind: the best against the whole band, with the trophy line notched on it. Nothing about a weight touches the bank — a fish is still one stack worth what it is worth — so the economy, the cooking pot and the hall's prices are exactly as they were. Save v13 adds the slab.

Every skill screen has a ? beside its title. It opens a card that says what the skill is, how a cycle actually resolves, what is lifting it in this save — the tool's cut, the oath, the worn capes, the hall's Hearth, the trader's second look, whatever doubles the haul — and how best to climb it, ending with the best xp an hour open to the hero right now and the next thing a level opens. Every number is read from the functions the sim rolls with (src/ui/derive-help.ts), so the panel cannot drift from the game; only the prose is written down (src/ui/screens/help.ts). The combat screen's is the fight's, and covers both styles at once.

Phase 19 is the ring: names fight names. Step in (opt in, both ways, like the open road) and any name in the ring may call you out for one thing you are wearing — and you them. You put up whatever you wear in the same slot, and it must be worth at least as much. The register fights it, not either client: it loads both stored saves, works out both fighters, draws a seed and runs fightBout (src/sim/bout.ts), which the screen then runs again from the same seed to draw the blows, so the replay cannot disagree with the verdict. Neither of you has to be online. The ring takes the numbers as worn — levels and gear — and nothing from the bank: no food, no favour, no boon. What changes hands is a numbered settlement collected on the loser's own next save, so a bout never makes anyone's open tab go stale; a debt whose item has been sold costs twice its worth in coin and is carried, never forgiven. A new highscores board ranks the ring. Save v15 adds it.

A note on trust, honestly. The register decides every fight, so no client can claim a win, and it counts a name's record off its own settlements rather than off the save, so the ring's board cannot be written to directly. What it still takes on the save's word is the gear a fighter fields — though since the ceiling landed (src/sim/ceiling.ts), a save can no longer claim to have got there faster than the clock allows. Because levels clamp at 99 and gear stats are content ids, the strongest fighter a forged save can present is one a legitimate save can also field; a cheat can reach the top of the ring early, but cannot invent a fighter nothing can beat. Re-simulating saves server-side is still the fix for all of it, and still a phase of its own. Play among friends.

The hill is live at game.onyxleeds.co.uk: one Ubuntu box running dist-server/main.js under systemd behind Caddy, the name proxied through Cloudflare, the register backed up daily (deploy/). scripts/deploy.sh builds and ships it.

Layout

Path Layer
src/sim/ Pure simulation: save schema, migrations, PRNG, actions, skills, highscores.ts (what a standing is), hall.ts (gifts and the rooms' perks), bout.ts (the ring: the fight, and what it settles). No DOM.
src/content/ Game content as JSON (skills, gods, materials, rarities, items, rocks, trees, waters, recipes, zones, monsters, drop tables), validated at load; progression.test.ts pins hours-to-99.
src/runtime/ Browser orchestration: save stores (IndexedDB and the server), leader election, channel, GameHost.
src/api/ The wire: protocol.ts (zod schemas both ends import) and client.ts (the game's calls).
src/look/ A look: the painted picture a name or a hall shows — the palette (the design's colours, append-only), the 16×16 paint, the shapes, the schema, flood fill and pressing a shape into the paint. Shared by both ends; no DOM.
server/ The register: db.ts (SQLite schema), auth.ts (scrypt, sessions, cookies, rate limits), register.ts (SQL), hall.ts (the clans), bout.ts (the ring: bouts, escrow, the debt ledger), chat.ts (the fire: rooms, words between names, the long poll), app.ts (routes + static files), main.ts (production), vite.ts (dev).
src/ui/ React: Shell, screens/, overlays/, derive.ts (pure view helpers), useChat.ts (the tab's one poll at the fire), app.css (the design's classes over the tokens).
src/icons/ Icon registry, SVG renderer + cache, badge glyphs, procedural sword geometry.
src/ui/theme/ Design tokens (CSS custom properties + TS object) and fonts.
src/ui/items/ Maps content onto renderer specs; <ItemTile> / <BareIcon>.
design/ Claude Design reference screens the tokens were extracted from.
deploy/ The box: setup.sh (node, Caddy, the anamnesia user, the service, the daily backup), the systemd units, the Caddyfile. scripts/deploy.sh runs it and ships builds.
scripts/ Icon vendoring / indexing pipeline; tune-combat.ts retunes monsters against the progression model; tune-boons.ts compares the gods' boons; tune-gear.ts prices javelins; tune-trader.ts prices the wares by income; tune-hall.ts/tune-wheel.ts price rooms and chips in hours of work; tune-sorcery.ts sets the sword and the staff side by side.

Commands

Command What it does
npm run dev Vite dev server with the API mounted (register in data/dev.sqlite). Game at /, icon browser at /dev/icons.html, item contact sheet at /dev/items.html.
npm run build Typecheck + production build of the game (dist/) and the server (dist-server/main.js).
npm start Serve dist/ and the API from one port. PORT (8787), ANAMNESIA_DB (data/anamnesia.sqlite), ANAMNESIA_STATIC (dist). Needs Node 24+.
npx tsx scripts/reset-password.ts Change a name's password from the command line; there is no email, so this is the only way back.
scripts/deploy.sh [setup] Build and ship to the box named in deploy.local (git-ignored: DEPLOY_HOST, PLAY_HOSTNAME); setup prepares a fresh box first.
npm test Vitest, single run.
npm run typecheck / lint / format The usual.
npm run icons:vendor -- --refresh Re-fetch vendor/game-icons (checked in) at the commit pinned in scripts/game-icons.lock.json.
npm run icons:index Rebuild src/assets/icon-index.json from the vendored SVGs.
npm run icons:ship Rebuild the shipped icon subset and ATTRIBUTION.md (runs automatically before dev/build).

Icons

All icons come from game-icons.net (CC BY 3.0 / CC0). Icon ids are author/slug, e.g. lorc/broadsword. To ship a new icon, reference it from content ("icon": "…") or add it to content/icon-manifest.json; ATTRIBUTION.md is regenerated from what ships. Curated search tags live in content/icon-tags.json.

About

Anamnesia Idle — a browser idle RPG with a generated item & icon pipeline

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages