@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
@@ -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; }
@@ -23,6 +23,13 @@
23
23
  * byte). A model that is refused or not there is drawn as a stand-in in the same colours (see "models" below): a
24
24
  * round never waits on, or breaks for, a model.
25
25
  *
26
+ * HEADINGS, AND THERE ARE TWO: a body's `yaw` is `Math.atan2(dx, dy)` in the game's own x and y, so yaw 0 faces +y
27
+ * (three.js's +z, the way the models are built) and it goes straight into `rotation.y`. That is also the yaw
28
+ * `@homie-rocks/camera` means (0 faces +Z, positive turns toward +X, radians), so a follow or chase camera from that
29
+ * package takes a body's `yaw` as it stands. The knock and aim angles (`ang`, `base`) are the OTHER kind,
30
+ * `Math.atan2(dy, dx)`: 0 faces +x. Never hand one of those to a camera or to `rotation.y` without converting it:
31
+ * `headingFrom({ zero: '+x', toward: '+z' })` from `@homie-rocks/camera/heading.js` is the adapter.
32
+ *
26
33
  * PHONE BUDGETS: under 100 draw calls and 150k triangles a frame (the `drawCalls` and `triangles` probes read
27
34
  * renderer.info), the pixel ratio at most 1.5 on a phone and 2 on a computer, and no shadow map on a phone: a soft
28
35
  * disc under each body, gem, tree and rock (a computer draws the sun's real shadows). Repeated things (gems, trees, flowers, the fence, the discs) are one
@@ -42,7 +49,7 @@ import {
42
49
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
43
50
  // The one model loader every studio game uses: checks each file, decodes meshopt and WebP, copies for placing.
44
51
  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';
52
+ 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';
46
53
  // The port toolkit: its probe (what `homie-studio port check` and `perf` read, and sandbox + audio shims), and name
47
54
  // labels that never pile up (port/view.ts).
48
55
  import { createBubbles, createLabels, exposePort, paintBubbles, type BubbleIn, type BubbleOut, type LabelIn, type LabelOut } from '@homie-rocks/studio/port';
@@ -110,7 +117,7 @@ interface Body extends Knock { slot: number; seat: number | null; name: string;
110
117
  interface Gem { id: number; x: number; y: number }
111
118
  /** Slow state, on the keyed state channel (net.state('zone', ...)), not in the 20 Hz snapshot. */
112
119
  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 }
120
+ interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[]; tick: number; gemSeq: number; occ?: [number, number][] }
114
121
 
115
122
  /* --------------------------------------------------------------- the net */
116
123
  /**
@@ -121,6 +128,9 @@ interface Ckpt { round: RoundInfo; bodies: Body[]; gems: Gem[]; roster: Slot[];
121
128
  const MOVEMENT: 'owner' | 'host' = (() => { try { return new URLSearchParams(location.search).get('movement') === 'host' ? 'host' : 'owner'; } catch { return 'owner'; } })();
122
129
  // caps: its bots read the dial ('skill'), and its Roster takes an AI's slot for it ('agents': the join passes p.agent).
123
130
  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() });
131
+ // A touch game guards its own page (NETPLAY.md section 24): a long press on the canvas never selects text or raises
132
+ // the copy/paste callout on a phone, and a touch on it never pans or zooms the page.
133
+ guardGestures({ touch: 'canvas' });
124
134
  /** The Roster keeps the server's AI seats (revision 6): the policy is read whenever it fills. */
125
135
  const policy = () => net.policy;
126
136
 
@@ -196,9 +206,12 @@ function newZone(n: number): Zone {
196
206
  }
197
207
  function setZone(z: Zone): void { zone = z; net.state('zone', z); }
198
208
 
199
- function startRound(n: number): void {
209
+ function startRound(n: number, rollover = false): void {
200
210
  const now = net.now();
201
211
  roster.trim();
212
+ // The next round of a running room: the trim is what makes room (the seats a server no longer keeps for AI go
213
+ // here), so whoever was seated with no body is given one now, before the bodies are placed.
214
+ if (rollover && !net.offline) seatWaiting();
202
215
  syncBodiesFromRoster();
203
216
  let i = 0;
204
217
  for (const b of [...bodies.values()].sort((a, c) => a.slot - c.slot)) {
@@ -240,7 +253,9 @@ function becomeHost(e: RoleChange<Snap, Ckpt>): void {
240
253
  startRound((e.round?.n ?? 0) + 1);
241
254
  }
242
255
  // 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 }));
256
+ 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 }));
257
+ // `occ` (NETPLAY.md section 25): which stay in its seat each peer's is, so a seat number that changed hands while
258
+ // nobody was hosting is claimed afresh instead of inheriting the last player's body in silence.
244
259
  const { claimed } = roster.reconcile(peers);
245
260
  syncBodiesFromRoster();
246
261
  for (const s of claimed) { const b = bodies.get(s.slot); if (b && b.seat !== mySeat()) hostMoved(b); }
@@ -257,7 +272,7 @@ function becomeHost(e: RoleChange<Snap, Ckpt>): void {
257
272
  function restore(e: RoleChange<Snap, Ckpt>): void {
258
273
  const ck = e.ckpt?.d ?? null;
259
274
  if (ck) {
260
- roster = Roster.from(ck.roster, { min: MIN_SLOTS, max: MAX_SLOTS, botName, policy });
275
+ roster = Roster.from(ck.roster, { min: MIN_SLOTS, max: MAX_SLOTS, botName, policy }, ck.occ ?? null);
261
276
  bodies = new Map(ck.bodies.map((b) => [b.slot, { ...b }]));
262
277
  gems = ck.gems.map((g) => ({ ...g }));
263
278
  gemSeq = ck.gemSeq;
@@ -298,6 +313,8 @@ function checkpoint(): Ckpt {
298
313
  bodies: [...bodies.values()].map((b) => ({ ...b })),
299
314
  gems: gems.map((g) => ({ ...g })),
300
315
  roster: roster.toJSON(),
316
+ // Whose stay each body is, beside the roster: how the next host tells a player who came back from a new one.
317
+ occ: roster.occupants(),
301
318
  tick,
302
319
  gemSeq,
303
320
  };
@@ -307,15 +324,30 @@ net.on('role', (e) => {
307
324
  if (e.role === 'host') becomeHost(e);
308
325
  else { hosting = false; }
309
326
  });
310
- net.on('join', (p) => {
311
- if (!hosting || p.seat === null) return;
327
+ /** Host: a body for somebody the relay seated. False when every body is somebody's (they wait: see seatWaiting). */
328
+ function giveBody(p: Peer): boolean {
329
+ if (p.seat === null) return false;
312
330
  // 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);
314
- if (!c) return; // full: they watch
331
+ const c = roster.claim(p.seat, p.name, p.agent ? { role: p.agent.role, hands: p.agent.hands } : null, p.occ ?? null);
332
+ if (!c) return false;
315
333
  syncBodiesFromRoster();
316
334
  const b = bodies.get(c.slot.slot);
317
335
  // The arriving human takes over the bot's body where it stands, score and all (reset = "adopt this position").
318
336
  if (b) { b.vx = 0; b.vy = 0; b.kvx = 0; b.kvy = 0; b.knockUntil = 0; hostMoved(b); }
337
+ return true;
338
+ }
339
+ /**
340
+ * SEAT WHOEVER IS WAITING (NETPLAY.md section 28). The relay seats a person; this host gives them a body. A claim that
341
+ * found every body taken (a server's AI seats were still kept, say) used to be tried once: that player watched for the
342
+ * rest of the visit, online, with nothing said. It is tried again whenever a body frees up and at every round start.
343
+ */
344
+ function seatWaiting(): boolean {
345
+ let any = false;
346
+ for (const p of net.peers.values()) if (p.seat !== null && !roster.bySeat(p.seat) && giveBody(p)) any = true;
347
+ return any;
348
+ }
349
+ net.on('join', (p) => {
350
+ if (!hosting || !giveBody(p)) return;
319
351
  publishRoster();
320
352
  net.snapshot(buildSnap(), tick, true);
321
353
  });
@@ -323,7 +355,10 @@ net.on('leave', (p) => {
323
355
  if (!hosting || p.seat === null) return;
324
356
  roster.release(p.seat); // their body stays, driven by a bot
325
357
  syncBodiesFromRoster();
358
+ // A body just went to a bot: somebody seated with none takes it now.
359
+ const seated = seatWaiting();
326
360
  publishRoster();
361
+ if (seated) net.snapshot(buildSnap(), tick, true);
327
362
  });
328
363
  // The server's policy changed: its AI seats come (or go between rounds) at once.
329
364
  net.on('policy', () => { if (!hosting) return; roster.fill(); syncBodiesFromRoster(); publishRoster(); });
@@ -610,7 +645,7 @@ function stepHost(dt: number): void {
610
645
  }
611
646
  }
612
647
  if (now >= round.endsAt) endRound();
613
- } else if (round && round.phase === 'over' && now >= round.endsAt) startRound(round.n + 1);
648
+ } else if (round && round.phase === 'over' && now >= round.endsAt) startRound(round.n + 1, true);
614
649
  if (net.snapshotDue()) net.snapshot(buildSnap(), tick);
615
650
  }
616
651
 
@@ -2014,7 +2049,8 @@ function hud(cw: number, ch: number, phone: boolean, list: { slot: number; seat:
2014
2049
  ctx.fillText(`${TOGETHER ? '' : `${row.place}. `}${mine && !net.watching ? 'You' : label(row.name, row.bot)}${mine && net.watching ? ' ◂' : ''} — ${row.score}`, cw / 2, ry + 1);
2015
2050
  });
2016
2051
  }
2017
- if (!net.offline && !net.connected && net.role !== 'host') {
2052
+ // Only a browser that WAS in its room is reconnecting; one still joining, or stopped for good, is not (section 22).
2053
+ if (net.link === 'reconnecting' && net.role !== 'host') {
2018
2054
  ctx.textAlign = 'center'; ctx.font = `700 15px ${FONT_BODY}`;
2019
2055
  const text = 'Reconnecting…'; const tw = ctx.measureText(text).width;
2020
2056
  chip(cw / 2 - tw / 2 - 12, ch - pad - 24, tw + 24, 26);
@@ -2095,6 +2131,9 @@ exposePort(net, {
2095
2131
  self: () => (me.has && mySeat() !== null ? { x: me.x, y: me.y } : null),
2096
2132
  size: R_AV,
2097
2133
  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; },
2134
+ // The renderer's own counters, by the names `homie-studio perf` and the playtest read on the port probe (measured
2135
+ // scene cost while playing; port/probe.ts PortExtra). Nobody dies in this game, so there is no `alive` to say.
2136
+ extra: { drawCalls: () => renderer.info.render.calls, triangles: () => renderer.info.render.triangles },
2098
2137
  });
2099
2138
 
2100
2139
  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; }