@homie-rocks/studio 0.30.2 → 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 (90) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/README.md +343 -21
  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 +136 -121
  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/deploy-state.mjs +166 -0
  23. package/lib/dev.mjs +430 -0
  24. package/lib/directory.mjs +116 -5
  25. package/lib/doctor.mjs +2 -2
  26. package/lib/handoff.mjs +2 -6
  27. package/lib/library.mjs +1 -1
  28. package/lib/mcp-tools.mjs +36 -35
  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 +693 -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 +132 -0
  43. package/lib/scaffold.mjs +81 -20
  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 +61 -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 +424 -16
  55. package/netplay/netplay.ts +756 -40
  56. package/package.json +10 -1
  57. package/parts/PARTS.md +163 -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 +103 -19
  63. package/site/SITE.md +102 -20
  64. package/starters/ember-vale/index.html +1 -1
  65. package/starters/ember-vale/src/main.ts +3 -1
  66. package/starters/gem-rush/index.html +1 -1
  67. package/starters/gem-rush/src/main.ts +13 -6
  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 +15 -5
  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 +15 -5
  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 +141 -23
  84. package/worker/parts.mjs +234 -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/assets/animate.ts CHANGED
@@ -49,7 +49,9 @@ const HERO_BUDGET = { triangles: 8000, texturePx: 1024, materials: 2, bytes: 153
49
49
  export const ANIM_TUNING = Object.freeze({
50
50
  fade: { value: 0.14, min: 0, max: 0.6, step: 0.01, unit: 's', group: 'Motion', note: 'How long one clip takes to blend into the next' },
51
51
  walkSpeed: { value: 1.5, min: 0.3, max: 6, step: 0.05, unit: 'm/s', group: 'Motion', note: 'The ground speed the walk clip\'s feet move at (feet slide when this is wrong)' },
52
- runSpeed: { value: 4.2, min: 1, max: 12, step: 0.05, unit: 'm/s', group: 'Motion', note: 'The ground speed the run clip\'s feet move at' },
52
+ // 4.2 m/s suits the starter library's chibi rigs. It is a default, NOT a reading of your rig: a clip retargeted onto
53
+ // a taller character covers more ground a cycle (`homie-studio anim plan` shows the speed measured on each rig).
54
+ runSpeed: { value: 4.2, min: 1, max: 12, step: 0.05, unit: 'm/s', group: 'Motion', note: 'The ground speed the run clip\'s feet move at (a default, not measured on your rig: anim plan shows the measured speed)' },
53
55
  runFrom: { value: 2.4, min: 0.5, max: 8, step: 0.05, unit: 'm/s', group: 'Motion', note: 'From this speed up, the walk becomes a run' },
54
56
  actSpeed: { value: 1.25, min: 0.5, max: 3, step: 0.05, group: 'Motion', note: 'How fast actions (attacks, casts) play: higher is snappier' },
55
57
  jumpStretch: { value: 0.16, min: 0, max: 0.5, step: 0.01, group: 'Motion', note: 'How tall and thin the body goes as it leaves the ground' },
@@ -64,8 +66,11 @@ export const ANIM_TUNING = Object.freeze({
64
66
  export type AnimTuning = { -readonly [K in keyof typeof ANIM_TUNING]?: number };
65
67
 
66
68
  export interface CharacterOptions {
67
- /** The clip library's address (default: the one the model names, beside it in ./anims/). */
68
- anims?: string | null;
69
+ /**
70
+ * The clip library's address (default: the one the model names, beside it in ./anims/); null for none; or a list of
71
+ * addresses when its skeleton has a supplemental library too (the base first).
72
+ */
73
+ anims?: string | string[] | null;
69
74
  /** Live numbers (lab.tunables(tuning)): any ANIM_TUNING key, read every frame. */
70
75
  tune?: AnimTuning;
71
76
  /** Bones that swing on springs (default: names with tail, ear, cape, hair, antenna). */
@@ -157,10 +162,15 @@ export async function loadCharacter(models: ModelSource, url: string, opts: Char
157
162
  const loaded = await models.load(url, { budget: HERO_BUDGET });
158
163
  const info = (loaded.scene.userData?.homie ?? {}) as { anims?: string; family?: string; sockets?: Record<string, string> };
159
164
  let clips = loaded.animations ?? [];
160
- const animsUrl = opts.anims === null ? null : (opts.anims ?? (info.anims ? new URL(info.anims, new URL(url, typeof location !== 'undefined' ? location.href : 'http://localhost/')).href : null));
161
- if (animsUrl) {
162
- try { clips = [...(await models.load(animsUrl, { budget: { triangles: 0, bytes: 3 * 1024 * 1024 } })).animations, ...clips]; } catch (error) { if (!clips.length) console.warn(`[animate] ${url}: its clips did not load (${(error as Error).message})`); }
165
+ // `anims`: the clip library to use instead of the one the model names; null for none; or a LIST, for a skeleton
166
+ // with a supplemental library beside its own (the base first: where two hold a verb, the earlier one's clip plays).
167
+ const own = info.anims ? new URL(info.anims, new URL(url, typeof location !== 'undefined' ? location.href : 'http://localhost/')).href : null;
168
+ const animsUrls = opts.anims === null ? [] : Array.isArray(opts.anims) ? opts.anims : [opts.anims ?? own].filter((u): u is string => Boolean(u));
169
+ const fromLibraries: AnimationClip[] = [];
170
+ for (const animsUrl of animsUrls) {
171
+ try { for (const c of (await models.load(animsUrl, { budget: { triangles: 0, bytes: 3 * 1024 * 1024 } })).animations) if (!fromLibraries.some((x) => x.name === c.name)) fromLibraries.push(c); } catch (error) { if (!clips.length && !fromLibraries.length) console.warn(`[animate] ${url}: its clips did not load (${(error as Error).message})`); }
163
172
  }
173
+ clips = [...fromLibraries, ...clips];
164
174
  const model = await models.instance(url, { budget: HERO_BUDGET });
165
175
  return createCharacter(model, clips, { ...opts, family: info.family ?? null, sockets: info.sockets ?? {} });
166
176
  }
package/assets/assets.ts CHANGED
@@ -18,7 +18,8 @@
18
18
  * runtime (homie.rocks's starter library is copied into the game, never hot-linked). `allowOrigins` opens one on
19
19
  * purpose.
20
20
  *
21
- * In development (localhost, or ?debug=1) a model over its budget says so in the console, and `stats()` (also
21
+ * In development (localhost, or ?debug=1) a model over its budget says so in the console (the budget is a small
22
+ * prop's unless the game sets its own: game.json `"assets": { "budgets": { "triangles", "bytes" } }`), and `stats()` (also
22
23
  * `window.__homieModels`) has the totals the perf skill reads: models, triangles, texture memory and refusals.
23
24
  *
24
25
  * KTX2 pictures (KHR_texture_basisu) need a transcoder (about 580 KB): pass your own KTX2Loader as `ktx2` when a
@@ -70,7 +71,7 @@ export interface LoadedModel {
70
71
  export interface ModelsOptions {
71
72
  /** Narrow or widen the file checks (LIMITS.game by default). */
72
73
  limits?: Partial<ModelLimits>;
73
- /** A per-model budget for the development warnings (the prop tier by default). */
74
+ /** A per-model budget for the development warnings (over game.json's `assets.budgets`, over the prop tier). */
74
75
  budget?: ModelBudget;
75
76
  /** A KTX2Loader the game set up (transcoder path, renderer support): only for models with KTX2 pictures. */
76
77
  ktx2?: { load?: unknown } | null;
@@ -98,6 +99,16 @@ export interface Models {
98
99
  }
99
100
 
100
101
  const PROP_BUDGET: Required<ModelBudget> = { triangles: 1500, texturePx: 512, materials: 1, bytes: 300 * 1024 };
102
+ /**
103
+ * The game's own budgets: game.json `"assets": { "budgets": { "triangles": 8000, "bytes": 1500000 } }`, which
104
+ * `homie-studio build` writes into the bundle as this constant (lib/build.mjs modelBudgetsOf). The defaults above are
105
+ * a small prop's; a 3D game whose every model is a character or a piece of level sets its own once, there, instead
106
+ * of reading a warning per model. Any other bundler leaves the name undefined, and the defaults stand.
107
+ */
108
+ declare const __HOMIE_MODEL_BUDGETS__: ModelBudget | undefined;
109
+ const gameBudget = (): ModelBudget => {
110
+ try { return typeof __HOMIE_MODEL_BUDGETS__ === 'object' && __HOMIE_MODEL_BUDGETS__ ? __HOMIE_MODEL_BUDGETS__ : {}; } catch { return {}; }
111
+ };
101
112
  const devHost = (): boolean => {
102
113
  try { return /^(localhost|127\.0\.0\.1|\[::1\])$/.test(location.hostname) || new URLSearchParams(location.search).get('debug') === '1'; } catch { return false; }
103
114
  };
@@ -461,7 +472,7 @@ export function createModels(opts: ModelsOptions = {}): Models {
461
472
  const key = url;
462
473
  let p = cache.get(key);
463
474
  if (!p) {
464
- p = loadOnce(url, { ...limits, ...(o.limits ?? {}) } as ModelLimits, { ...PROP_BUDGET, ...(opts.budget ?? {}), ...(o.budget ?? {}) });
475
+ p = loadOnce(url, { ...limits, ...(o.limits ?? {}) } as ModelLimits, { ...PROP_BUDGET, ...gameBudget(), ...(opts.budget ?? {}), ...(o.budget ?? {}) });
465
476
  cache.set(key, p);
466
477
  p.catch(() => cache.delete(key));
467
478
  }
@@ -35,7 +35,7 @@ interface Tokens {
35
35
  shape?: { language?: string; bevel?: number };
36
36
  proportions?: { heads?: number; heightM?: number };
37
37
  }
38
- interface ModelIn { id: string; glb: string; label?: string; place?: 'hero' | 'prop' | 'dressing'; scale?: number; retint?: boolean; tint?: string; pull?: number; anims?: string; pose?: string; poseAt?: number }
38
+ interface ModelIn { id: string; glb: string; label?: string; place?: 'hero' | 'prop' | 'dressing'; scale?: number; retint?: boolean; tint?: string; pull?: number; anims?: string | string[]; pose?: string; poseAt?: number }
39
39
 
40
40
  const canvas = document.createElement('canvas');
41
41
  document.body.appendChild(canvas);
@@ -51,7 +51,15 @@ async function parse(m: ModelIn): Promise<Group> {
51
51
  const gltf = await new Promise<{ scene: Group; animations: AnimationClip[] }>((res, rej) => loader.parse(b64(m.glb), '', res as never, rej));
52
52
  gltf.scene.name = m.id;
53
53
  // A character with a clip library stands in a pose (idle, a little way in), not in its bind pose.
54
- const clips = m.anims ? (await new Promise<{ animations: AnimationClip[] }>((res, rej) => loader.parse(b64(m.anims as string), '', res as never, rej))).animations : gltf.animations;
54
+ // One clip library, or several (its skeleton's own, then any declared supplemental): a verb's first clip is kept.
55
+ let clips = gltf.animations;
56
+ if (m.anims) {
57
+ clips = [];
58
+ for (const lib of Array.isArray(m.anims) ? m.anims : [m.anims]) {
59
+ const more = (await new Promise<{ animations: AnimationClip[] }>((res, rej) => loader.parse(b64(lib), '', res as never, rej))).animations;
60
+ for (const c of more) if (!clips.some((x) => x.name === c.name)) clips.push(c);
61
+ }
62
+ }
55
63
  gltf.scene.userData.clips = clips;
56
64
  if (clips?.length && (m.anims || m.pose)) {
57
65
  const clip = clips.find((c) => c.name === (m.pose ?? 'idle')) ?? clips.find((c) => c.name === 'idle') ?? clips[0];
package/assets/safety.mjs CHANGED
@@ -87,12 +87,18 @@ export function readGlb(bytes, { maxJson = LIMITS.import.json } = {}) {
87
87
  * { ok, problems: [plain words], warnings: [plain words], info: { bytes, triangles, vertices, meshes, nodes,
88
88
  * materials, images, textures, animations, skins, joints, extensions: { used, required } } }
89
89
  * Never throws: a file it cannot read is `ok: false` with the reason.
90
+ *
91
+ * The JSDoc types say what the code always took: any limits (numbers, a part of them is enough), and extension names
92
+ * as strings. Without them a strict TypeScript caller read the frozen defaults as literal types and the empty lists
93
+ * as never[].
94
+ * @param {Uint8Array} bytes
95
+ * @param {Partial<Record<keyof typeof LIMITS.game, number>>} [limits]
90
96
  */
91
97
  export function checkGlb(bytes, limits = LIMITS.game) {
92
98
  const L = { ...LIMITS.game, ...limits };
93
99
  const problems = [];
94
100
  const warnings = [];
95
- const info = { bytes: bytes?.byteLength ?? 0, triangles: 0, vertices: 0, meshes: 0, nodes: 0, materials: 0, images: 0, textures: 0, animations: 0, skins: 0, joints: 0, extensions: { used: [], required: [] } };
101
+ const info = { bytes: bytes?.byteLength ?? 0, triangles: 0, vertices: 0, meshes: 0, nodes: 0, materials: 0, images: 0, textures: 0, animations: 0, skins: 0, joints: 0, extensions: { used: /** @type {string[]} */ ([]), required: /** @type {string[]} */ ([]) } };
96
102
  if (info.bytes > L.bytes) problems.push(`the file is ${size(info.bytes)}, over the ${size(L.bytes)} a model may be here`);
97
103
  let glb;
98
104
  try { glb = readGlb(bytes, { maxJson: L.json }); } catch (error) { problems.push(error.message); return { ok: false, problems, warnings, info }; }