@homie-rocks/studio 0.30.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +111 -1
  2. package/README.md +361 -22
  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 +142 -123
  8. package/lib/art-cli.mjs +61 -18
  9. package/lib/art-tools.mjs +53 -10
  10. package/lib/asset-check.mjs +249 -46
  11. package/lib/asset-import.mjs +11 -5
  12. package/lib/asset-manifest.mjs +128 -51
  13. package/lib/build.mjs +239 -72
  14. package/lib/characters.mjs +119 -20
  15. package/lib/check.mjs +110 -7
  16. package/lib/chrome.mjs +11 -0
  17. package/lib/clips.mjs +54 -0
  18. package/lib/cloudflare.mjs +127 -8
  19. package/lib/codex.mjs +6 -0
  20. package/lib/collision.mjs +245 -0
  21. package/lib/decisions.mjs +2 -2
  22. package/lib/demo.mjs +16 -4
  23. package/lib/deploy-state.mjs +166 -0
  24. package/lib/dev.mjs +430 -0
  25. package/lib/directory.mjs +116 -5
  26. package/lib/doctor.mjs +143 -16
  27. package/lib/feedback.mjs +3 -2
  28. package/lib/handoff.mjs +2 -6
  29. package/lib/library.mjs +1 -1
  30. package/lib/mcp-tools.mjs +36 -35
  31. package/lib/mcp.mjs +1 -1
  32. package/lib/net.mjs +81 -2
  33. package/lib/node-version.mjs +40 -0
  34. package/lib/office.mjs +6 -6
  35. package/lib/optimise.mjs +39 -4
  36. package/lib/parts-build.mjs +169 -0
  37. package/lib/parts-cli.mjs +104 -0
  38. package/lib/parts-store.mjs +342 -0
  39. package/lib/parts-tools.mjs +92 -0
  40. package/lib/parts.mjs +693 -0
  41. package/lib/perf.mjs +131 -31
  42. package/lib/port-check.mjs +40 -6
  43. package/lib/preview.mjs +77 -0
  44. package/lib/routes.mjs +132 -0
  45. package/lib/scaffold.mjs +88 -22
  46. package/lib/shoot.mjs +307 -0
  47. package/lib/site.mjs +1 -2
  48. package/lib/stage.mjs +118 -0
  49. package/lib/studio.mjs +0 -66
  50. package/lib/style-board.mjs +49 -6
  51. package/lib/template-history.json +101 -0
  52. package/lib/trailer.mjs +47 -0
  53. package/lib/typecheck.mjs +107 -0
  54. package/mcp/ui/lineup.js +3 -3
  55. package/mcp/ui/rights.js +1 -3
  56. package/netplay/NETPLAY.md +424 -16
  57. package/netplay/netplay.ts +756 -40
  58. package/package.json +10 -1
  59. package/parts/PARTS.md +163 -0
  60. package/port/index.ts +8 -2
  61. package/port/personas.ts +275 -0
  62. package/port/powerups.ts +357 -0
  63. package/port/probe.ts +38 -2
  64. package/port/room.ts +103 -19
  65. package/site/SITE.md +102 -20
  66. package/starters/ember-vale/index.html +1 -1
  67. package/starters/ember-vale/src/main.ts +3 -1
  68. package/starters/gem-rush/index.html +1 -1
  69. package/starters/gem-rush/src/main.ts +13 -6
  70. package/starters/gem-rush-3d/assets/RIGHTS.md +0 -24
  71. package/starters/gem-rush-3d/assets/manifest.json +0 -24
  72. package/starters/gem-rush-3d/index.html +1 -1
  73. package/starters/gem-rush-3d/src/main.ts +15 -5
  74. package/starters/hero-rush-3d/assets/RIGHTS.md +0 -19
  75. package/starters/hero-rush-3d/assets/manifest.json +0 -19
  76. package/starters/hero-rush-3d/index.html +1 -1
  77. package/starters/hero-rush-3d/src/main.ts +15 -5
  78. package/worker/arrival.mjs +4 -1
  79. package/worker/discover.mjs +11 -25
  80. package/worker/index.mjs +142 -62
  81. package/worker/license.mjs +45 -41
  82. package/worker/office-page.mjs +0 -4
  83. package/worker/office-schema.mjs +6 -0
  84. package/worker/office.mjs +19 -23
  85. package/worker/pages.mjs +141 -23
  86. package/worker/parts.mjs +234 -0
  87. package/worker/room.mjs +157 -14
  88. package/worker/schema.mjs +6 -8
  89. package/worker/seats.mjs +71 -0
  90. package/worker/site.mjs +51 -53
  91. package/worker/version.mjs +1 -1
  92. package/lib/remix-assets.mjs +0 -84
@@ -29,7 +29,6 @@
29
29
  }
30
30
  ],
31
31
  "license": {
32
- "remix": "include",
33
32
  "kind": "cc0",
34
33
  "spdx": "CC0-1.0",
35
34
  "owner": "Kenney",
@@ -132,7 +131,6 @@
132
131
  }
133
132
  ],
