Skip to content

Maps: turn the building into a castle (or a map of your own), with a throne, a line of waiting workers and peasants who age as they work - #184

Merged
webdevcody merged 14 commits into
mainfrom
office/cosmo-7a95
Sep 29, 2026
Merged

webdevcody merged 14 commits into
mainfrom
office/cosmo-7a95

Conversation

@webdevcody

Copy link
Copy Markdown
Contributor

Maps: the building can be something other than the office — starting with a castle

Under ⚙️ Settings → 🏢 Building → Map, anyone can change what the whole building looks like inside, for everyone on every floor, and back again. Everything that makes the office work comes along unchanged: workers and their terminals, the issues / PR / queue / services boards and their board agents, the task queue, meetings, the merge gong, PRs, budget and limits.

🏰 The castle

  • The hall. A long stone hall under a timber roof. It has:
    • clustered pillars with pointed arches down both sides;
    • stained glass high in the walls and a rose window;
    • banners in each project's colours, the great one behind the throne carrying the project's name;
    • chandeliers, torches, braziers and a hearth;
    • stone knights and suits of armour;
    • great arched doors, and a red carpet up the middle.
  • A throne of iron blades stands on a dais at the far end. You arrive seated on it, and stay on it across floor switches and reloads.
  • The line.
    • A worker that's done, or needs input, gets up from its table and walks to the line before the throne. Whoever has waited longest is at the front (the same order as N).
    • From the throne, E is for whoever's first in line; P, O, X and C work as at a desk.
    • Once it's been seen to, it walks back to its seat and the others shuffle up.
  • The Hand of the King stands at your left.
    • Speak to him with E beside him, or K from the throne. Say what a new worker should do, and it runs off to the first free seat at the tables. Everyone sees it run.
    • Hand him an issue card and he sends someone out for it.
    • Queue-hired workers come in through the great doors. Workers sent home walk out through them, box in arms.
  • Peasants who wear out.
    • Workers wear a smock, rope belt and coif.
    • The longer one has worked, the more spent it looks. This is cumulative working time, tracked by the server and kept through restarts.
    • It shows as a beard that grows from brown to grey to white and down to the floor, dirt, patched clothes, bushy brows, bags under the eyes, a hunch and a slower shuffle. It's complete after 30 minutes of work.
  • The rest of the hall:
    • Long tables with benches. Laptops become leather tomes that show the live terminal.
    • Scribes at lecterns under the boards on the side walls (the board agents).
    • A "small council" round table for meetings, with its easel board.
    • The gong by the dais, and ale by the hearth (it works like the office's coffee).
    • Its own warm, torchlit lighting (no rain indoors), and its own sounds.

Maps are configuration

  • A map is plain data (MapConfig, in src/shared/maps/). The castle itself is just src/shared/maps/castle.ts.
  • Your own maps. Drop a JSON file in the office's .agent-office/maps/. You can either:
    • extend a built-in map and change a few numbers, e.g. { "id": "my-hall", "name": "My hall", "extends": "castle", "boards": { "issues": { "z": 3 } }, "stations": { "issues": { "z": 3 } } }; or
    • start from docs/maps/castle.json, a full copy of the castle to edit, which a test keeps in sync.
  • Loading and errors.
    • The folder is re-read whenever someone opens Settings or joins, so no restart is needed.
    • A map that won't load is listed with the reason: bad JSON, too few seats, something outside the hall or inside furniture, a prop reaching over the walls, an unknown prop kind, extending itself, and so on.
    • If the building was on it, the building falls back to the office and returns once the file is fixed.
  • Seats are map-agnostic. Every map places the office's own seat ids (16 desks, 12 overflow seats, the 3 board agents, 5 meeting chairs). So the server, the queue, meetings and saved workers work on any map, and a map can change while workers are working.
  • Adding a new style takes no main.ts changes. Register a builder in src/client/world/styles.ts that returns a World. New prop kinds go in an exhaustive table, so a missing builder won't compile. Details are in docs/maps.md.

How it's built

  • src/shared/maps/:
    • the schema, the castle, and the prop kinds (footprints, heights);
    • planMap, which validates every field the builder reads and works out where every seat, lectern, line spot and board goes, plus what's in the way;
    • extends resolution and custom-map checking.
  • src/shared/nav.ts: refactored into a NavGrid class, so any map gets its own walk grid. The office's behaviour and API are unchanged (checked against the old version on 12k random calls).
  • src/server/maps.ts:
    • the building's pick, kept in .agent-office/map.json;
    • map.set / map messages;
    • seats are validated against the current map;
    • a taken seat is refused (sit.refused).
  • src/server/workers.ts: keeps workedMs / workingSince for every worker.
  • src/client/world/world.ts: the World interface that main.ts uses (seats, boards, colliders, walks in and out, confetti, room, sounds, lighting, herald, dispose). The office is adapted to it.
  • main.ts: swaps worlds at runtime, the way it already swaps in the rooftop. Office-only features (elevator, garage, roof, ladder and poles, balcony, jukebox, dog, pictures, basketball) are gated to the office.
  • The castle, client side:
    • src/client/world/castle.ts, the builder;
    • court.ts, workers walking between their seats and the line;
    • the peasant outfit and ageing on Worker;
    • a tome style for the laptop;
    • a placeable gong;
    • Sky.setIndoors;
    • map-aware camera bounds and sounds.

Reviews

Three review rounds, each with independent reviewers, and every finding fixed:

  • Round 1: the main.ts integration, the castle/court/looks code, and the shared maps and server code.
  • Round 2: multiplayer and lifecycle edge cases, a fresh bug hunt over the whole diff, and architecture against the "new maps without code changes" goal.
  • Round 3: checked that the fixes were correct, that the office didn't regress, and walked through the original brief end to end.

Notable fixes from the reviews:

  • Hardening against custom maps that could hang browsers.
  • Only one person on the throne at once.
  • New hires that never walked to their seats.
  • A ride / switchFloor loop.
  • Map changes mid-trip.
  • A Web Audio crash.

Testing

  • npm run typecheck, npm run build and npm test all pass (294 tests).
  • New tests/maps.test.ts covers:
    • every built-in map places every seat id with the same roles;
    • every castle seat can walk to the door and to every line spot, and back;
    • extends merging and null removal;
    • custom-map errors, including configs that would hang a browser;
    • the castle JSON copy staying in sync;
    • every map in the docs loading;
    • the work clock.
  • Headless-Chrome smokes against a scratch server checked:
    • arriving on the throne;
    • fake workers in every state walking to the line in order, and back when answered;
    • sending one home from the line (out through the doors);
    • hiring through the Hand (the worker runs from beside him), and K from the throne with a line;
    • E at an empty seat, the council table, a lectern, the ale and the gong;
    • two floors in the castle (the floor menu has no roof);
    • a custom map and a broken one;
    • switching office ↔ castle live (the office comes back as it was);
    • about 110–220 fps on the GPU.

🤖 Generated with Claude Code

webdevcody and others added 14 commits September 29, 2026 12:50
…ve its own walk grid

The office's grid is now OFFICE_NAV; walkable/route/nearestWalkable/wayIn/wayHome keep their
behavior and signatures. wayFrom/wayTo (off a seat to a point, and from a point to a seat) move
onto the class so any floor plan can use them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s working time

- shared/maps: MapConfig (plain data) checked and worked out into a MapPlan: seats at tables,
  board agents' lecterns, the meeting table, the boards, a throne, a line in front of it, the
  herald who hires, props and what's in the way for walking. Every map places the office's seat
  ids, so the server, the queue, meetings and saved workers work the same on any of them.
- The castle is the first one, all data (shared/maps/castle.ts).
- Custom maps: JSON in .agent-office/maps/, optionally extending a built-in one.
- server/maps.ts keeps the building's pick in .agent-office/map.json (map.set / map), and
  seats are checked against the map you're on.
- Workers keep count of how long they've worked (workedMs / workingSince).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- world/world.ts: what main.ts needs from a map's world (seats, boards, colliders, walks in and
  out, confetti, lighting), with the office adapted to it. main.ts shows one world at a time and
  switches when the building's map changes, like the office and the rooftop.
- world/castle.ts builds a castle-style map from its plan: a long hall, pillars and pointed arches,
  a timber roof, stained glass, a dais with a throne of iron blades, long tables and benches with
  tomes for laptops, lecterns for the board agents, a round table for meetings, the boards on the
  side walls, fire and candlelight, great doors, and the Hand of the King.
- world/court.ts: workers waiting on someone get up and line up before the throne, longest waiting
  first, and go back to their seats once answered; new ones sent out by the Hand run to their seat.
- On the throne, E is for whoever's first in line (or the Hand when nobody's waiting).
- Workers dress as peasants there and wear out as they work: a beard that grows long and grey,
  dirt and patches, a hunch, droopy eyes and a slower walk, fully after 30 minutes' work.
