@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +108 -1
  2. package/README.md +349 -23
  3. package/assets/animate.ts +16 -6
  4. package/assets/assets.ts +14 -3
  5. package/assets/render-page.ts +10 -2
  6. package/assets/safety.mjs +7 -1
  7. package/bin/homie-studio.mjs +204 -124
  8. package/lib/art-cli.mjs +61 -18
  9. package/lib/art-tools.mjs +53 -10
  10. package/lib/asset-check.mjs +249 -46
  11. package/lib/asset-import.mjs +11 -5
  12. package/lib/asset-manifest.mjs +128 -51
  13. package/lib/build.mjs +239 -72
  14. package/lib/characters.mjs +119 -20
  15. package/lib/check.mjs +110 -7
  16. package/lib/chrome.mjs +11 -0
  17. package/lib/clips.mjs +54 -0
  18. package/lib/cloudflare.mjs +203 -10
  19. package/lib/codex.mjs +6 -0
  20. package/lib/collision.mjs +245 -0
  21. package/lib/decisions.mjs +2 -2
  22. package/lib/deploy-state.mjs +166 -0
  23. package/lib/dev.mjs +430 -0
  24. package/lib/directory.mjs +172 -9
  25. package/lib/doctor.mjs +12 -11
  26. package/lib/handoff.mjs +2 -6
  27. package/lib/library.mjs +1 -1
  28. package/lib/mcp-tools.mjs +42 -37
  29. package/lib/mcp.mjs +1 -1
  30. package/lib/net.mjs +81 -2
  31. package/lib/node-version.mjs +40 -0
  32. package/lib/office.mjs +6 -6
  33. package/lib/optimise.mjs +39 -4
  34. package/lib/parts-build.mjs +169 -0
  35. package/lib/parts-cli.mjs +104 -0
  36. package/lib/parts-store.mjs +342 -0
  37. package/lib/parts-tools.mjs +92 -0
  38. package/lib/parts.mjs +720 -0
  39. package/lib/perf.mjs +131 -31
  40. package/lib/port-check.mjs +40 -6
  41. package/lib/preview.mjs +77 -0
  42. package/lib/routes.mjs +240 -0
  43. package/lib/scaffold.mjs +83 -22
  44. package/lib/shoot.mjs +307 -0
  45. package/lib/site.mjs +1 -2
  46. package/lib/stage.mjs +118 -0
  47. package/lib/studio.mjs +0 -66
  48. package/lib/style-board.mjs +49 -6
  49. package/lib/template-history.json +101 -0
  50. package/lib/trailer.mjs +47 -0
  51. package/lib/typecheck.mjs +107 -0
  52. package/mcp/ui/lineup.js +3 -3
  53. package/mcp/ui/rights.js +1 -3
  54. package/netplay/NETPLAY.md +552 -16
  55. package/netplay/netplay.ts +950 -43
  56. package/package.json +10 -1
  57. package/parts/PARTS.md +169 -0
  58. package/port/index.ts +8 -2
  59. package/port/personas.ts +275 -0
  60. package/port/powerups.ts +357 -0
  61. package/port/probe.ts +38 -2
  62. package/port/room.ts +329 -40
  63. package/site/SITE.md +102 -20
  64. package/starters/ember-vale/index.html +1 -1
  65. package/starters/ember-vale/src/main.ts +7 -1
  66. package/starters/gem-rush/index.html +1 -1
  67. package/starters/gem-rush/src/main.ts +41 -12
  68. package/starters/gem-rush-3d/assets/RIGHTS.md +0 -24
  69. package/starters/gem-rush-3d/assets/manifest.json +0 -24
  70. package/starters/gem-rush-3d/index.html +1 -1
  71. package/starters/gem-rush-3d/src/main.ts +50 -11
  72. package/starters/hero-rush-3d/assets/RIGHTS.md +0 -19
  73. package/starters/hero-rush-3d/assets/manifest.json +0 -19
  74. package/starters/hero-rush-3d/index.html +1 -1
  75. package/starters/hero-rush-3d/src/main.ts +50 -11
  76. package/worker/arrival.mjs +4 -1
  77. package/worker/discover.mjs +11 -25
  78. package/worker/index.mjs +142 -62
  79. package/worker/license.mjs +45 -41
  80. package/worker/office-page.mjs +0 -4
  81. package/worker/office-schema.mjs +6 -0
  82. package/worker/office.mjs +19 -23
  83. package/worker/pages.mjs +156 -29
  84. package/worker/parts.mjs +275 -0
  85. package/worker/room.mjs +157 -14
  86. package/worker/schema.mjs +6 -8
  87. package/worker/seats.mjs +71 -0
  88. package/worker/site.mjs +51 -53
  89. package/worker/version.mjs +1 -1
  90. package/lib/remix-assets.mjs +0 -84
package/site/SITE.md CHANGED
@@ -17,14 +17,21 @@ and every game gets a landing page. Anything the studio puts in `site/` wins.
17
17
 
18
18
  A section with nothing in it has no tab, and its address answers 404. Every page ends with a
19
19
  "Made with Homie" link to https://homie.rocks/studio/. studio.json may add `"tagline"` (one line, on Home and in
20
- the feeds) and `"site": { "featured": "<game id>" }` (the game on Home; otherwise the first with its own footage).
20
+ the feeds), `"site": { "featured": "<game id>" }` (the game on Home; otherwise the first with its own footage) and
21
+ `"site": { "order": ["<game id>", "<game id>"] }`: **the order of the games** in every list (Home's cards, the
22
+ Games page, `/api/games`, `/sitemap.xml`, `/llms.txt` and `/.well-known/homie-studio.json`). The
23
+ games it names come first, as it names them; every other game follows by id, which is the order all of them had
24
+ before. An id that is not one of the studio's games is skipped, and the build says so. Without `featured`, Home's
25
+ hero is still the first game with footage, now looked for in that order.
21
26
 
22
27
  Every card (Home, Games, Rooms, a post's), every live room's row and the directory's manifest show a game's
23
28
  **landing still**: `hero/wide.jpg` (or `landing.hero.image`), else a trailer's poster, else game.json's `cover`.
24
29
 
25
30
  ## What the homie.rocks hub reads
26
31
 
27
- `/.well-known/homie-studio.json` is the studio's manifest: its games (each with its landing still as `cover`),
32
+ `/.well-known/homie-studio.json` is the studio's manifest: its games (in the site's order, each with its landing still
33
+ as `cover`, and `build`: `{ "hash", "bundle" }`, the digest `homie-studio build` printed for the game that is live
34
+ and the address of the bundle its page loads; [The build](#the-build)),
28
35
  songs (each with a `cover`), videos (each with a `poster`), the latest posts, and `rooms`, the address of
29
36
  `/api/rooms`, so the hub's Rooms page lists this studio's public rooms. With studio.json `"stats": { "share": true }`
30
37
  (`homie-studio stats share on`) it also says "played this week": `played` for the whole studio and each game's own
@@ -73,26 +80,41 @@ Made from the game's own files, nothing invented:
73
80
  `games/<id>/hero/` by the house brands' names (`wide.mp4` and `tall.mp4`, their AV1 cuts `wide.av1.mp4` and
74
81
  `tall.av1.mp4`, stills `wide.jpg` and `tall.jpg`); a trailer in `videos/manifest.json` whose `for.game` is the
75
82
  game (its 16:9 `video`, 9:16 `vertical` and `poster`). A loop of 8 to 15 s under 3 MB each, muted, plays best.
83
+
84
+ **The files, exactly** (all in `games/<id>/hero/`, every one optional; `homie-studio build` names the first two
85
+ when a landing has neither picture nor footage):
86
+
87
+ | File | Shape | What it is |
88
+ | --- | --- | --- |
89
+ | `wide.jpg` | 16:9, 1280×720 or larger | The still a computer, a TV and every card show (also `.webp`, `.png`, `.avif`) |
90
+ | `tall.jpg` | 9:16, 720×1280 | The still an upright phone shows; without it the wide one is cropped |
91
+ | `wide.mp4` | 16:9, 1280×720 to 1600×900 | Footage over the still: silent, 8 to 15 s, under 3 MB (also `.webm`) |
92
+ | `tall.mp4` | 9:16, 720×1280 | The same for an upright phone |
93
+ | `wide.av1.mp4`, `tall.av1.mp4` | as above | AV1 cuts, offered first to a browser that plays them |
94
+
95
+ The shapes are what fills the screen without waste, not a rule: any size is shown, cropped to fill
96
+ (`landing.hero.focus` says which part to keep). One file is 25 MiB at most. In the built site they are served
97
+ at `/games/<id>/_landing/<file>` (a static game's, which copies its whole folder, at `/games/<id>/hero/<file>`);
98
+ `/api/games` says each exactly, as `games[].landing.hero` (`wide`, `tall`, `wideImage`, `tallImage`).
76
99
  - **The pitch** (`landing.pitch`, else the blurb), a big **Play** button that drops into a public room at once,
77
100
  and the live line ("3 players playing right now", refreshed every 15 s from `/<id>/live`).
78
101
  - **Phone, computer, TV**: the controls for each (`landing.controls`, else `credits.json` `controls`), a code a
79
102
  phone scans to play, and how to put the game on a TV (`/<id>/tv` shows the room with its own join code).
80
103
  - **Live rooms**, each with Join. When studio.json shares its stats (`stats.share`), also "played this week".
81
104
  - **How to play**: `landing.about`, `landing.howToPlay` (a few lines), the controls, the room facts.
82
- - **Credits**: the studio and `landing.credits` (`[{ "role", "name", "url" }]`); for a remix, "Remix of
83
- <game> by <studio>" linked to the original's page (game.json `remixOf`, which `game remix` writes; it is also
105
+ - **Credits**: the studio and `landing.credits` (`[{ "role", "name", "url" }]`); for a game that was made from another
106
+ studio's, "Based on <game> by <studio>" linked to the original's page (game.json `remixOf`; it is also
84
107
  under the game's name in the hero); for a port, the original, its author and licence and every part inside it
85
108
  (`credits.json`, as the port skill writes it), with the full licence texts on `/<id>/credits`; Homie's open
86
- engine; and the source's licence.
87
- - **Make a game like this**: the two commands that install the Homie plugin and the words to say. When the game
88
- shares its source (the default; `"share": { "source": false }` keeps it closed, and the owner's Remixable
89
- switch withdraws it) and its licence allows remixing, the words remix it from `/games/<id>/source.json`, and
90
- the button goes to `https://homie.rocks/studio/?remix=<that address>`.
109
+ engine; and the licence the game names for itself, when it names one.
91
110
 
92
- A game's licence is game.json `"license"`, the owner's pick: `"remix-with-credit"` (the default: a remix says
93
- "Remix of <game> by <studio>" with a link back), `"remix-freely"` (the credit is welcome, not asked for) or
94
- `"no-remix"` (the source can be read; `game remix` refuses it), and `{ "kind": "<one of those>", "spdx": "MIT" }`
95
- names a licence too. The source.json carries it, with `credit`: the studio's name, the game's name and its page.
111
+ A game's licence is game.json `"license"`: an SPDX identifier (`"MIT"`, `"CC-BY-4.0"`), as the word or as
112
+ `{ "spdx": "MIT" }`. It is a statement about the game, said in its credits, its structured data and `/llms.txt`; a
113
+ game that names none says nothing. It offers nobody the game's source: no game is handed over whole. Games build on
114
+ each other through parts, the pieces a studio chooses to share (`parts/PARTS.md`; a game with shared parts gets a
115
+ "Parts from this game" band, and the studio a `/parts/` page). Remix was retired: the settings it had (`"remix"`,
116
+ `"share"`, `landing.make`, the licence words `"remix-with-credit"`, `"remix-freely"` and `"no-remix"`) are ignored
117
+ with one build note, and `/games/<id>/source.json` answers 410.
96
118
 
97
119
  game.json's `landing` block, every key optional:
98
120
 
@@ -161,12 +183,74 @@ atproto later. `/.well-known/homie-studio.json` lists the latest posts for the h
161
183
  | `site/theme.json` | The tokens every page uses: `bg`, `fg`, `accent`, `glow`, `panel`, `accentInk` (colours: `#hex`, `rgb()`, `hsl()`, `oklch()`), `display`, `text`, `mono` (font lists), `fonts` (`[{ "family", "src": "/fonts/x.woff2", "weight": "400 800" }]`, files in `site/public`), `radius` (0 to 40), `mark` (a logo), `icon`, `social` (the share picture), `wordmark`, `uppercase: false`, `scheme: "light"`. Or `palette`: neon, dock, gold, acid, ember, orchid, tide, candy. A value that is not what it should be is dropped, with a warning. |
162
184
  | `site/theme.css` | CSS after the site's own, on every generated page: restyle anything. |
163
185
  | `site/partials/<name>.html` | A piece of every generated page: `head` (in `<head>`), `header` (the top line and its tabs), `footer` (the "Made with Homie" footer), `home` (a band on Home), `game` (a band on every landing), `game-<id>` (a band on one), `post` (after a post). `{{studio.name}}`, `{{game.name}}`, `{{game.id}}`, `{{game.play}}`, `{{year}}` are filled in. A band partial sits in one of the page's bands, unless it is its own `<section class="band">`. |
164
- | `site/pages/<path>/index.html` (or `site/pages/<path>.html`) | A whole page at `/<path>/`, served as it is, instead of the generated one (`site/pages/index.html` is Home, `site/pages/<id>/index.html` a game's landing) or beside them (`/about/`). It may borrow the site's parts: `<!-- homie:style -->`, `<!-- homie:header -->`, `<!-- homie:footer -->`, `<!-- homie:script -->`, and in its `<head>` `<!-- homie:schema -->` (the structured data the generated page there would carry: Home's, a landing's, else the studio's Organization). The rooms, the game files, the API and the owner's pages are never a page's. |
186
+ | `site/pages/<path>/index.html` (or `site/pages/<path>.html`) | A whole page at `/<path>/`, served as it is, instead of the generated one (`site/pages/index.html` is Home, `site/pages/<id>/index.html` a game's landing) or beside them (`/about/`). It may borrow the site's parts: `<!-- homie:style -->`, `<!-- homie:header -->`, `<!-- homie:footer -->`, `<!-- homie:script -->`, in its `<head>` `<!-- homie:schema -->` (the structured data the generated page there would carry: Home's, a landing's, else the studio's Organization), and `<!-- homie:home-hero -->` (Home's generated hero for the featured game: its footage or art, title, pitch, Play and the live line) or `<!-- homie:home-hero <id> -->` (the same for the game it names), so a Home of the studio's own never writes a hero by hand. The rooms, the game files, the API and the owner's pages are never a page's. |
165
187
  | `site/public/` | Files served as they are at the same path: fonts, a logo, hero footage, a page's pictures. A `robots.txt`, `sitemap.xml`, `llms.txt` or `llms-full.txt` here replaces the one the site makes. |
166
188
  | `site/src/worker.mjs` | The Worker. A studio that keeps a whole brand site of its own (with its own routes) wraps the template's Worker here, as the house brands do; everything it does not answer goes to the template. |
167
189
 
168
190
  A studio with its own landing in `site/` keeps it: the template never replaces what the studio made.
169
191
 
192
+ **A Home of your own, with the generated hero.** `site/pages/index.html` replaces Home. To keep the hero the site
193
+ makes and change only what is under it (or only which game it shows), borrow it:
194
+
195
+ ```html
196
+ <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
197
+ <title>Night Owls</title><!-- homie:style --><!-- homie:schema --></head>
198
+ <body><!-- homie:header -->
199
+ <main><!-- homie:home-hero crown-thief -->
200
+ <section class="band"><div class="band-in"><h2>Whatever this studio wants here</h2></div></section></main>
201
+ <!-- homie:footer --><!-- homie:script --></body></html>
202
+ ```
203
+
204
+ The marker needs `<!-- homie:style -->` (the hero's CSS) and `<!-- homie:script -->` (its live line refreshes).
205
+ A game that is private, invite-only or not this studio's leaves nothing there. A page that does want a game's
206
+ hero files by address reads them from `/api/games` (`games[].landing.hero`); for a bundled game they are
207
+ `/games/<id>/_landing/wide.jpg`, `tall.jpg`, `wide.mp4` and `tall.mp4`, as the table under
208
+ [A game's landing](#a-games-landing-id) says. On the generated Home, `"site": { "featured": "<id>" }` already
209
+ picks the hero's game.
210
+
211
+ ## The build
212
+
213
+ `homie-studio build` (`npm run build`) makes `site/dist`, and `homie-studio build <id>` rebuilds one game in it.
214
+
215
+ - **All or nothing.** The site is built in a folder of its own (`.studio/build/`, git-ignored) and put in place
216
+ only when all of it is there. A game that does not build fails the command (exit 1) and leaves `site/dist`
217
+ exactly as it was: never a site without that game for the next deploy to ship. When it is put in place,
218
+ `site/dist` and its folders are not removed and made again: a file that did not change is not touched, a file
219
+ that did is renamed into place in one step, and what the new build no longer has is taken away last. A server
220
+ or a script running from `site/dist` keeps its files across a build.
221
+ - **A bundle named by its content.** A game's code is `games/<id>/assets/main-<HASH>.js`, and the built
222
+ `index.html` names it (write `./assets/main.js` in the game's own `index.html`, as always: the build points it at
223
+ the hashed file). A browser or an edge that kept the last build's bundle is never asked for the new one under the
224
+ old name. `assets/main.js` is still there, one line that imports the hashed bundle, for anything that knew the
225
+ old address; `bundle.json` beside the page says the real file and its chunks.
226
+ - **Code that loads later.** The bundle is ES modules with code splitting: `const scene = await import('./level-2')`
227
+ in a game becomes `assets/chunk-<HASH>.js`, fetched when the game asks for it, from beside the bundle, wherever
228
+ the game is served (the site, its frame at `/<id>/__game/`, `dev`, `preview`, the Game Lab). A 3D game can draw
229
+ its first screen before the rest of its code and three.js add-ons arrive. A game with no `import()` is one file.
230
+ - **What changed.** Each game's line says `new`, `changed` or `unchanged` against the build before, its **build
231
+ hash** (sixteen characters over every file of the game, the same digest `homie-studio perf` names a build by).
232
+ `site/dist/_site/build.json` keeps it for whatever runs next (`{ "v":
233
+ 1, "at", "commit", "games": { "<id>": { "hash", "bundle", "chunks", "changed" } } }`), and
234
+ the live site says each game's `build.hash` in `/.well-known/homie-studio.json`: "is the live game the one I
235
+ built?" is that against this, with no file to fetch and no edge cache to wait out.
236
+ - **`build --types`** checks the games' TypeScript first (the build itself only strips types, so a type error
237
+ ships otherwise). It uses the studio's own `typescript` (a new studio's `package.json` asks for it;
238
+ `npm install --save-dev typescript` in an older one; the toolkit does not depend on it), reads a game's own
239
+ `tsconfig.json` when it has one, else checks its entry and what that imports the way the build reads them
240
+ (ES2022, a browser, strict). A type error in a game's own files stops the build before anything is built;
241
+ errors in a package the game imports as source are counted and said, never fatal.
242
+ - **`homie-studio preview <id> [--port 8788]`** serves one built game's files and nothing else (no Wrangler, no
243
+ rooms, no database), on this computer only, for a capture, a screenshot or a perf script. It is the page as the
244
+ build made it, with no room to join: the game plays offline with its bots. It prints its address first (one
245
+ line of JSON with `--json`), reads each file when asked, so the next build is what the next reload gets, and
246
+ stops with Ctrl-C.
247
+ - **A 3D game's model budgets.** `createModels()` (`@homie-rocks/studio/assets`) warns in development about any
248
+ model over 1,500 triangles or 300 KB: a small prop's budget. A game whose models are bigger on purpose says so
249
+ once, in game.json: `"assets": { "budgets": { "triangles": 8000, "bytes": 1500000 } }` (per model; `texturePx`
250
+ and `materials` are taken too, and `createModels({ budget })` or a single `load(url, { budget })` still wins).
251
+ The build writes it into the bundle. A game copied from a 3D starter has `"assets": "library"` there, a note that
252
+ only mattered when it was copied: replace it with the object.
253
+
170
254
  ## The play page
171
255
 
172
256
  **The arrival (0.26.0).** The play page and the big screen never open blank. From the first paint, an arrival
@@ -338,7 +422,7 @@ schema.org `@graph`:
338
422
  | --- | --- |
339
423
  | Home | `Organization` (the studio: name, address, `site/theme.json` `mark` as its logo, `social` as its image, the tagline) and `WebSite` |
340
424
  | `/games/`, `/rooms/` | `BreadcrumbList`, `ItemList` of the public games' landings (the Rooms page's data never carries a live count) |
341
- | `/<id>/` | `BreadcrumbList`, `VideoGame` co-typed `WebApplication`: name, description, the pitch as `abstract`, its pictures and screenshots, `genre`, `numberOfPlayers` (min and max), `playMode`, `gamePlatform`, the studio as author and publisher, `license` (the source's licence, linked to its SPDX page), `isBasedOn` (a remix's original page, a port's original), `trailer` (a `VideoObject`), `datePublished` and `dateModified`, a `PlayAction`, and one free-to-play `Offer` (price 0) |
425
+ | `/<id>/` | `BreadcrumbList`, `VideoGame` co-typed `WebApplication`: name, description, the pitch as `abstract`, its pictures and screenshots, `genre`, `numberOfPlayers` (min and max), `playMode`, `gamePlatform`, the studio as author and publisher, `license` (the licence the game names, linked to its SPDX page; left out when it names none), `isBasedOn` (a port's original), `trailer` (a `VideoObject`), `datePublished` and `dateModified`, a `PlayAction`, and one free-to-play `Offer` (price 0) |
342
426
  | `/<id>/credits`, `/<id>/servers/`, a server's page | `BreadcrumbList` |
343
427
  | `/music/`, `/music/<slug>/` | `MusicAlbum` (every song names one album) or an `ItemList`; `MusicRecording` (`byArtist` the studio, its duration, its `AudioObject`, its cover, `inAlbum`, `musicalKey`, its date) |
344
428
  | `/videos/`, `/videos/<slug>/` | `ItemList`; `VideoObject` (`thumbnailUrl`, `uploadDate`, `duration`, `contentUrl`: the file at its own address, from R2 or the site) |
@@ -374,15 +458,13 @@ any property schema.org does not put on that type. A whole block of the owner's
374
458
  - `/sitemap.xml`: Home, Games and each landing (and its credits), Rooms, Music, Videos, Posts and every page in
375
459
  `site/pages`, with `lastmod` only where a real date says when it changed.
376
460
  - `/llms.txt`, following the llms.txt convention (an H1, a `>` summary, then `##` sections of links): what the
377
- studio is; each game with its pitch, players, rounds, Play, Watch and the big screen; which games are open to remix,
378
- with their licence, their `source.json` and the words to say to Claude Code or Codex with Homie (the plugin's
379
- `game_remix` tool, or `homie-studio game remix <source> --id <new-id>`); songs; videos; posts with their Atom and
461
+ studio is; each game with its pitch, players, rounds, Play, Watch and the big screen, the licence it names and
462
+ the game it was based on (when it has either); songs; videos; posts with their Atom and
380
463
  JSON feeds; the studio's own pages (a changelog among them); and homie.rocks's own llms.txt.
381
464
  - `/llms-full.txt`: the same, with each game's whole description, how to play, controls and credits, and every
382
465
  post's text.
383
466
 
384
- A game is offered for remixing only when its source is shared, its owner's Remixable switch is on and its licence
385
- allows it, as for the directory. They are cached for five minutes. A file of the studio's own in `site/public` wins.
467
+ They are cached for five minutes. A file of the studio's own in `site/public` wins.
386
468
 
387
469
  ## Headers
388
470
 
@@ -6,7 +6,7 @@
6
6
  <title>Ember Vale</title>
7
7
  <link rel="icon" href="data:,">
8
8
  <style>
9
- html, body { margin: 0; height: 100%; background: #0b0f0a; overflow: hidden; overscroll-behavior: none; -webkit-user-select: none; user-select: none; font: 15px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: #f3ead2; }
9
+ html, body { margin: 0; height: 100%; background: #0b0f0a; overflow: hidden; overscroll-behavior: none; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; font: 15px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: #f3ead2; }
10
10
  canvas { position: fixed; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; }
11
11
  .ui { position: fixed; z-index: 3; }
12
12
  .pill { left: calc(12px + env(safe-area-inset-left)); top: calc(86px + env(safe-area-inset-top)); display: flex; gap: 6px; }
@@ -174,6 +174,10 @@ const room = createRoom<Body, SlimeRow[], { slimes: Slime[]; seq: number }>({
174
174
  adopt: (b) => { me.x = b.x; me.y = b.y; me.has = true; },
175
175
  local: () => (me.has ? ({ ...mine(), x: me.x, y: me.y } as Body) : null),
176
176
  onTakeover: (b) => { b.score = 0; b.flags = 0; b.hp = b.maxHp; },
177
+ // SEAT OR SOLO (NETPLAY.md section 28). A hero who is in the room but has no body in tonight's hunt (every body is
178
+ // somebody's, or every seat is taken) is not left watching: after four seconds they hunt on their own with bots,
179
+ // the line over the game says so, and they step into the room's hunt the moment it has a body for them.
180
+ fallback: 'solo',
177
181
  });
178
182
  const net = room.net;
179
183
  const myLevel = (): number => hero?.level ?? 1;
@@ -1229,7 +1233,9 @@ exposePort(net, {
1229
1233
  size: R,
1230
1234
  score: () => { const s = room.mySeat(); return room.view().find((b) => b.seat === s && !b.bot)?.score ?? null; },
1231
1235
  busy: () => !ui.idle(),
1232
- extra: { level: () => hero?.level ?? 0, frames: () => frames, saves: () => saves.status().mode },
1236
+ // `alive` is read by name by the playtest (port/probe.ts PortExtra): a downed hero is waiting to stand up again, and a
1237
+ // press that moves nothing then is not a broken control.
1238
+ extra: { alive: () => !((myViewBody()?.flags ?? 0) & DOWN), level: () => hero?.level ?? 0, frames: () => frames, saves: () => saves.status().mode },
1233
1239
  });
1234
1240
 
1235
1241
  void net.ready.then(() => { const b = room.mine(); if (b) { me.x = b.x; me.y = b.y; me.has = true; } });
@@ -6,7 +6,7 @@
6
6
  <title>Gem Rush</title>
7
7
  <link rel="icon" href="data:,">
8
8
  <style>
9
- html, body { margin: 0; height: 100%; background: #04060c; overflow: hidden; overscroll-behavior: none; -webkit-user-select: none; user-select: none; }
9
+ html, body { margin: 0; height: 100%; background: #04060c; overflow: hidden; overscroll-behavior: none; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
10
10
  canvas { position: fixed; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; }
11
11
  </style>
12
12
  </head>
@@ -36,7 +36,7 @@
36
36
  *
37
37
  * Canvas 2D on purpose: the point is the contract, in ~700 readable lines.
38
38
  */
39
- import { createNetplay, Roster, q, lerp, capMove, PALETTE, AI_MARK, type RoleChange, type RoundInfo, type RoundResult, type Skill, type Slot, type Snapshot } from '@homie-rocks/studio/netplay';
39
+ import { createNetplay, guardGestures, Roster, q, lerp, capMove, PALETTE, AI_MARK, type Peer, type RoleChange, type RoundInfo, type RoundResult, type Skill, type Slot, type Snapshot } from '@homie-rocks/studio/netplay';
40
40
  // The port toolkit: its probe (what `homie-studio port check` reads for the owner tests, and sandbox + audio shims),
41
41
  // and a flat world on every screen (port/view.ts: the camera, and name labels that never pile up).
42
42
  import { BUBBLE_FONT, createBubbles, createLabels, exposePort, fitView, paintBubbles, type BubbleIn, type BubbleOut, type LabelIn, type LabelOut } from '@homie-rocks/studio/port';
@@ -82,7 +82,7 @@ interface Body extends Knock { slot: number; seat: number | null; name: string;
82
82
  interface Gem { id: number; x: number; y: number }
83
83
  /** Slow state, on the keyed state channel (net.state('zone', ...)), not in the 20 Hz snapshot. */
84
84
  interface Zone { n: number; x: number; y: number; r: number; until: number }
85
- interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[]; tick: number; gemSeq: number }
85
+ interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[]; tick: number; gemSeq: number; occ?: [number, number][] }
86
86
 
87
87
  /* --------------------------------------------------------------- the net */
88
88
  /**
@@ -93,6 +93,9 @@ interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[];
93
93
  const MOVEMENT: 'owner' | 'host' = (() => { try { return new URLSearchParams(location.search).get('movement') === 'host' ? 'host' : 'owner'; } catch { return 'owner'; } })();
94
94
  // caps: its bots read the dial ('skill'), and its Roster takes an AI's slot for it ('agents': the join passes p.agent).
95
95
  const net = createNetplay<Snap, Avatar, Ckpt>({ game: 'gem-rush', maxPlayers: MAX_SLOTS, movement: MOVEMENT, snapshotHz: 20, inputHz: 20, checkpointMs: 1000, caps: ['skill', 'agents'], checkpoint: () => checkpoint() });
96
+ // A touch game guards its own page (NETPLAY.md section 24): a long press on the canvas never selects text or raises
97
+ // the copy/paste callout on a phone, and a touch on it never pans or zooms the page.
98
+ guardGestures({ touch: 'canvas' });
96
99
  /** The Roster keeps the server's AI seats (revision 6): the policy is read whenever it fills. */
97
100
  const policy = () => net.policy;
98
101
 
@@ -164,9 +167,12 @@ function newZone(n: number): Zone {
164
167
  }
165
168
  function setZone(z: Zone): void { zone = z; net.state('zone', z); }
166
169
 
167
- function startRound(n: number): void {
170
+ function startRound(n: number, rollover = false): void {
168
171
  const now = net.now();
169
172
  roster.trim();
173
+ // The next round of a running room: the trim is what makes room (the seats a server no longer keeps for AI go
174
+ // here), so whoever was seated with no body is given one now, before the bodies are placed.
175
+ if (rollover && !net.offline) seatWaiting();
170
176
  syncBodiesFromRoster();
171
177
  let i = 0;
172
178
  for (const b of [...bodies.values()].sort((a, c) => a.slot - c.slot)) {
@@ -208,7 +214,9 @@ function becomeHost(e: RoleChange<Snap, Ckpt>): void {
208
214
  startRound((e.round?.n ?? 0) + 1);
209
215
  }
210
216
  // Whoever is connected now is who plays: seats that left during the gap become bots.
211
- const peers = net.offline ? [{ seat: 0, name: 'You' }] : [...net.peers.values()].filter((p) => p.seat !== null).map((p) => ({ seat: p.seat, name: p.name, agent: p.agent ?? null }));
217
+ const peers = net.offline ? [{ seat: 0, name: 'You' }] : [...net.peers.values()].filter((p) => p.seat !== null).map((p) => ({ seat: p.seat, name: p.name, agent: p.agent ?? null, occ: p.occ ?? null }));
218
+ // `occ` (NETPLAY.md section 25): which stay in its seat each peer's is, so a seat number that changed hands while
219
+ // nobody was hosting is claimed afresh instead of inheriting the last player's body in silence.
212
220
  const { claimed } = roster.reconcile(peers);
213
221
  syncBodiesFromRoster();
214
222
  for (const s of claimed) { const b = bodies.get(s.slot); if (b && b.seat !== mySeat()) hostMoved(b); }
@@ -225,7 +233,7 @@ function becomeHost(e: RoleChange<Snap, Ckpt>): void {
225
233
  function restore(e: RoleChange<Snap, Ckpt>): void {
226
234
  const ck = e.ckpt?.d ?? null;
227
235
  if (ck) {
228
- roster = Roster.from(ck.roster, { min: MIN_SLOTS, max: MAX_SLOTS, botName, policy });
236
+ roster = Roster.from(ck.roster, { min: MIN_SLOTS, max: MAX_SLOTS, botName, policy }, ck.occ ?? null);
229
237
  bodies = new Map(ck.bodies.map((b) => [b.slot, { ...b }]));
230
238
  gems = ck.gems.map((g) => ({ ...g }));
231
239
  gemSeq = ck.gemSeq;
@@ -266,6 +274,8 @@ function checkpoint(): Ckpt {
266
274
  bodies: [...bodies.values()].map((b) => ({ ...b })),
267
275
  gems: gems.map((g) => ({ ...g })),
268
276
  roster: roster.toJSON(),
277
+ // Whose stay each body is, beside the roster: how the next host tells a player who came back from a new one.
278
+ occ: roster.occupants(),
269
279
  tick,
270
280
  gemSeq,
271
281
  };
@@ -275,15 +285,30 @@ net.on('role', (e) => {
275
285
  if (e.role === 'host') becomeHost(e);
276
286
  else { hosting = false; }
277
287
  });
278
- net.on('join', (p) => {
279
- if (!hosting || p.seat === null) return;
288
+ /** Host: a body for somebody the relay seated. False when every body is somebody's (they wait: see seatWaiting). */
289
+ function giveBody(p: Peer): boolean {
290
+ if (p.seat === null) return false;
280
291
  // An AI takes a seat kept for AI, a person never does (revision 6: the Roster needs p.agent for that).
281
- const c = roster.claim(p.seat, p.name, p.agent ? { role: p.agent.role, hands: p.agent.hands } : null);
282
- if (!c) return; // full: they watch
292
+ const c = roster.claim(p.seat, p.name, p.agent ? { role: p.agent.role, hands: p.agent.hands } : null, p.occ ?? null);
293
+ if (!c) return false;
283
294
  syncBodiesFromRoster();
284
295
  const b = bodies.get(c.slot.slot);
285
296
  // The arriving human takes over the bot's body where it stands, score and all (reset = "adopt this position").
286
297
  if (b) { b.vx = 0; b.vy = 0; b.kvx = 0; b.kvy = 0; b.knockUntil = 0; hostMoved(b); }
298
+ return true;
299
+ }
300
+ /**
301
+ * SEAT WHOEVER IS WAITING (NETPLAY.md section 28). The relay seats a person; this host gives them a body. A claim that
302
+ * found every body taken (a server's AI seats were still kept, say) used to be tried once: that player watched for the
303
+ * rest of the visit, online, with nothing said. It is tried again whenever a body frees up and at every round start.
304
+ */
305
+ function seatWaiting(): boolean {
306
+ let any = false;
307
+ for (const p of net.peers.values()) if (p.seat !== null && !roster.bySeat(p.seat) && giveBody(p)) any = true;
308
+ return any;
309
+ }
310
+ net.on('join', (p) => {
311
+ if (!hosting || !giveBody(p)) return;
287
312
  publishRoster();
288
313
  net.snapshot(buildSnap(), tick, true);
289
314
  });
@@ -291,7 +316,10 @@ net.on('leave', (p) => {
291
316
  if (!hosting || p.seat === null) return;
292
317
  roster.release(p.seat); // their body stays, driven by a bot
293
318
  syncBodiesFromRoster();
319
+ // A body just went to a bot: somebody seated with none takes it now.
320
+ const seated = seatWaiting();
294
321
  publishRoster();
322
+ if (seated) net.snapshot(buildSnap(), tick, true);
295
323
  });
296
324
  // The server's policy changed: its AI seats come (or go between rounds) at once.
297
325
  net.on('policy', () => { if (!hosting) return; roster.fill(); syncBodiesFromRoster(); publishRoster(); });
@@ -577,7 +605,7 @@ function stepHost(dt: number): void {
577
605
  }
578
606
  }
579
607
  if (now >= round.endsAt) endRound();
580
- } else if (round && round.phase === 'over' && now >= round.endsAt) startRound(round.n + 1);
608
+ } else if (round && round.phase === 'over' && now >= round.endsAt) startRound(round.n + 1, true);
581
609
  if (net.snapshotDue()) net.snapshot(buildSnap(), tick);
582
610
  }
583
611
 
@@ -980,7 +1008,8 @@ function hud(cw: number, ch: number, phone: boolean, list: { slot: number; name:
980
1008
  ctx.fillText(`${row.place}. ${mine && !net.watching ? 'You' : label(row.name, row.bot)}${mine && net.watching ? ' ◂' : ''} — ${row.score}`, cw / 2, y + 56 + i * 26);
981
1009
  });
982
1010
  }
983
- if (!net.offline && !net.connected && net.role !== 'host') {
1011
+ // Only a browser that WAS in its room is reconnecting; one still joining, or stopped for good, is not (section 22).
1012
+ if (net.link === 'reconnecting' && net.role !== 'host') {
984
1013
  ctx.textAlign = 'center'; ctx.fillStyle = '#ffd166'; ctx.font = '600 14px ui-sans-serif, system-ui, sans-serif';
985
1014
  ctx.fillText('Reconnecting…', cw / 2, ch - pad);
986
1015
  }
@@ -1036,7 +1065,7 @@ net.expose({
1036
1065
  if (!hosting) return false;
1037
1066
  const b = [...bodies.values()].find((x) => x.seat === seat);
1038
1067
  if (!b) return false;
1039
- knock(b, b.x < W / 2 ? b.x - 1 : b.x + 1, b.y); // push toward the middle, away from the nearer wall
1068
+ knock(b, b.x < W / 2 ? b.x - 1 : b.x + 1, b.y, -1); // push toward the middle, away from the nearer wall (bumped by nobody)
1040
1069
  return true;
1041
1070
  },
1042
1071
  });
@@ -15,7 +15,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
15
15
  - **What:** character (Players/Cat): `public/models/animal-cat.glb`
16
16
  - **From:** from the Homie starter library: Kenney Cube Pets, animal-cat (https://kenney.nl/assets/cube-pets); library, in game (src/main.ts)
17
17
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
18
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
19
18
  - **Notes:** From the Homie starter library (kenney-cube-pets); the pack's own licence text: licenses/kenney-cube-pets.txt. `game new` fetches the file; the repository holds no models.
20
19
 
21
20
  ### animal-caterpillar
@@ -23,7 +22,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
23
22
  - **What:** character (Players/Caterpillar): `public/models/animal-caterpillar.glb`
24
23
  - **From:** from the Homie starter library: Kenney Cube Pets, animal-caterpillar (https://kenney.nl/assets/cube-pets); library, in game (src/main.ts)
25
24
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
26
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
27
25
  - **Notes:** From the Homie starter library (kenney-cube-pets); the pack's own licence text: licenses/kenney-cube-pets.txt. `game new` fetches the file; the repository holds no models.
28
26
 
29
27
  ### animal-chick
@@ -31,7 +29,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
31
29
  - **What:** character (Players/Chick): `public/models/animal-chick.glb`
32
30
  - **From:** from the Homie starter library: Kenney Cube Pets, animal-chick (https://kenney.nl/assets/cube-pets); library, in game (src/main.ts)
33
31
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
34
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
35
32
  - **Notes:** From the Homie starter library (kenney-cube-pets); the pack's own licence text: licenses/kenney-cube-pets.txt. `game new` fetches the file; the repository holds no models.
36
33
 
37
34
  ### animal-crab
@@ -39,7 +36,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
39
36
  - **What:** character (Players/Crab): `public/models/animal-crab.glb`
40
37
  - **From:** from the Homie starter library: Kenney Cube Pets, animal-crab (https://kenney.nl/assets/cube-pets); library, in game (src/main.ts)
41
38
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
42
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
43
39
  - **Notes:** From the Homie starter library (kenney-cube-pets); the pack's own licence text: licenses/kenney-cube-pets.txt. `game new` fetches the file; the repository holds no models.
44
40
 
45
41
  ### animal-elephant
@@ -47,7 +43,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
47
43
  - **What:** character (Players/Elephant): `public/models/animal-elephant.glb`
48
44
  - **From:** from the Homie starter library: Kenney Cube Pets, animal-elephant (https://kenney.nl/assets/cube-pets); library, in game (src/main.ts)
49
45
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
50
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
51
46
  - **Notes:** From the Homie starter library (kenney-cube-pets); the pack's own licence text: licenses/kenney-cube-pets.txt. `game new` fetches the file; the repository holds no models.
52
47
 
53
48
  ### animal-fox
@@ -55,7 +50,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
55
50
  - **What:** character (Players/Fox): `public/models/animal-fox.glb`
56
51
  - **From:** from the Homie starter library: Kenney Cube Pets, animal-fox (https://kenney.nl/assets/cube-pets); library, in game (src/main.ts)
57
52
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
58
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
59
53
  - **Notes:** From the Homie starter library (kenney-cube-pets); the pack's own licence text: licenses/kenney-cube-pets.txt. `game new` fetches the file; the repository holds no models.
60
54
 
61
55
  ### animal-panda
@@ -63,7 +57,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
63
57
  - **What:** character (Players/Panda): `public/models/animal-panda.glb`
64
58
  - **From:** from the Homie starter library: Kenney Cube Pets, animal-panda (https://kenney.nl/assets/cube-pets); library, in game (src/main.ts)
65
59
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
66
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
67
60
  - **Notes:** From the Homie starter library (kenney-cube-pets); the pack's own licence text: licenses/kenney-cube-pets.txt. `game new` fetches the file; the repository holds no models.
68
61
 
69
62
  ### animal-pig
@@ -71,7 +64,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
71
64
  - **What:** character (Players/Pig): `public/models/animal-pig.glb`
72
65
  - **From:** from the Homie starter library: Kenney Cube Pets, animal-pig (https://kenney.nl/assets/cube-pets); library, in game (src/main.ts)
73
66
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
74
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
75
67
  - **Notes:** From the Homie starter library (kenney-cube-pets); the pack's own licence text: licenses/kenney-cube-pets.txt. `game new` fetches the file; the repository holds no models.
76
68
 
77
69
  ### bush
@@ -79,7 +71,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
79
71
  - **What:** prop (Places/Bush): `public/models/bush.glb`
80
72
  - **From:** from the Homie starter library: Kenney Nature Kit, tree-fat (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
81
73
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
82
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
83
74
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
84
75
 
85
76
  ### campfire
@@ -87,7 +78,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
87
78
  - **What:** prop (Places/Campfire): `public/models/campfire.glb`
88
79
  - **From:** from the Homie starter library: Kenney Nature Kit, campfire-stones (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
89
80
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
90
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
91
81
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
92
82
 
93
83
  ### fence
@@ -95,7 +85,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
95
85
  - **What:** kit (Places/Fence): `public/models/fence.glb`
96
86
  - **From:** from the Homie starter library: Kenney Nature Kit, fence-simple (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
97
87
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
98
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
99
88
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
100
89
 
101
90
  ### firewood
@@ -103,7 +92,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
103
92
  - **What:** prop (Places/Firewood): `public/models/firewood.glb`
104
93
  - **From:** from the Homie starter library: Kenney Nature Kit, campfire-logs (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
105
94
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
106
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
107
95
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
108
96
 
109
97
  ### flower-purple
@@ -111,7 +99,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
111
99
  - **What:** prop (Places/Purple flower): `public/models/flower-purple.glb`
112
100
  - **From:** from the Homie starter library: Kenney Nature Kit, flower-purple-a (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
113
101
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
114
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
115
102
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
116
103
 
117
104
  ### flower-red
@@ -119,7 +106,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
119
106
  - **What:** prop (Places/Red flower): `public/models/flower-red.glb`
120
107
  - **From:** from the Homie starter library: Kenney Nature Kit, flower-red-a (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
121
108
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
122
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
123
109
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
124
110
 
125
111
  ### flower-yellow
@@ -127,7 +113,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
127
113
  - **What:** prop (Places/Yellow flower): `public/models/flower-yellow.glb`
128
114
  - **From:** from the Homie starter library: Kenney Nature Kit, flower-yellow-a (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
129
115
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
130
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
131
116
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
132
117
 
133
118
  ### gem
@@ -135,7 +120,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
135
120
  - **What:** prop (Items/Gem): `public/models/gem.glb`
136
121
  - **From:** from the Homie starter library: Kenney Platformer Kit, jewel (https://kenney.nl/assets/platformer-kit); library, in game (src/main.ts)
137
122
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
138
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
139
123
  - **Notes:** From the Homie starter library (kenney-platformer-kit); the pack's own licence text: licenses/kenney-platformer-kit.txt. `game new` fetches the file; the repository holds no models.
140
124
 
141
125
  ### grass
@@ -143,7 +127,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
143
127
  - **What:** prop (Places/Grass): `public/models/grass.glb`
144
128
  - **From:** from the Homie starter library: Kenney Nature Kit, grass-leafs-large (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
145
129
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
146
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
147
130
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
148
131
 
149
132
  ### log
@@ -151,7 +134,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
151
134
  - **What:** prop (Places/Log seat): `public/models/log.glb`
152
135
  - **From:** from the Homie starter library: Kenney Nature Kit, log-large (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
153
136
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
154
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
155
137
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
156
138
 
157
139
  ### mushrooms
@@ -159,7 +141,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
159
141
  - **What:** prop (Places/Mushrooms): `public/models/mushrooms.glb`
160
142
  - **From:** from the Homie starter library: Kenney Nature Kit, mushroom-red-group (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
161
143
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
162
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
163
144
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
164
145
 
165
146
  ### rock
@@ -167,7 +148,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
167
148
  - **What:** prop (Places/Rock): `public/models/rock.glb`
168
149
  - **From:** from the Homie starter library: Kenney Nature Kit, stone-large-d (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
169
150
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
170
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
171
151
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
172
152
 
173
153
  ### stone
@@ -175,7 +155,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
175
155
  - **What:** prop (Places/Standing stone): `public/models/stone.glb`
176
156
  - **From:** from the Homie starter library: Kenney Nature Kit, stone-tall-c (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
177
157
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
178
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
179
158
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
180
159
 
181
160
  ### tree-oak
@@ -183,7 +162,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
183
162
  - **What:** prop (Places/Oak tree): `public/models/tree-oak.glb`
184
163
  - **From:** from the Homie starter library: Kenney Nature Kit, tree-oak (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
185
164
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
186
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
187
165
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
188
166
 
189
167
  ### tree-pine
@@ -191,7 +169,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
191
169
  - **What:** prop (Places/Pine): `public/models/tree-pine.glb`
192
170
  - **From:** from the Homie starter library: Kenney Nature Kit, tree-pine-round-a (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
193
171
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
194
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
195
172
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.
196
173
 
197
174
  ### tree-round
@@ -199,5 +176,4 @@ Read on the dates shown. Terms change: read the live page again before publishin
199
176
  - **What:** prop (Places/Round tree): `public/models/tree-round.glb`
200
177
  - **From:** from the Homie starter library: Kenney Nature Kit, tree-default (https://kenney.nl/assets/nature-kit); library, in game (src/main.ts)
201
178
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: Kenney
202
- - **A remix:** a remix gets this file (fetched from this studio's site, checked by SHA-256)
203
179
  - **Notes:** From the Homie starter library (kenney-nature-kit); the pack's own licence text: licenses/kenney-nature-kit.txt. `game new` fetches the file; the repository holds no models.