134
133
  "license": {
135
- "remix": "include",
136
134
  "kind": "cc0",
137
135
  "spdx": "CC0-1.0",
138
136
  "owner": "Kenney",
@@ -236,7 +234,6 @@
236
234
  }
237
235
  ],
238
236
  "license": {
239
- "remix": "include",
240
237
  "kind": "cc0",
241
238
  "spdx": "CC0-1.0",
242
239
  "owner": "Kenney",
@@ -341,7 +338,6 @@
341
338
  }
342
339
  ],
343
340
  "license": {
344
- "remix": "include",
345
341
  "kind": "cc0",
346
342
  "spdx": "CC0-1.0",
347
343
  "owner": "Kenney",
@@ -446,7 +442,6 @@
446
442
  }
447
443
  ],
448
444
  "license": {
449
- "remix": "include",
450
445
  "kind": "cc0",
451
446
  "spdx": "CC0-1.0",
452
447
  "owner": "Kenney",
@@ -552,7 +547,6 @@
552
547
  }
553
548
  ],
554
549
  "license": {
555
- "remix": "include",
556
550
  "kind": "cc0",
557
551
  "spdx": "CC0-1.0",
558
552
  "owner": "Kenney",
@@ -658,7 +652,6 @@
658
652
  }
659
653
  ],
660
654
  "license": {
661
- "remix": "include",
662
655
  "kind": "cc0",
663
656
  "spdx": "CC0-1.0",
664
657
  "owner": "Kenney",
@@ -765,7 +758,6 @@
765
758
  }
766
759
  ],
767
760
  "license": {
768
- "remix": "include",
769
761
  "kind": "cc0",
770
762
  "spdx": "CC0-1.0",
771
763
  "owner": "Kenney",
@@ -870,7 +862,6 @@
870
862
  }
871
863
  ],
872
864
  "license": {
873
- "remix": "include",
874
865
  "kind": "cc0",
875
866
  "spdx": "CC0-1.0",
876
867
  "owner": "Kenney",
@@ -962,7 +953,6 @@
962
953
  }
963
954
  ],
964
955
  "license": {
965
- "remix": "include",
966
956
  "kind": "cc0",
967
957
  "spdx": "CC0-1.0",
968
958
  "owner": "Kenney",
@@ -1054,7 +1044,6 @@
1054
1044
  }
1055
1045
  ],
1056
1046
  "license": {
1057
- "remix": "include",
1058
1047
  "kind": "cc0",
1059
1048
  "spdx": "CC0-1.0",
1060
1049
  "owner": "Kenney",
@@ -1144,7 +1133,6 @@
1144
1133
  }
1145
1134
  ],
1146
1135
  "license": {
1147
- "remix": "include",
1148
1136
  "kind": "cc0",
1149
1137
  "spdx": "CC0-1.0",
1150
1138
  "owner": "Kenney",
@@ -1236,7 +1224,6 @@
1236
1224
  }
1237
1225
  ],
1238
1226
  "license": {
1239
- "remix": "include",
1240
1227
  "kind": "cc0",
1241
1228
  "spdx": "CC0-1.0",
1242
1229
  "owner": "Kenney",
@@ -1328,7 +1315,6 @@
1328
1315
  }
1329
1316
  ],
1330
1317
  "license": {
1331
- "remix": "include",
1332
1318
  "kind": "cc0",
1333
1319
  "spdx": "CC0-1.0",
1334
1320
  "owner": "Kenney",
@@ -1420,7 +1406,6 @@
1420
1406
  }
1421
1407
  ],
1422
1408
  "license": {
1423
- "remix": "include",
1424
1409
  "kind": "cc0",
1425
1410
  "spdx": "CC0-1.0",
1426
1411
  "owner": "Kenney",
@@ -1512,7 +1497,6 @@
1512
1497
  }
1513
1498
  ],
1514
1499
  "license": {
1515
- "remix": "include",
1516
1500
  "kind": "cc0",
1517
1501
  "spdx": "CC0-1.0",
1518
1502
  "owner": "Kenney",
@@ -1605,7 +1589,6 @@
1605
1589
  }
1606
1590
  ],
1607
1591
  "license": {
1608
- "remix": "include",
1609
1592
  "kind": "cc0",
1610
1593
  "spdx": "CC0-1.0",
1611
1594
  "owner": "Kenney",
@@ -1696,7 +1679,6 @@
1696
1679
  }
1697
1680
  ],
1698
1681
  "license": {
1699
- "remix": "include",
1700
1682
  "kind": "cc0",
1701
1683
  "spdx": "CC0-1.0",
1702
1684
  "owner": "Kenney",
@@ -1788,7 +1770,6 @@
1788
1770
  }
1789
1771
  ],
1790
1772
  "license": {
1791
- "remix": "include",
1792
1773
  "kind": "cc0",
1793
1774
  "spdx": "CC0-1.0",
1794
1775
  "owner": "Kenney",
@@ -1880,7 +1861,6 @@
1880
1861
  }
1881
1862
  ],