- Settings > Building > Map picks the map for everyone, and lists your own maps (or why they won't load).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d of loading

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he doorway

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, the gong and the windows where the hall has them, ale poured not ground

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A custom map is checked for everything its builder reads: sizes in range, whole numbers of
  seats, steps and spots in line, known table sides, colors as text, at most 40 tables, 12 seats
  a side and 400 props, and an id that's already trimmed. A zero-width window or a dais with a
  billion steps no longer hangs every browser.
- null takes away a map's throne, herald or line; with no dais the throne's on the default one,
  at the height the plan says.
- mergeConfig skips __proto__/constructor/prototype keys.
- The map changing by a read of the folder (a picked map breaking or coming back) is told like a
  pick: everyone's seats are forgotten on the server and in every browser, and a toast says why.
  A failed pick still tells everyone what the read found; 'picked by' only shows for the map in effect.
- Nobody sits where someone on the floor already is: the office refuses it (sit.refused), so two
  people arriving at an empty throne at once don't both sit on it.
- Past the folder's first 24 maps, each is listed with why it isn't read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A worker sent out by the Hand (or in off the queue) really runs to its seat: it was left
  'seated' where it came in, so it never set off, couldn't dance and left from the wrong place.
- Arriving (or reconnecting) on the castle no longer replays every worker walking in: the map
  switch put seatedAlready back to false in the middle of the welcome.
- A worker that left the line is its seat's again for a click (the spot's interactable stayed
  on it, and could open another worker's terminal); off interactables aren't aimed at.
- Up on the roof when the map changes to one without it: down to a floor, retried if the trip
  fails, and picking a floor meanwhile no longer loops ride/switchFloor forever.
- N (or Go to desk) to the worker at the front of the line sits you on the throne if it's free,
  else beside it, never inside the throne.
- The great banner gets the project's name; the office's basketball can't be picked up there;
  syncStack doesn't put the street back under the hall; walking to someone on another floor
  keeps walking.
- Getting up faces the way it was (no flip on a seat turned past a right angle), footsteps and
  strides go with the pace (running, shuffling), the tome's cover shuts over its pages, the gong
  and the herald stand on a raised floor, flames share their shapes, and an edited map of your
  own frees what it was built of.
- planOf answers from its cache without stringifying the config each time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… nothing of the castle

Review round 2, how it's put together:
- world/styles.ts maps each style (MAP_STYLES) to its builder; main.ts builds whatever a plan's
  style is. The herald, the room (wall thickness, roofed), the acoustics and dispose are World's,
  so there's no Castle type or cast left in main.ts.
- The plan carries the tables and the meeting table as checked, defaults filled in and the sides
  with seats worked out, so a builder reads them rather than working them out again.
- buildCastle is split into the shell, the dais, the tables and places, the council, the boards
  and the herald; each prop kind is an entry of an exhaustive PROPS table (a new kind won't
  compile without its builder).
- The herald's question and button, and the throne's label, are the map's (the castle keeps its
  'my liege'); the fire glow reuses the costumes' texture; planMap takes unknown.
