@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.
- package/CHANGELOG.md +76 -1
- package/README.md +343 -21
- package/assets/animate.ts +16 -6
- package/assets/assets.ts +14 -3
- package/assets/render-page.ts +10 -2
- package/assets/safety.mjs +7 -1
- package/bin/homie-studio.mjs +136 -121
- package/lib/art-cli.mjs +61 -18
- package/lib/art-tools.mjs +53 -10
- package/lib/asset-check.mjs +249 -46
- package/lib/asset-import.mjs +11 -5
- package/lib/asset-manifest.mjs +128 -51
- package/lib/build.mjs +239 -72
- package/lib/characters.mjs +119 -20
- package/lib/check.mjs +110 -7
- package/lib/chrome.mjs +11 -0
- package/lib/clips.mjs +54 -0
- package/lib/cloudflare.mjs +127 -8
- package/lib/codex.mjs +6 -0
- package/lib/collision.mjs +245 -0
- package/lib/decisions.mjs +2 -2
- package/lib/deploy-state.mjs +166 -0
- package/lib/dev.mjs +430 -0
- package/lib/directory.mjs +116 -5
- package/lib/doctor.mjs +2 -2
- package/lib/handoff.mjs +2 -6
- package/lib/library.mjs +1 -1
- package/lib/mcp-tools.mjs +36 -35
- package/lib/mcp.mjs +1 -1
- package/lib/net.mjs +81 -2
- package/lib/node-version.mjs +40 -0
- package/lib/office.mjs +6 -6
- package/lib/optimise.mjs +39 -4
- package/lib/parts-build.mjs +169 -0
- package/lib/parts-cli.mjs +104 -0
- package/lib/parts-store.mjs +342 -0
- package/lib/parts-tools.mjs +92 -0
- package/lib/parts.mjs +693 -0
- package/lib/perf.mjs +131 -31
- package/lib/port-check.mjs +40 -6
- package/lib/preview.mjs +77 -0
- package/lib/routes.mjs +132 -0
- package/lib/scaffold.mjs +81 -20
- package/lib/shoot.mjs +307 -0
- package/lib/site.mjs +1 -2
- package/lib/stage.mjs +118 -0
- package/lib/studio.mjs +0 -66
- package/lib/style-board.mjs +49 -6
- package/lib/template-history.json +61 -0
- package/lib/trailer.mjs +47 -0
- package/lib/typecheck.mjs +107 -0
- package/mcp/ui/lineup.js +3 -3
- package/mcp/ui/rights.js +1 -3
- package/netplay/NETPLAY.md +424 -16
- package/netplay/netplay.ts +756 -40
- package/package.json +10 -1
- package/parts/PARTS.md +163 -0
- package/port/index.ts +8 -2
- package/port/personas.ts +275 -0
- package/port/powerups.ts +357 -0
- package/port/probe.ts +38 -2
- package/port/room.ts +103 -19
- package/site/SITE.md +102 -20
- package/starters/ember-vale/index.html +1 -1
- package/starters/ember-vale/src/main.ts +3 -1
- package/starters/gem-rush/index.html +1 -1
- package/starters/gem-rush/src/main.ts +13 -6
- package/starters/gem-rush-3d/assets/RIGHTS.md +0 -24
- package/starters/gem-rush-3d/assets/manifest.json +0 -24
- package/starters/gem-rush-3d/index.html +1 -1
- package/starters/gem-rush-3d/src/main.ts +15 -5
- package/starters/hero-rush-3d/assets/RIGHTS.md +0 -19
- package/starters/hero-rush-3d/assets/manifest.json +0 -19
- package/starters/hero-rush-3d/index.html +1 -1
- package/starters/hero-rush-3d/src/main.ts +15 -5
- package/worker/arrival.mjs +4 -1
- package/worker/discover.mjs +11 -25
- package/worker/index.mjs +142 -62
- package/worker/license.mjs +45 -41
- package/worker/office-page.mjs +0 -4
- package/worker/office-schema.mjs +6 -0
- package/worker/office.mjs +19 -23
- package/worker/pages.mjs +141 -23
- package/worker/parts.mjs +234 -0
- package/worker/room.mjs +157 -14
- package/worker/schema.mjs +6 -8
- package/worker/seats.mjs +71 -0
- package/worker/site.mjs +51 -53
- package/worker/version.mjs +1 -1
- 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 /
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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,
|
|
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)
|
|
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
|
|
482
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
|
589
|
-
|
|
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.
|
|
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
|
|
678
|
-
`RIGHTS.md`, the credits on the game's landing, `assets check
|
|
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`).
|
|
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.
|
|
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
|
|
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. |
|