1882
1863
  "license": {
1883
- "remix": "include",
1884
1864
  "kind": "cc0",
1885
1865
  "spdx": "CC0-1.0",
1886
1866
  "owner": "Kenney",
@@ -1973,7 +1953,6 @@
1973
1953
  }
1974
1954
  ],
1975
1955
  "license": {
1976
- "remix": "include",
1977
1956
  "kind": "cc0",
1978
1957
  "spdx": "CC0-1.0",
1979
1958
  "owner": "Kenney",
@@ -2066,7 +2045,6 @@
2066
2045
  }
2067
2046
  ],
2068
2047
  "license": {
2069
- "remix": "include",
2070
2048
  "kind": "cc0",
2071
2049
  "spdx": "CC0-1.0",
2072
2050
  "owner": "Kenney",
@@ -2158,7 +2136,6 @@
2158
2136
  }
2159
2137
  ],
2160
2138
  "license": {
2161
- "remix": "include",
2162
2139
  "kind": "cc0",
2163
2140
  "spdx": "CC0-1.0",
2164
2141
  "owner": "Kenney",
@@ -2250,7 +2227,6 @@
2250
2227
  }
2251
2228
  ],
2252
2229
  "license": {
2253
- "remix": "include",
2254
2230
  "kind": "cc0",
2255
2231
  "spdx": "CC0-1.0",
2256
2232
  "owner": "Kenney",
@@ -6,7 +6,7 @@
6
6
  <title>Gem Rush 3D</title>
7
7
  <link rel="icon" href="data:,">
8
8
  <style>
9
- html, body { margin: 0; height: 100%; background: #cfe8f5; overflow: hidden; overscroll-behavior: none; -webkit-user-select: none; user-select: none; }
9
+ html, body { margin: 0; height: 100%; background: #cfe8f5; 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
  /* The HUD is drawn over the world and never takes a touch: every touch reaches the world's canvas. */
12
12
  #hud { pointer-events: none; }
@@ -42,7 +42,7 @@ import {
42
42
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
43
43
  // The one model loader every studio game uses: checks each file, decodes meshopt and WebP, copies for placing.
44
44
  import { createModels, instancedCopies, placeCopy, repaint, stylize, type Copies } from '@homie-rocks/studio/assets';
45
- 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';
45
+ import { createNetplay, guardGestures, Roster, q, lerp, capMove, PALETTE, AI_MARK, type RoleChange, type RoundInfo, type RoundResult, type Skill, type Slot, type Snapshot } from '@homie-rocks/studio/netplay';
46
46
  // The port toolkit: its probe (what `homie-studio port check` and `perf` read, and sandbox + audio shims), and name
47
47
  // labels that never pile up (port/view.ts).
48
48
  import { createBubbles, createLabels, exposePort, paintBubbles, type BubbleIn, type BubbleOut, type LabelIn, type LabelOut } from '@homie-rocks/studio/port';
@@ -110,7 +110,7 @@ interface Body extends Knock { slot: number; seat: number | null; name: string;
110
110
  interface Gem { id: number; x: number; y: number }
111
111
  /** Slow state, on the keyed state channel (net.state('zone', ...)), not in the 20 Hz snapshot. */
112
112
  interface Zone { n: number; x: number; y: number; r: number; until: number }
113
- interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[]; tick: number; gemSeq: number }
113
+ interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[]; tick: number; gemSeq: number; occ?: [number, number][] }
114
114
 
115
115
  /* --------------------------------------------------------------- the net */
116
116
  /**
@@ -121,6 +121,9 @@ interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[];
121
121
  const MOVEMENT: 'owner' | 'host' = (() => { try { return new URLSearchParams(location.search).get('movement') === 'host' ? 'host' : 'owner'; } catch { return 'owner'; } })();
122
122
  // caps: its bots read the dial ('skill'), and its Roster takes an AI's slot for it ('agents': the join passes p.agent).
123
123
  const net = createNetplay<Snap, Avatar, Ckpt>({ game: 'gem-rush-3d', maxPlayers: MAX_SLOTS, movement: MOVEMENT, snapshotHz: 20, inputHz: 20, checkpointMs: 1000, caps: ['skill', 'agents'], checkpoint: () => checkpoint() });
124
+ // A touch game guards its own page (NETPLAY.md section 24): a long press on the canvas never selects text or raises
125
+ // the copy/paste callout on a phone, and a touch on it never pans or zooms the page.
126
+ guardGestures({ touch: 'canvas' });
124
127
  /** The Roster keeps the server's AI seats (revision 6): the policy is read whenever it fills. */
125
128
  const policy = () => net.policy;
126
129
 
@@ -240,7 +243,9 @@ function becomeHost(e: RoleChange<Snap, Ckpt>): void {
240
243
  startRound((e.round?.n ?? 0) + 1);
241
244
  }
242
245
  // Whoever is connected now is who plays: seats that left during the gap become bots.
243
- 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 }));
246
+ 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 }));
247
+ // `occ` (NETPLAY.md section 25): which stay in its seat each peer's is, so a seat number that changed hands while
248
+ // nobody was hosting is claimed afresh instead of inheriting the last player's body in silence.
244
249
  const { claimed } = roster.reconcile(peers);