- docs/maps/castle.json is the castle as a map of your own to copy (a test keeps it in step with
  castle.ts), and docs/maps.md has a map from nothing (tested to load), what's required, what
  happens when a map breaks, what it can't do yet, and adding a style.
- The toast says when a map of your own came back as well as when it broke.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The map changing while you're on your way to a floor (elevator, floor list, ladder) puts you
  where the new map has you come in once you land (or if the trip fails), instead of at your old
  spot, which could be outside the castle's walls. Arriving on the throne waits until you're there.
- A saved spot outside a hall of its own isn't come back to on a reload.
- Holding the basketball when the office goes: it leaves your hands, your character's and
  everyone else's; Minesweeper and the arcade cabinet stop.
- Refused a seat, you're on your feet for everyone, not left on the one you had.
- Workers sent out from the herald are remembered per floor and for half a minute, a second one
  goes to the next free seat rather than the same one, and the map changing forgets them.
- Only walking over to someone keeps walking through a floor switch; a walk to someone on a roof
  the map doesn't have stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ale at the cask no longer schedules the unused grinder in the past (a RangeError just after
  the sound starts); the pour just starts.
- N / Go to desk on a map of its own lands on open floor, not in a pillar; to a worker still on
  its way to the line, at its spot in the line.
- The round meeting table's collider fits inside its top.
- Court keeps the line's interactables (and the clickable models of who's in it) up to date when
  someone takes or leaves a spot, not every frame, and isn't raycast twice.
- Boards, their windows and the arcade name seats the way the map you're on does.
- A map change on a ride down to the garage is caught when the doors open.
- A map's benches and board agents have to fit in its hall too.
- Tests: the default dais, and every walk back to a seat as well as away from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- K on the throne speaks to the Hand of the King whoever's in line, and the throne's hint says
  so (and how to get up). Hand him an issue card and he sends someone out for it. His hint says
  when the office is full or the budget's spent.
- A worker sent out by the Hand runs from beside him for everyone, not just whoever sent it
  (worker.spawn / WorkerInfo via: 'herald'); a second one goes to the next free seat.
- You stay on the throne through a floor switch and a reload (if nobody's taken it).
- No stained-glass window inside the hearth's chimney; the docs' example moves the issues board
  and its scribe to an empty bay; a lectern's rotY is documented the way it works.
- A map's line, herald, spawn and door have to be clear of its furniture.
- The toasts name a map as it's called (🏰 Castle), not 'the the barn'.
- 📚 Docs in the ☰ menu off the office, where there's no bookshelf.
- A reconnect's old connection doesn't refuse you your own seat; the fridge doesn't clunk in the
  hall; the elevator doors open when a map change catches you arriving; a failed trip off a roof
  the map doesn't have is tried again; the boards take the new map's seat names at once; Go to
  desk never lands outside a hall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e maps branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@webdevcody
webdevcody merged commit a872450 into main Sep 29, 2026
1 check passed
webdevcody added a commit that referenced this pull request Sep 29, 2026
…) into the desk signs branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant