From ec7b82bd815f5a777531728b710621d5be88896e Mon Sep 17 00:00:00 2001 From: Anwar Selo Date: Sat, 3 Oct 2026 11:04:13 +0400 Subject: [PATCH 01/35] =?UTF-8?q?Checkpoint:=20realism=20pass,=20caf=C3=A9?= =?UTF-8?q?=20rooftop,=20garden,=20ambience,=20chess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- docs/code-layout.md | 2 +- docs/features.md | 5 +- docs/how-it-works.md | 2 +- docs/maps.md | 2 +- src/client/core/arrival.ts | 8 +- src/client/core/context.ts | 2 + src/client/core/ctx.ts | 3 + src/client/core/loop.ts | 8 +- src/client/core/maps.ts | 2 +- src/client/core/post.ts | 53 ++ src/client/core/scene.ts | 30 +- src/client/core/travel.ts | 7 +- src/client/features/ambience/engine.ts | 142 +++ src/client/features/ambience/forest.ts | 155 ++++ src/client/features/ambience/index.ts | 41 + src/client/features/ambience/noise.ts | 137 +++ src/client/features/ambience/rig.ts | 99 +++ src/client/features/ambience/ui.css | 20 + src/client/features/ambience/ui.ts | 64 ++ src/client/features/ambience/water.ts | 103 +++ src/client/features/bar/booze.ts | 9 +- src/client/features/bar/drunk.ts | 1 + src/client/features/bar/index.ts | 180 ++-- src/client/features/bar/sound.ts | 159 +--- src/client/features/bar/ui.ts | 91 +- src/client/features/bargames/world.ts | 8 +- src/client/features/basketball/world.ts | 2 +- src/client/features/bookshelf/book.ts | 4 +- src/client/features/carrying/card.ts | 2 +- src/client/features/chess/board.ts | 455 ++++++++++ src/client/features/chess/controller.ts | 228 +++++ src/client/features/chess/index.ts | 47 + src/client/features/chess/pieces.ts | 202 +++++ src/client/features/chess/ui.css | 3 + src/client/features/chess/world.ts | 178 ++++ src/client/features/dog/coat.ts | 2 +- src/client/features/dog/world.ts | 4 +- src/client/features/golf/world.ts | 7 +- src/client/features/hud/index.ts | 2 +- src/client/features/jukebox/sound.ts | 2 +- src/client/features/peers/index.ts | 8 +- src/client/features/rooftop/cafe-seating.ts | 82 ++ src/client/features/rooftop/cafe.ts | 311 +++++++ src/client/features/rooftop/garden.ts | 264 ++++++ src/client/features/rooftop/garden/ground.ts | 85 ++ src/client/features/rooftop/garden/leaves.ts | 262 ++++++ src/client/features/rooftop/garden/plants.ts | 250 ++++++ src/client/features/rooftop/garden/seats.ts | 51 ++ .../features/rooftop/garden/structures.ts | 88 ++ src/client/features/rooftop/index.ts | 25 +- src/client/features/rooftop/kit.ts | 46 + src/client/features/rooftop/world.ts | 833 ++---------------- src/client/features/workers/leaving.ts | 4 +- src/client/lab/props.ts | 2 +- src/client/main.ts | 13 +- src/client/sound/index.ts | 34 +- src/client/ui/elevator.ts | 6 +- src/client/ui/floormenu.ts | 6 +- src/client/ui/help.ts | 5 +- src/client/ui/people.ts | 2 +- src/client/ui/settings.ts | 10 +- src/client/ui/whereabouts.ts | 10 +- src/client/world/castle/kit.ts | 2 +- src/client/world/castle/props.ts | 2 +- src/client/world/castle/textures.ts | 2 +- src/client/world/character/index.ts | 2 +- src/client/world/character/person-hair.ts | 2 +- src/client/world/character/person.ts | 20 +- src/client/world/character/props.ts | 93 +- src/client/world/character/rig.ts | 4 +- src/client/world/character/serving.ts | 182 ++++ src/client/world/character/worker.ts | 4 +- src/client/world/city.ts | 7 +- src/client/world/costumes.ts | 10 +- src/client/world/hand/fire.ts | 20 + src/client/world/hand/pose.ts | 60 ++ src/client/world/hand/rig.ts | 248 ++++++ src/client/world/hands.ts | 134 +-- src/client/world/holiday.ts | 9 +- src/client/world/office/balcony.ts | 2 +- src/client/world/office/build.ts | 2 +- src/client/world/office/loft.ts | 2 +- src/client/world/office/materials.ts | 4 +- src/client/world/outside.ts | 16 +- src/client/world/scenic/kit.ts | 4 +- src/client/world/scenic/water.ts | 4 +- src/client/world/sky.ts | 10 +- src/client/world/station/textures.ts | 2 +- src/client/world/toon.ts | 29 +- src/client/world/tower.ts | 33 +- src/server/chess.ts | 103 +++ src/server/office/context.ts | 2 +- src/server/office/navigation.ts | 2 +- src/server/office/views.ts | 2 +- src/server/ws/handlers/chess.ts | 107 +++ src/server/ws/handlers/index.ts | 4 +- src/server/ws/handlers/presence.ts | 6 +- src/server/ws/handlers/rooftop.ts | 3 +- src/shared/bargames.ts | 2 +- src/shared/chess-seats.ts | 40 + src/shared/chess.ts | 309 +++++++ src/shared/garden.ts | 21 + src/shared/layout.ts | 51 +- src/shared/protocol.ts | 8 +- src/shared/protocol/chess.ts | 52 ++ src/shared/protocol/presence.ts | 14 +- src/shared/protocol/rooftop.ts | 8 +- src/shared/rooftop.ts | 88 +- src/shared/theme.ts | 6 +- tests/ambience.test.ts | 70 ++ tests/chess-board.test.ts | 134 +++ tests/chess-pieces.test.ts | 31 + tests/chess-tables.test.ts | 193 ++++ tests/chess.test.ts | 214 +++++ tests/size.test.ts | 1 - tests/whereabouts.test.ts | 8 +- 117 files changed, 5739 insertions(+), 1519 deletions(-) create mode 100644 src/client/core/post.ts create mode 100644 src/client/features/ambience/engine.ts create mode 100644 src/client/features/ambience/forest.ts create mode 100644 src/client/features/ambience/index.ts create mode 100644 src/client/features/ambience/noise.ts create mode 100644 src/client/features/ambience/rig.ts create mode 100644 src/client/features/ambience/ui.css create mode 100644 src/client/features/ambience/ui.ts create mode 100644 src/client/features/ambience/water.ts create mode 100644 src/client/features/chess/board.ts create mode 100644 src/client/features/chess/controller.ts create mode 100644 src/client/features/chess/index.ts create mode 100644 src/client/features/chess/pieces.ts create mode 100644 src/client/features/chess/ui.css create mode 100644 src/client/features/chess/world.ts create mode 100644 src/client/features/rooftop/cafe-seating.ts create mode 100644 src/client/features/rooftop/cafe.ts create mode 100644 src/client/features/rooftop/garden.ts create mode 100644 src/client/features/rooftop/garden/ground.ts create mode 100644 src/client/features/rooftop/garden/leaves.ts create mode 100644 src/client/features/rooftop/garden/plants.ts create mode 100644 src/client/features/rooftop/garden/seats.ts create mode 100644 src/client/features/rooftop/garden/structures.ts create mode 100644 src/client/features/rooftop/kit.ts create mode 100644 src/client/world/character/serving.ts create mode 100644 src/client/world/hand/fire.ts create mode 100644 src/client/world/hand/pose.ts create mode 100644 src/client/world/hand/rig.ts create mode 100644 src/server/chess.ts create mode 100644 src/server/ws/handlers/chess.ts create mode 100644 src/shared/chess-seats.ts create mode 100644 src/shared/chess.ts create mode 100644 src/shared/garden.ts create mode 100644 src/shared/protocol/chess.ts create mode 100644 tests/ambience.test.ts create mode 100644 tests/chess-board.test.ts create mode 100644 tests/chess-pieces.test.ts create mode 100644 tests/chess-tables.test.ts create mode 100644 tests/chess.test.ts diff --git a/README.md b/README.md index 0c8d672ba..77b8793c0 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ curl -fsSL https://raw.githubusercontent.com/AgentSystemLabs/agent-office/main/i - **Other maps.** Turn the whole building into a castle: sit on a throne of iron blades while your workers line up before you when they're done, send new ones off through the Hand of the King, and watch their beards grow long and grey as they toil. Send one home and the Kingsguard runs up from the dungeon, marches it down the stairs and throws it in a cell, where it starves, dies and rots down to a skeleton. Or into a space station in orbit, the Earth turning outside its windows: you run it from the captain's chair on the bridge, and a worker sent home is marched to the airlock and blown out into space, to drift off past the observation windows with everyone who went before it. Or make a map of your own, with its own way of seeing workers off in JSON ([docs/maps.md](docs/maps.md)). -There's a lot more (a rooftop bar, an office dog, an arcade, supercars in the garage to drive round a scenic loop past a farm, pines, mountains and a beach): see [docs/features.md](docs/features.md). +There's a lot more (a rooftop café, an office dog, an arcade, supercars in the garage to drive round a scenic loop past a farm, pines, mountains and a beach): see [docs/features.md](docs/features.md). ## Requirements diff --git a/docs/code-layout.md b/docs/code-layout.md index d76e3d42a..6d1edbb20 100644 --- a/docs/code-layout.md +++ b/docs/code-layout.md @@ -25,7 +25,7 @@ They're in `core/registry.ts`, and each is a field of `ctx`. Every registration | `ctx.ticks` | What runs each frame. `add(phase, fn)`, where the phases run in `TICK_PHASES` order: `pre`, `steer`, `vehicles`, `move`, `moved`, `play`, `me`, `others`, `world`, `env`, `aim`, `hud`, `render`. Within a phase, ticks run in install order. | | `ctx.activities` | Something you're in the middle of that takes over the controls (the ladder, the golf tee, a car). It gets keys before the office's own, draws the hint bar, and is stopped by `stopAll(why)` when you start something else. Its place among the others is `ACTIVITY_ORDER` in `core/ctx.ts`. | | `ctx.interactions` | What each kind of thing you can use does: `define(kind, { reach, hint, use })`, once per kind. | -| `ctx.view` | What what you're doing does to your view each frame: something you hold on to, the field of view, covering the screen, or a filter (the drunk vision). | +| `ctx.view` | What what you're doing does to your view each frame: something you hold on to, the field of view, covering the screen, or a filter. | | `ctx.windowOpened` | What lets go when a window opens (a shot being wound up, the emote wheel). | | `ctx.usables` | Things to use that aren't part of the building and move about (the pictures, the dog, the ball): what each has to use, and what the aim can land on. | diff --git a/docs/features.md b/docs/features.md index f320d9b99..dbd4185d3 100644 --- a/docs/features.md +++ b/docs/features.md @@ -65,8 +65,9 @@ Everything in the office, room by room. Back to the [README](../README.md). - **Office sounds.** Busy workers clatter away at their keyboards, footsteps knock across the planks (and tick on the garage's concrete, swish through the grass and echo off the castle's flagstones, quicker and harder at a run), the fridge hums, birds chirp outside the windows by day and crickets at night, rain patters on the glass, thunder rolls in a storm, the coffee machine grinds and gurgles, the gong booms when a PR merges and the dog barks. It's all synthesized in the browser and placed where it happens, so it gets louder as you walk closer. Turn it down or mute it under **⚙️**, which also covers the worker dings but not voice chat. - **Arcade cabinet.** Workers take minutes, so there's an arcade cabinet in the lounge, next to the jukebox. Press **E** at it and the camera glides up to its screen to play BLOCKFALL, a falling-blocks game: arrows (or WASD) move and turn, **Space** drops, **C** holds a piece for later, **P** pauses. Everyone else on your floor sees your game on the cabinet as you play, and anyone can walk up and press **E** to watch it up close over your shoulder. When one of your workers needs input, the game pauses and says which one, with a button to its terminal. Walk away and your game waits, paused, until you come back. The high-score table is the whole building's: it shows on every floor's cabinet, and it survives restarts. - **Basketball.** Every floor has a hoop on the west wall, by the exit door, with a key painted on the floor in front of it and a ball waiting underneath. Walk up to the ball and press **E** to pick it up. Hold **E** (or the mouse button, in first person) to wind up: a meter runs up and down, and letting go in its green band sinks it, while a little late banks in off the glass and too early or too late misses. In first person the ball goes where you look, so put the crosshair on the rim. In third person your character squares up to the hoop when you're facing roughly its way. **Q** drops the ball. Everyone on the floor sees the ball in your hands and the same shot fly, bounce off the rim and swish through the net, and grab the rebound (**E**, even in mid-air). A basket from past 6.75 m is worth three, and you're told how many you've sunk in a row. The ball stays on its floor: take the elevator with it and it goes back under the hoop. -- **The rooftop bar.** The elevator goes up to the roof, over every floor. A DJ plays drum and bass on a stage under a rig of moving lights and lasers, with an LED wall behind and a dance floor lighting up in front, all flashing in time with the music. The Sky Bar is under a pergola hung with string lights, there's a lounge round a fire pit, and sun loungers look out over the street. The city goes on all around, and its windows, street lamps and traffic light up at night. Press **E** at the bar (or sitting on a stool at it) for a drink: a beer, wine, a cocktail or a shot goes to your head for a minute or so, and the view sways, doubles and smears while you stagger a little. The more you have, the worse it gets, until the bartender pours you a water. Everyone up there sees the glass in your hand and hears the same bar of music. Press **E** at the DJ booth to blow the air horn for all of them. -- **Darts and axe throwing.** In the rooftop bar's north-west corner, past the DJ, there's a wooden axe-throwing booth with a target on its back wall, and a dart board in a cabinet on the outside of it. Walk up to either and press **E** to step up to the line: the camera looks over your shoulder at the target. Move the mouse (or use the arrow keys) to aim; your hand wanders a little, and more after a few drinks from the bar. Hold **Space** (or the mouse button) to draw back while a meter runs up and down, and let go in its green: a dart flies true, too soft and it drops, too hard and it sails high. An axe let go in the green turns over just right and sticks; off it, it comes in handle first, bounces off and clanks onto the mat. A visit is three darts (180 is the most) and an axe round five throws, scored 1 to 4 in the rings, 6 for the bullseye and 8 for either blue killshot dot. Everyone up there sees you throw, the darts and axes stuck in the target, and your round chalked up on the board beside it. Your best round at each is kept in your browser. **E** steps back from the line. +- **The rooftop café.** The elevator goes up to the roof, over every floor. It's a calm, alcohol-free café (and nothing on the menu has pork in it): a plaster and timber counter along the east edge with an espresso machine, a grinder, a glass case of croissants and a chalkboard menu, a barista in an apron, and a whitewashed pergola hung with warm string lights and pendant lamps. Round tables with a chair either side of each, tall tables to stand at and stools along the counter fill the middle, and sun loungers look out over the street. The city goes on all around, and its windows, street lamps and traffic light up at night. Press **E** at the counter (or sitting on a stool at it) for the menu: espresso, cappuccino, flat white, mint or black tea, fresh orange juice, lemonade, sparkling water or hot chocolate, and a butter or almond croissant, a pain au chocolat, a cheese and tomato toastie, avocado toast, a fruit cup, a bagel with cream cheese or a falafel wrap. The barista makes it and slides it over, and you hold it for a minute (a cup, a tall glass or a plate), sipping or nibbling. Everyone up there sees what's in your hand. The south-west corner and a patch against the north edge are kept clear for other things (`ROOF_CLEAR` in `src/shared/layout.ts`). +- **Darts and axe throwing.** In the rooftop café's north-west corner there's a wooden axe-throwing booth with a target on its back wall, and a dart board in a cabinet on the outside of it. Walk up to either and press **E** to step up to the line: the camera looks over your shoulder at the target. Move the mouse (or use the arrow keys) to aim; your hand wanders a little. Hold **Space** (or the mouse button) to draw back while a meter runs up and down, and let go in its green: a dart flies true, too soft and it drops, too hard and it sails high. An axe let go in the green turns over just right and sticks; off it, it comes in handle first, bounces off and clanks onto the mat. A visit is three darts (180 is the most) and an axe round five throws, scored 1 to 4 in the rings, 6 for the bullseye and 8 for either blue killshot dot. Everyone up there sees you throw, the darts and axes stuck in the target, and your round chalked up on the board beside it. Your best round at each is kept in your browser. **E** steps back from the line. +- **Chess at the Merge Conflict corner.** Where the DJ used to play, on the roof's north side, there are three chess tables on a rug under a sign ("Merge Conflict — resolve it over a game of chess"), each with a polished board inlaid in its top and a chair either side. Sit in a chair (**E**) to take that side: the south chair is White, the north chair Black. Whoever sits across from you plays you, and everyone else up there can watch (and sees what's been taken laid out along the table's edges). It's a full game: castling, en passant, promotion (to a queen), check, checkmate, stalemate and draws for too little material or fifty moves without a capture or a pawn move. Look at your own pieces to light them, click one (or press **E** while looking at it) and then one of the glowing squares to move it; the last move and a king in check are lit for everyone. The little flag on your edge of the table resigns (click it twice) and the gold coin sets the pieces up again once a game is over. **W A S D** gets you up, and the game waits for whoever sits there next; a table goes back to the start once everyone has left it. Games live in the office's memory only. - **Jukebox.** A jukebox glows in the corner of the lounge. Walk up and press **E** to put on one of its four lo-fi tunes, skip to the next, turn it off, or paste a link to internet radio or an audio file. Everyone on the floor hears the same song from the same bar, coming from the jukebox: loud on the couch, faint at the desks. The tunes are synthesized in the browser like the office sounds, and it keeps playing when you switch tabs. The jukebox has its own volume under **⚙️**, just for you. - **Screen sharing.** Your screen appears on the lounge TV for everyone, and there's a full-screen viewer. - **An account for everyone.** Open **🔑 Accounts** from the **☰** menu and make an invite link. Whoever opens it picks a password and gets an account in their own name. That name is the one on their character, in chat and on every terminal they type into (**⌨️** in the terminal header shows who typed last), and nobody else can take it. Admins see everyone's accounts there, can make someone an admin, and can revoke an account, which signs that person out at once. The shared office password keeps working alongside the accounts until an admin switches it off. Sessions are signed cookies, and login attempts are rate limited. diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 471783328..e9e375d97 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -39,7 +39,7 @@ browser ──HTTPS/WSS──▶ agent-office (Node) - **Golf.** A shot is three numbers, its heading, loft and power, sent to everyone on the floor. Each browser works out the flight from those the same way, a fixed 240 steps a second: gravity and a little drag, the balcony's railing and wall, the neighbours' walls and roofs, the balconies further down, then bounces and a roll that depend on what it lands on (green, fringe, fairway, rough, sand or road), and the cup. So everyone sees the same ball land in the same place without it being streamed. The hitter's browser adds a little mishit to the numbers before sending them, so no two swings are exactly alike. - **Jukebox.** Each floor's `jukebox.json` says only what's on and when it started, so it keeps playing through a restart. Every browser plays it for itself: a tune is decided note by note by its bar and step, so starting from the same moment is enough for everyone to hear the same thing. To agree on that moment, each page pings the office a few times on arriving and keeps the quickest answer to line its clock up with the office's. A tune is drums, bass, an FM electric piano and a melody picked from the chord tones, with record crackle and tape wobble, in 32-bar rounds. It's scheduled a second ahead on a timer rather than each frame, which lets it carry on in a background tab. It goes through a panner at the jukebox and a filter that muffles it with distance. Most streams can't be routed through Web Audio without CORS, so a stream plays in a plain `