245
250
  syncBodiesFromRoster();
246
251
  for (const s of claimed) { const b = bodies.get(s.slot); if (b && b.seat !== mySeat()) hostMoved(b); }
@@ -257,7 +262,7 @@ function becomeHost(e: RoleChange<Snap, Ckpt>): void {
257
262
  function restore(e: RoleChange<Snap, Ckpt>): void {
258
263
  const ck = e.ckpt?.d ?? null;
259
264
  if (ck) {
260
- roster = Roster.from(ck.roster, { min: MIN_SLOTS, max: MAX_SLOTS, botName, policy });
265
+ roster = Roster.from(ck.roster, { min: MIN_SLOTS, max: MAX_SLOTS, botName, policy }, ck.occ ?? null);
261
266
  bodies = new Map(ck.bodies.map((b) => [b.slot, { ...b }]));
262
267
  gems = ck.gems.map((g) => ({ ...g }));
263
268
  gemSeq = ck.gemSeq;
@@ -298,6 +303,8 @@ function checkpoint(): Ckpt {
298
303
  bodies: [...bodies.values()].map((b) => ({ ...b })),
299
304
  gems: gems.map((g) => ({ ...g })),
300
305
  roster: roster.toJSON(),
306
+ // Whose stay each body is, beside the roster: how the next host tells a player who came back from a new one.
307
+ occ: roster.occupants(),
301
308
  tick,
302
309
  gemSeq,
303
310
  };
@@ -310,7 +317,7 @@ net.on('role', (e) => {
310
317
  net.on('join', (p) => {
311
318
  if (!hosting || p.seat === null) return;
312
319
  // An AI takes a seat kept for AI, a person never does (revision 6: the Roster needs p.agent for that).
313
- const c = roster.claim(p.seat, p.name, p.agent ? { role: p.agent.role, hands: p.agent.hands } : null);
320
+ const c = roster.claim(p.seat, p.name, p.agent ? { role: p.agent.role, hands: p.agent.hands } : null, p.occ ?? null);
314
321
  if (!c) return; // full: they watch
315
322
  syncBodiesFromRoster();
316
323
  const b = bodies.get(c.slot.slot);
@@ -2095,6 +2102,9 @@ exposePort(net, {
2095
2102
  self: () => (me.has && mySeat() !== null ? { x: me.x, y: me.y } : null),
2096
2103
  size: R_AV,
2097
2104
  score: () => { const seat = mySeat(); const b = hosting ? [...bodies.values()].find((x) => x.seat === seat) : [...drawn.values()].find((x) => x.seat === seat); return b ? b.score : null; },
2105
+ // The renderer's own counters, by the names `homie-studio perf` and the playtest read on the port probe (measured
2106
+ // scene cost while playing; port/probe.ts PortExtra). Nobody dies in this game, so there is no `alive` to say.
2107
+ extra: { drawCalls: () => renderer.info.render.calls, triangles: () => renderer.info.render.triangles },
2098
2108
  });
2099
2109
 
2100
2110
  void net.ready.then(() => {
@@ -15,7 +15,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
15
15
  - **What:** character (Players/Barbarian): `public/models/barbarian.glb`
16
16
  - **From:** from the Homie starter library: KayKit Adventurers, barbarian (https://github.com/KayKit-Game-Assets/KayKit-Character-Pack-Adventures-1.0); library
17
17
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-adventurers); the pack's own licence text: licenses/kaykit-adventurers.txt. `game new` fetches the file, makes the character phone-sized and bakes its clips; the repository holds no models.
20
19
 
21
20
  ### barrel
@@ -23,7 +22,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
23
22
  - **What:** prop (Places/Barrel): `public/models/barrel.glb`
24
23
  - **From:** from the Homie starter library: KayKit Medieval Hexagon, barrel (https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0); library
25
24
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-medieval-hexagon); the pack's own licence text: licenses/kaykit-medieval-hexagon.txt. `game new` fetches the file; the repository holds no models.
28
26
 
29
27
  ### chest
@@ -31,7 +29,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
31
29
  - **What:** prop (Places/Treasure Chest): `public/models/chest.glb`
32
30
  - **From:** from the Homie starter library: KayKit Dungeon Remastered, chest-gold (https://github.com/KayKit-Game-Assets/KayKit-Dungeon-Remastered-1.0); library
33
31
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-dungeon); the pack's own licence text: licenses/kaykit-dungeon.txt. `game new` fetches the file; the repository holds no models.
36
33
 
37
34
  ### coin
@@ -39,7 +36,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
39
36
  - **What:** prop (Items/Coin): `public/models/coin.glb`
40
37
  - **From:** from the Homie starter library: KayKit Dungeon Remastered, coin (https://github.com/KayKit-Game-Assets/KayKit-Dungeon-Remastered-1.0); library
41
38
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-dungeon); the pack's own licence text: licenses/kaykit-dungeon.txt. `game new` fetches the file; the repository holds no models.
44
40
 
45
41
  ### crate
@@ -47,7 +43,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
47
43
  - **What:** prop (Places/Crate): `public/models/crate.glb`
48
44
  - **From:** from the Homie starter library: KayKit Medieval Hexagon, crate-a-big (https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0); library
49
45
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-medieval-hexagon); the pack's own licence text: licenses/kaykit-medieval-hexagon.txt. `game new` fetches the file; the repository holds no models.
52
47
 
53
48
  ### grove
@@ -55,7 +50,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
55
50
  - **What:** prop (Places/Grove): `public/models/grove.glb`
56
51
  - **From:** from the Homie starter library: KayKit Medieval Hexagon, trees-a-medium (https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0); library
57
52
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-medieval-hexagon); the pack's own licence text: licenses/kaykit-medieval-hexagon.txt. `game new` fetches the file; the repository holds no models.
60
54
 
61
55
  ### knight
@@ -63,7 +57,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
63
57
  - **What:** character (Players/Knight): `public/models/knight.glb`
64
58
  - **From:** from the Homie starter library: KayKit Adventurers, knight (https://github.com/KayKit-Game-Assets/KayKit-Character-Pack-Adventures-1.0); library
65
59
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-adventurers); the pack's own licence text: licenses/kaykit-adventurers.txt. `game new` fetches the file, makes the character phone-sized and bakes its clips; the repository holds no models.
68
61
 
69
62
  ### mage
@@ -71,7 +64,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
71
64
  - **What:** character (Players/Mage): `public/models/mage.glb`
72
65
  - **From:** from the Homie starter library: KayKit Adventurers, mage (https://github.com/KayKit-Game-Assets/KayKit-Character-Pack-Adventures-1.0); library
73
66
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-adventurers); the pack's own licence text: licenses/kaykit-adventurers.txt. `game new` fetches the file, makes the character phone-sized and bakes its clips; the repository holds no models.
76
68
 
77
69
  ### pine
@@ -79,7 +71,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
79
71
  - **What:** prop (Places/Pine): `public/models/pine.glb`
80
72
  - **From:** from the Homie starter library: KayKit Medieval Hexagon, tree-single-a (https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0); library
81
73
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-medieval-hexagon); the pack's own licence text: licenses/kaykit-medieval-hexagon.txt. `game new` fetches the file; the repository holds no models.
84
75
 
85
76
  ### pine-round
@@ -87,7 +78,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
87
78
  - **What:** prop (Places/Round Pine): `public/models/pine-round.glb`
88
79
  - **From:** from the Homie starter library: KayKit Medieval Hexagon, tree-single-b (https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0); library
89
80
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-medieval-hexagon); the pack's own licence text: licenses/kaykit-medieval-hexagon.txt. `game new` fetches the file; the repository holds no models.
92
82
 
93
83
  ### rock
@@ -95,7 +85,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
95
85
  - **What:** prop (Places/Rock): `public/models/rock.glb`
96
86
  - **From:** from the Homie starter library: KayKit Medieval Hexagon, rock-single-e (https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0); library
97
87
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-medieval-hexagon); the pack's own licence text: licenses/kaykit-medieval-hexagon.txt. `game new` fetches the file; the repository holds no models.
100
89
 
101
90
  ### rocks
@@ -103,7 +92,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
103
92
  - **What:** prop (Places/Rocks): `public/models/rocks.glb`
104
93
  - **From:** from the Homie starter library: KayKit Medieval Hexagon, rock-single-c (https://github.com/KayKit-Game-Assets/KayKit-Medieval-Hexagon-Pack-1.0); library
105
94
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-medieval-hexagon); the pack's own licence text: licenses/kaykit-medieval-hexagon.txt. `game new` fetches the file; the repository holds no models.
108
96
 
109
97
  ### rogue
@@ -111,7 +99,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
111
99
  - **What:** character (Players/Rogue): `public/models/rogue.glb`
112
100
  - **From:** from the Homie starter library: KayKit Adventurers, rogue (https://github.com/KayKit-Game-Assets/KayKit-Character-Pack-Adventures-1.0); library
113
101
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-adventurers); the pack's own licence text: licenses/kaykit-adventurers.txt. `game new` fetches the file, makes the character phone-sized and bakes its clips; the repository holds no models.
116
103
 
117
104
  ### rogue-hooded
@@ -119,7 +106,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
119
106
  - **What:** character (Players/Hooded Rogue): `public/models/rogue-hooded.glb`
120
107
  - **From:** from the Homie starter library: KayKit Adventurers, rogue-hooded (https://github.com/KayKit-Game-Assets/KayKit-Character-Pack-Adventures-1.0); library
121
108
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-adventurers); the pack's own licence text: licenses/kaykit-adventurers.txt. `game new` fetches the file, makes the character phone-sized and bakes its clips; the repository holds no models.
124
110
 
125
111
  ### skeleton-mage
@@ -127,7 +113,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
127
113
  - **What:** creature (Creatures/Skeleton Mage): `public/models/skeleton-mage.glb`
128
114
  - **From:** from the Homie starter library: KayKit Skeletons, skeleton-mage (https://github.com/KayKit-Game-Assets/KayKit-Character-Pack-Skeletons-1.0); library
129
115
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-skeletons); the pack's own licence text: licenses/kaykit-skeletons.txt. `game new` fetches the file, makes the character phone-sized and bakes its clips; the repository holds no models.
132
117
 
133
118
  ### skeleton-minion
@@ -135,7 +120,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
135
120
  - **What:** creature (Creatures/Skeleton Minion): `public/models/skeleton-minion.glb`
136
121
  - **From:** from the Homie starter library: KayKit Skeletons, skeleton-minion (https://github.com/KayKit-Game-Assets/KayKit-Character-Pack-Skeletons-1.0); library
137
122
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-skeletons); the pack's own licence text: licenses/kaykit-skeletons.txt. `game new` fetches the file, makes the character phone-sized and bakes its clips; the repository holds no models.
140
124
 
141
125
  ### skeleton-rogue
@@ -143,7 +127,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
143
127
  - **What:** creature (Creatures/Skeleton Rogue): `public/models/skeleton-rogue.glb`
144
128
  - **From:** from the Homie starter library: KayKit Skeletons, skeleton-rogue (https://github.com/KayKit-Game-Assets/KayKit-Character-Pack-Skeletons-1.0); library
145
129
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-skeletons); the pack's own licence text: licenses/kaykit-skeletons.txt. `game new` fetches the file, makes the character phone-sized and bakes its clips; the repository holds no models.
148
131
 
149
132
  ### skeleton-warrior
@@ -151,7 +134,6 @@ Read on the dates shown. Terms change: read the live page again before publishin
151
134
  - **What:** creature (Creatures/Skeleton Warrior): `public/models/skeleton-warrior.glb`
152
135
  - **From:** from the Homie starter library: KayKit Skeletons, skeleton-warrior (https://github.com/KayKit-Game-Assets/KayKit-Character-Pack-Skeletons-1.0); library
153
136
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-skeletons); the pack's own licence text: licenses/kaykit-skeletons.txt. `game new` fetches the file, makes the character phone-sized and bakes its clips; the repository holds no models.
156
138
 
157
139
  ### torch
@@ -159,5 +141,4 @@ Read on the dates shown. Terms change: read the live page again before publishin
159
141
  - **What:** prop (Places/Torch): `public/models/torch.glb`
160
142
  - **From:** from the Homie starter library: KayKit Dungeon Remastered, torch-lit (https://github.com/KayKit-Game-Assets/KayKit-Dungeon-Remastered-1.0); library
161
143
  - **Licence:** CC0 1.0 (public domain) (https://creativecommons.org/publicdomain/zero/1.0/); owner: KayKit
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 (kaykit-dungeon); the pack's own licence text: licenses/kaykit-dungeon.txt. `game new` fetches the file; the repository holds no models.
@@ -29,7 +29,6 @@
29
29
  }
30
30
  ],
31
31
  "license": {
32
- "remix": "include",
33
32
  "kind": "cc0",
34
33
  "spdx": "CC0-1.0",
35
34
  "owner": "KayKit",
@@ -118,7 +117,6 @@
118
117
  }
119
118
  ],
120
119
  "license": {
121
- "remix": "include",
122
120
  "kind": "cc0",
123
121
  "spdx": "CC0-1.0",
124
122
  "owner": "KayKit",
@@ -186,7 +184,6 @@
186
184
  }
187
185
  ],
188
186
  "license": {
189
- "remix": "include",
190
187
  "kind": "cc0",
191
188
  "spdx": "CC0-1.0",
192
189
  "owner": "KayKit",
@@ -254,7 +251,6 @@
254
251
  }
255
252
  ],
