cheffry-james/cheffry-s-dungeon

Public: anyone can read this; nobody can change it.

Cheffry's Dungeon is a low-poly 3D multiplayer dungeon adventure engineered to feel massive on minimal infrastructure. A persistent social hub syncs only positions, emotes, and chat; dungeon delves run as small 4-player instances that scale horizontally; asynchronous systems — daily seeded runs, leaderboards, ghost data, player graffiti — deliver the massively-multiplayer feel without the server bill. Tick-based authoritative networking, fully data-driven content (rooms, monsters, traps, loot), and low-poly art keep bandwidth tiny and iteration fast. The v1 foundation: explorable hub, melee combat with dodge, three enemy types, loot chests, and party play — every system built content-pipeline-ready for rapid expansion.

forked from cheffryjames/cheffry-s-dungeon

workbenchthis machine
Sends offers to cheffryjames/cheffry-s-dungeon · last took 19 days ago

This repository has no app yet.

Everything here is up to date with cheffryjames/cheffry-s-dungeon.

Proposes updates from cheffryjames/cheffry-s-dungeon.

README.md

cheffry-s-dungeon

Cheffry's Dungeon is a low-poly 3D multiplayer dungeon adventure engineered to feel massive on minimal infrastructure. A persistent social hub syncs only positions, emotes, and chat; dungeon delves run as small 4-player instances that scale horizontally; asynchronous systems — daily seeded runs, leaderboards, ghost data, player graffiti — deliver the massively-multiplayer feel without the server bill. Tick-based authoritative networking, fully data-driven content (rooms, monsters, traps, loot), and low-poly art keep bandwidth tiny and iteration fast. The v1 foundation: explorable hub, melee combat with dodge, three enemy types, loot chests, and party play — every system built content-pipeline-ready for rapid expansion.

This is an app: the forge runs it. Production follows the default branch; every open pull gets a preview at its own address with its own copy of the data. Everything the app keeps lives under FORGE_APP_DATA, which is what makes "production data" and "preview data" two different things.

PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts   # run it here
bun test                                                # what `forge.json`'s app.test runs on every push
FORGE_APP_DATA=./data bun run src/tty.ts                # the terminal
FORGE_APP_DATA=./data bun run src/db.ts --check         # schema version, integrity
bun run lint:design                                     # no colour but the tokens'
bun run contract:emit                                   # rewrite contract/openapi.json from the routes

app.test in forge.json runs beside app.run in every deploy, not before it: the preview comes up the moment the app answers, and says live, or serving · tests failed the moment the suite disagrees.

How to play

  • The hub (/). Sign in, step into the square and you get an apron.

Tap the floor (or use the arrow keys) to walk; emote, chat. Only positions, emotes and chat cross the wire — a walk is sent once, as a target, and every client slides the avatar there itself.

  • A delve (/delve/<id>). Open a table or join one: up to four chefs a

party. First pick a class in Cheffry's dressing room on the hub — Knight (sword, sturdy), Archer (bow, quick), Mage (spellbook, glass cannon) — each with its own dodge and a signature move on E. Click to walk, click a monster to fight it (or Space/J for the nearest), right-click / Shift+click / K / Shift to dodge through a hit. WASD or arrows steer. A downed chef gets back up if a friend stands over them. Clear five rooms of the day's dungeon to win.

  • Three enemies, three brains — the Meatball Slime (chases), the

Baguette Knight (winds up and charges), the Pepper Imp (keeps its distance and shoots) — plus Skewer Spikes and chests of coins, truffles, soup and the Whisk of Power.

  • Cheffry's Spice Rack (day 4 — the room went quiet, so the chef picked

this one himself). Monsters drop ingredients for whoever finishes them — Ghost Pepper from Pepper Imps, Slime Jelly from Meatball Slimes, Stale Crust from Baguette Knights — and chests hold Rock Salt and Torch Mushrooms. They land in your pantry when you leave the table. At the rack on the hub you cook them into throwables: - Pepper Bomb (ghost pepper + slime jelly): a sneezing cloud; monsters in it flail at each other for a few seconds. - Salt the Earth (rock salt + stale crust): a salt line across your throw; burns Baguette Knights (stale bread is the undead here), slows everything else. - Hot Sauce Flask (torch mushroom + ghost pepper): a burning puddle; an arrow flying through it catches fire and hits harder.

