@homie-rocks/studio 0.30.2 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/README.md +343 -21
  3. package/assets/animate.ts +16 -6
  4. package/assets/assets.ts +14 -3
  5. package/assets/render-page.ts +10 -2
  6. package/assets/safety.mjs +7 -1
  7. package/bin/homie-studio.mjs +136 -121
  8. package/lib/art-cli.mjs +61 -18
  9. package/lib/art-tools.mjs +53 -10
  10. package/lib/asset-check.mjs +249 -46
  11. package/lib/asset-import.mjs +11 -5
  12. package/lib/asset-manifest.mjs +128 -51
  13. package/lib/build.mjs +239 -72
  14. package/lib/characters.mjs +119 -20
  15. package/lib/check.mjs +110 -7
  16. package/lib/chrome.mjs +11 -0
  17. package/lib/clips.mjs +54 -0
  18. package/lib/cloudflare.mjs +127 -8
  19. package/lib/codex.mjs +6 -0
  20. package/lib/collision.mjs +245 -0
  21. package/lib/decisions.mjs +2 -2
  22. package/lib/deploy-state.mjs +166 -0
  23. package/lib/dev.mjs +430 -0
  24. package/lib/directory.mjs +116 -5
  25. package/lib/doctor.mjs +2 -2
  26. package/lib/handoff.mjs +2 -6
  27. package/lib/library.mjs +1 -1
  28. package/lib/mcp-tools.mjs +36 -35
  29. package/lib/mcp.mjs +1 -1
  30. package/lib/net.mjs +81 -2
  31. package/lib/node-version.mjs +40 -0
  32. package/lib/office.mjs +6 -6
  33. package/lib/optimise.mjs +39 -4
  34. package/lib/parts-build.mjs +169 -0
  35. package/lib/parts-cli.mjs +104 -0
  36. package/lib/parts-store.mjs +342 -0
  37. package/lib/parts-tools.mjs +92 -0
  38. package/lib/parts.mjs +693 -0
  39. package/lib/perf.mjs +131 -31
  40. package/lib/port-check.mjs +40 -6
  41. package/lib/preview.mjs +77 -0
  42. package/lib/routes.mjs +132 -0
  43. package/lib/scaffold.mjs +81 -20
  44. package/lib/shoot.mjs +307 -0
  45. package/lib/site.mjs +1 -2
  46. package/lib/stage.mjs +118 -0
  47. package/lib/studio.mjs +0 -66
  48. package/lib/style-board.mjs +49 -6
  49. package/lib/template-history.json +61 -0
  50. package/lib/trailer.mjs +47 -0
  51. package/lib/typecheck.mjs +107 -0
  52. package/mcp/ui/lineup.js +3 -3
  53. package/mcp/ui/rights.js +1 -3
  54. package/netplay/NETPLAY.md +424 -16
  55. package/netplay/netplay.ts +756 -40
  56. package/package.json +10 -1
  57. package/parts/PARTS.md +163 -0
  58. package/port/index.ts +8 -2
  59. package/port/personas.ts +275 -0
  60. package/port/powerups.ts +357 -0
  61. package/port/probe.ts +38 -2
  62. package/port/room.ts +103 -19
  63. package/site/SITE.md +102 -20
  64. package/starters/ember-vale/index.html +1 -1
  65. package/starters/ember-vale/src/main.ts +3 -1
  66. package/starters/gem-rush/index.html +1 -1
  67. package/starters/gem-rush/src/main.ts +13 -6
  68. package/starters/gem-rush-3d/assets/RIGHTS.md +0 -24
  69. package/starters/gem-rush-3d/assets/manifest.json +0 -24
  70. package/starters/gem-rush-3d/index.html +1 -1
  71. package/starters/gem-rush-3d/src/main.ts +15 -5
  72. package/starters/hero-rush-3d/assets/RIGHTS.md +0 -19
  73. package/starters/hero-rush-3d/assets/manifest.json +0 -19
  74. package/starters/hero-rush-3d/index.html +1 -1
  75. package/starters/hero-rush-3d/src/main.ts +15 -5
  76. package/worker/arrival.mjs +4 -1
  77. package/worker/discover.mjs +11 -25
  78. package/worker/index.mjs +142 -62
  79. package/worker/license.mjs +45 -41
  80. package/worker/office-page.mjs +0 -4
  81. package/worker/office-schema.mjs +6 -0
  82. package/worker/office.mjs +19 -23
  83. package/worker/pages.mjs +141 -23
  84. package/worker/parts.mjs +234 -0
  85. package/worker/room.mjs +157 -14
  86. package/worker/schema.mjs +6 -8
  87. package/worker/seats.mjs +71 -0
  88. package/worker/site.mjs +51 -53
  89. package/worker/version.mjs +1 -1
  90. package/lib/remix-assets.mjs +0 -84