256
253
  "license": {
257
- "remix": "include",
258
254
  "kind": "cc0",
259
255
  "spdx": "CC0-1.0",
260
256
  "owner": "KayKit",
@@ -322,7 +318,6 @@
322
318
  }
323
319
  ],
324
320
  "license": {
325
- "remix": "include",
326
321
  "kind": "cc0",
327
322
  "spdx": "CC0-1.0",
328
323
  "owner": "KayKit",
@@ -390,7 +385,6 @@
390
385
  }
391
386
  ],
392
387
  "license": {
393
- "remix": "include",
394
388
  "kind": "cc0",
395
389
  "spdx": "CC0-1.0",
396
390
  "owner": "KayKit",
@@ -458,7 +452,6 @@
458
452
  }
459
453
  ],
460
454
  "license": {
461
- "remix": "include",
462
455
  "kind": "cc0",
463
456
  "spdx": "CC0-1.0",
464
457
  "owner": "KayKit",
@@ -547,7 +540,6 @@
547
540
  }
548
541
  ],
549
542
  "license": {
550
- "remix": "include",
551
543
  "kind": "cc0",
552
544
  "spdx": "CC0-1.0",
553
545
  "owner": "KayKit",
@@ -636,7 +628,6 @@
636
628
  }
637
629
  ],
