@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.
- package/CHANGELOG.md +76 -1
- package/README.md +343 -21
- package/assets/animate.ts +16 -6
- package/assets/assets.ts +14 -3
- package/assets/render-page.ts +10 -2
- package/assets/safety.mjs +7 -1
- package/bin/homie-studio.mjs +136 -121
- package/lib/art-cli.mjs +61 -18
- package/lib/art-tools.mjs +53 -10
- package/lib/asset-check.mjs +249 -46
- package/lib/asset-import.mjs +11 -5
- package/lib/asset-manifest.mjs +128 -51
- package/lib/build.mjs +239 -72
- package/lib/characters.mjs +119 -20
- package/lib/check.mjs +110 -7
- package/lib/chrome.mjs +11 -0
- package/lib/clips.mjs +54 -0
- package/lib/cloudflare.mjs +127 -8
- package/lib/codex.mjs +6 -0
- package/lib/collision.mjs +245 -0
- package/lib/decisions.mjs +2 -2
- package/lib/deploy-state.mjs +166 -0
- package/lib/dev.mjs +430 -0
- package/lib/directory.mjs +116 -5
- package/lib/doctor.mjs +2 -2
- package/lib/handoff.mjs +2 -6
- package/lib/library.mjs +1 -1
- package/lib/mcp-tools.mjs +36 -35
- package/lib/mcp.mjs +1 -1
- package/lib/net.mjs +81 -2
- package/lib/node-version.mjs +40 -0
- package/lib/office.mjs +6 -6
- package/lib/optimise.mjs +39 -4
- package/lib/parts-build.mjs +169 -0
- package/lib/parts-cli.mjs +104 -0
- package/lib/parts-store.mjs +342 -0
- package/lib/parts-tools.mjs +92 -0
- package/lib/parts.mjs +693 -0
- package/lib/perf.mjs +131 -31
- package/lib/port-check.mjs +40 -6
- package/lib/preview.mjs +77 -0
- package/lib/routes.mjs +132 -0
- package/lib/scaffold.mjs +81 -20
- package/lib/shoot.mjs +307 -0
- package/lib/site.mjs +1 -2
- package/lib/stage.mjs +118 -0
- package/lib/studio.mjs +0 -66
- package/lib/style-board.mjs +49 -6
- package/lib/template-history.json +61 -0
- package/lib/trailer.mjs +47 -0
- package/lib/typecheck.mjs +107 -0
- package/mcp/ui/lineup.js +3 -3
- package/mcp/ui/rights.js +1 -3
- package/netplay/NETPLAY.md +424 -16
- package/netplay/netplay.ts +756 -40
- package/package.json +10 -1
- package/parts/PARTS.md +163 -0
- package/port/index.ts +8 -2
- package/port/personas.ts +275 -0
- package/port/powerups.ts +357 -0
- package/port/probe.ts +38 -2
- package/port/room.ts +103 -19
- package/site/SITE.md +102 -20
- package/starters/ember-vale/index.html +1 -1
- package/starters/ember-vale/src/main.ts +3 -1
- package/starters/gem-rush/index.html +1 -1
- package/starters/gem-rush/src/main.ts +13 -6
- package/starters/gem-rush-3d/assets/RIGHTS.md +0 -24
- package/starters/gem-rush-3d/assets/manifest.json +0 -24
- package/starters/gem-rush-3d/index.html +1 -1
- package/starters/gem-rush-3d/src/main.ts +15 -5
- package/starters/hero-rush-3d/assets/RIGHTS.md +0 -19
- package/starters/hero-rush-3d/assets/manifest.json +0 -19
- package/starters/hero-rush-3d/index.html +1 -1
- package/starters/hero-rush-3d/src/main.ts +15 -5
- package/worker/arrival.mjs +4 -1
- package/worker/discover.mjs +11 -25
- package/worker/index.mjs +142 -62
- package/worker/license.mjs +45 -41
- package/worker/office-page.mjs +0 -4
- package/worker/office-schema.mjs +6 -0
- package/worker/office.mjs +19 -23
- package/worker/pages.mjs +141 -23
- package/worker/parts.mjs +234 -0
- package/worker/room.mjs +157 -14
- package/worker/schema.mjs +6 -8
- package/worker/seats.mjs +71 -0
- package/worker/site.mjs +51 -53
- package/worker/version.mjs +1 -1
- package/lib/remix-assets.mjs +0 -84
package/assets/animate.ts
CHANGED
|
@@ -49,7 +49,9 @@ const HERO_BUDGET = { triangles: 8000, texturePx: 1024, materials: 2, bytes: 153
|
|
|
49
49
|
export const ANIM_TUNING = Object.freeze({
|
|
50
50
|
fade: { value: 0.14, min: 0, max: 0.6, step: 0.01, unit: 's', group: 'Motion', note: 'How long one clip takes to blend into the next' },
|
|
51
51
|
walkSpeed: { value: 1.5, min: 0.3, max: 6, step: 0.05, unit: 'm/s', group: 'Motion', note: 'The ground speed the walk clip\'s feet move at (feet slide when this is wrong)' },
|
|
52
|
-
|
|
52
|
+
// 4.2 m/s suits the starter library's chibi rigs. It is a default, NOT a reading of your rig: a clip retargeted onto
|
|
53
|
+
// a taller character covers more ground a cycle (`homie-studio anim plan` shows the speed measured on each rig).
|
|
54
|
+
runSpeed: { value: 4.2, min: 1, max: 12, step: 0.05, unit: 'm/s', group: 'Motion', note: 'The ground speed the run clip\'s feet move at (a default, not measured on your rig: anim plan shows the measured speed)' },
|
|
53
55
|
runFrom: { value: 2.4, min: 0.5, max: 8, step: 0.05, unit: 'm/s', group: 'Motion', note: 'From this speed up, the walk becomes a run' },
|
|
54
56
|
actSpeed: { value: 1.25, min: 0.5, max: 3, step: 0.05, group: 'Motion', note: 'How fast actions (attacks, casts) play: higher is snappier' },
|
|
55
57
|
jumpStretch: { value: 0.16, min: 0, max: 0.5, step: 0.01, group: 'Motion', note: 'How tall and thin the body goes as it leaves the ground' },
|
|
@@ -64,8 +66,11 @@ export const ANIM_TUNING = Object.freeze({
|
|
|
64
66
|
export type AnimTuning = { -readonly [K in keyof typeof ANIM_TUNING]?: number };
|
|
65
67
|
|
|
66
68
|
export interface CharacterOptions {
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The clip library's address (default: the one the model names, beside it in ./anims/); null for none; or a list of
|
|
71
|
+
* addresses when its skeleton has a supplemental library too (the base first).
|
|
72
|
+
*/
|
|
73
|
+
anims?: string | string[] | null;
|
|
69
74
|
/** Live numbers (lab.tunables(tuning)): any ANIM_TUNING key, read every frame. */
|
|
70
75
|
tune?: AnimTuning;
|
|
71
76
|
/** Bones that swing on springs (default: names with tail, ear, cape, hair, antenna). */
|
|
@@ -157,10 +162,15 @@ export async function loadCharacter(models: ModelSource, url: string, opts: Char
|
|
|
157
162
|
const loaded = await models.load(url, { budget: HERO_BUDGET });
|
|
158
163
|
const info = (loaded.scene.userData?.homie ?? {}) as { anims?: string; family?: string; sockets?: Record<string, string> };
|
|
159
164
|
let clips = loaded.animations ?? [];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
// `anims`: the clip library to use instead of the one the model names; null for none; or a LIST, for a skeleton
|
|
166
|
+
// with a supplemental library beside its own (the base first: where two hold a verb, the earlier one's clip plays).
|
|
167
|
+
const own = info.anims ? new URL(info.anims, new URL(url, typeof location !== 'undefined' ? location.href : 'http://localhost/')).href : null;
|
|
168
|
+
const animsUrls = opts.anims === null ? [] : Array.isArray(opts.anims) ? opts.anims : [opts.anims ?? own].filter((u): u is string => Boolean(u));
|
|
169
|
+
const fromLibraries: AnimationClip[] = [];
|
|
170
|
+
for (const animsUrl of animsUrls) {
|
|
171
|
+
try { for (const c of (await models.load(animsUrl, { budget: { triangles: 0, bytes: 3 * 1024 * 1024 } })).animations) if (!fromLibraries.some((x) => x.name === c.name)) fromLibraries.push(c); } catch (error) { if (!clips.length && !fromLibraries.length) console.warn(`[animate] ${url}: its clips did not load (${(error as Error).message})`); }
|
|
163
172
|
}
|
|
173
|
+
clips = [...fromLibraries, ...clips];
|
|
164
174
|
const model = await models.instance(url, { budget: HERO_BUDGET });
|
|
165
175
|
return createCharacter(model, clips, { ...opts, family: info.family ?? null, sockets: info.sockets ?? {} });
|
|
166
176
|
}
|
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
|
|
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
|
|
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
|
}
|
package/assets/render-page.ts
CHANGED
|
@@ -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
|
-
|
|
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 }; }
|