package/CHANGELOG.md CHANGED
@@ -17,9 +17,84 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
17
17
  `npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
18
18
  file) and what the upgrade would change, and changes nothing until you agree.
19
19
 
20
+ ## [0.31.0] - 2026-10-06
21
+
22
+ **Plugin 0.32.0** · [#50](https://github.com/homie-rocks/homie/pull/50)
23
+
24
+ Fixes from two creators' logs of building real studios: games now build on each other by sharing parts (remix is retired), a slow 3D game keeps its host, local play stays local, and synthesized bass plays at its written pitch.
25
+
26
+ ### Added
27
+
28
+ - **Parts: how games build on each other.** A part is a piece of a game that its studio chooses to share: a creature, a level generator, a camera, a bot brain, an audio pack. Never the whole game. In chat, `parts_find` looks for pieces other studios shared, `part_add` brings one into your studio (every file checked, the original credited, yours to tune from then on), `part_new` lifts a piece out of one of your games (the game still builds and plays the same), and `part_share` shares it under a licence you pick. A part is private until you share it, and live after your next deploy. Your studio lists what it shares at `/.well-known/homie-parts.json`; homie.rocks lists the shared parts of listed studios at `/parts/`. The packages a part builds on are installed by npm. The design is `parts/PARTS.md`.
29
+ - Planning or making a game now starts by looking for packages and for parts before anything is written from scratch, and a game's Codex has a Built from section saying what came from where. A deploy's plan shows the licences and credits of the parts a game uses, and says when they cannot be combined.
30
+ - `homie-studio preview <id>`: one built game's files at an address on this computer, with no Wrangler and no rooms, for a capture or a script.
31
+ - `homie-studio shoot <id>`: a two-browser seat check, then frames taken on a stepped clock, so a 3D game can be captured without a GPU.
32
+ - `homie-studio trailer <id>`: films a game frame by frame on a virtual clock, rebuilds its sound from the game's own files, picks the shots from what the game played, and delivers 16:9, 1:1 and 9:16. Sound that does not go through the sound player (live-synthesized voices, speech) is not in the mix, and the result says so.
33
+ - `homie-studio build --types` checks your games' TypeScript before building. New studios come with TypeScript; an older one runs `npm install --save-dev typescript`.
34
+ - A game can `await import()`: it becomes a file that loads when asked for, so a 3D game can load its later scenes late.
35
+ - game.json `netplay`: `stallMs` (how long a host may go quiet, 1.5 to 10 s), `version` (after a deploy, old tabs and new tabs no longer share a room; an old tab is told to reload at its round's break), and `params` (your own names passed from `/play?…` to the game as `net.params`; `debug` and `q` always pass).
36
+ - `net.link` and a "Reconnecting…" line over the game say when a game is cut off from its room. `net.offline` and `net.connected` are documented against it, and `net.reconnects` counts.
37
+ - `net.prefs` keeps a game's settings and personal bests (16 KB a game). `localStorage` throws inside the game's frame on purpose, and the docs now say why.
38
+ - `createRoom({ admit })` chooses which body a newcomer takes, for fresh joins and for a room revived from its checkpoint alike; `createRoom({ ties: 'shared' })` gives equal scores the same place; `features` lets a host turn a new mechanic on only when every player's build has it. NETPLAY.md has a rollout recipe for changing a game's rules while rooms are open, and a late-join checklist that includes "everyone leaves, then a new visitor enters before the room expires".
39
+ - `guardGestures()` for a touch game: a long press never selects text or raises copy and paste. The play page and its frame refuse selection too. Try it on a real phone; a harness cannot.
40
+ - `net.shell` tells a game where the play page's own buttons sit, so a HUD stays clear of them.
41
+ - A game declares what it scores with: game.json `playtest.primary` (a key or the mouse on a computer; a control or a region on a phone), and `"scoring": "together"` for a cooperative game.
42
+ - studio.json `"site": { "order": [...] }` sets the order of your games everywhere they are listed. A Home of your own takes the generated hero with `<!-- homie:home-hero -->`.
43
+ - A 3D game sets its own model budgets: game.json `"assets": { "budgets": { "triangles", "bytes" } }`, read by the game, by `assets check` and by `assets add`.
44
+ - `assets use <game> <asset> unused` marks a file the game no longer draws. The lineup draws the inventory, the cast and the environment separately.
45
+ - `collision_bake` (in chat; `homie-studio collision bake <game> <model.glb>`) turns a model into something figures can stand on, tied to the model by a checksum; `collision_check` fails when the model changed since, or a bot cannot walk a route you authored. It uses `@homie-rocks/heightfield`, which your AI installs in the studio the first time.
46
+ - `assets add --lo 0.2` also writes `<asset>_lo.glb`, the cheap copy for the far band of a prop drawn at two levels of detail.
47
+ - `homie-studio shoot <id> --preview` takes pictures of the built game with no site running, and names the build they are of.
48
+ - The models skill brings in a finished job from a connected fal account under the same budget and receipts (`models.mjs import`), and tells a missing local key apart from a connector's sign-in.
49
+ - New modules in the packages (each stands alone; install from npm): a third-person follow camera that never ends up in a wall or the player's head (`@homie-rocks/camera`); quality tiers, dynamic resolution that undoes a drop that did not help, and near/far instanced props (`@homie-rocks/render`); a ready-made post stack per tier (`@homie-rocks/postfx`); combo, hit-stop and bursts (`@homie-rocks/fx`); a per-sound limiter (`@homie-rocks/audio`); buildings with floors, doors and stairs, routes checked the way bots walk them, markers on every level, and grounded capture positions (`@homie-rocks/heightfield`); five bot personas and host-owned power-ups whose timers survive a host change (`@homie-rocks/studio/personas`, `/powerups`).
50
+
51
+ ### Changed
52
+
53
+ - **Remix is retired.** A game is no longer handed over whole. `game remix`, the `game_remix` tool, the "Make a game like this" band, the office's Remixable switch and the Studio pane's Remix button are gone, and a studio no longer serves a game's source (its old address answers 410 and points to `/parts/`). A game that was a remix keeps its credit, "Based on <game> by <studio>".
54
+ - A game's bundle is named by its content, so a fresh page never loads the previous bundle from the edge after a deploy. The build says for each game whether it changed and its build hash; a deploy says which games it changes; the live site says the same hash in `/.well-known/homie-studio.json`.
55
+ - A build is put in place only when all of it is there. It no longer deletes `site/dist`, so anything running from it keeps its files.
56
+ - `dev` keeps rooms local even when the studio has a custom domain: production routes are left out of the local configuration, and `dev` stops with a clear reason if a game would be handed a socket anywhere but this computer. A wrapper script is no longer needed for that.
57
+ - `deploy` keeps your custom-domain and exact-host routes, refuses to deploy a wildcard or catch-all route, and says which route to add when another Worker's wildcard answers your domain first. Only one deploy of a studio runs at a time.
58
+ - A playtest records what state the game was in at every press and picture (round phase, alive, time left, the link), and says BLOCKED or N/A instead of failing a row that landed on a results screen, a dead body, the loading cover or a dropped connection. "First picture" is the first picture of the game itself; the loading card is timed separately. UI coverage is measured with the thumb held on the stick, so numbers differ from earlier runs.
59
+ - One scripted round is reported as one round: a bot win is a measured gap with a recommendation to repeat it, never "people cannot win". The report states that UI coverage is DOM-only and that canvas HUDs were not checked, and calls out a reconnect beside a completed round.
60
+ - The playtest's review step asks one approval question naming exactly which files it sends and where, has a local fallback that sends nothing, and the report says which review ran, or that none did.
61
+ - `assets check` calls its scene numbers an inventory estimate and lists what it cannot see without running the game. "First play" is now "shipped payload": the same number and budget, honestly named (`firstPlayMB` still works). `perf` reports what was really fetched before play and, when your game exposes them, the renderer's draw calls and triangles.
62
+ - `check`, `perf`, `shoot`, `port check` and the playtest say the same sentence when this computer cannot look a site up. `perf` reports the arrival mode and when the game itself said it was ready, and the playtest reports a HUD element sitting under one of the play page's own controls.
63
+ - Listing a studio shows the licences and credits of the parts its games use before it is listed.
64
+ - `publish` says how many publishes are left today, and listing a studio again after a deploy is a re-read that does not count against the day.
65
+ - A room's log lines carry a time, the game and the room, and tell a player leaving apart from a failed operation. Room and error lines under `dev` carry a time (`--timestamps` for every line).
66
+ - The build names the hero files a landing wants and their shapes: `hero/wide.jpg` (16:9), `hero/tall.jpg` (9:16), and `wide.mp4` / `tall.mp4`.
67
+
68
+ ### Fixed
69
+
70
+ - **Synthesized bass played octaves too high.** A number is now always hertz and a MIDI note is given as `midi`. A2 was written as 110 Hz and played as MIDI note 110 (4.7 kHz); the kick, the tom and several effects had the same fault. They sound different now: as written.
71
+ - A loop keeps its seam through the limiter, and a loop that does not repeat is listed in the warnings instead of beside an empty list. The "steady tone" warning gives the tone's level and how much of the time it is there, and no longer fires on a melody's brief high partial. The sound player no longer hangs on an effect listed as a single file.
72
+ - A trailer's cuts are counted in frames and stay on the beat, and every delivery is yuv420p, limited range, tagged BT.709.
73
+ - A game that does not build fails `homie-studio build` and leaves the built site as it was.
74
+ - A slow 3D game no longer swaps hosts on every hitch: the helper sends a heartbeat while a host's frames are stuck. A game that boots for seconds can wait longer for its room (`connectOpenMaxMs`) or start the wait itself (`net.start()`).
75
+ - A visitor who revives an emptied room takes a body over properly (`onTakeover`, `adopt`). A player who joins while the host is briefly disconnected gets a body. A returning player's score is no longer reset.
76
+ - Every command says at once when Node.js is older than 22, instead of ending without a word.
77
+ - `dev` and `dev --stop` clean up after a server that was stopped another way, and a game added while `dev` runs is picked up.
78
+ - A stuck screenshot decoder stops as BLOCKED after 20 seconds instead of hanging, and the playtest exits when its report is written.
79
+ - A press into a wall is no longer reported as unresponsive controls: the playtest tries the other way and reports both.
80
+ - The big-screen check no longer fails a local preview for a missing join QR; it still requires one on a deployed address.
81
+ - A hostname this computer cannot look up is reported as this computer's network, with local testing offered, not as a broken site or game.
82
+ - Standalone textures and skies are measured and counted in picture memory, and pictures shipped with no record are listed. Normal and roughness maps the game does not draw can be declared (`"inGame": { "maps": "base" }`) and leave the estimate.
83
+ - A second clip library for one skeleton is refused unless declared supplemental; a supplemental one joins coverage and previews. Removing a starter character keeps its pack's credit while its retargeted clips remain. `anim plan` and `anim preview` show the ground speed each character's walk and run cover on its own rig.
84
+ - A `net.playable()` that arrives after the automatic arrival warns: pass `arrival: 'game'`.
85
+
86
+ ### Upgrade notes
87
+
88
+ - Remix settings do nothing now: `"remix"`, `"share"`, `landing.make` and the licence words `remix-with-credit`, `remix-freely` and `no-remix`. The build says so once; delete them when convenient. Leave `"remixOf"` in a game that was a remix: it is the credit. Nothing is removed from your database.
89
+ - A game's `"license"` is now the SPDX licence it names for itself (`"MIT"`), shown in its credits.
90
+ - Sound made with the sound skill plays at its written pitch after this release. If you raised a bass part an octave to work around the old fault, lower it again.
91
+ - A game with no port probe gets its playtest `move` row BLOCKED where it used to pass silently.
92
+ - The first deploy after upgrading asks the homie.rocks directory to read a listed studio again, once.
93
+ - The packages that gained modules are new versions: `@homie-rocks/camera`, `render`, `postfx`, `fx`, `audio` and `heightfield` 0.2.0.
94
+
20
95
  ## [0.30.2] - 2026-10-04
21
96
 
22
- **Plugin 0.31.2** · [#48](https://github.com/homie-rocks/homie/pull/48)
97
+ **Plugin 0.31.2** · [#48](https://github.com/homie-rocks/homie/pull/48) · [release-2026-10-04-studio-0.30.2](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-04-studio-0.30.2)
23
98
 
24
99
  Fixes from first runs in Codex and Grok: a missing connector no longer stops a new studio, you are told when Homie's holds are off (in Grok they are), Grok has an install command, and a one-sentence studio gives you a Play link before it polishes.
25
100
 
package/README.md CHANGED
@@ -15,6 +15,7 @@ cd night-owls && npm install
15
15
  npx homie-studio game new crown-thief --from gem-rush --name "Crown Thief"
16
16
  npx homie-studio dev # the whole site locally
17
17
  npx homie-studio check crown-thief --url http://127.0.0.1:8787
18
+ npx homie-studio shoot crown-thief --url http://127.0.0.1:8787 # frames on a stepped clock (works without a GPU) and a two-client seat smoke check
18
19
  npx homie-studio perf crown-thief --url http://127.0.0.1:8787 # how fast it runs: a computer and an emulated phone, host and replica
19
20
  npx homie-studio lab crown-thief # the Game Lab: how a move feels, New beside Today, frame by frame
20
21
  npx homie-studio style board crown-thief # its look as decisions: three directions drawn by the engine (free)
@@ -42,7 +43,7 @@ npx -y @homie-rocks/studio mcp --studios "<the folder the studios live in>" #
42
43
  ```
43
44
 
44
45
  The toolkit as MCP tools, so the chat that shows Homie's cards also does the work: the setup status, a new studio,
45
- the live demo, make / remix / port / plan a game and its Game Codex, build, run it here, the two-browser check, a
46
+ the live demo, make / port / plan a game and its Game Codex, build, run it here, the two-browser check, a
46
47
  playtest, deploy, publish, the progress feed, the studio's files, and the music, sound, art and video scripts where
47
48
  their provider is set up. Where a tool overlaps the remote Homie MCP (homie.rocks/mcp) it has the same name and
48
49
  input shape. Long work (npm install, a check, a deploy) runs in the background and reports through the progress
@@ -99,10 +100,11 @@ featured game, live rooms, the latest posts), Games, Music, Videos, Rooms (every
99
100
  joinable and watchable) and Posts (`posts/*.md`, with Atom and JSON feeds). A section with nothing in it has no tab, and its
100
101
  page answers 404. Every game gets a landing at `/<game>/`: a full-bleed hero from its own footage or art, the
101
102
  pitch, a big Play button into a public room, how to play on a phone, a computer and a TV (with the join code),
102
- live rooms, credits (the original and its licence for a port; "Remix of <game> by <studio>", linked, for a
103
- remix), and "Make a game like this". A game's shared source (`/games/<id>/source.json`) says who made it and the
104
- licence its owner picked in game.json `"license"`: `"remix-with-credit"` (the default), `"remix-freely"` or
105
- `"no-remix"`, which `game remix` refuses. Every page ends with "Made with Homie". Anything the studio puts in `site/` wins: a whole page, a partial, its tokens, its CSS, its
103
+ live rooms and credits (the original and its licence for a port; "Based on <game> by <studio>", linked, for a
104
+ game that was made from another studio's). game.json `"license": "MIT"` (an SPDX identifier) is the licence the game
105
+ names for itself, said in its credits; a game that names none says nothing. No game's source is served: games build on
106
+ each other through parts ("Game parts", below). studio.json `"site": { "order": ["<id>", …] }` is the
107
+ order of the games in every list, the directory's manifest included. Every page ends with "Made with Homie". Anything the studio puts in `site/` wins: a whole page, a partial, its tokens, its CSS, its
106
108
  files. `site/SITE.md` says all of it.
107
109
 
108
110
  The play page never opens blank (0.26.0): from its first paint an arrival card shows the game's own look (its title,
@@ -118,7 +120,7 @@ site. A game made with Homie's arcade controls that knocks for a Homie box (`/__
118
120
 
119
121
  Search engines and AI agents read a studio correctly (0.27.0): every generated page carries schema.org JSON-LD
120
122
  (`worker/schema.mjs`: the studio's `Organization` and `WebSite` on Home, a full `VideoGame` on each landing with its
121
- players, platforms, licence, remix original, trailer, dates and a free-to-play offer whose add-ons are what the shop
123
+ players, platforms, licence, trailer, dates and a free-to-play offer whose add-ons are what the shop
122
124
  really sells, `MusicRecording`, `VideoObject`, `BlogPosting`, `ItemList` and `BreadcrumbList`; never a rating), and the
123
125
  site makes `/robots.txt`, `/sitemap.xml`, `/llms.txt` and `/llms-full.txt` from its public catalogue
124
126
  (`worker/discover.mjs`). `lib/schema-check.mjs` checks every block against schema.org's own vocabulary and what Google
@@ -155,6 +157,29 @@ shows how it differs from the template. `--apply` also pins this version in pack
155
157
  `homie.studio`. When the template's words change, `node scripts/studio-template-history.mjs` records them (a test
156
158
  fails until it has).
157
159
 
160
+ ### Remix was retired
161
+
162
+ Handing over a whole game (`game remix`, the `game_remix` tool, a game's `/games/<id>/source.json`, the office's
163
+ Remixable switch, the "Make a game like this" band) is gone. Games build on each other through **parts**: pieces of a
164
+ game its studio chooses to share, each with its own licence ("Game parts", below). For a studio that upgrades:
165
+
166
+ - **Old settings still build.** A game.json that carries `"remix"`, `"share"`, `landing.make`, or a `"license"` that
167
+ is one of the old words (`"remix-with-credit"`, `"remix-freely"`, `"no-remix"`, or an object with `"kind"`) builds
168
+ as before. The keys do nothing, and the build says so in one note that lists them; delete them when convenient. An
169
+ SPDX identifier in `"license"` (`"MIT"`, or `{ "spdx": "MIT" }`) is still the licence the game names on its page.
170
+ - **A game that was a remix keeps its credit.** game.json `"remixOf"` is still read: the landing and the credits page
171
+ say "Based on <game> by <studio>", linked to the original. That credit is owed, so leave the key where it is. The
172
+ files such a game carried keep saying whose they were in its `assets/RIGHTS.md`.
173
+ - **No source is served.** The build writes no `source.json` or `assets.json` and takes out the ones an older build
174
+ left. `/games/<id>/source.json` answers 410 with `{ "retired": "remix", "see": "/parts/" }`, and
175
+ `/.well-known/homie-studio.json` no longer has `remix`, `source` or `remixOf`. The first deploy after the upgrade
176
+ asks the directory to read a listed studio again, once, so it drops the old offer.
177
+ - **Nothing is dropped from the database.** The office's `office_games.remix` column stays in D1 with whatever it
178
+ held; nothing reads or writes it.
179
+ - **Asset records.** `license.remix` on an entry of `assets/manifest.json` is ignored and no longer written, and
180
+ RIGHTS.md has no "A remix" line. Whether a file may be handed on in a shared part is still its licence's.
181
+ - `homie-studio game remix` answers with a sentence that points to parts; `office launch --remixable` is ignored.
182
+
158
183
  ## Rooms of up to 32