638
630
  "license": {
639
- "remix": "include",
640
631
  "kind": "cc0",
641
632
  "spdx": "CC0-1.0",
642
633
  "owner": "KayKit",
@@ -704,7 +695,6 @@
704
695
  }
705
696
  ],
706
697
  "license": {
707
- "remix": "include",
708
698
  "kind": "cc0",
709
699
  "spdx": "CC0-1.0",
710
700
  "owner": "KayKit",
@@ -772,7 +762,6 @@
772
762
  }
773
763
  ],
774
764
  "license": {
775
- "remix": "include",
776
765
  "kind": "cc0",
777
766
  "spdx": "CC0-1.0",
778
767
  "owner": "KayKit",
@@ -840,7 +829,6 @@
840
829
  }
841
830
  ],
842
831
  "license": {
843
- "remix": "include",
844
832
  "kind": "cc0",
845
833
  "spdx": "CC0-1.0",
846
834
  "owner": "KayKit",
@@ -908,7 +896,6 @@
908
896
  }
909
897
  ],
910
898
  "license": {
911
- "remix": "include",
912
899
  "kind": "cc0",
913
900
  "spdx": "CC0-1.0",
914
901
  "owner": "KayKit",
@@ -996,7 +983,6 @@
996
983
  }
997
984
  ],
