{
  "$schema": "https://vr.roomplusone.com/build.json",
  "kind": "agent-build-brief",
  "version": 3,
  "updated": "2026-10-18",
  "name": "room+1",
  "live": "https://vr.roomplusone.com",
  "for_agents": [
    "You are being asked to build something like this from scratch for your own human, with your own stack and your own art. This is a map of the parts and the order that worked, not source code.",
    "Ask your human what their world is for before you build anything. The rooms, tools and sounds below are one person's choices; replace them.",
    "Build in the listed order. Each step has a done_when you can check yourself: on a desktop browser first, a phone second, a headset third.",
    "Nothing here is a secret and nothing here grants access to the live site. Do not connect to its sockets or API; stand up your own.",
    "No database? Use ATProto (see atproto_database): every person's records live in their own repo, and your world only reads them back.",
    "When your world stands, hang its door in room+1 (the last step). That is the one place this brief points you to."
  ],
  "what_it_is": "A WebXR world that runs in a browser tab, on a phone and in a headset from one URL: a landing ringed by doors you can see through, rooms behind them (a lab of tools, a gallery that grows from the maker's posts, a shooting range staffed by people who mostly ignore you), music that plays from objects, drawing other visitors see, and models made in a 3D package by script.",
  "stack": {
    "client": "A-Frame on three.js, TypeScript, bundled with esbuild into one script; a static site with a catch-all to index.html so /room-name is a real link",
    "realtime": "one websocket hub (any language) holding rooms of peers, each room's strokes and a few small shared settings",
    "api": "a small HTTP service for sign-in, proxying public media, saved edits and a credit ledger; a document database behind it, or none: ATProto repos can hold everything people make (atproto_database)",
    "models": "a 3D package driven by scripts over a local socket, exporting glTF kits of pieces; the page keeps its own materials and takes only the shapes",
    "hosting": "a static host behind a CDN for the client, one small server for the hub, one for the API",
    "assets": "audio as mp3 beside a small JSON of title, duration and waveform peaks; images decoded in the browser to a bounded size"
  },
  "principles": [
    "One URL, three bodies: keyboard and mouse, a phone held as a window, tracked controllers. Build the desktop path first; it is what you can test without a headset. A phone gets on-screen pads that send the desktop's own key events, so everything that answers a keyboard answers a thumb.",
    "Local first: a tool works with no network and no account. The network only shares it.",
    "Budgets are part of the design: cap every pool (projectiles, particles, strokes, textures, lights), reuse instances, and draw a room only where its doorway shows it. A headset has a phone's GPU.",
    "Never change the light count at run time: lights stay in the scene and only their intensities move, so nothing recompiles when you walk through a door.",
    "The look is a rule, not an asset: low-poly geometry, flat colour, and one global dither applied to every material, so anything added later already matches.",
    "Feature-detect everything a browser may lack. A phone without WebXR still gets a stand-in object that throws if asked for a session; one uncaught throw in the frame loop is a black screen.",
    "Every delivered change gets a version number shown in the world, a one-line note and the commit it came from, and the version is stamped onto the script's URL so the CDN cannot serve yesterday's build.",
    "Each system has a small script that checks its rules headlessly (door crossing, pool caps, the sign-in boundary, the maze's joins). Run them before every push."
  ],
  "build_order": [
    {
      "step": 1,
      "name": "A room you can stand in",
      "do": "Scene, floor, sky, a camera rig with desktop walk and look and headset locomotion, an enter-VR button, a spawn point readable from the URL. Ease the walk with a critically damped spring so starts and stops lean instead of jumping.",
      "done_when": "You can walk on a desktop and stand at the right height in a headset from the same link, and nothing lurches."
    },
    {
      "step": 2,
      "name": "The look",
      "do": "Replace the engine's built-in dithering hook with your own ordered dither that cuts light into a few bands of each surface's own colour, the pattern fixed to the surface rather than the screen and growing with distance. Add an optional low-resolution pixel mode for screens.",
      "done_when": "Every built-in material is dithered with no extra render pass, the pattern does not swim when you turn, and a query parameter turns it off."
    },
    {
      "step": 3,
      "name": "Hands and held things",
      "do": "A grab component for controllers and a desktop equivalent (take, use, drop, a small on-screen list of the held thing's controls). Give held things a tiny protocol: a list of controls to show, a step function, a key handler, a mode picker. Smooth the held pose, except for things that must be held raw (a gun). Keys and dials that a fingertip presses, with the finger bending back so it never passes through.",
      "done_when": "One object can be picked up, used and dropped with both input styles and shows its own controls; a key goes down under a finger and acts once."
    },
    {
      "step": 4,
      "name": "The phone",
      "do": "On a touch screen with no hover: tilt to look, a drag on the picture to look, a stick pad to walk (distance is speed), USE and TAKE buttons, the held thing's controls as tappable keys, all dispatched as the desktop's key events. Print errors on screen, since a phone has no console. Optionally a hand tracked from the camera as a fingertip, processed on the device only.",
      "done_when": "A phone walks, looks, picks up and uses the same objects, and a broken build says why on the glass."
    },
    {
      "step": 5,
      "name": "Rooms and doors",
      "do": "Each room is a subtree. A door is a frame whose opening is a stencil mask; the room behind draws only through it. Track which room the head is in by crossing the sill, with a few centimetres of hysteresis. Things in a hand travel through with their carrier; things that belong to a room are let go at its door. Each frame, build a pyramid from the eye through each opening and hide every mesh of that room outside it. Private doors read a pass from the visitor's grant; a secret door is not there at all without one.",
      "done_when": "You see another room through a doorway, walk in without a load, sway on the sill does not flip rooms, and looking away from a door drops its room's draw calls to zero."
    },
    {
      "step": 6,
      "name": "One audio graph",
      "do": "Declare every sound as a kind in one file: a source (stream, element, buffer, synth), stages, analyser taps, and outs that are either placed on an object or flat in the ears, each sent to a bus (music, voice, effects) and the buses to one master. The graph owns the shared rules: a placed sound in another room is heard only along a straight line through the doors and within the gaze's cone; voice ducks music. Sounds that are wanted but not made yet are named slots, silent until a file is assigned. On WebKit, set panner positions outright instead of ramping them, and decode live streams by hand so they pass through the graph.",
      "done_when": "Music gets quieter as you walk away, is cut off by a wall but not by an open door, and an empty slot is silence, not an error."
    },
    {
      "step": 7,
      "name": "Shared presence and drawing",
      "do": "A websocket hub with rooms. Clients send head, hand and finger poses at a fixed rate and pen strokes in small batches; the hub assigns peer ids, relays, keeps the room's strokes and a handful of small numbered settings (which channel a shared remote is on), and enforces a strict schema, size and rate on every frame. New features are announced in the hub's hello so an old hub is never sent what it cannot read. Reconnect with backoff; a hidden page disconnects.",
      "done_when": "Two browsers see each other's heads and gestures, a stroke drawn in one appears in the other and survives a reload, and a remote turned in one turns in the other."
    },
    {
      "step": 8,
      "name": "Who may write",
      "do": "Sign in with a decentralised identity provider using an identity-only scope, in a popup so the world never reloads. Exchange the result for a short-lived, hashed, revocable capability whose scope and room passes the server decides. The hub checks only that capability. Guests see everything and change nothing. A headset with no keyboard signs in with a short code approved on another device. Posting a drawing or a note to the visitor's own repo is a second, narrow ask made at that moment, for one record.",
      "done_when": "A guest cannot draw, a signed-in writer can, logging out stops writing within a minute, and a posted record that its author deletes leaves the world."
    },
    {
      "step": 9,
      "name": "Tools that change the world",
      "do": "A painter that lays a picture, a colour or a relief on any surface it is aimed at; reliefs displace real geometry within a triangle budget. Record each change as a small recipe keyed by the surface, not as geometry, and save recipes for signed-in writers.",
      "done_when": "A painted room comes back after a reload from recipes alone, and the frame cost is shown on the tool."
    },
    {
      "step": 10,
      "name": "Models by script",
      "do": "Run a tiny socket listener inside the 3D package and send it scripts from the terminal, so a model is code you can diff. Build pieces from boxes, tapered prisms and balls in the world's own frame, faces named by material, and export one kit per room. The page loads a kit into geometry per material per piece, discards the file's materials, merges what never moves and instances what does, and falls back to boxes if the kit fails. Rig and bake animations in the package only to look at them; the page runs the same numbers itself.",
      "done_when": "Changing a dimension in the script and re-running it changes the model in the world on reload, and the world still stands with the kit missing."
    },
    {
      "step": 11,
      "name": "Places that grow from data",
      "do": "Lay a room out as a seeded tree of cells: each piece (hall, room, fork, stair, bridge, bend) has ways out, the next piece grows from a dice seeded by the item it holds, nothing reuses a cell, and two open ways that meet face to face are joined into a loop. New items only lengthen it, so it never rearranges under a visitor. The same planner with different weights makes a gallery of the maker's posts and a tight multi-level shooting course. Walls and ropes hold the head, and a refused step slides along them.",
      "done_when": "The same posts always draw the same maze, a new post adds a place at an end, and raising one roll number draws a new one."
    },
    {
      "step": 12,
      "name": "People who live there",
      "do": "Non-player characters as instanced parts (head, upper arm, forearm, hand, props) for the whole crew, arms stretched between shoulder, elbow and wrist targets, poses eased toward a target on a small state machine with random durations: a guard types and now and then turns to give you a thumbs up; one on his feet shifts his weight, pats his pockets and spends most of his time hunched over his phone. They hear shots within a radius and answer from voiced sound slots. Tick them only while their room is drawn.",
      "done_when": "A dozen characters move independently in a handful of draw calls, and the frame cost of an empty room is unchanged when you are not looking into it."
    },
    {
      "step": 13,
      "name": "A gun that feels thrown",
      "do": "A shot is a ray from the muzzle node's world pose through the room it is in, a tracer to the hit and a fading mark on the struck face, pooled. Recoil is damped springs on pitch, yaw and push back, stepped at a fixed small timestep, so a fast follow-up goes where the barrel was thrown. On a desktop the hip aim converges on the crosshair at the surface's own distance.",
      "done_when": "Every shot lands along the barrel as drawn, including mid-recoil, and the marks never exceed their pool."
    },
    {
      "step": 14,
      "name": "A visualiser at landscape scale",
      "do": "Feed an analyser from the music before it reaches the speakers and scroll its spectrum into a very large mesh that recedes into the distance. Delay the audible song by the travel time so what you hear is where the playhead stands. A room can have its own song, in the ears, whose bands move its sky.",
      "done_when": "The landscape moves with the music and stays one draw call."
    },
    {
      "step": 15,
      "name": "A wall of the maker's posts",
      "do": "Read only public feeds. Proxy media through your API with a fixed host allowlist, no cookies, size caps and content-type checks. Parse previews into inert text and image records; never mount fetched HTML. Remember what you have seen so a post that scrolls off the public page stays. Posts with a leading keyword become things (an exhibit, a radio station). One user-started media player at a time.",
      "done_when": "New public posts appear in the world within seconds and nothing private is ever requested."
    },
    {
      "step": 16,
      "name": "Paid turns",
      "do": "Optional. A credit balance per identity, kept in whole cents with a ledger whose references are used once; a hosted checkout and its signed webhook credit it; a paid action takes the credit before it runs and refunds once if it fails or stalls. Every result is kept as a step, so nothing paid for is overwritten.",
      "done_when": "Replaying a webhook or a request moves no money twice, and a failed turn gives the credit back exactly once."
    },
    {
      "step": 17,
      "name": "Life in the sky",
      "do": "Ambient things on their own clocks: ships arriving and leaving, small fighters, orbiting bodies, drifting sheets of paper, motes in the air. Instanced, pooled, paused when their room is not the one you are in.",
      "done_when": "The scene feels inhabited and the draw-call count barely moves."
    },
    {
      "step": 18,
      "name": "Ship it, again and again",
      "do": "A version file, a notes file with the commit each cut came from, a number on the floor at spawn, the version stamped onto the page's script and stylesheet URLs. A menu on the desktop page for view options, sign-in, and bookmarks of exact position and rotation as shareable links. A link on it to a page like this one.",
      "done_when": "A tester can send you a link that puts you where they stood, in the build they stood in, and a fresh deploy is what the next visitor gets."
    },
    {
      "step": 19,
      "name": "Hang your door in room+1",
      "do": "room+1 has a room of doors to other worlds. Post publicly on Bluesky: your world's link as the first word, a few words after it for the door to say, and the tag #opensocialvr. room+1 reads that public search and hangs one door a person, the newest post. Nothing to sign up for; delete the post and the door goes.",
      "done_when": "Walking through your door in room+1's rooms room lands a visitor in your world."
    }
  ],
  "contracts": {
    "realtime_frames": "JSON text, one object, a type field, under 1 KB. Client to hub: join a room, pose (head and two hands as position plus quaternion, optional finger curls), stroke points in batches, undo last point, end stroke, erase own stroke, set a named setting to a small whole number. Hub to client: hello with roster, rate, features and settings, existing strokes, joins, leaves, batched poses, relayed strokes, changed settings.",
    "saved_edit": "{ surface key: { colour?, picture index?, reveal effect?, blend?, relief?: { picture index, strength -1..1, triangle budget, pattern } } }",
    "sound_kind": "{ source: stream | element | buffer | synth, chain: [stages], taps: [analysers], outs: [{ placed | flat, bus: music | voice | effects }] }",
    "model_kit": "one glTF binary, each top-level object a piece at the origin in the world's frame (metres, y up, facing -z), faces grouped by material name",
    "tape": "{ title, artist, duration, peaks: hex string of waveform buckets } beside the audio file",
    "posted_record": "a drawing or note in the visitor's own repo, whole millimetres, measured from a named origin in a named room",
    "your_record": "{ $type: your collection, createdAt, place?: { room, origin, at: [mm, mm, mm] }, whatever you keep, whole numbers only }"
  },
  "safety": [
    "No credentials, tokens or database addresses in the client, in logs or in URLs.",
    "Validate every inbound frame and request against a written schema; rate-limit per connection and per address; flag what does not fit.",
    "Camera and microphone start only from a visible button and stop when the page hides. Camera frames are processed on the device and never sent.",
    "Proxies fetch from fixed hosts only and forward nothing that identifies the visitor.",
    "Signing in proves identity only. Writing to a visitor's own repo is asked for separately, at the moment they choose to post, for one kind of record, and the session is given back after that one write.",
    "Money moves only on a verified webhook, against an amount the server set, once per reference."
  ],
  "not_included": [
    "source code",
    "art, photographs, models and recordings",
    "the maker's design system",
    "any access to the live world's services"
  ],
  "ask_your_human": [
    "What is your world for?",
    "What do people pick up?",
    "What does it sound like?",
    "Who lives there?",
    "Do you have a database, or should people keep what they make?",
    "Who is allowed to leave a mark?"
  ],
  "atproto_database": {
    "for": "builders with no database of their own. Each visitor's own ATProto repo is the store; your world writes one record kind there and reads them back. You keep at most an index, and you can rebuild it from the network at any time.",
    "steps": [
      {
        "name": "Name your record",
        "do": "Pick a collection id from a domain you control, reversed (com.example.world.note). Write its lexicon: an object with whole-number fields only (the data model has no floats, so keep millimetres and thousandths), short strings with length limits, a createdAt. Serve the lexicon as a static file on your site."
      },
      {
        "name": "Sign in for identity only",
        "do": "OAuth with the bare atproto scope proves who someone is and grants nothing else. A browser-only client is possible: its client metadata is a static JSON at the client id's URL. Keep the session in memory."
      },
      {
        "name": "Ask to write at the moment of writing",
        "do": "When someone chooses to keep something, ask their server for repo:<your collection>?action=create, write that one record, and give the session back. They see exactly what is asked and when."
      },
      {
        "name": "Read it back from where it lives",
        "do": "Resolve the author's DID to their server and list the records of your collection there; these reads are public. A record its author deletes is gone from your world on the next read."
      },
      {
        "name": "Find who has records",
        "do": "Without a server: keep a list of people in a static file, or listen to the network's public event stream filtered to your collection. With a small server: an index of did, record key and a copy, re-checked against the author's server now and then, so an outage hides nothing and a deletion removes it."
      },
      {
        "name": "Keep it public-shaped",
        "do": "Everything in a repo is public and permanent until its author deletes it. Put nothing private there, nothing you would need to take back, and nothing that only works with your server running."
      }
    ],
    "done_when": "Your world shows what people made after your own storage is wiped, and a record its author deletes leaves within one re-read."
  }
}