159
184
 
160
185
  Every room of a game has the seats its netplay manifest names: game.json's `netplay`
@@ -164,6 +189,24 @@ Bots, a late joiner taking a bot's body and a new host after the old one leaves
164
189
  the same at 32 as at 4. One address may hold every seat plus four sockets, so a party
165
190
  on one Wi-Fi (or strangers behind one carrier's address) fills a room with a TV beside it.
166
191
 
192
+ ## A game's netplay manifest
193
+
194
+ game.json `"netplay"` says more than the room's size. All of it is optional, and `homie-studio build`
195
+ writes it to the catalogue for the site's rooms (`netplay/NETPLAY.md` sections 22 to 24):
196
+
197
+ | Field | What it does |
198
+ |---|---|
199
+ | `maxPlayers` | Seats in a room, up to 32. |
200
+ | `version` | The game's revision (a word or a number). Bump it when a change makes an already-open tab unable to play with a new one: a room then runs one build at a time, strangers are matched within the live build, and an old tab is told to reload at its round's break. |
201
+ | `stallMs` | How long a host may send no snapshot before the room is handed to another browser: 1500 (the default) to 10000. A heavy 3D game whose frames hitch on a slow phone raises it; the helper also sends a heartbeat for up to 4 s while a host's frames are stuck. |
202
+ | `params` | Names of the play page's query parameters to hand the game (`/<id>/play?seed=42`), beside `debug` and `q`, which always are. The game reads them as `net.params`. |
203
+
204
+ The game's frame is sandboxed without `allow-same-origin` (it must never read the site's storage or
205
+ the owner's session), so `localStorage` throws inside it: a setting or a personal best goes in
206
+ `net.prefs` (the play page keeps 16 KB a game), and progress that must last in saves, below.
207
+ `net.link` says whether the browser is in its room, reconnecting, or playing alone because the room
208
+ never answered; `guardGestures()` stops a long press on a phone from selecting text in a touch game.
209
+
167
210
  ## Player accounts and cloud saves
168
211
 
169
212
  A game whose progress must last (a character that levels up for days, unlocks, a collection, a hardcore mode)
@@ -228,7 +271,7 @@ it):
228
271
  - **Close** a room: everyone is sent out with a thank-you; nobody gets in until it opens.
229
272
  - Per game: **players per room**, the **launch state** (`private`: only the owner;
230
273
  `invite`: an invite-only beta, where each invite link or code lets a browser in;
231
- `public`: anyone, listed), and **remixable** (publish or withdraw the game's source).
274
+ `public`: anyone, listed).
232
275
  A game that is not public is in no list and not in the directory manifest, so the
233
276
  directory drops it the next time it reads the studio. A new game is private from its
234
277
  first deploy with `"launch": "private"` in its game.json.
@@ -478,8 +521,8 @@ redraws itself whenever the feed changes and refreshes in the browser while a bu
478
521
  puts each game's codex on the site at `/_studio/codex/<id>/`, served only to the studio's owner (the same
479
522
  one-time sign-in as the stats page), never listed or indexed. Text is escaped and the page runs only its own
480
523
  script (the site allows it by its hash); pictures and fonts come only from inside the studio and are
481
- embedded, so it loads nothing but Google Fonts. CODEX.md is never in a game's remix source or among a
482
- static game's served files.
524
+ embedded, so it loads nothing but Google Fonts. CODEX.md is never among a static game's
525
+ served files.
483
526
 
484
527
  ## The Claude Code status line
485
528
 
@@ -519,7 +562,7 @@ one runs, a shared feed asks whether Stop was pressed; a stopped command closes
519
562
  browsers and says so, and nothing already built or deployed is undone.
520
563
 
521
564
  **From the Claude app.** The chat can open the build first (the Homie MCP tool `build_open`, or the "Build it"
522
- card on a make, port or remix) and hand the work to a Claude Code session with a prefilled prompt. The session
565
+ card on a make or a port) and hand the work to a Claude Code session with a prefilled prompt. The session
523
566
  takes the build once, with no key in the prompt, and the card follows it; the change goes out as a pull request
524
567
  the person merges with one tap:
525
568
 
@@ -564,12 +607,99 @@ hours so the Claude app can show it: the feed only (plain bounded text, pictures
564
607
  never printed; the shared id is printed for the MCP tool `build_progress`. Without an open
565
608
  feed every command behaves exactly as before. `lib/progress.mjs` has the whole format.
566
609
 
567
- ## The site's address
610
+ ## Running it here: `homie-studio dev`
611
+
612
+ ```sh
613
+ npx homie-studio dev [--port 8787] [--timestamps] # the whole site on this computer, until it is stopped
614
+ npx homie-studio dev --stop # this studio's dev server only
615
+ ```
616
+
617
+ Every command needs Node.js 22 or newer and says so at once on an older one, before it does anything.
618
+
619
+ **Rooms here are always local.** A studio that is live on its own domain has routes in `wrangler.jsonc`, and Wrangler
620
+ serves a config with a route as that production hostname: games would be handed a room socket on the live site. So
621
+ `dev` runs Wrangler from a copy of the config without `routes` (in `.wrangler/`; `wrangler.jsonc` is never changed),
622
+ then reads one game's page back and checks that the room socket it hands out is this computer's own address, port
623
+ included. If it is not (a `dev.host` in the config, say), `dev` stops and says so. No wrapper script is needed for a
624
+ route-free preview.
625
+
626
+ **A game added while it runs** is picked up. `dev` serves what `build` wrote: a new game folder that is not built is
627
+ named with the command, and once it is built, a running site that still answers 404 for it is restarted by `dev`
628
+ itself, on the same address.
629
+
630
+ **Stopping, and stale state.** `dev --stop` stops exactly the processes this studio's dev server registered, and
631
+ nothing else. A server stopped another way (a closed terminal, a kill by process id or by port) leaves its
632
+ registration behind; `dev` and `dev --stop` both notice, remove it and say it was stale, stop a local runtime left on
633
+ the port when it runs from this studio's folder, and name anything else on the port without touching it. Never stop
634
+ a dev server with `pkill` or by port: another project's would go too.
635
+
636
+ The registration is one file, `<folder of wrangler.jsonc>/.wrangler/homie-dev.json`, and it is the whole contract:
637
+
638
+ ```json
639
+ { "pid": 4242, "child": 4243, "port": 8787, "at": "2026-10-06T12:00:00.000Z" }
640
+ ```
641
+
642
+ `pid` is the process that owns the server, `child` the Wrangler it started (optional), `port` the local port, `at`
643
+ when it started. `dev --stop` and the MCP tools `preview_run` / `preview_stop` read only this. A process it names is
644
+ signalled only while it is still this studio's: alive, and its command line names the studio's folder, or its working
645
+ directory is inside it, or it is still the kind of program its key says (`pid`: homie-studio; `child`: wrangler or
646
+ workerd). So a studio's own wrapper script that writes this file, and is started from the studio's folder, is stopped
647
+ by `dev --stop` like the built-in one; it should delete the file when it ends.
648
+
649
+ **Room lines carry a time.** Wrangler prints a room socket opening (`GET /<game>/__net 101`) and a runtime error such
650
+ as `Network connection lost` with no time on them. `dev` puts this computer's clock in front of those lines (every
651
+ line with `--timestamps`), so they can be laid beside a check's own report. A connection-loss line is printed when a
652
+ browser or a host leaves abruptly, and also when a room operation really failed: the time is what tells them apart.
653
+
654
+ ## The site's address, its routes, and what a deploy says
568
655
 