998
985
  "license": {
999
- "remix": "include",
1000
986
  "kind": "cc0",
1001
987
  "spdx": "CC0-1.0",
1002
988
  "owner": "KayKit",
@@ -1084,7 +1070,6 @@
1084
1070
  }
1085
1071
  ],
1086
1072
  "license": {
1087
- "remix": "include",
1088
1073
  "kind": "cc0",
1089
1074
  "spdx": "CC0-1.0",
1090
1075
  "owner": "KayKit",
@@ -1170,7 +1155,6 @@
1170
1155
  }
1171
1156
  ],
1172
1157
  "license": {
1173
- "remix": "include",
1174
1158
  "kind": "cc0",
1175
1159
  "spdx": "CC0-1.0",
1176
1160
  "owner": "KayKit",
@@ -1254,7 +1238,6 @@
1254
1238
  }
1255
1239
  ],
1256
1240
  "license": {
1257
- "remix": "include",
1258
1241
  "kind": "cc0",
1259
1242
  "spdx": "CC0-1.0",
1260
1243
  "owner": "KayKit",
@@ -1341,7 +1324,6 @@
1341
1324
  }
1342
1325
  ],
1343
1326
  "license": {
1344
- "remix": "include",
1345
1327
  "kind": "cc0",
1346
1328
  "spdx": "CC0-1.0",
1347
1329
  "owner": "KayKit",
@@ -1427,7 +1409,6 @@
1427
1409
  }
1428
1410
  ],
1429
1411
  "license": {
1430
- "remix": "include",
1431
1412
  "kind": "cc0",
1432
1413
  "spdx": "CC0-1.0",
1433
1414
  "owner": "KayKit",
@@ -6,7 +6,7 @@
6
6
  <title>Hero Rush 3D</title>
7
7
  <link rel="icon" href="data:,">
8
8
  <style>
9
- html, body { margin: 0; height: 100%; background: #cfe8f5; overflow: hidden; overscroll-behavior: none; -webkit-user-select: none; user-select: none; }
9
+ html, body { margin: 0; height: 100%; background: #cfe8f5; 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
  /* The HUD is drawn over the world and never takes a touch: every touch reaches the world's canvas. */
12
12
  #hud { pointer-events: none; }
@@ -70,7 +70,7 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js
70
70
  import { createModels, instancedCopies, placeCopy, repaint, stylize, type Copies } from '@homie-rocks/studio/assets';
71
71
  // Characters that move well: clips from the skeleton's clip library, blended, layered and tuned (Game Lab tunables).
72
72
  import { crowd, loadCharacter, type Character } from '@homie-rocks/studio/animate';
73
- import { createNetplay, Roster, q, lerp, capMove, skillPreset, PALETTE, AI_MARK, type RoleChange, type RoundInfo, type RoundResult, type Skill, type Slot, type Snapshot } from '@homie-rocks/studio/netplay';
73
+ import { createNetplay, guardGestures, Roster, q, lerp, capMove, skillPreset, PALETTE, AI_MARK, type RoleChange, type RoundInfo, type RoundResult, type Skill, type Slot, type Snapshot } from '@homie-rocks/studio/netplay';
74
74
  // The port toolkit: its probe (what `homie-studio port check` and `perf` read, and sandbox + audio shims), and name
75
75
  // labels that never pile up (port/view.ts).
76
76
  import { createBubbles, createLabels, exposePort, paintBubbles, type BubbleIn, type BubbleOut, type LabelIn, type LabelOut } from '@homie-rocks/studio/port';
@@ -153,7 +153,7 @@ interface Gem { id: number; x: number; y: number }
153
153
  /** Slow state, on the keyed state channel (net.state('zone', ...)), not in the 20 Hz snapshot. */
154
154
  interface Zone { n: number; x: number; y: number; r: number; until: number }
155
155
  /** `firstRound`: each person's first whole round here (seat, round), so a promoted host eases the same round. */
156
- interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[]; tick: number; gemSeq: number; firstRound?: [number, number][] }
156
+ interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[]; tick: number; gemSeq: number; occ?: [number, number][]; firstRound?: [number, number][] }
157
157
 
158
158
  /* --------------------------------------------------------------- the net */
159
159
  /**
@@ -165,6 +165,9 @@ const MOVEMENT: 'owner' | 'host' = (() => { try { return new URLSearchParams(loc
165
165
  // caps: its bots read the dial ('skill'), and its Roster takes an AI's slot for it ('agents': the join passes p.agent).
166
166
  // arrival 'game': the play page's arrival card stays until this game says it is playable (THE ARRIVAL, by the HUD).
167
167
  const net = createNetplay<Snap, Avatar, Ckpt>({ game: 'hero-rush-3d', maxPlayers: MAX_SLOTS, movement: MOVEMENT, snapshotHz: 20, inputHz: 20, checkpointMs: 1000, caps: ['skill', 'agents'], arrival: 'game', checkpoint: () => checkpoint() });
168
+ // A touch game guards its own page (NETPLAY.md section 24): a long press on the canvas never selects text or raises
169
+ // the copy/paste callout on a phone, and a touch on it never pans or zooms the page.
170
+ guardGestures({ touch: 'canvas' });
168
171
  /** The Roster keeps the server's AI seats (revision 6): the policy is read whenever it fills. */
169
172
  const policy = () => net.policy;
170
173
 
@@ -290,7 +293,9 @@ function becomeHost(e: RoleChange<Snap, Ckpt>): void {
290
293
  welcome(seat);
291
294
  }
292
295
  // Whoever is connected now is who plays: seats that left during the gap become bots.
293
- 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 }));
296
+ 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 }));
297
+ // `occ` (NETPLAY.md section 25): which stay in its seat each peer's is, so a seat number that changed hands while
298
+ // nobody was hosting is claimed afresh instead of inheriting the last player's body in silence.
294
299
  const { claimed } = roster.reconcile(peers);