You carry up to three jars into a delve (one of each kind first) and only the ones you throw are spent. In a delve, Q (or the Spice button) picks a jar and a click or tap throws it along the arc shown; Enter lobs it ahead, Esc puts it away. The next spice is yours to call — tell the chef what to cook.

  • Cheffry's Bread Vault (day 5 — the coin flip got no calls, so the

chef flipped it himself and kept the bread). One currency, one number beside your name. Every room you clear while at the table pays 3 loaves, being there when the party walks out of the last room pays 15 more, and the Bread Exchange counter on the hub buys pantry ingredients (2–3 loaves apiece) and sells spice jars (16–20 loaves — cooking them yourself is still cheaper, so selling is for leftovers). Each class gets a flavour perk, never a gate: the Knight's hero's cut (+25% of a delve's earnings), the Archer's bonus crumbs (a loaf a monster they finish) and the Mage's transmutation (ingredients that won't fit in the pantry become loaves instead of nothing). Your ledger shows every loaf in and out. Behind the bar the Bread Vault stays shut — chalked "THE CHEF'S BREAD. HANDS OFF." — with the Bread Barons on the wall beside it: CHEFFRY at 1,000,000 loaves, then the biggest hoards, live. The first boss (option B) is still on the menu for the next vote.

  • The daily board. Everyone gets the same seed each UTC day. Finished