569
656
  `deploy` keeps the `workers.dev` address in `.studio/local.json`, which git ignores: it
570
657
  names the Cloudflare account, often after its owner. A custom domain goes in studio.json
571
658
  as `cloudflare.domain` and is what the directory claim, `publish`, `check` and `stats` use.
572
659
 
660
+ **Routes.** `deploy` writes `wrangler.jsonc` again each time, and keeps the routes that are the studio's own exactly
661
+ as they were written: a custom domain (`{ "pattern": "play.example.com", "custom_domain": true }`) and an exact-host
662
+ route (`{ "pattern": "play.example.com/*", "zone_name": "example.com" }`). A zone's other routes are never listed,
663
+ changed or removed. When the zone already has a wildcard route on another Worker (`*example.com/*`), that Worker
664
+ answers the studio's hostname first; the deploy notices that its own domain answers as something else and prints the
665
+ exact-host route to add, once. A wildcard or catch-all route (`*/*`) in the studio's own config would hand every
666
+ hostname of the zone to the studio's Worker, so `deploy` refuses it before it asks Cloudflare anything; a studio that
667
+ really owns the whole zone says so in studio.json (`"cloudflare": { "allowWildcardRoutes": true }`).
668
+
669
+ **One deploy at a time.** A second `deploy` of the same studio is refused while one runs (`.studio/deploy.lock` names
670
+ its process and start time); a lock left by a deploy that was killed is taken over by the next one.
671
+
672
+ **Which games changed.** Each deploy prints, per game, `changed`, `unchanged` or `new` against the last deploy from
673
+ this computer, and the game's **build hash**: the one `homie-studio build` printed for it, that
674
+ `site/dist/_site/build.json` keeps, that `perf` names a run's build by, and that the live site answers with in
675
+ `/.well-known/homie-studio.json` (`games[].build.hash`). There is one hash, so a local build is matched to what is
676
+ live by comparing two strings. `build` says changed "since the last build here"; `deploy` says changed "since the
677
+ last deploy from this computer", from its own record in `.studio/local.json`. A deploy from another computer, or the first one, has nothing to compare with and says so.
678
+
679
+ **The directory.** Going online lists nothing. For a studio this computer already listed with `publish`, the first
680
+ deploy after an older toolkit's (one that still offered a game's whole source) asks the directory to read the studio
681
+ again, once, so it does not keep that offer. `publish` says how many publishes are left today when the directory gives the number (the
682
+ beta has a daily cap), and before it sends, how many this computer has sent today; a refusal for the cap is said as
683
+ that, with when it ends.
684
+
685
+ **This computer's network.** When Node.js cannot look a public hostname up (a browser on the same computer may still
686
+ open it), the command says "network preflight failed", not that the site or a game is broken, and offers the local
687
+ dev site instead.
688
+
689
+ ## A trailer: `homie-studio trailer`
690
+
691
+ ```sh
692
+ npx homie-studio trailer crown-thief --url http://127.0.0.1:8787 --seconds 45 --length 20 --title "CROWN THIEF"
693
+ ```
694
+
695
+ One command, into `videos/crown-thief-trailer/`: the game rendered frame by frame on a virtual clock (its
696
+ `requestAnimationFrame`, `performance.now`, `Date`, timers and CSS and Web animations all move 1/30 s a frame, so no
697
+ frame is held however heavy the game), its sound rebuilt from the game's own sound files and the log its sound player
698
+ keeps of what it played, the shots picked from that log, an end card, and 16:9, 1:1 and 9:16 files. The work is the
699
+ Homie plugin's video skill; this command finds its script and hands over (`--skills <folder>` when the plugin is not
700
+ beside the package). The skill's guide says what the virtual clock does not reach (the audio clock, media elements,
701
+ Web Workers, a live server) and what a game must do for its sound to be in the film.
702
+
573
703
  ## How fast it runs: `homie-studio perf`
574
704
 
575
705
  ```sh
@@ -585,15 +715,28 @@ on a fast computer is less than its rate). They play the same seeded presses thr
585
715
  window, and each run's JSON says, per browser: the time between animation frames (median, p95, p99, the share over 33
586
716
  and 50 ms), the game's JavaScript per frame (every requestAnimationFrame callback timed), the main thread per frame
587
717
  (Chrome's TaskDuration), time to the first meaningful frame (`load.look`: the play page's arrival card), to the
588
- game's first frame, to a seat and to playable, the game's files on the wire, the heap
589
- after a garbage collection, and netplay messages and kilobytes a second each way. `--profile` adds a CPU profile per
718
+ game's first frame, to a seat and to playable (control-ready: seated, a body reported, the loading cover gone) and,
719
+ beside it, `load.ready` (when the game itself called `net.playable()`) with the arrival mode (`load.arrival`: `auto`,
720
+ `game`, or `seat` for an older helper; under `auto`, `lateMs` is how long after the cover lifted the game said it was
721
+ ready, which is a game that should declare `arrival: 'game'`), the game's files on the wire, the heap
722
+ after a garbage collection, and netplay messages and kilobytes a second each way. Two more are measurements of the
723
+ running game, not estimates from its files: `load.prePlay` (every request the browser really made up to playable,
724
+ with the bytes that had arrived of any still in flight; the metric is `load.prePlayKb`), which is first-play traffic
725
+ and not the shipped payload that `perf sizes` and `assets check` total from the built folder; and `render` (the
726
+ renderer's own draw calls and triangles while playing, metrics `render.calls` and `render.triangles`), read when the
727
+ game exposes them on its port probe (`exposePort(net, { extra: { drawCalls: () => renderer.info.render.calls,
728
+ triangles: () => renderer.info.render.triangles } })`, as the 3D starters do; `PortExtra` in the port kit types these
729
+ and the other names the playtest reads: `alive`, `mode`, `loadout`, `touchHeld`). A game that exposes neither has no
730
+ `render` numbers: the run says the cost was not measured, never zero. `--profile` adds a CPU profile per
590
731
  browser in a window of its own (a `.cpuprofile` for Chrome DevTools) and its hottest functions, read through the
591
732
  game's source map when the build kept one (`homie-studio build --maps` keeps it in `.studio/maps/<id>/`, never in
592
733
  `site/dist`).
593
734
 
594
735
  The computer is shared, so every run waits for the 1-minute load to fall under 0.8 per core (`--max-load`) and
595
736
  records the load before and after; a run that started busy says `loaded` and `compare` leaves it out. A software
596
- renderer (SwiftShader) makes the run `blocked`: nothing on it is judged. `compare` calls a metric better only when a
737
+ renderer (SwiftShader) makes the run `blocked`: nothing on it is judged. So does an address this computer's Node.js
738
+ cannot look up (Node and a browser do not look names up the same way): "network preflight failed", with the local
739
+ site as the way round it, never "the site is down". `compare` calls a metric better only when a
597
740
  rank test says it is unlikely to be chance (p < 0.05), the 95% bootstrap interval of the change stays below zero, and
598
741
  it is at least 3% better; runs taken side by side with the same `--pair` label are judged as pairs (a signed-rank
599
742
  test on their ratios), so a computer that drifted busier cancels. Guards (frame time, main thread per frame, time to
@@ -607,6 +750,48 @@ Minified JavaScript gzips about as well as source text, and a bundle with three.
607
750
  source in strings, which no minifier touches; the line for such a file says `minified, 19% GLSL shader source in
