@homie-rocks/studio 0.30.2 → 0.31.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 +108 -1
- package/README.md +349 -23
- 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 +204 -124
- 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 +203 -10
- 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 +172 -9
- package/lib/doctor.mjs +12 -11
- package/lib/handoff.mjs +2 -6
- package/lib/library.mjs +1 -1
- package/lib/mcp-tools.mjs +42 -37
- 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 +720 -0
- package/lib/perf.mjs +131 -31
- package/lib/port-check.mjs +40 -6
- package/lib/preview.mjs +77 -0
- package/lib/routes.mjs +240 -0
- package/lib/scaffold.mjs +83 -22
- 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 +101 -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 +552 -16
- package/netplay/netplay.ts +950 -43
- package/package.json +10 -1
- package/parts/PARTS.md +169 -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 +329 -40
- package/site/SITE.md +102 -20
- package/starters/ember-vale/index.html +1 -1
- package/starters/ember-vale/src/main.ts +7 -1
- package/starters/gem-rush/index.html +1 -1
- package/starters/gem-rush/src/main.ts +41 -12
- 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 +50 -11
- 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 +50 -11
- 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 +156 -29
- package/worker/parts.mjs +275 -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/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.
|
|
@@ -453,8 +496,8 @@ this toolkit alone (`new`, `deploy`, `publish`), and the connector row says so.
|
|
|
453
496
|
an address or a command; the command itself is never read out.
|
|
454
497
|
- **Homie's holds**: whether the plugin's hooks ran in this app in the last ten minutes, from the dated mark
|
|
455
498
|
they leave in `.cache/homie-studio/holds/` of the home folder. Off means nothing is held: Codex runs no plugin's hooks
|
|
456
|
-
until the person trusts them in `/hooks`, and
|
|
457
|
-
|
|
499
|
+
until the person trusts them in `/hooks`, and Grok Build runs them once the plugin is trusted
|
|
500
|
+
(`grok plugin install … --trust`); the row says which.
|
|
458
501
|
|
|
459
502
|
`homie-studio demo` answers without a network too (0.30.2): where it cannot reach the arcade it names the arcade's
|
|
460
503
|
standing first pick, with `reached: false`.
|
|
@@ -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,103 @@ 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
|
|
663
|
+
changed or removed. On a custom domain, `deploy` and `deploy --plan` read the domain's Worker routes first (two GETs
|
|
664
|
+
with the sign-in Wrangler already has) and warn, before anything is deployed, when another Worker's catch-all (`*/*`)
|
|
665
|
+
or wildcard (`*example.com/*`) covers the studio's hostname: that route answers the hostname before the studio does,
|
|
666
|
+
it belongs to another site on the domain, and it must not be edited or removed. The warning gives the one safe fix,
|
|
667
|
+
the studio's own exact-host route, as the exact line; `deploy --own-route` adds that line and nothing else, only when
|
|
668
|
+
the read showed it is needed. Routes that could not be read are said as unmeasured, never as fine; after the deploy,
|
|
669
|
+
a domain that answers as something else still gets the route to add. A wildcard or catch-all route (`*/*`) in the studio's own config would hand every
|
|
670
|
+
hostname of the zone to the studio's Worker, so `deploy` refuses it before it asks Cloudflare anything; a studio that
|
|
671
|
+
really owns the whole zone says so in studio.json (`"cloudflare": { "allowWildcardRoutes": true }`).
|
|
672
|
+
|
|
673
|
+
**One deploy at a time.** A second `deploy` of the same studio is refused while one runs (`.studio/deploy.lock` names
|
|
674
|
+
its process and start time); a lock left by a deploy that was killed is taken over by the next one.
|
|
675
|
+
|
|
676
|
+
**Which games changed.** Each deploy prints, per game, `changed`, `unchanged` or `new` against the last deploy from
|
|
677
|
+
this computer, and the game's **build hash**: the one `homie-studio build` printed for it, that
|
|
678
|
+
`site/dist/_site/build.json` keeps, that `perf` names a run's build by, and that the live site answers with in
|
|
679
|
+
`/.well-known/homie-studio.json` (`games[].build.hash`). There is one hash, so a local build is matched to what is
|
|
680
|
+
live by comparing two strings. `build` says changed "since the last build here"; `deploy` says changed "since the
|
|
681
|
+
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.
|
|
682
|
+
|
|
683
|
+
**The directory.** Going online lists nothing. For a studio this computer already listed with `publish`, the first
|
|
684
|
+
deploy after an older toolkit's (one that still offered a game's whole source) asks the directory to read the studio
|
|
685
|
+
again, once, so it does not keep that offer. `publish` says how many publishes are left today when the directory gives the number (the
|
|
686
|
+
beta has a daily cap), and before it sends, how many this computer has sent today; a refusal for the cap is said as
|
|
687
|
+
that, with when it ends.
|
|
688
|
+
|
|
689
|
+
**This computer's network.** When Node.js cannot look a public hostname up (a browser on the same computer may still
|
|
690
|
+
open it), the command says "network preflight failed", not that the site or a game is broken, and offers the local
|
|
691
|
+
dev site instead.
|
|
692
|
+
|
|
693
|
+
## A trailer: `homie-studio trailer`
|
|
694
|
+
|
|
695
|
+
```sh
|
|
696
|
+
npx homie-studio trailer crown-thief --url http://127.0.0.1:8787 --seconds 45 --length 20 --title "CROWN THIEF"
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
One command, into `videos/crown-thief-trailer/`: the game rendered frame by frame on a virtual clock (its
|
|
700
|
+
`requestAnimationFrame`, `performance.now`, `Date`, timers and CSS and Web animations all move 1/30 s a frame, so no
|
|
701
|
+
frame is held however heavy the game), its sound rebuilt from the game's own sound files and the log its sound player
|
|
702
|
+
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
|
|
703
|
+
Homie plugin's video skill; this command finds its script and hands over (`--skills <folder>` when the plugin is not
|
|
704
|
+
beside the package). The skill's guide says what the virtual clock does not reach (the audio clock, media elements,
|
|
705
|
+
Web Workers, a live server) and what a game must do for its sound to be in the film.
|
|
706
|
+
|
|
573
707
|
## How fast it runs: `homie-studio perf`
|
|
574
708
|
|
|
575
709
|
```sh
|
|
@@ -585,15 +719,28 @@ on a fast computer is less than its rate). They play the same seeded presses thr
|
|
|
585
719
|
window, and each run's JSON says, per browser: the time between animation frames (median, p95, p99, the share over 33
|
|
586
720
|
and 50 ms), the game's JavaScript per frame (every requestAnimationFrame callback timed), the main thread per frame
|
|
587
721
|
(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
|
-
|
|
722
|
+
game's first frame, to a seat and to playable (control-ready: seated, a body reported, the loading cover gone) and,
|
|
723
|
+
beside it, `load.ready` (when the game itself called `net.playable()`) with the arrival mode (`load.arrival`: `auto`,
|
|
724
|
+
`game`, or `seat` for an older helper; under `auto`, `lateMs` is how long after the cover lifted the game said it was
|
|
725
|
+
ready, which is a game that should declare `arrival: 'game'`), the game's files on the wire, the heap
|
|
726
|
+
after a garbage collection, and netplay messages and kilobytes a second each way. Two more are measurements of the
|
|
727
|
+
running game, not estimates from its files: `load.prePlay` (every request the browser really made up to playable,
|
|
728
|
+
with the bytes that had arrived of any still in flight; the metric is `load.prePlayKb`), which is first-play traffic
|
|
729
|
+
and not the shipped payload that `perf sizes` and `assets check` total from the built folder; and `render` (the
|
|
730
|
+
renderer's own draw calls and triangles while playing, metrics `render.calls` and `render.triangles`), read when the
|
|
731
|
+
game exposes them on its port probe (`exposePort(net, { extra: { drawCalls: () => renderer.info.render.calls,
|
|
732
|
+
triangles: () => renderer.info.render.triangles } })`, as the 3D starters do; `PortExtra` in the port kit types these
|
|
733
|
+
and the other names the playtest reads: `alive`, `mode`, `loadout`, `touchHeld`). A game that exposes neither has no
|
|
734
|
+
`render` numbers: the run says the cost was not measured, never zero. `--profile` adds a CPU profile per
|
|
590
735
|
browser in a window of its own (a `.cpuprofile` for Chrome DevTools) and its hottest functions, read through the
|
|
591
736
|
game's source map when the build kept one (`homie-studio build --maps` keeps it in `.studio/maps/<id>/`, never in
|
|
592
737
|
`site/dist`).
|
|
593
738
|
|
|
594
739
|
The computer is shared, so every run waits for the 1-minute load to fall under 0.8 per core (`--max-load`) and
|
|
595
740
|
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.
|
|
741
|
+
renderer (SwiftShader) makes the run `blocked`: nothing on it is judged. So does an address this computer's Node.js
|
|
742
|
+
cannot look up (Node and a browser do not look names up the same way): "network preflight failed", with the local
|
|
743
|
+
site as the way round it, never "the site is down". `compare` calls a metric better only when a
|
|
597
744
|
rank test says it is unlikely to be chance (p < 0.05), the 95% bootstrap interval of the change stays below zero, and
|
|
598
745
|
it is at least 3% better; runs taken side by side with the same `--pair` label are judged as pairs (a signed-rank
|
|
599
746
|
test on their ratios), so a computer that drifted busier cancels. Guards (frame time, main thread per frame, time to
|
|
@@ -607,6 +754,48 @@ Minified JavaScript gzips about as well as source text, and a bundle with three.
|
|
|
607
754
|
source in strings, which no minifier touches; the line for such a file says `minified, 19% GLSL shader source in
|
|
608
755
|
strings`, and the JSON's `code` has every measure.
|
|
609
756
|
|
|
757
|
+
## Game parts: how games build on each other
|
|
758
|
+
|
|
759
|
+
Games build on each other by sharing **parts**: a piece of a game that its studio chooses to share, so somebody
|
|
760
|
+
making another game can use it. A creature from one game, a level from another, a bot brain from a third. Without
|
|
761
|
+
parts, a creator's AI has to come up with everything on its own. [`parts/PARTS.md`](parts/PARTS.md) is the design;
|
|
762
|
+
the plugin's `parts` skill is the method.
|
|
763
|
+
|
|
764
|
+
Creators work in chat and never type a command: "what parts are there for a chase camera?", "add the Pickup field
|
|
765
|
+
part from owls.example/pickup-field to my game", "make my creature a part", "share it under CC BY". Four tools do
|
|
766
|
+
it, named like the asset tools: `parts_find`, `part_add`, `part_new` and `part_share`.
|
|
767
|
+
|
|
768
|
+
- **A part is a piece of a game, never the whole game**, in whatever form suits it: code, assets, data, a JSON
|
|
769
|
+
description, a tuned config, or a mix.
|
|
770
|
+
- **Agents look first.** `game_plan` and the game and plan skills tell an agent to check the `@homie-rocks/*`
|
|
771
|
+
packages for the general mechanism and `parts_find` for pieces other studios shared, before writing from
|
|
772
|
+
scratch, and to record in the game's CODEX (Built from) and its credits what came from where. `parts_find` reads
|
|
773
|
+
the hub's catalogue and the studio's own parts; when the hub cannot be reached it says so and still answers with
|
|
774
|
+
the studio's own, so an empty list never stands in for "could not look".
|
|
775
|
+
- **Packages are not parts.** `@homie-rocks/*` packages come from npm and are never in the catalogue. A part says
|
|
776
|
+
which packages it builds on, and npm does the rest: `npm ls` says whether the studio has one, `npm install`
|
|
777
|
+
installs what is missing after one line saying what and why, and npm's own output is relayed. There is no
|
|
778
|
+
resolver, no version ranges and no lockfile of Homie's.
|
|
779
|
+
- **Use.** `part_add` fetches one exact version (the latest shared unless named), checks every file's SHA-256 and
|
|
780
|
+
size before a byte is written, copies it into `parts/_vendor/<site>/<id>/` where the studio owns and tunes it,
|
|
781
|
+
records where it came from (`parts/origins.json`), and credits it in the game's `credits.json` with its licence,
|
|
782
|
+
who made it and the game it came from. The game imports it in one line,
|
|
783
|
+
`import { createPickupField } from '@parts/owls.example/pickup-field'` (one of the studio's own is
|
|
784
|
+
`@parts/<id>`). Adding it again brings the newer version: what changed is shown, `tuning.json` is kept, and a
|
|
785
|
+
file the studio edited is never replaced without saying so.
|
|
786
|
+
- **Share.** `part_new` lifts a piece out of a game (the game still builds and plays the same) and records the
|
|
787
|
+
game and studio it came from. A part is private until `part_share`, which needs an SPDX licence, an attribution
|
|
788
|
+
when the licence asks for credit, and known rights to every file. It is live after the studio's next deploy.
|
|
789
|
+
- **What the site serves** is `sharedPartsOf(studio)` and nothing else: `/.well-known/homie-parts.json`, each
|
|
790
|
+
shared version's content at `/parts/<id>/<version>/…` exactly as hashed (immutable, CORS open for GET), the
|
|
791
|
+
studio's own pages `/parts/` and `/parts/<id>/`, and a **Parts from this game** band on a game's landing. A
|
|
792
|
+
private part is never copied into the built site, and the Worker answers only what the built index names.
|
|
793
|
+
- **Publishing a game that uses parts**: `deploy --plan` shows each part's licence and attribution and says
|
|
794
|
+
plainly when licences cannot be combined; `publish` (and `studio_publish`, also with `before: true`) says the
|
|
795
|
+
same lines before the studio is listed. `parts add --no-install` brings a part in and installs nothing:
|
|
796
|
+
`package.json` is left as it is, and a package the part builds on that the studio lacks is named NOT INSTALLED
|
|
797
|
+
with the npm command that installs it.
|
|
798
|
+
|
|
610
799
|
## How it feels: the Game Lab (`homie-studio lab`)
|
|
611
800
|
|
|
612
801
|
```sh
|
|
@@ -659,7 +848,9 @@ npx homie-studio assets find "fox" --kind creature # the starter library
|
|
|
659
848
|
npx homie-studio assets add crown-thief kenney-cube-pets/animal-fox --height 0.7 --card "Characters/Fox"
|
|
660
849
|
npx homie-studio assets add crown-thief --file ./lantern.glb --license own --height 0.6
|
|
661
850
|
npx homie-studio assets check crown-thief # budgets, the glTF-Validator, licences, staleness, big files
|
|
851
|
+
npx homie-studio assets use crown-thief old-tree unused # what the game does with it now: cast, prop, environment, unused
|
|
662
852
|
npx homie-studio assets lineup crown-thief # true scale, silhouettes, palette drift: .studio/art/<id>/
|
|
853
|
+
# three lineups: the inventory (every recorded model), the cast (what is in play), the environment (the scenery)
|
|
663
854
|
npx homie-studio assets rights crown-thief # games/<id>/assets/RIGHTS.md and the credits
|
|
664
855
|
npx homie-studio assets redo crown-thief lantern # made again from its kept raw file, free
|
|
665
856
|
npx homie-studio assets remove crown-thief lantern # the record and the copy the game ships
|
|
@@ -674,14 +865,40 @@ what remaking it would cost; nothing is remade by itself. `games/<id>/style.json
|
|
|
674
865
|
and camera the game, its landing and its title cards draw with.
|
|
675
866
|
|
|
676
867
|
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
|
|
868
|
+
library, generated, imported), each step that made it, its licence, its measurements. From it:
|
|
869
|
+
`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
870
|
from an address or are over the size caps; `assets add` makes them phone-sized (meshopt geometry, WebP pictures, the
|
|
682
871
|
pivot at the bottom centre, scaled to metres) with @gltf-transform, meshoptimizer and sharp; raw files stay in
|
|
683
872
|
`art/<slug>/raw/`, which git ignores.
|
|
684
873
|
|
|
874
|
+
`assets check` is an **inventory estimate**, and says so in everything it prints: the recorded files' own triangles,
|
|
875
|
+
draw calls and pictures, each times its declared `placements`. It never runs the game, so geometry the code builds,
|
|
876
|
+
copies the code draws beyond `placements`, effects, and shadow and post passes are not in it; a pass is not a measured
|
|
877
|
+
frame (the playtest and perf guides measure the running game). What it does measure:
|
|
878
|
+
|
|
879
|
+
- **Standalone textures and skies** (a `texture` or `sky` record's own pictures): there, the file recorded (SHA-256),
|
|
880
|
+
decodable, within size (a note past 1,024 px, a problem past 2,048 px; a sky twice that), and their mipmapped memory
|
|
881
|
+
added once per unique picture. A row with nothing measured prints `??` and why, never `ok`. Shipped pictures under
|
|
882
|
+
`public/` that no record names are listed with their size, apart from pictures the folder keeps but never ships;
|
|
883
|
+
neither is in the sum.
|
|
884
|
+
- **Maps the game does not draw.** A model's normal and roughness maps are counted unless the manifest says the game
|
|
885
|
+
draws base colour only (`stylize()` with a toon, flat, pixel or hand-painted material model keeps nothing else):
|
|
886
|
+
`"inGame": { "maps": "base" }` on a record, or once at the top of `assets/manifest.json` for every model
|
|
887
|
+
(`"maps": "all"` on a record keeps its own). The check never guesses this from the code; without the declaration it
|
|
888
|
+
says how many MB are such maps.
|
|
889
|
+
- **The game's own model budget.** game.json `"assets": { "budgets": { "triangles": 4000, "bytes": 600000 } }` (the key
|
|
890
|
+
the loader's development warnings read) raises what a model may be in the check and in `assets add`, never past a
|
|
891
|
+
hard cap and never lowering a hero's. One key, read once: the check, `assets add` and the build (for the loader)
|
|
892
|
+
take the same numbers. A budget may also be set for one tier (`"hero": { "triangles": 12000 }`; tiers are hero,
|
|
893
|
+
npc, prop, signature, kit); the loader knows no tiers, so it is given the loosest one, and a number past a hard cap
|
|
894
|
+
is held to the cap by all three (the build says so).
|
|
895
|
+
- **Usage.** `assets use <game> <asset> unused` marks a file the game no longer draws: it stays in the inventory
|
|
896
|
+
lineup, leaves the cast and environment lineups and the scene sums, and still counts in the shipped payload until
|
|
897
|
+
`assets remove`. Unset, usage follows the asset's kind.
|
|
898
|
+
- **Shipped payload** (`totals.shippedPayloadMB`; `firstPlayMB` is its old name, kept for existing readers): every
|
|
899
|
+
file of the last build, gzipped. A conservative gate on what the game ships, not a trace of what a browser fetched
|
|
900
|
+
before the first round.
|
|
901
|
+
|
|
685
902
|
In a three.js game, `@homie-rocks/studio/assets` is the one loader:
|
|
686
903
|
|
|
687
904
|
```ts
|
|
@@ -692,7 +909,8 @@ const fox = await models.instance('./models/fox.glb').catch(() => models.placeho
|
|
|
692
909
|
|
|
693
910
|
It checks every file before three.js parses it (no external URIs, no buffer over the cap, only extensions three.js
|
|
694
911
|
reads), decodes meshopt, clones skinned models properly, and says in development when a model is over its budget
|
|
695
|
-
(`stats()`, `window.__homieModels`).
|
|
912
|
+
(`stats()`, `window.__homieModels`). The budget is a small prop's (1,500 triangles, 300 KB) unless the game sets its
|
|
913
|
+
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
914
|
The local MCP's `style_explore`, `decision_set`, `assets_plan`, `assets_find`, `asset_add`, `asset_make`,
|
|
697
915
|
`asset_check`, `asset_lineup` and `asset_rights` do the same with cards. Generated props (on the person's own fal
|
|
698
916
|
account, under a budget, receipted) are the Homie plugin's `models` skill.
|
|
@@ -717,7 +935,14 @@ clip library `public/anims/<skeleton>.glb`, shared by every character with that
|
|
|
717
935
|
has them, else retargeted at build time from the starter library's CC0 humanoid clips (KayKit's): every bone turns in
|
|
718
936
|
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
937
|
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.
|
|
938
|
+
Node (no renderer), so it runs in CI and a cloud session too. One clip library a skeleton: a second one is refused
|
|
939
|
+
when it is recorded (it used to hide the first), unless its record says `"rig": { "supplemental": true }`, in which case
|
|
940
|
+
its verbs join the character's coverage and previews and the game loads both
|
|
941
|
+
(`loadCharacter(models, url, { anims: [base, extra] })`). `anim plan` and `anim preview` also show how fast each
|
|
942
|
+
character's walk and run move its feet over the ground on its own rig (kept as `rig.gait`): animate's default
|
|
943
|
+
`runSpeed` of 4.2 m/s is not measured on your rig, and a clip retargeted onto a taller character covers more ground,
|
|
944
|
+
so start `tune: { runSpeed }` from that number. It is a guide from the foot bones, not a verified foot contact.
|
|
945
|
+
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
946
|
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
947
|
and the skinning a room costs on a phone (players times the heaviest character: 60,000 vertices and 1,200 bones a
|
|
723
948
|
frame).
|
|
@@ -743,6 +968,71 @@ move), `anim_add`, `anim_preview` and `character_make` do the same with cards. A
|
|
|
743
968
|
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
969
|
receipted) is the Homie plugin's `models` skill; rigs, clips and feel are its `animate` skill.
|
|
745
970
|
|
|
971
|
+
## A model you can stand on (`homie-studio collision`)
|
|
972
|
+
|
|
973
|
+
Importing a model records what it looks like. It does not say where a figure may walk on it, so the first playable
|
|
974
|
+
version of a generated landmark is one figures fall through.
|
|
975
|
+
|
|
976
|
+
```sh
|
|
977
|
+
npx homie-studio collision bake harbour-run lighthouse.glb --cell 0.25 # a height grid off its triangles, beside it
|
|
978
|
+
npx homie-studio collision check harbour-run # stale? proxies well formed? every route walked?
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
`bake` writes `lighthouse.collision.json` (the grid sampled off the model's outside surface at the cell size you give,
|
|
982
|
+
the SHA-256 of the file and a checksum of its triangles) and `lighthouse.collision.svg`, the plan at true scale (40 px to
|
|
983
|
+
the metre). Three things in the JSON are yours, and a re-bake keeps them: `"proxies"` (boxes, cylinders and ramps for
|
|
984
|
+
what a figure walks around), `"mover"` (the body that must fit: radius, height, step, and the arrival radius the
|
|
985
|
+
game's bots really use) and `"routes"` (ways a bot should be able to walk, as lists of points). `check` fails, with a
|
|
986
|
+
row for each thing wrong, when the model's surface changed since the bake, a proxy is malformed, or a body of the
|
|
987
|
+
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.
|
|
988
|
+
|
|
989
|
+
The game loads the JSON with `unpackBake` and `bakeField` from `@homie-rocks/heightfield/MeshBake.js`, and calls
|
|
990
|
+
`bakeStale` against the mesh it loaded. The command needs `@homie-rocks/heightfield` installed in the studio (it says
|
|
991
|
+
so when it is not). Buildings with rooms and roofs, zone marks on every level and staged capture positions are that
|
|
992
|
+
package's `Levels.js`, `Route.js`, `Markers.js` and `Pose.js`: its README has them.
|
|
993
|
+
|
|
994
|
+
## Bots that want different things (`@homie-rocks/studio/personas`)
|
|
995
|
+
|
|
996
|
+
```ts
|
|
997
|
+
import { PERSONAS, createPersonaBrain, botStats, seededRng } from '@homie-rocks/studio/personas';
|
|
998
|
+
|
|
999
|
+
const who = createPersonaBrain(PERSONAS.bully, { rng: seededRng(roomSeed + slot), arena: { solid } });
|
|
1000
|
+
const target = who.choose(self, candidates, rivals); // candidates: { x, y, kind, value, size? }
|
|
1001
|
+
const push = who.steer(self, seek(self, target)); // turned off any wall ahead
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
One scorer (value against distance, with a target behind, a wall in the way, company near it and a bigger rival each
|
|
1005
|
+
costing some of the score) and five rows of weights: `glutton`, `bully`, `racer`, `scavenger`, `sneak`.
|
|
1006
|
+
`persona('sneak', { kinds: { beacon: 6 } })` extends one. `solid(x, y)` is the game's own wall test; line of sight
|
|
1007
|
+
and wall avoidance use nothing else. `who.chooser(...)` is the `choose` the port kit's `BotBrain.think` takes, so the
|
|
1008
|
+
room's skill dial still sets how late a bot reacts and how well it aims: a persona is what it wants.
|
|
1009
|
+
`who.stats()` / `botStats([...])` count what each bot picked (kinds, mean value, distance, facing, company, picks
|
|
1010
|
+
behind walls) and `personaSpread` is one number for how unlike each other they were: assert on those in the game's
|
|
1011
|
+
own test, because weights that look different often play the same. The only randomness is the `rng` you pass. The
|
|
1012
|
+
host owns the bots; nothing here is sent anywhere.
|
|
1013
|
+
|
|
1014
|
+
## Power-ups whose timers survive a host change (`@homie-rocks/studio/powerups`)
|
|
1015
|
+
|
|
1016
|
+
```ts
|
|
1017
|
+
import { createPowerups, stepPowerups, collectPowerups, resolveHit, biteRadius, collides, packPowerups, unpackPowerups } from '@homie-rocks/studio/powerups';
|
|
1018
|
+
|
|
1019
|
+
let power = createPowerups(SPOTS.length, roomSeed); // host
|
|
1020
|
+
({ state: power } = stepPowerups(power, dtMs)); // host tick: timers, expiry, spawns
|
|
1021
|
+
({ state: power, events } = collectPowerups(power, bodies, SPOTS)); // who touched what (lower slot on a tie)
|
|
1022
|
+
snapshot.power = packPowerups(power); // host: in the snapshot AND the checkpoint
|
|
1023
|
+
power = unpackPowerups(snapshot.power); // replicas, and a new host, once
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
Everything is one plain object and pure functions over it, so the rules are unit-tested and nothing reads a clock.
|
|
1027
|
+
The state holds how long each power-up has **left**, not a deadline, and the spawn sequence's seed: a browser that
|
|
1028
|
+
becomes host mid-timer carries on from the last snapshot and spawns the same next pickup. Three kinds are built in:
|
|
1029
|
+
Magnet (`biteRadius` is wider, `magnetPull` draws loose things toward the mouth), Phase (`collides(state, a, b)` is
|
|
1030
|
+
false) and Shield (`resolveHit` blocks one hit and returns the velocity to bounce the attacker with). Add a kind by
|
|
1031
|
+
adding a row to the rules, last (a kind travels as its index). `powerFlags` is a bitmask per body. It is the game's
|
|
1032
|
+
own snapshot data: the netplay wire protocol is unchanged, and a tab on an older build drops kinds it does not know.
|
|
1033
|
+
`powerDraws` fills instance matrices for three instanced meshes the game owns (gems, magnet rings, shield bubbles) and
|
|
1034
|
+
`hudPills` says what the HUD shows; both are optional and import no renderer.
|
|
1035
|
+
|
|
746
1036
|
## Checks on a computer without a GPU
|
|
747
1037
|
|
|
748
1038
|
`check`, `port check` and `look` run Chrome headless. On a Mac they use its GPU. On Linux (a Claude Code cloud
|
|
@@ -751,6 +1041,40 @@ the home folder's `.cache/homie-studio` (from storage.googleapis.com, which a cl
|
|
|
751
1041
|
renders with SwiftShader. `check` reports how fast each browser drew the game and on what renderer; on SwiftShader
|
|
752
1042
|
it says the frame rate is not a person's, and judges only seats, rooms and rounds.
|
|
753
1043
|
|
|
1044
|
+
`check` reports three things apart: **seated** (a room, a seat, a role), **ready** (`readiness`: the play page's
|
|
1045
|
+
loading cover has lifted; the picture it calls `<browser>-playing.png` is taken only then, and a browser still
|
|
1046
|
+
covered is saved as `<browser>-loading.png`), and **connected** (`connection`, `reconnects`, `uninterrupted`: a
|
|
1047
|
+
round both browsers finished is `ok`, and whether one of them reconnected on the way is said beside it; a browser
|
|
1048
|
+
seen `reconnecting`, `alone`, `offline` or `closed` (the helper's link) is listed as `cutOff` and the connection is
|
|
1049
|
+
not called uninterrupted, whether or not a counter moved). `perf` leaves out a run whose browser ended its window cut
|
|
1050
|
+
off from the room. A site this computer's Node.js cannot look up does not stop `check` or `shoot` (their browsers may
|
|
1051
|
+
still open it); if the browsers cannot either, they report the same BLOCKED network preflight as `perf` and `port check`.
|
|
1052
|
+
Nothing listening on this computer's own address stops all four at once: "does not answer: start the site".
|
|
1053
|
+
`port check`'s big-screen row requires the join QR on an address a phone can reach and marks it not applicable on a
|
|
1054
|
+
loopback preview, where the page hides the join card on purpose.
|
|
1055
|
+
|
|
1056
|
+
### Pictures on a stepped clock: `homie-studio shoot`
|
|
1057
|
+
|
|
1058
|
+
```sh
|
|
1059
|
+
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]
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
For a 3D game where there is no GPU and the software renderer draws two frames a second. First a smoke check: two
|
|
1063
|
+
clients (a computer and a phone) open the play page in one private room (`?room=shoot-…`) and must both get a seat
|
|
1064
|
+
there, on different seats, with neither playing alone offline. Then the first client's clocks are frozen and stepped:
|
|
1065
|
+
`requestAnimationFrame`, `performance.now`, `Date`, `setTimeout`, `setInterval` and CSS/Web animations move by
|
|
1066
|
+
exactly 1/fps of a second per frame, in the page and in the game's frame, and each step is one picture
|
|
1067
|
+
(`.studio/shoot/<id>/frame-0001.png` …, with `shoot.json`: each frame's time, and the round phase, position and
|
|
1068
|
+
renderer counters when the game has a port probe). Not stepped, and said in the result: the room's socket and its
|
|
1069
|
+
server clock (other players arrive in real time), Web Audio's clock, `<video>`, workers. The frames show what was
|
|
1070
|
+
drawn; they are never a frame rate. Everything is bounded by `--timeout`; a run that hits it keeps the frames it has
|
|
1071
|
+
and says `partial`.
|
|
1072
|
+
|
|
1073
|
+
`shoot <id> --preview` needs no site running: it serves the built game itself (the light server `preview <id>` uses:
|
|
1074
|
+
no Wrangler, no rooms, the game alone and offline with its bots) and shoots that page. The smoke check is not run
|
|
1075
|
+
there (it needs rooms: use `dev` and `--url`). The result names the build the frames are of, by the hash `build`
|
|
1076
|
+
printed and the hashed bundle the page loaded.
|
|
1077
|
+
|
|
754
1078
|
## Another way to Cloudflare: Stripe Projects (a prototype, 0.24.3)
|
|
755
1079
|
|
|
756
1080
|
`npx wrangler login` stays the way. For a creator with no Cloudflare account (and no ElevenLabs), Stripe Projects
|
|
@@ -812,7 +1136,9 @@ npx homie-studio port check my-game --url http://127.0.0.1:8787 # the owner te
|
|
|
812
1136
|
| --- | --- |
|
|
813
1137
|
| `bin/homie-studio.mjs` | The CLI. |
|
|
814
1138
|
| `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
|
|
1139
|
+
| `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"). |
|
|
1140
|
+
| `lib/typecheck.mjs` | `build --types`: the games' TypeScript checked with the studio's own `typescript` before anything is built. |
|
|
1141
|
+
| `lib/preview.mjs` | `preview <id>`: one built game's files at an address on this computer, with no Wrangler and no rooms. |
|
|
816
1142
|
| `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
1143
|
| `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
1144
|
| `lib/upgrade.mjs`, `lib/template-history.json` | `upgrade`: an existing studio takes what a newer template adds, never over its own edits. |
|
package/assets/animate.ts
CHANGED
|
@@ -49,7 +49,9 @@ const HERO_BUDGET = { triangles: 8000, texturePx: 1024, materials: 2, bytes: 153
|
|
|
49
49
|
export const ANIM_TUNING = Object.freeze({
|
|
50
50
|
fade: { value: 0.14, min: 0, max: 0.6, step: 0.01, unit: 's', group: 'Motion', note: 'How long one clip takes to blend into the next' },
|
|
51
51
|
walkSpeed: { value: 1.5, min: 0.3, max: 6, step: 0.05, unit: 'm/s', group: 'Motion', note: 'The ground speed the walk clip\'s feet move at (feet slide when this is wrong)' },
|
|
52
|
-
|
|
52
|
+
// 4.2 m/s suits the starter library's chibi rigs. It is a default, NOT a reading of your rig: a clip retargeted onto
|
|
53
|
+
// a taller character covers more ground a cycle (`homie-studio anim plan` shows the speed measured on each rig).
|
|
54
|
+
runSpeed: { value: 4.2, min: 1, max: 12, step: 0.05, unit: 'm/s', group: 'Motion', note: 'The ground speed the run clip\'s feet move at (a default, not measured on your rig: anim plan shows the measured speed)' },
|
|
53
55
|
runFrom: { value: 2.4, min: 0.5, max: 8, step: 0.05, unit: 'm/s', group: 'Motion', note: 'From this speed up, the walk becomes a run' },
|
|
54
56
|
actSpeed: { value: 1.25, min: 0.5, max: 3, step: 0.05, group: 'Motion', note: 'How fast actions (attacks, casts) play: higher is snappier' },
|
|
55
57
|
jumpStretch: { value: 0.16, min: 0, max: 0.5, step: 0.01, group: 'Motion', note: 'How tall and thin the body goes as it leaves the ground' },
|
|
@@ -64,8 +66,11 @@ export const ANIM_TUNING = Object.freeze({
|
|
|
64
66
|
export type AnimTuning = { -readonly [K in keyof typeof ANIM_TUNING]?: number };
|
|
65
67
|
|
|
66
68
|
export interface CharacterOptions {
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The clip library's address (default: the one the model names, beside it in ./anims/); null for none; or a list of
|
|
71
|
+
* addresses when its skeleton has a supplemental library too (the base first).
|
|
72
|
+
*/
|
|
73
|
+
anims?: string | string[] | null;
|
|
69
74
|
/** Live numbers (lab.tunables(tuning)): any ANIM_TUNING key, read every frame. */
|
|
70
75
|
tune?: AnimTuning;
|
|
71
76
|
/** Bones that swing on springs (default: names with tail, ear, cape, hair, antenna). */
|
|
@@ -157,10 +162,15 @@ export async function loadCharacter(models: ModelSource, url: string, opts: Char
|
|
|
157
162
|
const loaded = await models.load(url, { budget: HERO_BUDGET });
|
|
158
163
|
const info = (loaded.scene.userData?.homie ?? {}) as { anims?: string; family?: string; sockets?: Record<string, string> };
|
|
159
164
|
let clips = loaded.animations ?? [];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
// `anims`: the clip library to use instead of the one the model names; null for none; or a LIST, for a skeleton
|
|
166
|
+
// with a supplemental library beside its own (the base first: where two hold a verb, the earlier one's clip plays).
|
|
167
|
+
const own = info.anims ? new URL(info.anims, new URL(url, typeof location !== 'undefined' ? location.href : 'http://localhost/')).href : null;
|
|
168
|
+
const animsUrls = opts.anims === null ? [] : Array.isArray(opts.anims) ? opts.anims : [opts.anims ?? own].filter((u): u is string => Boolean(u));
|
|
169
|
+
const fromLibraries: AnimationClip[] = [];
|
|
170
|
+
for (const animsUrl of animsUrls) {
|
|
171
|
+
try { for (const c of (await models.load(animsUrl, { budget: { triangles: 0, bytes: 3 * 1024 * 1024 } })).animations) if (!fromLibraries.some((x) => x.name === c.name)) fromLibraries.push(c); } catch (error) { if (!clips.length && !fromLibraries.length) console.warn(`[animate] ${url}: its clips did not load (${(error as Error).message})`); }
|
|
163
172
|
}
|
|
173
|
+
clips = [...fromLibraries, ...clips];
|
|
164
174
|
const model = await models.instance(url, { budget: HERO_BUDGET });
|
|
165
175
|
return createCharacter(model, clips, { ...opts, family: info.family ?? null, sockets: info.sockets ?? {} });
|
|
166
176
|
}
|