@homie-rocks/studio 0.44.3 → 0.45.1

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 CHANGED
@@ -17,6 +17,46 @@ 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.45.1] - 2026-10-09
21
+
22
+ **Plugin 0.45.1** · [#75](https://github.com/homie-rocks/homie/pull/75)
23
+
24
+ Crowds keep finding their way while a world changes.
25
+
26
+ ### Added
27
+
28
+ - **Navigation for walking agents in games and apps.** `@homie-rocks/nav` bakes tiled walking surfaces, finds routes across slopes and links, carves obstacles, streams tiles and moves crowds at a fixed tick. It supports either up axis, seeded queries and grid paths, with matching saved-state continuation in browsers and Workers.
29
+ - **Edits and chase targets preserve useful progress.** An unrelated edit leaves active searches alone. Replaced tiles and polygons invalidate only requests that touch them. Repeating a goal is a no-op; a nearby moving goal keeps the route already being found. Neighbour storage depends on agents rather than the distance between loaded tiles.
30
+ - **Portable room saves.** Save the mesh and crowd together and restore them with the original tile assets in any order. Malformed topology and span connections are refused with navigation errors. Crowds have an explicit `detach()` and meshes never retain discarded crowds.
31
+
32
+ ### Upgrade notes
33
+
34
+ - Based directly on main after [#85](https://github.com/homie-rocks/homie/pull/85) released studio 0.45.0. If main advances again, rebase and run `node scripts/renumber-release.mjs`.
35
+ - Navigation is optional for games and apps; existing studios gain no dependency. After its first npm publication, install `@homie-rocks/nav@0.1.0` in a studio that needs it. Its README has the imports, storage contract, larger test commands and CPU measurements for Node and workerd, including edits and full room wakes.
36
+
37
+ - **First npm publication needs a maintainer.** Navigation remains private and studio does not depend on it, so releases skip it safely. After merge, run `node scripts/first-publish-private.mjs nav`, then remove the private flag and update the lockfile and package contract test in a follow-up after trusted publishing is registered.
38
+
39
+ ## [0.45.0] - 2026-10-10
40
+
41
+ **Plugin 0.45.0** · [#85](https://github.com/homie-rocks/homie/pull/85)
42
+
43
+ Choose room sizes in the hundreds and reduce the state each phone downloads.
44
+
45
+ ### Added
46
+
47
+ - Gate connections and batched concentrator links, with seat resumption after a link restart.
48
+ - Studio-selected `room.view.precisionM`, `nearM` and `farHz` for distant bodies. The controlled body keeps its exact movement state.
49
+
50
+ ### Changed
51
+
52
+ - For rules games, public room admission, rules compilation and deploy planning use the studio's requested `players.max` without the 32-player clamp.
53
+ - Ordered snapshot deltas and a shared spatial index reduce repeated state work. Rules rooms have no automatic guest ceiling for shared carrier addresses.
54
+
55
+ ### Upgrade notes
56
+
57
+ - Rebuilding generates the Gate and Concentrator exports, bindings and SQLite migration, including for studios whose Worker entry predates Gates. Custom Worker source and routes are preserved.
58
+ - Existing browser-hosted netplay games keep building, checking and running with the restored port host/HUD helpers and importer. Their 32-seat limit remains; a larger request prints the rewrite guide. New games and starters use rules plus view. A release gate packs this version and checks two browsers finishing a round in the unchanged released 0.32.1 starter. Retirement is a later step, after studio migrations and a shipped one-command assisted rewrite.
59
+
20
60
  ## [0.44.3] - 2026-10-10
21
61
 
22
62
  **Plugin 0.44.3**
package/README.md CHANGED
@@ -202,30 +202,28 @@ game its studio chooses to share, each with its own licence ("Game parts", below
202
202
  RIGHTS.md has no "A remix" line. Whether a file may be handed on in a shared part is still its licence's.
203
203
  - `homie-studio game remix` answers with a sentence that points to parts; `office launch --remixable` is ignored.
204
204
 
205
- ## Rooms of up to 32
205
+ ## Room size and delivery
206
206
 
207
- Every room of a game has the seats its netplay manifest names: game.json's `netplay`
208
- block (`maxPlayers`), or a `netplay.json` beside game.json or in the game's build
209
- (`maxPlayers` or `players.max`), else game.json's `players.max`, else 8; at most 32.
210
- Bots and a late joiner taking a bot's body work the same at 32 as at 4.
211
- Server rules keep running when a browser leaves; older browser-hosted games elect a new host. One address may hold every seat plus four sockets, so a party
212
- on one Wi-Fi (or strangers behind one carrier's address) fills a room with a TV beside it.
207
+ `players.max` in game.json is the room's chosen positive integer capacity; omitted means 8.
208
+ There is no 32-player ceiling or default per-address admission limit. Public games use
209
+ rules plus view with `room.host: "server"`. Gates carry the browser connections and
210
+ concentrators combine Gate links for larger rooms. Server rules survive browser departures.
211
+ New browser/offline games use the same rules contract. Existing netplay games and their
212
+ port host helpers remain supported for upgrades, with their original 32-seat limit.
213
+ For larger rooms, follow `guides/game/REWRITE.md`.
213
214
 
214
- ## A game's netplay manifest
215
+ For example, a 300-player room can choose spatial delivery explicitly:
215
216
 
216
- For rules games the build derives the revision from the rules and view; do not bump
217
- `netplay.version` or tune browser host election. `room` sets hosting and tick rate.
218
- The version and stall settings below describe existing browser-hosted games.
219
-
220
- game.json `"netplay"` says more than the room's size. All of it is optional, and `homie-studio build`
221
- writes it to the catalogue for the site's rooms (`netplay/NETPLAY.md` sections 22 to 24):
217
+ ```json
218
+ {"players":{"min":1,"max":300},"entry":"src/view.ts","room":{"host":"server","view":{"radiusM":12,"precisionM":0.01,"nearM":4,"farHz":5}}}
219
+ ```
222
220
 
223
- | Field | What it does |
224
- |---|---|
225
- | `maxPlayers` | Seats in a room, up to 32. |
226
- | `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. |
227
- | `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. |
228
- | `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`. |
221
+ The studio chooses the radius, precision and distant update rate for its game. The
222
+ controlled body stays precise. Whole-room state remains the default. The build runs a
223
+ local capacity trial; its numbers are not Cloudflare capacity guarantees. Test the actual
224
+ mechanic at the chosen size. Rules builds derive their revision from rules and view;
225
+ do not manually bump `netplay.version`. Optional `netplay.params` still selects play-page
226
+ query parameters passed to the view, beside `debug` and `q`.
229
227
 
230
228
  The game's frame is sandboxed without `allow-same-origin` (it must never read the site's storage or
231
229
  the owner's session), so `localStorage` throws inside it: a setting or a personal best goes in
@@ -1238,8 +1236,8 @@ npx homie-studio port check my-game --url http://127.0.0.1:8787 # the owner te
1238
1236
  | `template/` (repository root), `scripts/template.mjs` | The public "Deploy to Cloudflare" template, generated from `new --template`. |
1239
1237
  | `lib/port.mjs` | `port plan` (reads a game and grades the port) and `port import`. |
1240
1238
  | `lib/port-check.mjs` | `port check`: held and alternating directions on keys, Android Chrome and iPhone WebKit touch, UI cover, two browsers finishing a round, a killed host, a late joiner, the big screen. |
1241
- | `port/` | The port toolkit (`@homie-rocks/studio/port`, or `window.HomiePort` from `homie-port.js` in a static game): `createRoom`, the touch kit, keys, camera rules, bots, a HUD, sandbox shims, first-touch audio, `exposePort`; `fitView` and `createLabels` (`port/view.ts`): a flat world on every screen (the whole of it where it reads, else filling a phone held upright and following the player) and names that never pile up when bodies crowd (yours placed first, the rest moved or faded). |
1242
- | `worker/index.mjs` | The site Worker and the `Table` (netplay relay, `room.mjs`) and `Lobby` Durable Objects; `/<game>/tv` is the big screen with a join QR (`qr.mjs`); `/<game>/watch` watches a live room from any player's view (NETPLAY.md section 16); `/music/<slug>/` and `/videos/<slug>/` are song and video pages (their files served with byte ranges, from the site's files or, once moved, from the studio's R2 at the same address; a loose file from `media put` at `/media/<key>`). `seats.mjs`: room sizes (up to 32). |
1239
+ | `port/` | The port toolkit (`@homie-rocks/studio/port`, or `window.HomiePort` from `homie-port.js` in a static game): the touch kit, keys, camera rules, bots, sandbox shims, first-touch audio, `exposePort`; `fitView` and `createLabels` (`port/view.ts`): a flat world on every screen (the whole of it where it reads, else filling a phone held upright and following the player) and names that never pile up when bodies crowd (yours placed first, the rest moved or faded). |
1240
+ | `worker/index.mjs` | The site Worker and the `Table` (netplay relay, `room.mjs`) and `Lobby` Durable Objects; `/<game>/tv` is the big screen with a join QR (`qr.mjs`); `/<game>/watch` watches a live room from any player's view (NETPLAY.md section 16); `/music/<slug>/` and `/videos/<slug>/` are song and video pages (their files served with byte ranges, from the site's files or, once moved, from the studio's R2 at the same address; a loose file from `media put` at `/media/<key>`). `seats.mjs`: studio-chosen room sizes. |
1243
1241
  | `worker/site.mjs`, `lib/site.mjs`, `lib/markdown.mjs`, `site/SITE.md` | The site: its sections, each game's landing, posts and their feeds, the look (theme tokens) and what the studio's `site/` folder overrides; the safe markdown posts are written in. `worker/pages.mjs`: the play page; `worker/arrival.mjs`: its arrival card. |
1244
1242
  | `worker/stats.mjs`, `worker/stats-page.mjs`, `lib/stats.mjs` | The studio's own stats: what is counted and how, the owner-only `/api/stats` and `/_studio/stats`, and `homie-studio stats`. |
1245
1243
  | `lib/media.mjs`, `media/MEDIA.md` | The `music/` and `videos/` manifests: which entries get a page, and where each file's bytes come from (the site itself up to 25 MiB a file, the studio's R2, or a link). `media list` shows it; `media move` puts the big ones in R2 and records each one's SHA-256; `media put` uploads a loose file to `/media/<key>`. |
@@ -1,8 +1,10 @@
1
1
  # Rewrite a browser-hosted game as rules plus view
2
2
 
3
3
  Use this path when the owner asks to move an old game's rules onto the server.
4
- There is no automatic converter. An ordinary edit is not permission to rewrite;
5
- old games continue to work as they are. Read RULES.md before starting.
4
+ There is no automatic converter yet. Version 0.45.0 keeps existing netplay games
5
+ and their port host helpers working, with their original 32-seat limit. Larger
6
+ rooms require this rewrite. Keep a baseline copy and preserve the game throughout.
7
+ Read RULES.md before starting.
6
8
 
7
9
  1. Read this game's codex, manifest, entry, host loop, input, packing/checkpoint
8
10
  code, bots, saves and tests. Record what players must keep: controls, timings,
@@ -36,7 +36,7 @@ export default defineRules({
36
36
 
37
37
  The map must contain a nonempty `spots.start` list for that example:
38
38
  `{"bounds":{"min":[-12,-7],"max":[12,7]},"boxes":[],"spots":{"start":[[-5,0],[5,0]]}}`.
39
- Declare `players.max` in game.json (1–32); use `entry: "src/view.ts"` and
39
+ Declare `players.max` in game.json (a positive integer, including hundreds); use `entry: "src/view.ts"` and
40
40
  `room: { host: "server" }`. Only public tunables are visible in move.ts.
41
41
 
42
42
  `index.html` is the browser entry document; include `<script type="module" src="./assets/main.js"></script>`. The manifest names the source, but HTML loads this relative built bundle. Never point HTML at /src/view.ts: that URL is not served. The build rewrites assets/main.js to its hashed bundle. A minimal manifest is:
@@ -134,7 +134,7 @@ with a real touchscreen tap in Chrome; calling `element.click()` does not test
134
134
  touch delivery.
135
135
  Use `rounds: { seconds: 0, breakSeconds: 0 }` for manual rounds, and no fill bots
136
136
  for an app. A wall/TV watches without taking a seat. Each interactive phone
137
- takes a seat (up to 32); shared state is replicated, public and transient. An app's
137
+ takes one of the room's declared seats; shared state is replicated, public and transient. An app's
138
138
  lasting, authorized business data belongs in `createAppRecords`, not in room fields.
139
139
 
140
140
  ## State and field types
@@ -680,8 +680,7 @@ check green. Never rename rules.ts away or fall back to old-style hosting.
680
680
  `room.probe` adds truthful frame/mechanic/render counters to the built-in rules probe.
681
681
  Use the normal build, local dev and two-browser check, then play the requested mechanic
682
682
  for a minute with delay. Movement should respond before the authoritative response;
683
- score and other shared outcomes still arrive with the network. Milestone 1 supports
684
- up to 32 seats; neither frame rate nor feel is guaranteed by a passing build.
683
+ score and other shared outcomes still arrive with the network. The studio chooses `players.max`; neither frame rate nor feel is guaranteed by a passing build.
685
684
 
686
685
  Room state is saved automatically (default movementSeconds: 1, plus round end,
687
686
  pause and finish). After 60 seconds with no person the room ends even if screens or
@@ -690,7 +689,7 @@ server-verified money. All replicated fields are public, including disguised rol
690
689
  Use a separate authorized records service for private or lasting app records.
691
690
 
692
691
 
693
- ## Spatial delivery (0.44.0; milestone 2 slice 1)
692
+ ## Spatial delivery (0.45.0; milestone 2 slice 2)
694
693
 
695
694
  For a server game with a larger map, the studio's AI can set
696
695
  `"room": { "view": { "radiusM": 32 } }` in game.json to send each player only
@@ -708,12 +707,33 @@ may be absent from room.each while its geometry still blocks movement.
708
707
  Shared state, effects, rosters and watcher state are still public: this is not a
709
708
  hidden-information feature. Browser hosting sends the whole room.
710
709
 
711
- A request for hundreds of players also needs the next Gates release: 0.44.0's
712
- public build/join path still has the milestone 1 restriction of 32 seats. Do not
713
- silently clamp a requested bigger game or claim this setting raises admission.
714
- When Gates ship, `players.max` will choose the room's layout automatically; the
715
- studio will not create or find infrastructure by hand. The slice plan and local
716
- 300-client measurements are in docs/rooms-milestone-2-notes.md in Homie's repo.
710
+ Set the room size in `players.max`. The same setting feeds rules compilation,
711
+ public admission and the automatic Gate layout; there is no 32-seat clamp.
712
+ For example, in game.json:
713
+
714
+ ```json
715
+ {
716
+ "players": { "min": 1, "max": 300 },
717
+ "room": {
718
+ "host": "server",
719
+ "view": { "radiusM": 12, "precisionM": 0.01, "nearM": 4, "farHz": 5 }
720
+ }
721
+ }
722
+ ```
723
+
724
+ These are example visual settings, not defaults. `precisionM` rounds remote
725
+ positions and velocities in the delivered view; controlled bodies stay exact.
726
+ Remote bodies outside `nearM` update at `farHz`; visibility exits are immediate.
727
+ Rules, collision, saves and shared state stay authoritative and unchanged.
728
+ Views must interpolate distant motion and dispose/recreate entities on view exit
729
+ and entry. Omit the settings for full precision and the room's normal tick rate.
730
+
731
+ A small room embeds delivery in its Table. Larger rooms open Gate connections;
732
+ more than eight Gates use concentrators. The studio template supplies their exports, bindings and
733
+ migration; upgrade the template before deploying an existing studio. No infrastructure naming or studio admission ceiling is required.
734
+ Read the local measurements and their limitations in
735
+ `docs/rooms-milestone-2-notes.md` before making a capacity claim.
736
+
717
737
  ## Live collision geometry (0.44.1)
718
738
 
719
739
  ```ts
@@ -30,9 +30,10 @@ other games and every existing test file, including its import paths. If an old
30
30
  do not redirect the tests to a new helper file. Run the original test command.
31
31
 
32
32
  For a request involving a larger room or map, read RULES.md's **Spatial delivery**
33
- section. 0.44.0 can reduce what a server player receives with `room.view.radiusM`;
34
- Gates and public admission above 32 seats are the next slice. Choose a radius
35
- only when it fits the game. No rules rewrite or new profile is needed.
33
+ section. `players.max` chooses the room size and Gate layout. Choose `room.view.radiusM`
34
+ for the playable neighbourhood, and visual precision and far update rates to fit
35
+ the game. Local measurements are in docs/rooms-milestone-2-notes.md; they are not
36
+ Cloudflare or physical-phone capacity guarantees.
36
37
 
37
38
  ## Start it
38
39
 
@@ -80,7 +81,7 @@ make every change it needs in ONE file_edit with an edits list. Build after each
80
81
  coherent change across rules, movement and view; use diagnostics to guide the next edit.
81
82
 
82
83
  New games use `game.json` `"entry": "src/view.ts"`, `"room": { "host": "server" }`,
83
- and `players.max` at most 32. HTML loads `<script type="module"
84
+ and a studio-chosen `players.max`. HTML loads `<script type="module"
84
85
  src="./assets/main.js"></script>`, not the source-file URL. Choose seats, tick rate (20 by default), dimensions and
85
86
  starter yourself from the request; never ask the person an engineering question.
86
87
  `coin-dash` is the smallest reference; `gem-rush` adds bumps; `ember-vale` adds
@@ -142,9 +143,10 @@ a plain browser bundle is not a multiplayer game.
142
143
  responsible half, preserve unrelated play, and rebuild. The build/state hashes
143
144
  manage compatibility automatically; do not hand-pack snapshots or bump netplay.version.
144
145
  Recheck the changed mechanic, a late join and a reload in two browsers.
145
- - **Changing an old-style game:** ordinary requested edits keep it working untouched
146
- in its existing architecture. When its owner asks to move rules to the server,
147
- follow [REWRITE.md](REWRITE.md). There is no automatic converter.
146
+ - **Changing an old-style game:** existing netplay games keep working on 0.45.0
147
+ with their original 32-seat limit. Preserve their behavior on ordinary edits.
148
+ For a server migration or larger room, follow [REWRITE.md](REWRITE.md).
149
+ New games use rules plus view; there is no automatic converter yet.
148
150
  - Phones get touch, computers get keys; typed names and chat are text only.
149
151
  Keep native DOM buttons outside `guardGestures`' touch-drag selector; otherwise bind
150
152
  touch pointers explicitly. Verify actual touchscreen taps, not only `.click()`.
package/lib/build.mjs CHANGED
@@ -62,7 +62,7 @@ import { PACKAGE_ROOT, isRulesGame, listExperiences as listGames, readStudio } f
62
62
  // module is: Homie for Claude Desktop starts the toolkit with no node_modules beside it (scripts/desktop.mjs).
63
63
  let rulesBuildModule = null;
64
64
  const rulesBuild = () => (rulesBuildModule ??= import('./rules-build.mjs'));
65
- import { SEAT_MAX, netplayRow } from '../worker/seats.mjs';
65
+ import { seatCount, SEAT_MAX, netplayRow } from '../worker/seats.mjs';
66
66
  import { STUDIO_VERSION } from './version.mjs';
67
67
  import { basedOnRow, licenseOf, retiredKeys } from '../worker/license.mjs';
68
68
  import { openStage, swapIn } from './stage.mjs';
@@ -266,10 +266,14 @@ export function netplayOf(g, out = null) {
266
266
  }
267
267
 
268
268
  /**
269
- * How many seats a room of this game has: the netplay manifest's `maxPlayers` (or `players.max`), else game.json's
270
- * `players.max`, else 8; at most SEAT_MAX (32). `asked` is what the game named, so a build can say it was capped.
269
+ * Room size comes from game.json's players.max, else 8 (32 for apps).
270
+ * Rules games have no admission ceiling; existing netplay games retain their manifest precedence and 32-seat limit.
271
271
  */
272
272
  export function seatsFor(g, net = netplayOf(g)) {
273
+ if (isRulesGame(g)) {
274
+ const max = seatCount(g.players?.max);
275
+ return { min: Math.min(max, seatCount(g.players?.min, 1)), max, asked: max };
276
+ }
273
277
  const named = [net.maxPlayers, net.players?.max, g.players?.max].map((n) => Math.floor(Number(n))).find((n) => Number.isFinite(n) && n >= 1);
274
278
  const asked = named ?? (g.kind === 'app' ? 32 : 8);
275
279
  const max = Math.min(SEAT_MAX, asked);
@@ -390,8 +394,7 @@ export async function buildGameFiles(esbuild, root, g, out, { maps = false, sour
390
394
  let chunks = [];
391
395
  const budgets = modelBudgetsOf(g, log);
392
396
  // RULES PLUS VIEW (rooms on the server). A game with "room" in game.json and a src/rules.ts has its rules checked, guarded and loaded first:
393
- // a rule the wall refuses stops the build here, with the line named. A game without one is hosted by a player's
394
- // browser exactly as before, and cannot ask for the server.
397
+ // a rule the wall refuses stops the build here, with the line named. Existing netplay games retain their browser host.
395
398
  const ruled = isRulesGame(g);
396
399
  if (!ruled && g.room?.host === 'server') {
397
400
  throw new Error(`${label}/${manifest} asks for "room": { "host": "server" }, but ${mode === 'bundle' ? 'this game has no src/rules.ts: its rules are inside its own code and run in a player\'s browser. Ask for it to be rewritten as rules plus view' : 'a ported game is someone else\'s browser code, which the server cannot run. It stays hosted by a player\'s browser'}.`);
@@ -517,8 +520,8 @@ export async function build(root, { only = null, log = () => {}, deploy = proces
517
520
  for (const g of games) if (g.kind === 'app') { const bad = appProblems(g); if (bad.length) throw new Error(`apps/${g.id}/app.json: ${bad.join('; ')}`); }
518
521
  // `--types`: the games' TypeScript is checked first (esbuild only strips types, it never reads them), and a type
519
522
  // error stops the build before anything is built.
520
- // Rules always use their generated capability types, including the view. The legacy
521
- // compiler sees broad source types and can falsely reject valid rules callbacks.
523
+ // Rules always use generated capability types, including the view. Existing netplay games and local apps
524
+ // use the source type checker.
522
525
  const legacy = games.filter((g) => !isRulesGame(g));
523
526
  const typed = types && legacy.length ? typecheck(root, legacy, { log }) : null;
524
527
  // What the build before this one made, to say which games changed.
@@ -561,7 +564,7 @@ async function buildInto(dist, { esbuild, studio, shop, live, games, before, typ
561
564
  const bytes = bundle ? statSync(join(out, bundle)).size : dirBytes(out);
562
565
  const later = chunks.reduce((n, c) => n + statSync(join(out, c)).size, 0);
563
566
  const seats = seatsFor(g, netplayOf(g, out));
564
- if (seats.asked > SEAT_MAX) log(`warning: games/${g.id} asks for ${seats.asked} players; a room holds at most ${SEAT_MAX}, so its rooms have ${SEAT_MAX} seats`);
567
+ if (seats.asked > SEAT_MAX && !rules) log(`games/${g.id} asks for ${seats.asked} players; browser-hosted netplay rooms retain their ${SEAT_MAX}-player limit, so this build uses ${SEAT_MAX} seats. For larger rooms, rewrite as rules plus view: node_modules/@homie-rocks/studio/guides/game/REWRITE.md`);
565
568
  built.push({
566
569
  id: g.id, name: g.name, mode, bytes, ms: Date.now() - started, warnings, seats: seats.max,
567
570
  ...(rules ? { capacityTrial: rules.check.capacityTrial } : {}),
@@ -1,4 +1,5 @@
1
1
  import {workerEntry} from './worker-build.mjs';
2
+ import { seatCount } from '../worker/seats.mjs';
2
3
  import { refreshWorkerConfig } from './worker-config.mjs';
3
4
  import { paidReleases, uploadPaidParts } from './parts-upload.mjs';
4
5
  /**
@@ -184,7 +185,7 @@ export function deployPlan(root) {
184
185
  const studio = readStudio(root);
185
186
  // Games whose rules run on the server (a src/rules.ts, and not "room": { "host": "browser" }): one line each.
186
187
  const hosted = listGames(root).filter((g) => isRulesGame(g) && g.room?.host !== 'browser').map((g) => {
187
- const seats = Math.max(1, Math.min(32, Math.floor(Number(g.players?.max)) || 8));
188
+ const seats = seatCount(g.players?.max);
188
189
  const tickHz = Math.max(1, Math.min(60, Math.floor(Number(g.room?.tickHz)) || 20));
189
190
  const use = hostedUse({ seats, inputHz: Math.max(1, Math.min(tickHz, Math.floor(Number(g.room?.inputHz)) || tickHz)) });
190
191
  return { id: g.id, name: g.name ?? g.id, ...use };
package/lib/mcp.mjs CHANGED
@@ -35,7 +35,7 @@ export const INSTRUCTIONS = `Homie Studio, on this computer: make game studios a
35
35
 
36
36
  Carry the person's request through to a working result without a lesson or an interview. Use their name or business name, otherwise "My Studio". Setup, dependencies, a plan, building and checking are AI work, not approval gates. Choose engineering and design defaults from the request; record them in CODEX.md so the person can change them later by asking. Phone and computer controls, a small first version, local sound and CC0 art are the defaults. No paid media unless requested with a budget. A business or charity may need an app or a site, not a game: do not make them play a demo or choose a genre.
37
37
  Run setup_status (for a studio that is not made yet, pass its name as studio), studio_scaffold (NO game until one is requested), then build the requested thing and check it. A demo and a small-change lesson are optional only when requested. Look for existing engine mechanisms and parts_find before building systems. Use few, large edits: one file_edit carries every change to a file in its edits list. game_plan records your decisions; it does not require an interview. Install missing prerequisites yourself with setup_prepare; follow studio_job without asking the person to retry. Optional providers never block.
38
- New games and apps needing transient shared live state use rules plus view with room.host server. Read studio_guide topic game and its file RULES.md before writing rules: declared entity/shared state and events, guarded deterministic move for immediate local prediction, and a browser view for input and rendering. Read file REWRITE.md when an owner asks to move an old game's rules to the server; no automatic converter exists and ordinary old games keep working. Repair the full build diagnostics and prove the requested behavior in two browsers. Current rooms have at most 32 seats; larger rooms and private per-seat state are not delivered. Apps use authorized app records for lasting/private business data; room hosting is not staff permission.
38
+ New games and apps needing transient shared live state use rules plus view with room.host server. Read studio_guide topic game and its file RULES.md before writing rules: declared entity/shared state and events, guarded deterministic move for immediate local prediction, and a browser view for input and rendering. Read file REWRITE.md when an owner asks to move an old game's rules to the server; no automatic converter exists; existing netplay games keep building and running with their original 32-seat limit; larger rooms require a rewrite. Repair the full build diagnostics and prove the requested behavior in two browsers. Set players.max to the chosen room size, including hundreds; public connections use Gates. Choose room.view radiusM, precisionM, nearM and farHz explicitly and measure at the requested size. Private per-seat state is not delivered. Apps use authorized app records for lasting/private business data; room hosting is not staff permission.
39
39
  Show progress briefly and give the preview link as soon as it works. Only when going live was requested, studio_deploy with plan: true says what it creates and costs; cloudflare_login asks for browser approval once; then studio_deploy. Publish to the directory only when requested (offer once after going live). Honour an existing request to deploy or list; do not ask again except for a host's mandatory approval.
40
40
  For a shop, stripe_login (shop connect) syncs Products, Prices, Payment Links and the webhook through one Stripe browser approval. No human-handled key. Follow studio_job and the result's single next step; rerun after shop.json changes. Test purchase, webhook, grant and refund with stand-ins before going live. Live payments only when requested.
41
41
  For a custom domain already on Cloudflare, studio_domain prepares the exact hostname route using the existing sign-in; studio_deploy applies it and Cloudflare creates DNS and TLS. Never ask for zone or account ids. Keep workers.dev until a domain is requested.
@@ -1,3 +1,4 @@
1
+ import { seatsOf } from '../worker/seats.mjs';
1
2
  /**
2
3
  * What `homie-studio build` does for a game written as rules plus view (rooms-milestone-1-design.md, section 8).
3
4
  *
@@ -145,7 +146,7 @@ export async function prepareRules(esbuild, root, g, { log = () => {}, longCheck
145
146
  if (!guarded.ok) throw new Error(`${g.kind === 'app' ? 'apps' : 'games'}/${g.id}: its rules were refused.\n${guarded.problems.slice(0, 20).map((p) => ` ${problemLine(p)}`).join('\n')}${guarded.problems.length > 20 ? `\n … and ${guarded.problems.length - 20} more` : ''}`);
146
147
  const tune = readJson(join(g.dir, 'tunables.json')) ?? {};
147
148
  const map = readMap(g);
148
- const seats = Math.max(1, Math.min(32, Math.floor(Number(g.players?.max)) || 8));
149
+ const seats = seatsOf(g);
149
150
  const bundle = await loadRules(esbuild, root, g.id, guarded.code, { bundleOnly: true });
150
151
  const data = { id: g.id, tune, map, room: g.room, seats, vocab: readJson(join(g.dir, 'agents.json')), sites: guarded.sites };
151
152
  // The declarations first, for the type check: a wrong program is named by the compiler before anything is played.
package/lib/scaffold.mjs CHANGED
@@ -71,7 +71,7 @@ export function slugify(name) {
71
71
  export function wranglerConfig({ worker, name, d1, d1Id = null, r2 = null, layout = 'root', main = null, ai = false, routes = null, paidParts = false, partsRateLimit = null, triggers = null }) {
72
72
  const at = layout === 'site' ? { schema: '../node_modules', main: 'src/runtime/worker.js', dist: './dist', migrations: 'migrations' }
73
73
  : { schema: 'node_modules', main: 'site/src/runtime/worker.js', dist: './site/dist', migrations: 'site/migrations' };
74
- const rooms = [{ name: 'TABLE', class_name: 'Table' }, { name: 'LOBBY', class_name: 'Lobby' }];
74
+ const rooms = [{ name: 'TABLE', class_name: 'Table' }, { name: 'LOBBY', class_name: 'Lobby' }, { name: 'GATE', class_name: 'Gate' }, { name: 'CONCENTRATOR', class_name: 'Concentrator' }];
75
75
  const config = {
76
76
  $schema: `${at.schema}/wrangler/config-schema.json`,
77
77
  name: worker,
@@ -94,7 +94,7 @@ export function wranglerConfig({ worker, name, d1, d1Id = null, r2 = null, layou
94
94
  ...(triggers ? { triggers: paidParts ? { ...triggers, crons: [...new Set([...(triggers.crons ?? []), '*/5 * * * *'])] } : triggers } : {}),
95
95
  assets: { directory: at.dist, binding: 'ASSETS', run_worker_first: true },
96
96
  durable_objects: { bindings: rooms },
97
- migrations: [{ tag: 'v1', new_sqlite_classes: ['Table', 'Lobby'] }],
97
+ migrations: [{ tag: 'v1', new_sqlite_classes: ['Table', 'Lobby'] }, { tag: 'v2-gates', new_sqlite_classes: ['Gate', 'Concentrator'] }],
98
98
  d1_databases: [{ binding: 'DB', database_name: d1, ...(d1Id ? { database_id: d1Id } : {}), migrations_dir: at.migrations }],
99
99
  ...(r2 ? { r2_buckets: [{ binding: 'MEDIA', bucket_name: r2 }, ...(paidParts ? [{ binding: 'PURCHASE_MEDIA', bucket_name: `${r2.slice(0, 52)}-purchases` }] : [])] } : {}),
100
100
  // Workers AI, only when a server's AI guides think with it (agents_brain workers-ai; deploy adds it, 0.17.0).
@@ -291,7 +291,7 @@ studio's pinned copy, never a registry lookup of the bare name.
291
291
  Start with coin-dash, gem-rush, ember-vale, gem-rush-3d or hero-rush-3d; all five follow this contract.
292
292
  In games/<id>/src/rules.ts, defineRules declares entities, fields, handlers, events, rounds and companions.
293
293
  The view calls openRoom and sends input/commands; it draws, plays sound and handles controls. It never
294
- decides a hit, score, spawn, turn or result. Keep game.json room.host set to server, up to 32 seats.
294
+ decides a hit, score, spawn, turn or result. Keep game.json room.host set to server and players.max to the chosen room size.
295
295
  - **Movement feels immediate through prediction.** Put the shared defineMove callback in src/move.ts;
296
296
  it runs authoritatively on the server and immediately for your own view, then reconciles. Other bodies
297
297
  interpolate. Use the starter's sweep/slide and room.me/room.each poses; no second browser physics loop,
@@ -312,10 +312,10 @@ studio's pinned copy, never a registry lookup of the bare name.
312
312
  then check with two browsers and play the requested mechanic with network delay. Info is not a failure.
313
313
  Never remove rules, weaken checks, forge a probe or switch hosting to get past a message.
314
314
  Rules hashes and room saves are automatic; character saves are player-owned, untrusted input. Room state
315
- is replicated, not secret. Bigger rooms and private state are not delivered by milestone 1.
315
+ is replicated, not secret. Rooms may have hundreds of seats; choose players.max and spatial delivery explicitly and measure. Private state is not delivered.
316
316
  - **Existing games:** change rules truth in rules and presentation in view. Browser hosting is for offline,
317
- local development and private friends games. Untouched older createNetplay/createRoom games still work;
318
- when asked to rewrite one, read the game skill's REWRITE.md and preserve its art, view and feel. There is
317
+ local development and private friends games. Existing netplay games and port host helpers remain supported at their original 32-seat limit;
318
+ to rewrite one, read the game skill's REWRITE.md and preserve its art, view and feel. There is
319
319
  no automatic converter. NETPLAY.md section 29 is the rules contract; earlier sections describe older APIs.
320
320
  - Phones and computers: touch controls on phones only, keys on computers; keep the
321
321
  centre of the screen clear during play.
@@ -473,7 +473,7 @@ studio's pinned copy, never a registry lookup of the bare name.
473
473
  aimNoise, aggression, positioning }\`. The party sets it by voting on a card in the play page (the middle vote
474
474
  wins, capped by the server's ceiling). Make a game's bots honour it: \`net.skillOf(slot)\` in their step (the
475
475
  snippet is in \`NETPLAY.md\` section 17; \`BotBrain\` from the port kit reads it with a rebuild), and declare
476
- \`caps: ['skill', 'agents']\` in \`createNetplay\` (a game on \`createRoom\` has \`agents\` already). A build
476
+ \`caps: ['skill', 'agents']\` in \`createNetplay\` (rules rooms provide the agent runtime). A build
477
477
  from before 0.16.0 still plays on every server; the office says it predates servers until it is rebuilt.
478
478
  - **Agent passes:** \`npx --no-install homie-studio agents pass <id> --label Claude [--server <server>]\` gives an
479
479
  AI its way into a seat (shown once; \`agents revoke <pass>\` ends it). It sits with \`POST /<id>/api/agent\`
@@ -545,7 +545,7 @@ chat's card follows the work from then on) and, for a studio still being set up,
545
545
  - The site's workers.dev address names the Cloudflare account (often after its owner): \`deploy\` keeps it in
546
546
  \`.studio/local.json\`, which git ignores. Never copy it into a committed file. A custom domain goes in
547
547
  studio.json as \`cloudflare.domain\`.
548
- - A game's room size is its netplay manifest's \`maxPlayers\` (game.json \`netplay\`, or netplay.json), up to 32.
548
+ - A game's room size is game.json \`players.max\`, a positive integer including hundreds; measure the chosen size.
549
549
  - Nothing in this studio needs \`~/.homie\` or a Homie box.
550
550
  - This studio is this folder. A studio in a folder beside it (another folder with its own \`studio.json\`) is
551
551
  somebody else's work: never read it or copy from it unless the person asks.
@@ -783,7 +783,7 @@ import rules from './rules/index.mjs';
783
783
 
784
784
  hostRules(rules);
785
785
  useTools(async () => (await import('./tools/index.mjs')).default);
786
- export { default, Table, Lobby } from '@homie-rocks/studio/worker';
786
+ export { default, Table, Lobby, Gate, Concentrator } from '@homie-rocks/studio/worker';
787
787
  `,
788
788
  'site/src/rules/index.mjs': rulesIndex([]),
789
789
  'site/migrations/0001_studio.sql': MIGRATION,
@@ -2447,14 +2447,24 @@
2447
2447
  "## Apps: one screen, roles and parts": "8596f2b25edda3e2",
2448
2448
  "## Commands (all through the pinned CLI in node_modules)": "159151b8861fc051",
2449
2449
  "## The site": "0cb8dd9a6f8a0096",
2450
- "## Making games": "9e44559de535d842",
2450
+ "## Making games": [
2451
+ "78950c6b4eb98ea6",
2452
+ "796cc9389236c713",
2453
+ "9e44559de535d842"
2454
+ ],
2451
2455
  "## The Game Codex and progress": "0738ef719cef3a4c",
2452
2456
  "## Art direction and models": "a48fff7bf0a5f2fc",
2453
2457
  "## Running live games (the back office)": "51ebf2ab756f6e4d",
2454
- "## Servers and AI seats": "d0028361fe567add",
2458
+ "## Servers and AI seats": [
2459
+ "1ccfd549d2369a60",
2460
+ "d0028361fe567add"
2461
+ ],
2455
2462
  "## Selling things (the shop)": "9e020d69dd6a954e",
2456
2463
  "## Continuing a build from the Claude app": "0d68081e924ca419",
2457
- "## Rules": "2e6b29afb85dcf7e",
2464
+ "## Rules": [
2465
+ "2e6b29afb85dcf7e",
2466
+ "c48971c1437414c2"
2467
+ ],
2458
2468
  "## Telling Homie": "80b46fdd9e578db8",
2459
2469
  "## Beta": "9d02638207a26e73"
2460
2470
  }
@@ -2510,6 +2520,93 @@
2510
2520
  }
2511
2521
  }
2512
2522
  }
2523
+ },
2524
+ "0.45.0": {
2525
+ "files": {
2526
+ "CLAUDE.md": "ddf2b2a0376202f6",
2527
+ "README.md": "684b3cca8001673c",
2528
+ "HANDOFF.md": "29337e2ace95b2e7",
2529
+ "games/README.md": "5ad32fe80c61f117",
2530
+ "music/README.md": "a3dc042dadd39df8",
2531
+ "videos/README.md": "16babc3437d5545b",
2532
+ "posts/README.md": "37841aae476bd540",
2533
+ "site/README.md": "732894273a32dc3f",
2534
+ "site/src/worker.mjs": "6cc5c6131eaf6fd0"
2535
+ },
2536
+ "sections": {
2537
+ "AGENTS.md": {
2538
+ "(intro)": "214aa46eb3a895d5",
2539
+ "## Version-matched authoring guides": "f6261a0e5801b96b",
2540
+ "## Layout": "f3a280b8c96cd068",
2541
+ "## Apps: one screen, roles and parts": "8596f2b25edda3e2",
2542
+ "## Commands (all through the pinned CLI in node_modules)": "159151b8861fc051",
2543
+ "## The site": "0cb8dd9a6f8a0096",
2544
+ "## Making games": [
2545
+ "78950c6b4eb98ea6",
2546
+ "bb715272fd1a615b"
2547
+ ],
2548
+ "## The Game Codex and progress": "0738ef719cef3a4c",
2549
+ "## Art direction and models": "a48fff7bf0a5f2fc",
2550
+ "## Running live games (the back office)": "51ebf2ab756f6e4d",
2551
+ "## Servers and AI seats": "1ccfd549d2369a60",
2552
+ "## Selling things (the shop)": "9e020d69dd6a954e",
2553
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
2554
+ "## Rules": "c48971c1437414c2",
2555
+ "## Telling Homie": "80b46fdd9e578db8",
2556
+ "## Beta": "9d02638207a26e73"
2557
+ }
2558
+ },
2559
+ "named": {
2560
+ "Homie Arcade": {
2561
+ "sections": {
2562
+ "AGENTS.md": {
2563
+ "## Commands (all through the pinned CLI in node_modules)": "2cdd530e48170bfd"
2564
+ }
2565
+ }
2566
+ }
2567
+ }
2568
+ },
2569
+ "0.45.1": {
2570
+ "files": {
2571
+ "CLAUDE.md": "ddf2b2a0376202f6",
2572
+ "README.md": "684b3cca8001673c",
2573
+ "HANDOFF.md": "29337e2ace95b2e7",
2574
+ "games/README.md": "5ad32fe80c61f117",
2575
+ "music/README.md": "a3dc042dadd39df8",
2576
+ "videos/README.md": "16babc3437d5545b",
2577
+ "posts/README.md": "37841aae476bd540",
2578
+ "site/README.md": "732894273a32dc3f",
2579
+ "site/src/worker.mjs": "6cc5c6131eaf6fd0"
2580
+ },
2581
+ "sections": {
2582
+ "AGENTS.md": {
2583
+ "(intro)": "214aa46eb3a895d5",
2584
+ "## Version-matched authoring guides": "f6261a0e5801b96b",
2585
+ "## Layout": "f3a280b8c96cd068",
2586
+ "## Apps: one screen, roles and parts": "8596f2b25edda3e2",
2587
+ "## Commands (all through the pinned CLI in node_modules)": "159151b8861fc051",
2588
+ "## The site": "0cb8dd9a6f8a0096",
2589
+ "## Making games": "bb715272fd1a615b",
2590
+ "## The Game Codex and progress": "0738ef719cef3a4c",
2591
+ "## Art direction and models": "a48fff7bf0a5f2fc",
2592
+ "## Running live games (the back office)": "51ebf2ab756f6e4d",
2593
+ "## Servers and AI seats": "1ccfd549d2369a60",
2594
+ "## Selling things (the shop)": "9e020d69dd6a954e",
2595
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
2596
+ "## Rules": "c48971c1437414c2",
2597
+ "## Telling Homie": "80b46fdd9e578db8",
2598
+ "## Beta": "9d02638207a26e73"
2599
+ }
2600
+ },
2601
+ "named": {
2602
+ "Homie Arcade": {
2603
+ "sections": {
2604
+ "AGENTS.md": {
2605
+ "## Commands (all through the pinned CLI in node_modules)": "2cdd530e48170bfd"
2606
+ }
2607
+ }
2608
+ }
2609
+ }
2513
2610
  }
2514
2611
  }
2515
2612
  }
package/lib/typecheck.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import {listOwnParts, readOrigins, vendorDir} from './parts.mjs';
2
+ import {resolvePartImport} from './parts-build.mjs';
1
3
  import { runRulesCompiler } from './rules-compiler.mjs';
2
4
  /**
3
5
  * `homie-studio build --types` — the games' TypeScript, checked before anything is built.
@@ -7,7 +9,7 @@ import { runRulesCompiler } from './rules-compiler.mjs';
7
9
  * of its own to run `npx tsc` with.
8
10
  *
9
11
  * - The compiler is the STUDIO'S OWN (`typescript` in its node_modules, which a new studio's package.json asks
10
- * for). This optional check for legacy games keeps using that compiler. Rules games use the toolkit's own
12
+ * for). This optional check for legacy games and local apps keeps using that compiler. Rules games use the toolkit's own
11
13
  * pinned compiler through typecheckRules below, on every build.
12
14
  * - A game with a tsconfig.json of its own is checked with it. Any other game is checked the way esbuild reads
13
15
  * it: its entry and whatever that imports, as ES2022 for a browser, strictly, with the pictures, sounds and
@@ -15,7 +17,7 @@ import { runRulesCompiler } from './rules-compiler.mjs';
15
17
  * - Only errors in the game's own files stop the build. A game imports the toolkit's helpers as TypeScript
16
18
  * source, and a creator can do nothing about a line in node_modules; those are counted and said, never fatal.
17
19
  *
18
- * The legacy check runs only when asked with --types. The rules check is mandatory.
20
+ * The legacy/app check runs only when asked with --types. The rules check is mandatory.
19
21
  */
20
22
  import { spawnSync } from 'node:child_process';
21
23
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
@@ -116,8 +118,17 @@ export async function typecheckRules(root, g, checked, tune, { log = () => {} }
116
118
  writeFileSync(join(dir, 'rules.d.ts'), rulesTypes({ ...checked, answerScopes: await rulesAnswerScopes(join(g.dir, 'src/rules.ts')) }, tune));
117
119
  writeFileSync(join(dir, 'view.d.ts'), rulesViewTypes(checked));
118
120
  writeFileSync(join(dir, 'files.d.ts'), AMBIENT);
121
+ const partPaths = {};
122
+ const origins = readOrigins(root);
123
+ for (const ref of [...listOwnParts(root).map(p => p.id), ...Object.keys(origins.parts)]) {
124
+ const base = origins.parts[ref] ? vendorDir(root, ref) : join(root, 'parts', ref);
125
+ partPaths[`@parts/${ref}/*`] = [join(base, '*')];
126
+ const entry = resolvePartImport(root, `@parts/${ref}`, origins);
127
+ if (entry.path) partPaths[`@parts/${ref}`] = [entry.path];
128
+ }
119
129
  const project = join(dir, 'tsconfig.json');
120
130
  writeFileSync(project, `${JSON.stringify({ compilerOptions: { ...OPTIONS, checkJs: true, paths: {
131
+ ...partPaths,
121
132
  ...Object.fromEntries(Object.entries(JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).exports)
122
133
  .filter(([, entry]) => entry?.types).map(([name, entry]) => [`@homie-rocks/studio/${name.slice(2)}`, [join(PACKAGE_ROOT, entry.types)]])),
123
134
  '@homie-rocks/studio/rules': [join(dir, 'rules.d.ts')],
@@ -131,6 +142,9 @@ export async function typecheckRules(root, g, checked, tune, { log = () => {} }
131
142
  const at = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/.exec(line);
132
143
  if (at) {
133
144
  const file = resolve(at[1]);
145
+ const missingPart = /Cannot find module '(@parts\/[^']+)'/.exec(at[5]);
146
+ if (missingPart) { const result = resolvePartImport(root, missingPart[1]); if (result.error) at[5] = result.error; }
147
+
134
148
  // Imported game helpers count too. Only the toolkit's own implementation is outside the author's remit.
135
149
  if (!file.startsWith(`${PACKAGE_ROOT}${sep}`) && !file.includes(`${sep}node_modules${sep}`) || file.startsWith(`${g.dir}${sep}`)) {
136
150
  const sourceLine = readFileSync(file,'utf8').split(/\r?\n/)[Number(at[2])-1] ?? '';