608
751
  strings`, and the JSON's `code` has every measure.
609
752
 
753
+ ## Game parts: how games build on each other
754
+
755
+ Games build on each other by sharing **parts**: a piece of a game that its studio chooses to share, so somebody
756
+ making another game can use it. A creature from one game, a level from another, a bot brain from a third. Without
757
+ parts, a creator's AI has to come up with everything on its own. [`parts/PARTS.md`](parts/PARTS.md) is the design;
758
+ the plugin's `parts` skill is the method.
759
+
760
+ Creators work in chat and never type a command: "what parts are there for a chase camera?", "add the Pickup field
761
+ part from owls.example/pickup-field to my game", "make my creature a part", "share it under CC BY". Four tools do
762
+ it, named like the asset tools: `parts_find`, `part_add`, `part_new` and `part_share`.
763
+
764
+ - **A part is a piece of a game, never the whole game**, in whatever form suits it: code, assets, data, a JSON
765
+ description, a tuned config, or a mix.
766
+ - **Agents look first.** `game_plan` and the game and plan skills tell an agent to check the `@homie-rocks/*`
767
+ packages for the general mechanism and `parts_find` for pieces other studios shared, before writing from
768
+ scratch, and to record in the game's CODEX (Built from) and its credits what came from where. `parts_find` reads
769
+ the hub's catalogue and the studio's own parts; when the hub cannot be reached it says so and still answers with
770
+ the studio's own, so an empty list never stands in for "could not look".
771
+ - **Packages are not parts.** `@homie-rocks/*` packages come from npm and are never in the catalogue. A part says
772
+ which packages it builds on, and npm does the rest: `npm ls` says whether the studio has one, `npm install`
773
+ installs what is missing after one line saying what and why, and npm's own output is relayed. There is no
774
+ resolver, no version ranges and no lockfile of Homie's.
775
+ - **Use.** `part_add` fetches one exact version (the latest shared unless named), checks every file's SHA-256 and
776
+ size before a byte is written, copies it into `parts/_vendor/<site>/<id>/` where the studio owns and tunes it,
777
+ records where it came from (`parts/origins.json`), and credits it in the game's `credits.json` with its licence,
778
+ who made it and the game it came from. The game imports it in one line,
779
+ `import { createPickupField } from '@parts/owls.example/pickup-field'` (one of the studio's own is
780
+ `@parts/<id>`). Adding it again brings the newer version: what changed is shown, `tuning.json` is kept, and a
781
+ file the studio edited is never replaced without saying so.
782
+ - **Share.** `part_new` lifts a piece out of a game (the game still builds and plays the same) and records the
783
+ game and studio it came from. A part is private until `part_share`, which needs an SPDX licence, an attribution
784
+ when the licence asks for credit, and known rights to every file. It is live after the studio's next deploy.
785
+ - **What the site serves** is `sharedPartsOf(studio)` and nothing else: `/.well-known/homie-parts.json`, each
786
+ shared version's content at `/parts/<id>/<version>/…` exactly as hashed (immutable, CORS open for GET), the
787
+ studio's own pages `/parts/` and `/parts/<id>/`, and a **Parts from this game** band on a game's landing. A
788
+ private part is never copied into the built site, and the Worker answers only what the built index names.
789
+ - **Publishing a game that uses parts**: `deploy --plan` shows each part's licence and attribution and says
790
+ plainly when licences cannot be combined; `publish` (and `studio_publish`, also with `before: true`) says the
791
+ same lines before the studio is listed. `parts add --no-install` brings a part in and installs nothing:
792
+ `package.json` is left as it is, and a package the part builds on that the studio lacks is named NOT INSTALLED
793
+ with the npm command that installs it.
794
+
610
795
  ## How it feels: the Game Lab (`homie-studio lab`)
611
796
 
612
797
  ```sh
@@ -659,7 +844,9 @@ npx homie-studio assets find "fox" --kind creature # the starter library
659
844
  npx homie-studio assets add crown-thief kenney-cube-pets/animal-fox --height 0.7 --card "Characters/Fox"
660
845
  npx homie-studio assets add crown-thief --file ./lantern.glb --license own --height 0.6
661
846
  npx homie-studio assets check crown-thief # budgets, the glTF-Validator, licences, staleness, big files
847
+ npx homie-studio assets use crown-thief old-tree unused # what the game does with it now: cast, prop, environment, unused
662
848
  npx homie-studio assets lineup crown-thief # true scale, silhouettes, palette drift: .studio/art/<id>/
849
+ # three lineups: the inventory (every recorded model), the cast (what is in play), the environment (the scenery)
663
850
  npx homie-studio assets rights crown-thief # games/<id>/assets/RIGHTS.md and the credits
664
851
  npx homie-studio assets redo crown-thief lantern # made again from its kept raw file, free
665
852
  npx homie-studio assets remove crown-thief lantern # the record and the copy the game ships
@@ -674,14 +861,40 @@ what remaking it would cost; nothing is remade by itself. `games/<id>/style.json
674
861
  and camera the game, its landing and its title cards draw with.
675
862
 
676
863
  Every model, texture and sky a game ships has an entry in `games/<id>/assets/manifest.json`: its route (procedural,
677
- library, generated, imported), each step that made it, its licence and what a remix gets, its measurements. From it:
678
- `RIGHTS.md`, the credits on the game's landing, `assets check`, the licence check `publish` makes, and
679
- `/games/<id>/assets.json` on the site, from which `game remix` fetches every model its licence lets a remix carry
680
- (checked by SHA-256; a grey placeholder of the same size for the rest). Models are refused when they load anything
864
+ library, generated, imported), each step that made it, its licence, its measurements. From it:
865
+ `RIGHTS.md`, the credits on the game's landing, `assets check` and the licence check `publish` makes. Models are refused when they load anything
681
866
  from an address or are over the size caps; `assets add` makes them phone-sized (meshopt geometry, WebP pictures, the
682
867
  pivot at the bottom centre, scaled to metres) with @gltf-transform, meshoptimizer and sharp; raw files stay in
683
868
  `art/<slug>/raw/`, which git ignores.
684
869
 
870
+ `assets check` is an **inventory estimate**, and says so in everything it prints: the recorded files' own triangles,
871
+ draw calls and pictures, each times its declared `placements`. It never runs the game, so geometry the code builds,
872
+ copies the code draws beyond `placements`, effects, and shadow and post passes are not in it; a pass is not a measured
873
+ frame (the playtest and perf guides measure the running game). What it does measure:
874
+
875
+ - **Standalone textures and skies** (a `texture` or `sky` record's own pictures): there, the file recorded (SHA-256),
876
+ decodable, within size (a note past 1,024 px, a problem past 2,048 px; a sky twice that), and their mipmapped memory
877
+ added once per unique picture. A row with nothing measured prints `??` and why, never `ok`. Shipped pictures under
878
+ `public/` that no record names are listed with their size, apart from pictures the folder keeps but never ships;
879
+ neither is in the sum.
880
+ - **Maps the game does not draw.** A model's normal and roughness maps are counted unless the manifest says the game
881
+ draws base colour only (`stylize()` with a toon, flat, pixel or hand-painted material model keeps nothing else):
882
+ `"inGame": { "maps": "base" }` on a record, or once at the top of `assets/manifest.json` for every model
883
+ (`"maps": "all"` on a record keeps its own). The check never guesses this from the code; without the declaration it
884
+ says how many MB are such maps.
885
+ - **The game's own model budget.** game.json `"assets": { "budgets": { "triangles": 4000, "bytes": 600000 } }` (the key
886
+ the loader's development warnings read) raises what a model may be in the check and in `assets add`, never past a
887
+ hard cap and never lowering a hero's. One key, read once: the check, `assets add` and the build (for the loader)
888
+ take the same numbers. A budget may also be set for one tier (`"hero": { "triangles": 12000 }`; tiers are hero,
889
+ npc, prop, signature, kit); the loader knows no tiers, so it is given the loosest one, and a number past a hard cap
890
+ is held to the cap by all three (the build says so).
891
+ - **Usage.** `assets use <game> <asset> unused` marks a file the game no longer draws: it stays in the inventory
892
+ lineup, leaves the cast and environment lineups and the scene sums, and still counts in the shipped payload until
893
+ `assets remove`. Unset, usage follows the asset's kind.
894
+ - **Shipped payload** (`totals.shippedPayloadMB`; `firstPlayMB` is its old name, kept for existing readers): every
895
+ file of the last build, gzipped. A conservative gate on what the game ships, not a trace of what a browser fetched
896
+ before the first round.
897
+
685
898
  In a three.js game, `@homie-rocks/studio/assets` is the one loader:
686
899
 
687
900
  ```ts
@@ -692,7 +905,8 @@ const fox = await models.instance('./models/fox.glb').catch(() => models.placeho
692
905
 
693
906
  It checks every file before three.js parses it (no external URIs, no buffer over the cap, only extensions three.js
694
907
  reads), decodes meshopt, clones skinned models properly, and says in development when a model is over its budget
695
- (`stats()`, `window.__homieModels`). `game new <id> --from gem-rush-3d` is Gem Rush in 3D, dressed from the library.
908
+ (`stats()`, `window.__homieModels`). The budget is a small prop's (1,500 triangles, 300 KB) unless the game sets its
909
+ own once, in game.json: `"assets": { "budgets": { "triangles": 8000, "bytes": 1500000 } }` (per model). `game new <id> --from gem-rush-3d` is Gem Rush in 3D, dressed from the library.
696
910
  The local MCP's `style_explore`, `decision_set`, `assets_plan`, `assets_find`, `asset_add`, `asset_make`,
697
911
  `asset_check`, `asset_lineup` and `asset_rights` do the same with cards. Generated props (on the person's own fal
698
912
  account, under a budget, receipted) are the Homie plugin's `models` skill.
@@ -717,7 +931,14 @@ clip library `public/anims/<skeleton>.glb`, shared by every character with that
717
931
  has them, else retargeted at build time from the starter library's CC0 humanoid clips (KayKit's): every bone turns in
718
932
  the world the way the source's turned from its rest, after the two rest poses are lined up bone by bone, so a T-pose
719
933
  clip plays on an A-pose rig; a one-piece limb aims where the source's whole limb points. It is plain matrix math in
720
- Node (no renderer), so it runs in CI and a cloud session too. `assets check` holds characters to four influences a
934
+ Node (no renderer), so it runs in CI and a cloud session too. One clip library a skeleton: a second one is refused
935
+ when it is recorded (it used to hide the first), unless its record says `"rig": { "supplemental": true }`, in which case
936
+ its verbs join the character's coverage and previews and the game loads both
937
+ (`loadCharacter(models, url, { anims: [base, extra] })`). `anim plan` and `anim preview` also show how fast each
938
+ character's walk and run move its feet over the ground on its own rig (kept as `rig.gait`): animate's default
939
+ `runSpeed` of 4.2 m/s is not measured on your rig, and a clip retargeted onto a taller character covers more ground,
940
+ so start `tune: { runSpeed }` from that number. It is a guide from the foot bones, not a verified foot contact.
941
+ Removing a starter character keeps its pack's credit while any clip retargeted from it remains. `assets check` holds characters to four influences a
721
942
  vertex, the tier's bones, a clip library of 1 MB at 30 samples a second, every verb the game's `anim.clips` names,
722
943
  and the skinning a room costs on a phone (players times the heaviest character: 60,000 vertices and 1,200 bones a
723
944
  frame).
@@ -743,6 +964,71 @@ move), `anim_add`, `anim_preview` and `character_make` do the same with cards. A
743
964
  in the locked style, then Meshy's mesh and auto-rig on the person's own fal account, about US$1.44, priced and
744
965
  receipted) is the Homie plugin's `models` skill; rigs, clips and feel are its `animate` skill.
745
966
 
967
+ ## A model you can stand on (`homie-studio collision`)
968
+
969
+ Importing a model records what it looks like. It does not say where a figure may walk on it, so the first playable
970
+ version of a generated landmark is one figures fall through.
971
+
972
+ ```sh
973
+ npx homie-studio collision bake harbour-run lighthouse.glb --cell 0.25 # a height grid off its triangles, beside it
974
+ npx homie-studio collision check harbour-run # stale? proxies well formed? every route walked?
975
+ ```
976
+
977
+ `bake` writes `lighthouse.collision.json` (the grid sampled off the model's outside surface at the cell size you give,
978
+ the SHA-256 of the file and a checksum of its triangles) and `lighthouse.collision.svg`, the plan at true scale (40 px to
979
+ the metre). Three things in the JSON are yours, and a re-bake keeps them: `"proxies"` (boxes, cylinders and ramps for
980
+ what a figure walks around), `"mover"` (the body that must fit: radius, height, step, and the arrival radius the
981
+ game's bots really use) and `"routes"` (ways a bot should be able to walk, as lists of points). `check` fails, with a
982
+ row for each thing wrong, when the model's surface changed since the bake, a proxy is malformed, or a body of the
983
+ mover's size cannot walk a route: it says where it fell or what was in the way, and marks the spot on the plan.
984
+
985
+ The game loads the JSON with `unpackBake` and `bakeField` from `@homie-rocks/heightfield/MeshBake.js`, and calls
986
+ `bakeStale` against the mesh it loaded. The command needs `@homie-rocks/heightfield` installed in the studio (it says
987
+ so when it is not). Buildings with rooms and roofs, zone marks on every level and staged capture positions are that
988
+ package's `Levels.js`, `Route.js`, `Markers.js` and `Pose.js`: its README has them.
989
+
990
+ ## Bots that want different things (`@homie-rocks/studio/personas`)
991
+
992
+ ```ts
993
+ import { PERSONAS, createPersonaBrain, botStats, seededRng } from '@homie-rocks/studio/personas';
994
+
995
+ const who = createPersonaBrain(PERSONAS.bully, { rng: seededRng(roomSeed + slot), arena: { solid } });
996
+ const target = who.choose(self, candidates, rivals); // candidates: { x, y, kind, value, size? }
997
+ const push = who.steer(self, seek(self, target)); // turned off any wall ahead
998
+ ```
999
+
1000
+ One scorer (value against distance, with a target behind, a wall in the way, company near it and a bigger rival each
1001
+ costing some of the score) and five rows of weights: `glutton`, `bully`, `racer`, `scavenger`, `sneak`.
1002
+ `persona('sneak', { kinds: { beacon: 6 } })` extends one. `solid(x, y)` is the game's own wall test; line of sight
1003
+ and wall avoidance use nothing else. `who.chooser(...)` is the `choose` the port kit's `BotBrain.think` takes, so the
1004
+ room's skill dial still sets how late a bot reacts and how well it aims: a persona is what it wants.
1005
+ `who.stats()` / `botStats([...])` count what each bot picked (kinds, mean value, distance, facing, company, picks
1006
+ behind walls) and `personaSpread` is one number for how unlike each other they were: assert on those in the game's
1007
+ own test, because weights that look different often play the same. The only randomness is the `rng` you pass. The
1008
+ host owns the bots; nothing here is sent anywhere.
1009
+
1010
+ ## Power-ups whose timers survive a host change (`@homie-rocks/studio/powerups`)
1011
+
1012
+ ```ts
1013
+ import { createPowerups, stepPowerups, collectPowerups, resolveHit, biteRadius, collides, packPowerups, unpackPowerups } from '@homie-rocks/studio/powerups';
1014
+
1015
+ let power = createPowerups(SPOTS.length, roomSeed); // host
1016
+ ({ state: power } = stepPowerups(power, dtMs)); // host tick: timers, expiry, spawns
1017
+ ({ state: power, events } = collectPowerups(power, bodies, SPOTS)); // who touched what (lower slot on a tie)
1018
+ snapshot.power = packPowerups(power); // host: in the snapshot AND the checkpoint
1019
+ power = unpackPowerups(snapshot.power); // replicas, and a new host, once
1020
+ ```
1021
+
1022
+ Everything is one plain object and pure functions over it, so the rules are unit-tested and nothing reads a clock.
1023
+ The state holds how long each power-up has **left**, not a deadline, and the spawn sequence's seed: a browser that
1024
+ becomes host mid-timer carries on from the last snapshot and spawns the same next pickup. Three kinds are built in:
1025
+ Magnet (`biteRadius` is wider, `magnetPull` draws loose things toward the mouth), Phase (`collides(state, a, b)` is
1026
+ false) and Shield (`resolveHit` blocks one hit and returns the velocity to bounce the attacker with). Add a kind by
1027
+ adding a row to the rules, last (a kind travels as its index). `powerFlags` is a bitmask per body. It is the game's
1028
+ own snapshot data: the netplay wire protocol is unchanged, and a tab on an older build drops kinds it does not know.
1029
+ `powerDraws` fills instance matrices for three instanced meshes the game owns (gems, magnet rings, shield bubbles) and
1030
+ `hudPills` says what the HUD shows; both are optional and import no renderer.
1031
+
746
1032
  ## Checks on a computer without a GPU
747
1033
 
748
1034
  `check`, `port check` and `look` run Chrome headless. On a Mac they use its GPU. On Linux (a Claude Code cloud
@@ -751,6 +1037,40 @@ the home folder's `.cache/homie-studio` (from storage.googleapis.com, which a cl
751
1037
  renders with SwiftShader. `check` reports how fast each browser drew the game and on what renderer; on SwiftShader
752
1038
  it says the frame rate is not a person's, and judges only seats, rooms and rounds.
753
1039
 
1040
+ `check` reports three things apart: **seated** (a room, a seat, a role), **ready** (`readiness`: the play page's
1041
+ loading cover has lifted; the picture it calls `<browser>-playing.png` is taken only then, and a browser still
1042
+ covered is saved as `<browser>-loading.png`), and **connected** (`connection`, `reconnects`, `uninterrupted`: a
1043
+ round both browsers finished is `ok`, and whether one of them reconnected on the way is said beside it; a browser
1044
+ seen `reconnecting`, `alone`, `offline` or `closed` (the helper's link) is listed as `cutOff` and the connection is
1045
+ not called uninterrupted, whether or not a counter moved). `perf` leaves out a run whose browser ended its window cut
1046
+ off from the room. A site this computer's Node.js cannot look up does not stop `check` or `shoot` (their browsers may
1047
+ still open it); if the browsers cannot either, they report the same BLOCKED network preflight as `perf` and `port check`.
1048
+ Nothing listening on this computer's own address stops all four at once: "does not answer: start the site".
1049
+ `port check`'s big-screen row requires the join QR on an address a phone can reach and marks it not applicable on a
1050
+ loopback preview, where the page hides the join card on purpose.
1051
+
1052
+ ### Pictures on a stepped clock: `homie-studio shoot`
1053
+
1054
+ ```sh
1055
+ npx homie-studio shoot crown-thief --url http://127.0.0.1:8787 [--frames 60] [--fps 30] [--device computer|phone] [--hold KeyD] [--no-smoke] [--timeout 180]
1056
+ ```
1057
+
1058
+ For a 3D game where there is no GPU and the software renderer draws two frames a second. First a smoke check: two
1059
+ clients (a computer and a phone) open the play page in one private room (`?room=shoot-…`) and must both get a seat
1060
+ there, on different seats, with neither playing alone offline. Then the first client's clocks are frozen and stepped:
1061
+ `requestAnimationFrame`, `performance.now`, `Date`, `setTimeout`, `setInterval` and CSS/Web animations move by
1062
+ exactly 1/fps of a second per frame, in the page and in the game's frame, and each step is one picture
1063
+ (`.studio/shoot/<id>/frame-0001.png` …, with `shoot.json`: each frame's time, and the round phase, position and
1064
+ renderer counters when the game has a port probe). Not stepped, and said in the result: the room's socket and its
1065
+ server clock (other players arrive in real time), Web Audio's clock, `<video>`, workers. The frames show what was
1066
+ drawn; they are never a frame rate. Everything is bounded by `--timeout`; a run that hits it keeps the frames it has
1067
+ and says `partial`.
1068
+
1069
+ `shoot <id> --preview` needs no site running: it serves the built game itself (the light server `preview <id>` uses:
1070
+ no Wrangler, no rooms, the game alone and offline with its bots) and shoots that page. The smoke check is not run
1071
+ there (it needs rooms: use `dev` and `--url`). The result names the build the frames are of, by the hash `build`
1072
+ printed and the hashed bundle the page loaded.
1073
+
754
1074
  ## Another way to Cloudflare: Stripe Projects (a prototype, 0.24.3)
755
1075
 
756
1076
  `npx wrangler login` stays the way. For a creator with no Cloudflare account (and no ElevenLabs), Stripe Projects
@@ -812,7 +1132,9 @@ npx homie-studio port check my-game --url http://127.0.0.1:8787 # the owner te
812
1132
  | --- | --- |
813
1133
  | `bin/homie-studio.mjs` | The CLI. |
814
1134
  | `lib/scaffold.mjs` | `new`: the studio monorepo, only into a new or empty folder, every file listed. |
815
- | `lib/build.mjs` | `build`: games bundled with esbuild into `site/dist`, `games.json`, each game's shared `source.json`. |
1135
+ | `lib/build.mjs`, `lib/stage.mjs` | `build`: games bundled with esbuild (ES modules with code splitting, the bundle named by its content) into `site/dist`, `games.json` and `_site/build.json` (each game's build hash and whether it changed). Built in a folder of its own and put in place only when all of it is there; a game that does not build fails the command and leaves `site/dist` as it was (`site/SITE.md`, "The build"). |
1136
+ | `lib/typecheck.mjs` | `build --types`: the games' TypeScript checked with the studio's own `typescript` before anything is built. |
1137
+ | `lib/preview.mjs` | `preview <id>`: one built game's files at an address on this computer, with no Wrangler and no rooms. |
816
1138
  | `lib/cloudflare.mjs` | `deploy` (and `deploy --plan`): Wrangler, D1, migrations; refuses resources it did not create. In Workers Builds, migrations and deploy only. `storage add`: the optional R2 bucket; `media move`: big media into it, checked by SHA-256 (every deploy runs it). |
817
1139
  | `lib/check.mjs` | `check`: two fresh Chrome processes (computer + phone) must share a room and finish a round with both in it, with each one's frame rate; on a busy computer it waits out a round a browser was dropped from, and says why when none counts. `lib/chrome.mjs`: which Chrome, and how (the GPU on a Mac, SwiftShader on Linux). |
818
1140
  | `lib/upgrade.mjs`, `lib/template-history.json` | `upgrade`: an existing studio takes what a newer template adds, never over its own edits. |