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
This repository has no app yet.
following cheffryjames/cheffry-s-dungeon in step
Everything here is up to date with cheffryjames/cheffry-s-dungeon.
Proposes updates from cheffryjames/cheffry-s-dungeon.
main
- content
- contract
- design
- scripts
- src
- static
- .gitignore 43 B · ~13 tok
- AGENTS.md 7.4 KB · ~1.9k tok
- contract.json 2.3 KB · ~797 tok
- forge.json 137 B · ~46 tok
- package.json 514 B · ~172 tok
- README.md 14.0 KB · ~3.6k tok
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.
| file | what it is |
|---|---|
src/actions.ts | notes, avatars, delves, heroes, runs, pantries, spices, wallets, trades; the audit trail, read |
src/sim.ts | the 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.ts | the square in memory: who stands where, emotes, chat |
src/delves.ts | the live instances, their parties, ticking; runs and ghosts |
contract.json | who may do what: roles, and the least role each verb needs |
src/contract.ts, src/can.ts | the table, read at boot; the forge's evaluator, verbatim |
src/security.ts | the door: rationing, same origin, the table before the body |
src/audit.ts | every change and every refused one, as a row |
src/who.ts | who is asking, from the forge's signed headers |
src/db.ts | SQLite under FORGE_APP_DATA, WAL, forward-only migrations |
src/live.ts | the in-process bus and GET /events |
src/mcp.ts | the actions as MCP tools at POST /mcp |
src/routes.ts | every path, in one table, each API route described beside it |
src/openapi.ts | the routes as OpenAPI; committed at contract/openapi.json |
src/pages/ | the HTML — hub, delve, layout — rendered on the server |
design/tokens.json, src/tokens.ts | the look as data, light and dark; compiled to /tokens.css |
src/http.ts | the floor: headers, body cap, files |
src/server.ts | boot: open the database, serve |
src/tty.ts | the terminal, over the same actions |
src/env.ts | what the forge told this process |
static/app.css, static/live.js | the look in the tokens' words, and the list that stays current |
static/gl.js | a small hand-rolled WebGL renderer for the low-poly scene |
static/hub.js, static/delve.js | the hub and delve clients: input out, snapshots in |
What every app stands on
- The door. Before a route sees a request,
src/security.tsrations
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.jsoniscontract/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 /healthzanswers{ ok, version, uptime }; the
forge waits on it before it sends anyone to a deploy.
- The look.
design/tokens.jsonholds 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.