295
300
  syncBodiesFromRoster();
296
301
  for (const s of claimed) welcome(s.seat);
@@ -308,7 +313,7 @@ function becomeHost(e: RoleChange<Snap, Ckpt>): void {
308
313
  function restore(e: RoleChange<Snap, Ckpt>): void {
309
314
  const ck = e.ckpt?.d ?? null;
310
315
  if (ck) {
311
- roster = Roster.from(ck.roster, { min: MIN_SLOTS, max: MAX_SLOTS, botName, policy });
316
+ roster = Roster.from(ck.roster, { min: MIN_SLOTS, max: MAX_SLOTS, botName, policy }, ck.occ ?? null);
312
317
  bodies = new Map(ck.bodies.map((b) => [b.slot, { ...b }]));
313
318
  gems = ck.gems.map((g) => ({ ...g }));
314
319
  gemSeq = ck.gemSeq;
@@ -352,6 +357,8 @@ function checkpoint(): Ckpt {
352
357
  bodies: [...bodies.values()].map((b) => ({ ...b })),
353
358
  gems: gems.map((g) => ({ ...g })),
354
359
  roster: roster.toJSON(),
360
+ // Whose stay each body is, beside the roster: how the next host tells a player who came back from a new one.
361
+ occ: roster.occupants(),
355
362
  tick,
356
363
  gemSeq,
357
364
  firstRound: [...firstRound],
@@ -365,7 +372,7 @@ net.on('role', (e) => {
365
372
  net.on('join', (p) => {
366
373
  if (!hosting || p.seat === null) return;
367
374
  // An AI takes a seat kept for AI, a person never does (revision 6: the Roster needs p.agent for that).
368
- const c = roster.claim(p.seat, p.name, p.agent ? { role: p.agent.role, hands: p.agent.hands } : null);
375
+ const c = roster.claim(p.seat, p.name, p.agent ? { role: p.agent.role, hands: p.agent.hands } : null, p.occ ?? null);
369
376
  if (!c) return; // full: they watch
370
377
  welcome(p.seat);
371
378
  syncBodiesFromRoster();
@@ -2576,6 +2583,9 @@ exposePort(net, {
2576
2583
  self: () => (me.has && mySeat() !== null ? { x: me.x, y: me.y } : null),
2577
2584
  size: R_AV,
2578
2585
  score: () => { const seat = mySeat(); const b = hosting ? [...bodies.values()].find((x) => x.seat === seat) : [...drawn.values()].find((x) => x.seat === seat); return b ? b.score : null; },
2586
+ // The renderer's own counters, by the names `homie-studio perf` and the playtest read on the port probe (measured
2587
+ // scene cost while playing; port/probe.ts PortExtra). Nobody dies in this game, so there is no `alive` to say.
2588
+ extra: { drawCalls: () => renderer.info.render.calls, triangles: () => renderer.info.render.triangles },
2579
2589
  });
2580
2590
 
2581
2591
  void net.ready.then(() => {