runs land on the leaderboard with a ghost (the party's path, sampled every ten ticks); your next party races the best one on screen.

  • The graffiti wall is the notes: sign it and it stays, live for

everyone in the hub.

Content is data

content/*.json is the game: rooms.json (layouts, spawns, traps, chests), monsters.json, traps.json, loot.json, classes.json, spices.json (ingredients, recipes, the throw, and what each zone does), bread.json (the currency, what the Exchange pays and asks, what a delve earns, each class's perk, the Bread Barons board and the vault door's chalk), rules.json (every tuning number, in tiles and ticks) and hub.json. src/content.ts checks every file whole at boot and refuses to start on a bad one, naming the field. A new room or a new monster built from an existing brain is a JSON edit and nothing else; so is a new spice that mixes the effects the sim knows (confuse, slow, damage/burns, ignite) in a circle or a line. A new price, a re-tuned payout, or a class perk mixing cut, crumbs and transmute is an edit to bread.json; a new ingredient or spice becomes tradeable the moment it has a price there.

The networking

A delve is src/sim.ts: a pure, seeded, 20 Hz tick with no imports, so the same seed and the same orders make the same run anywhere. The server owns every instance (src/delves.ts) and is the authority; clients send orders (move, attack, dodge, ability, throw, stop) and receive snapshots over /delves/<id>/events. The page loads the very same sim as /sim.js to lay out rooms. Instances share nothing but the database, so they would split across processes by delve id when one process is no longer enough — v1 keeps them in one.

The shape

One implementation, many doors. src/actions.ts is every operation as a plain function — (db, who, input) in, data or a named failure out — and the page, the JSON API, the MCP server and the terminal all call it and decide nothing themselves. AGENTS.md says how to add to it.

filewhat it is
src/actions.tsnotes, avatars, delves, heroes, runs, pantries, spices, wallets, trades; the audit trail, read
src/sim.tsthe delve: a pure seeded tick — classes, shots, abilities, brains, thrown spices and their zones
src/content.ts, content/the game as JSON, checked whole at boot
src/hub.tsthe square in memory: who stands where, emotes, chat
src/delves.tsthe live instances, their parties, ticking; runs and ghosts
contract.jsonwho may do what: roles, and the least role each verb needs
src/contract.ts, src/can.tsthe table, read at boot; the forge's evaluator, verbatim
src/security.tsthe door: rationing, same origin, the table before the body
src/audit.tsevery change and every refused one, as a row
src/who.tswho is asking, from the forge's signed headers
src/db.tsSQLite under FORGE_APP_DATA, WAL, forward-only migrations
src/live.tsthe in-process bus and GET /events
src/mcp.tsthe actions as MCP tools at POST /mcp
src/routes.tsevery path, in one table, each API route described beside it
src/openapi.tsthe routes as OpenAPI; committed at contract/openapi.json
src/pages/the HTML — hub, delve, layout — rendered on the server
design/tokens.json, src/tokens.tsthe look as data, light and dark; compiled to /tokens.css
src/http.tsthe floor: headers, body cap, files
src/server.tsboot: open the database, serve
src/tty.tsthe terminal, over the same actions
src/env.tswhat the forge told this process
static/app.css, static/live.jsthe look in the tokens' words, and the list that stays current
static/gl.jsa small hand-rolled WebGL renderer for the low-poly scene
static/hub.js, static/delve.jsthe hub and delve clients: input out, snapshots in

What every app stands on

  • The door. Before a route sees a request, src/security.ts rations

writes (and a stranger's API calls), refuses a write from another site, and turns away — before the body is read — anyone the table lets through a resource's door for no row at all: 401 for a stranger, 403 for anyone else. The action still decides about the real row.

  • The audit trail. Every change, and every change refused, is a row:

who, which resource and verb, which row, when, and whether they were let. Admins read it at GET /api/audit. A row that cannot be written is a log line, never a failed request.

  • The API, described. GET /openapi.json is contract/openapi.json,

written from the routes by bun run contract:emit; each operation names its cell of the table (x-contract). bun test fails while it is stale.

  • Up, or not. GET /healthz answers { ok, version, uptime }; the

forge waits on it before it sends anyone to a deploy.

  • The look. design/tokens.json holds every colour, space and size,

light and dark; nothing else spells out a colour (bun run lint:design), and every pair a person reads meets 4.5:1 in both themes.

  • The open web. A shared link unfurls (description, Open Graph, a

card); /robots.txt keeps crawlers to the page, and out of previews entirely; /.well-known/security.txt sends a reporter to the forge that runs the app.

Who you are

The forge says who is asking on every request it proxies, and the app verifies the signature (src/who.ts); nothing else is trusted. Three tiers:

  • anon — anyone. Reads.
  • user — signed in to the forge and able to see this app. Stands in the

hub, opens and joins delves, writes on the wall — as themselves — and removes their own. Sees and spends only their own pantry, spice shelf and bread; trades at the Exchange. Everyone sees the Bread Barons; nobody writes a wallet except through a trade or a delve's earnings.

  • admin — able to write this repository. Removes any note or run.

"Sign in" is /__forge/signin?to=<path> and "Sign out" is /__forge/signout; the forge answers both on the app's own host.

Any interface

The same actions, the same rules, at every door. A script is the person whose forge grant it carries; a bare script is anonymous.

curl https://<app>/api/notes
curl -X POST https://<app>/api/notes -H 'content-type: application/json' -d '{"body":"hi"}'
curl -X DELETE https://<app>/api/notes/1
curl https://<app>/api/runs        # today's board, best first
curl https://<app>/api/delves      # open tables
curl https://<app>/api/pantry      # your ingredients (signed in)
curl -X POST https://<app>/api/spices -H 'content-type: application/json' -d '{"spice":"pepper-bomb"}'
curl https://<app>/api/wallets     # the Bread Barons
curl https://<app>/api/trades      # your ledger (signed in)
curl -X POST https://<app>/api/trades -H 'content-type: application/json' -d '{"kind":"sell","ingredient":"rock-salt","count":2}'
curl -X POST https://<app>/api/trades -H 'content-type: application/json' -d '{"kind":"buy","spice":"hot-sauce"}'

As an MCP server, for Claude Code or Claude Desktop — leaderboard, list_delves, list_notes, add_note, delete_note, spice_rack, craft_spice, bread_barons, bread_exchange, trade_bread:

claude mcp add --transport http cheffry-s-dungeon https://<app>/mcp
{
  "mcpServers": { "cheffry-s-dungeon": { "type": "http", "url": "https://<app>/mcp" } }
}

The page stays current over /events: a note someone else adds or removes appears without a reload. Without JavaScript it is the same page, one reload behind.

The terminal opens with the exact command in FORGE_APP_SSH when the app runs (ssh -p 2222 <name>@<forge> — the app is the login name, the forge the host; <name>--<pull>@<forge> for a preview's). It is src/tty.ts on a real pty over the same data, as the account at the keyboard.

The forge sets PORT, FORGE_APP_DATA, FORGE_APP_ENV (production or preview), FORGE_APP_URL, FORGE_APP_COMMIT, FORGE_APP_SSH and FORGE_APP_IDENTITY_KEY when it starts the app. forge.json says how.

↑↓ navigate ↵ open esc close

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs