@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.
- package/CHANGELOG.md +111 -1
- package/README.md +361 -22
- 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 +142 -123
- 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/demo.mjs +16 -4
- package/lib/deploy-state.mjs +166 -0
- package/lib/dev.mjs +430 -0
- package/lib/directory.mjs +116 -5
- package/lib/doctor.mjs +143 -16
- package/lib/feedback.mjs +3 -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 +88 -22
- 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 +101 -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/lib/art-cli.mjs
CHANGED
|
@@ -21,9 +21,16 @@
|
|
|
21
21
|
* assets add <id> <library item> [--as <id>] [--height <m>] [--card "Items/Berry"]
|
|
22
22
|
* assets add <id> --file <model> --license <kind> [--attribution "…"] [--kind prop] [--as <id>] [--height <m>]
|
|
23
23
|
* [--triangles <n>] [--texture <px>] [--route imported|generated] [--steps <steps.json>] [--concept <image>]
|
|
24
|
-
* assets optimise <in> --out <file> [--triangles 1500] [--texture 512] [--height <m>]
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* assets optimise <in> --out <file> [--triangles 1500] [--texture 512] [--height <m>] [--lo 0.2]
|
|
25
|
+
* --lo <ratio> (here and on `assets add --file`): also write <name>_lo.glb, the same model
|
|
26
|
+
* at that share of its triangles, for the far band of a prop drawn at two levels of detail
|
|
27
|
+
* assets check <id> budgets, validator, licences, staleness, big files, and an INVENTORY ESTIMATE of the scene
|
|
28
|
+
* (the recorded files' own numbers; the running game is measured by the playtest and perf)
|
|
29
|
+
* assets lineup <id> [--scope inventory|cast|environment]
|
|
30
|
+
* the lineup, silhouettes and palette drift (pictures in .studio/art/<id>/): the whole
|
|
31
|
+
* inventory, and apart from it what is in play (the cast) and the scenery
|
|
32
|
+
* assets use <id> <asset> cast|prop|environment|unused|auto
|
|
33
|
+
* what the game does with an asset now (unused: kept in the folder, drawn by nothing)
|
|
27
34
|
* assets review <id> --score <1-10> [--outliers a,b] [--note "…"]
|
|
28
35
|
* assets rights <id> RIGHTS.md and credits.json written from the manifest
|
|
29
36
|
* assets stale <id> assets made under an older decision
|
|
@@ -40,7 +47,7 @@
|
|
|
40
47
|
*/
|
|
41
48
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
42
49
|
import { join, resolve } from 'node:path';
|
|
43
|
-
import { licenceProblems, readManifest, removeAsset, rightsMarkdown, syncCredits, writeRights } from './asset-manifest.mjs';
|
|
50
|
+
import { USAGES, licenceProblems, readManifest, removeAsset, rightsMarkdown, setUsage, syncCredits, usageOf, writeRights } from './asset-manifest.mjs';
|
|
44
51
|
import { CATALOGUE_IDS, PHASES, blastRadius, decisionRows, derivedPrompt, initDecisions, lockDecision, oneLine, phaseProgress, pickDirection, readDecisions, setDecision, staleAssets, steerDecision, unlockDecision } from './decisions.mjs';
|
|
45
52
|
import { listGames } from './studio.mjs';
|
|
46
53
|
import { checkLines } from './asset-check.mjs';
|
|
@@ -98,7 +105,7 @@ export function writeArtSummary(root, id) {
|
|
|
98
105
|
stale: doc ? staleAssets(doc, manifest).map((s) => s.id) : [],
|
|
99
106
|
licence: licenceProblems(root, id, manifest),
|
|
100
107
|
spend: artSpend(root, id),
|
|
101
|
-
check: check ? { ok: check.ok, at: check.at, totals: check.totals, budgets: { drawCalls: check.budgets?.drawCalls, triangles: check.budgets?.triangles, textureMB: check.budgets?.textureMB, firstPlayMB: check.budgets?.firstPlayMB }, failing: check.rows.filter((r) => !r.ok).map((r) => r.id) } : null,
|
|
108
|
+
check: check ? { ok: check.ok, at: check.at, totals: check.totals, budgets: { drawCalls: check.budgets?.drawCalls, triangles: check.budgets?.triangles, textureMB: check.budgets?.textureMB, shippedPayloadMB: check.budgets?.shippedPayloadMB ?? check.budgets?.firstPlayMB, firstPlayMB: check.budgets?.firstPlayMB }, scope: check.scope ?? 'inventory-estimate', failing: check.rows.filter((r) => !r.ok).map((r) => r.id) } : null,
|
|
102
109
|
lineup: lineup ? { at: lineup.at, flagged: lineup.flagged, images: lineup.images } : null,
|
|
103
110
|
// The characters and their clips (lib/characters.mjs animPlan): the Art tab's Characters, /cast and /clips.
|
|
104
111
|
...(() => {
|
|
@@ -170,7 +177,7 @@ export async function styleCommand(root, sub, positional, flags, { log = () => {
|
|
|
170
177
|
}
|
|
171
178
|
|
|
172
179
|
export async function assetsCommand(root, sub, positional, flags, { log = () => {} } = {}) {
|
|
173
|
-
const known = ['list', 'find', 'add', 'redo', 'optimise', 'optimize', 'check', 'lineup', 'review', 'rights', 'stale', 'remove'];
|
|
180
|
+
const known = ['list', 'find', 'add', 'redo', 'optimise', 'optimize', 'check', 'lineup', 'review', 'rights', 'stale', 'remove', 'use'];
|
|
174
181
|
const verb = known.includes(sub) ? (sub === 'optimize' ? 'optimise' : sub) : 'list';
|
|
175
182
|
const args = known.includes(sub) ? positional.slice(2) : positional.slice(1);
|
|
176
183
|
if (verb === 'find') {
|
|
@@ -181,17 +188,18 @@ export async function assetsCommand(root, sub, positional, flags, { log = () =>
|
|
|
181
188
|
}
|
|
182
189
|
if (verb === 'optimise') {
|
|
183
190
|
const { optimiseModel } = await import('./optimise.mjs');
|
|
184
|
-
if (!args[0] || !flags.get('out')) return { ok: false, command: 'assets optimise', why: 'usage: homie-studio assets optimise <model.glb> --out <file.glb> [--triangles 1500] [--texture 512] [--height <m>]' };
|
|
185
|
-
const r = await optimiseModel(resolve(args[0]), { out: resolve(String(flags.get('out'))), triangles: Number(flags.get('triangles') ?? 1500), texture: Number(flags.get('texture') ?? 512), height: flags.has('height') ? Number(flags.get('height')) : null, rigged: flags.has('rigged'), log });
|
|
186
|
-
const { glb, ...rest } = r;
|
|
187
|
-
|
|
191
|
+
if (!args[0] || !flags.get('out')) return { ok: false, command: 'assets optimise', why: 'usage: homie-studio assets optimise <model.glb> --out <file.glb> [--triangles 1500] [--texture 512] [--height <m>] [--lo 0.2]' };
|
|
192
|
+
const r = await optimiseModel(resolve(args[0]), { out: resolve(String(flags.get('out'))), triangles: Number(flags.get('triangles') ?? 1500), texture: Number(flags.get('texture') ?? 512), height: flags.has('height') ? Number(flags.get('height')) : null, rigged: flags.has('rigged'), ...(flags.has('lo') ? { lo: { ratio: flags.get('lo') === true ? 0.2 : Number(flags.get('lo')) } } : {}), log });
|
|
193
|
+
const { glb, lo, ...rest } = r;
|
|
194
|
+
// The low copy's bytes stay out of the answer too: its path and numbers are what a person reads.
|
|
195
|
+
return { ...rest, ...(lo !== undefined ? { lo: lo ? { path: lo.path, triangles: lo.triangles, bytes: lo.bytes, sha256: lo.sha256 } : null } : {}), command: 'assets optimise' };
|
|
188
196
|
}
|
|
189
197
|
const id = gameOf(root, args[0], `assets ${verb}`);
|
|
190
198
|
let r;
|
|
191
199
|
if (verb === 'list') {
|
|
192
200
|
const m = readManifest(root, id);
|
|
193
201
|
const doc = readDecisions(root, id);
|
|
194
|
-
r = { ok: true, command: 'assets', id, assets: m.assets.map((a) => ({ id: a.id, kind: a.kind, route: a.route, tier: a.tier ?? null, card: a.card ?? null, license: a.license?.kind ?? null,
|
|
202
|
+
r = { ok: true, command: 'assets', id, assets: m.assets.map((a) => ({ id: a.id, kind: a.kind, usage: usageOf(a), route: a.route, tier: a.tier ?? null, card: a.card ?? null, license: a.license?.kind ?? null, file: (a.files ?? []).find((f) => f.role === 'model')?.path ?? null, tris: a.measured?.tris ?? null, kb: a.measured?.glbKB ?? null, usd: +(a.made?.steps ?? []).reduce((n, s) => n + (Number(s.usd) || 0), 0).toFixed(3) })), stale: doc ? staleAssets(doc, m).map((s) => s.id) : [], spend: artSpend(root, id) };
|
|
195
203
|
} else if (verb === 'add') {
|
|
196
204
|
const rigFlags = { keep: flags.has('keep') ? list(flags.get('keep')) : null, verbs: list(flags.get('verbs')), clipsFrom: flags.get('clips-from') ?? null, texture: flags.get('texture') ?? null, triangles: flags.get('triangles') ?? null };
|
|
197
205
|
const characterKind = ['character', 'creature'].includes(String(flags.get('kind') ?? ''));
|
|
@@ -206,7 +214,7 @@ export async function assetsCommand(root, sub, positional, flags, { log = () =>
|
|
|
206
214
|
const { importModel } = await import('./asset-import.mjs');
|
|
207
215
|
let steps = [];
|
|
208
216
|
if (flags.get('steps')) steps = JSON.parse(readFileSync(resolve(String(flags.get('steps'))), 'utf8'));
|
|
209
|
-
r = await importModel(root, id, String(flags.get('file')), { as: flags.get('as') ?? args[1] ?? null, kind: flags.get('kind') ?? 'prop', tier: flags.get('tier') ?? null, card: flags.get('card') ?? null, height: flags.get('height') ?? null, triangles: flags.get('triangles') ?? null, texture: flags.get('texture') ?? null, license: flags.get('license') ?? null, attribution: flags.get('attribution') ?? null, notes: flags.get('notes') ?? null, owner: flags.get('owner') ?? null, route: flags.get('route') ?? 'imported', steps, concept: flags.get('concept') ?? null, slug: flags.get('slug') ?? null, rigged: flags.has('rigged') });
|
|
217
|
+
r = await importModel(root, id, String(flags.get('file')), { as: flags.get('as') ?? args[1] ?? null, kind: flags.get('kind') ?? 'prop', tier: flags.get('tier') ?? null, card: flags.get('card') ?? null, height: flags.get('height') ?? null, triangles: flags.get('triangles') ?? null, texture: flags.get('texture') ?? null, license: flags.get('license') ?? null, attribution: flags.get('attribution') ?? null, notes: flags.get('notes') ?? null, owner: flags.get('owner') ?? null, route: flags.get('route') ?? 'imported', steps, concept: flags.get('concept') ?? null, slug: flags.get('slug') ?? null, rigged: flags.has('rigged'), lo: flags.get('lo') && flags.get('lo') !== true ? flags.get('lo') : flags.has('lo') ? 0.2 : null });
|
|
210
218
|
} else {
|
|
211
219
|
if (!args[1]) return { ok: false, command: 'assets add', why: 'usage: homie-studio assets add <game> <library item> (assets find "<words>" lists them), or --file <model> --license <kind>' };
|
|
212
220
|
const { addFromLibrary, libraryBase, loadIndex } = await import('./library.mjs');
|
|
@@ -223,7 +231,13 @@ export async function assetsCommand(root, sub, positional, flags, { log = () =>
|
|
|
223
231
|
const { redoModel } = await import('./asset-import.mjs');
|
|
224
232
|
r = await redoModel(root, id, args[1]);
|
|
225
233
|
} else if (verb === 'check') { const { assetsCheck } = await import('./asset-check.mjs'); r = await assetsCheck(root, id, { validate: !flags.has('no-validate') }); }
|
|
226
|
-
else if (verb === 'lineup') { const { assetsLineup } = await import('./style-board.mjs'); r = await assetsLineup(root, id, { log }); }
|
|
234
|
+
else if (verb === 'lineup') { const { assetsLineup } = await import('./style-board.mjs'); r = await assetsLineup(root, id, { log, scope: flags.get('scope') ?? null }); }
|
|
235
|
+
else if (verb === 'use') {
|
|
236
|
+
// What the game does with an asset now: `assets use <id> <asset> cast|prop|environment|unused|auto`.
|
|
237
|
+
if (!args[1] || !args[2]) return { ok: false, command: 'assets use', why: `usage: homie-studio assets use <game> <asset> <${USAGES.join('|')}|auto> (unused: a file kept in the folder that the game no longer draws)` };
|
|
238
|
+
const x = setUsage(root, id, args[1], String(args[2]));
|
|
239
|
+
r = { ok: true, command: 'assets use', id, ...x };
|
|
240
|
+
}
|
|
227
241
|
else if (verb === 'review') { const { recordReview } = await import('./style-board.mjs'); r = recordReview(root, id, { score: flags.get('score'), outliers: list(flags.get('outliers')), note: flags.get('note') ?? null }); }
|
|
228
242
|
else if (verb === 'rights') { const m = readManifest(root, id); const file = writeRights(root, id, m); syncCredits(root, id, m); r = { ok: true, command: 'assets rights', id, file, text: rightsMarkdown(root, id, m), licence: licenceProblems(root, id, m) }; }
|
|
229
243
|
else if (verb === 'stale') { const doc = readDecisions(root, id); r = { ok: true, command: 'assets stale', id, stale: doc ? staleAssets(doc, readManifest(root, id)) : [] }; }
|
|
@@ -325,7 +339,7 @@ export function artLines(r) {
|
|
|
325
339
|
break;
|
|
326
340
|
case 'assets':
|
|
327
341
|
L.push(`${r.id}: ${r.assets.length} asset${r.assets.length === 1 ? '' : 's'}${r.spend.used ? `, US$${r.spend.used} spent${r.spend.cap !== null ? ` of US$${r.spend.cap}` : ''}` : ', US$0 spent'}`);
|
|
328
|
-
for (const a of r.assets) L.push(` ${a.id.padEnd(20)} ${a.kind.padEnd(9)} ${a.route.padEnd(10)} ${String(a.license ?? 'NO LICENCE').padEnd(10)} ${a.tris ?? '?'} tris, ${a.kb ?? '?'} KB${a.usd ? `, US$${a.usd}` : ''}${r.stale.includes(a.id) ? ' STALE' : ''}`);
|
|
342
|
+
for (const a of r.assets) L.push(` ${a.id.padEnd(20)} ${a.kind.padEnd(9)} ${a.route.padEnd(10)} ${String(a.license ?? 'NO LICENCE').padEnd(10)} ${a.tris ?? '?'} tris, ${a.kb ?? '?'} KB${a.usd ? `, US$${a.usd}` : ''}${a.usage === 'unused' ? ' unused' : ''}${r.stale.includes(a.id) ? ' STALE' : ''}`);
|
|
329
343
|
break;
|
|
330
344
|
case 'assets find':
|
|
331
345
|
L.push(`${r.items.length} of ${r.total} library items for "${r.query}" (CC0, free; ${r.library}):`);
|
|
@@ -344,9 +358,19 @@ export function artLines(r) {
|
|
|
344
358
|
break;
|
|
345
359
|
case 'assets lineup':
|
|
346
360
|
if (r.ok === false) { L.push(r.why); break; }
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
361
|
+
// Three different questions, never one picture: everything in the folder, what is in play, and the scenery.
|
|
362
|
+
L.push(`Inventory lineup of ${r.rows.length} asset${r.rows.length === 1 ? '' : 's'} (every recorded model; ${r.flagged} flagged${r.unused?.length ? `; ${r.unused.length} marked unused, which the game does not draw` : ''}), pictures: ${r.images.front}, ${r.images.quarter}, ${r.images.silhouettes}`);
|
|
363
|
+
for (const [name, what] of [['cast', 'what is in play (characters, creatures, props)'], ['environment', 'the scenery']]) {
|
|
364
|
+
const l = r.lineups?.[name];
|
|
365
|
+
if (l?.images) L.push(`${name[0].toUpperCase()}${name.slice(1)} lineup of ${l.ids.length} (${what}): ${l.images.front}, ${l.images.quarter}, ${l.images.silhouettes}`);
|
|
366
|
+
else if (l?.same) L.push(`${name[0].toUpperCase()}${name.slice(1)} lineup: the same ${l.ids.length} as the inventory (no separate picture).`);
|
|
367
|
+
}
|
|
368
|
+
for (const x of r.rows) L.push(` ${x.flags.length ? 'FLAG' : 'ok '} ${x.id} [${x.usage}${x.usageExplicit ? '' : ', by its kind'}]${x.size ? ` (${x.size[1]} m)` : ''}${x.drift ? (x.drift.repaint ? ', repainted from style.json in the game' : `, palette distance ${x.drift.mean}`) : ''}${x.flags.length ? `: ${x.flags.join('; ')}` : ''}`);
|
|
369
|
+
if (!r.rows.some((x) => x.usageExplicit)) L.push(' Usage is read from each asset\'s kind. An asset the game no longer draws: homie-studio assets use <id> <asset> unused (it leaves the cast and environment lineups and the scene estimate).');
|
|
370
|
+
L.push(' Next: a fresh reviewer scores "one game?" from the board, golden images and the CAST and ENVIRONMENT lineups (what the game draws); the inventory lineup is the folder, not the game (assets review <id> --score <n>).');
|
|
371
|
+
break;
|
|
372
|
+
case 'assets use':
|
|
373
|
+
L.push(`${r.asset}: ${r.usage}${r.explicit ? '' : ' (by its kind)'}.${r.usage === 'unused' ? ' It is left out of the scene estimate and of the cast and environment lineups; it still ships until it is removed (assets remove).' : ''}`);
|
|
350
374
|
break;
|
|
351
375
|
case 'assets review':
|
|
352
376
|
L.push(`Review: ${r.review.score}/10, ${r.review.verdict}${r.review.outliers.length ? `; needs review: ${r.review.outliers.join(', ')}` : ''}`);
|
|
@@ -372,12 +396,14 @@ export function artLines(r) {
|
|
|
372
396
|
out.push(` ${row.id} (${row.familyLabel}, ${row.skeleton}, ${row.bones} bones${row.animsKB ? `, its clip library ${row.animsKB} KB` : ''})`);
|
|
373
397
|
for (const c of row.clips) out.push(` ${c.have ? 'ok ' : 'MISS'} ${c.verb}${c.have ? ` <- ${c.source}${c.retargeted ? ` (retargeted from ${c.from})` : ''}` : ''}`);
|
|
374
398
|
if (row.extra.length) out.push(` also: ${row.extra.join(', ')}`);
|
|
399
|
+
if (row.supplemental?.length) out.push(` with the supplemental librar${row.supplemental.length === 1 ? 'y' : 'ies'} ${row.supplemental.join(', ')} (the game loads ${row.supplemental.length === 1 ? 'it' : 'them'} itself: loadCharacter(models, url, { anims: [...] }))`);
|
|
400
|
+
out.push(...gaitLines(row.gait).map((l) => ` ${l}`));
|
|
375
401
|
}
|
|
376
402
|
for (const u of r.unrigged) out.push(` ${u}: no rig (a static model; the animate guide says how to rig one)`);
|
|
377
403
|
return out;
|
|
378
404
|
}
|
|
379
405
|
case 'anim add': return [`${r.asset}: ${r.added.length ? `added ${r.added.join(', ')}` : 'nothing new'} (its skeleton ${r.skeleton}: ${r.verbs.join(', ')}${r.kb ? `; ${r.kb} KB` : ''})${r.missing.length ? `; no source has ${r.missing.join(', ')}` : ''}${r.why ? ` (${r.why})` : ''}`];
|
|
380
|
-
case 'anim preview': return [`Previews in .studio/art/${r.game}/anim/:`, ...r.rows.
|
|
406
|
+
case 'anim preview': return [`Previews in .studio/art/${r.game}/anim/:`, ...r.rows.flatMap((row) => [` ${row.id}: ${row.verbs.map((v) => v.verb).join(', ')}${row.sheet ? ` (sheet ${row.sheet})` : ''}${row.why ? ` (${row.why})` : ''}`, ...gaitLines(row.gait).map((l) => ` ${l}`)])];
|
|
381
407
|
case 'assets check':
|
|
382
408
|
L.push(...checkLines(r));
|
|
383
409
|
break;
|
|
@@ -387,6 +413,23 @@ export function artLines(r) {
|
|
|
387
413
|
return L;
|
|
388
414
|
}
|
|
389
415
|
|
|
416
|
+
/**
|
|
417
|
+
* A character's measured ground speed, in words: what its walk and run cover on THIS rig beside animate's defaults,
|
|
418
|
+
* and what to do when they differ. With nothing measured, it says the default is unverified rather than nothing.
|
|
419
|
+
*/
|
|
420
|
+
export function gaitLines(gait) {
|
|
421
|
+
if (!gait || (!gait.walk && !gait.run)) return ['ground speed: not measured on this rig (no walk or run clip, or no foot bones), so animate\'s default walkSpeed 1.5 and runSpeed 4.2 m/s are unverified for it: look at its feet in the game'];
|
|
422
|
+
const out = [];
|
|
423
|
+
for (const [verb, key] of [['walk', 'walkSpeed'], ['run', 'runSpeed']]) {
|
|
424
|
+
const g = gait[verb];
|
|
425
|
+
if (!g) continue;
|
|
426
|
+
const off = Math.abs(g.mps / g.default - 1) > 0.2;
|
|
427
|
+
out.push(`${verb}: its feet cover about ${g.mps} m/s on this rig (animate's default ${key} is ${g.default})${off ? `: its feet will slide at the default; start from loadCharacter(models, url, { tune: { ${key}: ${g.mps} } })` : ''}`);
|
|
428
|
+
}
|
|
429
|
+
out.push('(a guide from the foot bones while they are down, not a verified foot contact)');
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
|
|
390
433
|
export function blastLines(b) {
|
|
391
434
|
const L = [`Changing the ${b.name} (${b.from ?? '?'}${b.to ? ` -> ${b.to}` : ''}): ${b.assets.length} asset${b.assets.length === 1 ? '' : 's'} would go stale${b.assets.length ? `; remaking the ones with no free fix costs about US$${b.totals.usd.toFixed(2)}` : ''}.`];
|
|
392
435
|
for (const a of b.assets) L.push(` ${a.id} (${a.route}${a.card ? `, ${a.card}` : ''}): ${a.free ? `free: ${a.free}` : a.remake.usd ? `US$${a.remake.usd.toFixed(2)} (${a.remake.how})` : a.remake.how}`);
|
package/lib/art-tools.mjs
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* asset_add a library item, or a file with its licence, into the game (checked, optimised, recorded)
|
|
12
12
|
* asset_make one generated prop on the person's own fal account: a priced dry run unless approve; the
|
|
13
13
|
* concept first, then (after looking) the mesh lineup card
|
|
14
|
-
* asset_check phone budgets, the validator, licences, staleness
|
|
14
|
+
* asset_check phone budgets, the validator, licences, staleness: an inventory estimate, never a measured frame
|
|
15
|
+
* collision_bake a model's walkable surface as a height grid beside it, with a checksum and a true-scale plan
|
|
16
|
+
* collision_check every collision file: stale against its model, its proxies, its routes walked by a bot
|
|
15
17
|
* asset_lineup true scale, silhouettes, palette drift, flags lineup card
|
|
16
18
|
* asset_rights RIGHTS.md and every asset's licence rights card
|
|
17
19
|
* cast_plan the characters: proportions, silhouette, palette, skeleton family, source, clips, cost cast card
|
|
@@ -346,7 +348,7 @@ export function artToolDefs(ctx, h) {
|
|
|
346
348
|
},
|
|
347
349
|
{
|
|
348
350
|
name: 'asset_check', title: 'Check the assets',
|
|
349
|
-
description: 'Every asset of a game against the phone budgets
|
|
351
|
+
description: 'Every asset of a game against the phone budgets, the Khronos glTF-Validator, its licence record, and whether a decision it was made under has changed; standalone textures and skies measured (size, hash, decode, mipmapped memory); big files in git. Its scene numbers are an INVENTORY ESTIMATE (the recorded files\' own triangles, draw calls and pictures times their declared placements): it never ran the game, so procedural geometry, repeated instances, effects and shadow passes are not in it, and a pass is not a measured frame (playtest and perf measure the running game). "Shipped payload" is every built file gzipped, not a measured first-play download. Free, a few seconds.',
|
|
350
352
|
inputSchema: { type: 'object', properties: { ...GAME } },
|
|
351
353
|
annotations: { title: 'Check the assets', ...RO },
|
|
352
354
|
run: async (a) => {
|
|
@@ -358,32 +360,73 @@ export function artToolDefs(ctx, h) {
|
|
|
358
360
|
const x = r.result;
|
|
359
361
|
if (!x?.rows) return fail(`The check did not run: ${whyOf(r.job)}`);
|
|
360
362
|
const { checkLines } = await import('./asset-check.mjs');
|
|
361
|
-
return ok(checkLines(x).join('\n'), { kind: 'check', ...x });
|
|
363
|
+
return ok(checkLines(x).join('\n'), { kind: 'check', ...x, about: { scope: 'An inventory estimate from the recorded files. Not a measurement of the running game.', totals: { drawCalls: 'draw calls of the recorded models times their declared placements (estimate)', triangles: 'triangles of the recorded models times their declared placements (estimate)', textureMB: 'mipmapped RGBA memory of every recorded picture, each unique picture once (estimate; maps declared unused are left out)', standaloneTextureMB: 'the part of textureMB that is standalone texture and sky records', unusedMapMB: 'normal and roughness maps left out because the manifest declares base colour only', shippedPayloadMB: 'every file of the last build, gzipped: what ships, not what a browser fetched before the first round', firstPlayMB: 'the old name of shippedPayloadMB (the same number); it never was a measured first-play download' } } });
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
// A model that can be drawn is not yet one that can be stood on. The two collision commands, for a creator who
|
|
367
|
+
// works in chat and never types one (lib/collision.mjs: the same code, through the studio's own CLI).
|
|
368
|
+
{
|
|
369
|
+
name: 'collision_bake', title: 'Make a model walkable',
|
|
370
|
+
description: 'Bake a model\'s walkable surface: a height grid read off its triangles, written beside it as <model>.collision.json with a checksum of the mesh and a true-scale plan (<model>.collision.svg, 40 px to the metre). Use it after asset_add or asset_make for anything figures stand or walk on (a landmark, a bridge, a deck): without it they fall through. Authored "proxies" (boxes, cylinders, ramps to walk around), "mover" (the body that must fit) and "routes" (ways a bot should be able to walk) in the JSON are kept by a re-bake; edit them with file_edit, then collision_check. Free, local. The game loads the file with @homie-rocks/heightfield (unpackBake, bakeField).',
|
|
371
|
+
inputSchema: { type: 'object', properties: { ...GAME, model: str('The model, as a path inside the game, e.g. assets/models/pier.glb'), cell: { type: 'number', description: 'Metres between grid posts (default 0.25, which suits a walkway)' }, side: str('top (default: the upper surface) or under', { enum: ['top', 'under'] }), maxY: { type: 'number', description: 'Optional: ignore triangles above this height in metres (a roof over a floor)' } }, required: ['model'] },
|
|
372
|
+
annotations: { title: 'Make a model walkable', ...RW },
|
|
373
|
+
run: async (a) => {
|
|
374
|
+
const root = ctx.root(a.studio);
|
|
375
|
+
const need = needsInstall(ctx, root); if (need) return need;
|
|
376
|
+
const game = gameOf(root, a.game);
|
|
377
|
+
if (!a.model) return fail('model: the .glb to bake, as a path inside the game');
|
|
378
|
+
const r = await cli(ctx, root, `collision bake ${game}`, ['collision', 'bake', game, String(a.model), ...(a.cell ? ['--cell', String(a.cell)] : []), ...(a.side ? ['--side', String(a.side)] : []), ...(a.maxY !== undefined ? ['--max-y', String(a.maxY)] : [])]);
|
|
379
|
+
if (!r.ended) return stillRunning(r.job, 'The bake');
|
|
380
|
+
const x = r.result;
|
|
381
|
+
if (!x?.ok) return fail(`Not baked: ${x?.why ?? whyOf(r.job)}${x?.instead ? ` (${x.instead})` : ''}`);
|
|
382
|
+
return ok(x.lines.join('\n'), { kind: 'collision', ...x });
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: 'collision_check', title: 'Check where figures can walk',
|
|
387
|
+
description: 'The gate for a game\'s collision files (every <model>.collision.json, or one model\'s): the model is still the one that was baked (a changed surface is "stale: bake it again", by name; a re-export with the same triangles is not), the authored proxies are well formed, and a body of the mover\'s size passes every authored route and walks it to its end. Each failure names the place. Redraws each plan (.collision.svg) with a mark where a route fails. Run it after a model changes and before a deploy. Free, local.',
|
|
388
|
+
inputSchema: { type: 'object', properties: { ...GAME, model: str('Optional: only this model\'s collision file') } },
|
|
389
|
+
annotations: { title: 'Check where figures can walk', ...RW },
|
|
390
|
+
run: async (a) => {
|
|
391
|
+
const root = ctx.root(a.studio);
|
|
392
|
+
const need = needsInstall(ctx, root); if (need) return need;
|
|
393
|
+
const game = gameOf(root, a.game);
|
|
394
|
+
const r = await cli(ctx, root, `collision check ${game}`, ['collision', 'check', game, ...(a.model ? [String(a.model)] : [])]);
|
|
395
|
+
if (!r.ended) return stillRunning(r.job, 'The collision check');
|
|
396
|
+
const x = r.result;
|
|
397
|
+
if (!x?.models) return fail(`The check did not run: ${x?.why ?? whyOf(r.job)}${x?.instead ? ` (${x.instead})` : ''}`);
|
|
398
|
+
const text = x.lines.join('\n');
|
|
399
|
+
return x.ok ? ok(text, { kind: 'collision', ...x }) : fail(`${text}\n${x.models.filter((m) => !m.ok).length} of ${x.models.length} collision file(s) need attention.`);
|
|
362
400
|
},
|
|
363
401
|
},
|
|
364
402
|
{
|
|
365
403
|
name: 'asset_lineup', title: 'Lineup',
|
|
366
|
-
description: 'The made assets of a game side by side at true scale on a 1 m grid under the game\'s light (front and three-quarter), their silhouettes at phone size, and flags: over budget, palette drift from the locked palette, an unreadable silhouette, another library family, stale.
|
|
367
|
-
inputSchema: { type: 'object', properties: { ...GAME } },
|
|
404
|
+
description: 'The made assets of a game side by side at true scale on a 1 m grid under the game\'s light (front and three-quarter), their silhouettes at phone size, and flags: over budget, palette drift from the locked palette, an unreadable silhouette, another library family, stale. THREE lineups, never one: the inventory (every recorded model, including any the game no longer draws), the cast (what is in play: characters, creatures, props) and the environment (scenery). A reviewer judging "does it look like one game?" looks at the cast and environment lineups; the inventory is the folder. use marks what the game does with an asset now ({ "old-tree": "unused" }: it leaves the cast and environment lineups and the scene estimate). Free.',
|
|
405
|
+
inputSchema: { type: 'object', properties: { ...GAME, scope: str('Optional: draw only the cast or the environment lineup beside the inventory', { enum: ['inventory', 'cast', 'environment'] }), use: { type: 'object', description: 'Optional, before drawing: what the game does with an asset now, { "<asset id>": "cast" | "prop" | "environment" | "unused" | "auto" } (only when the person or the game\'s code says so)' } } },
|
|
368
406
|
annotations: { title: 'Lineup', ...RO }, _meta: ui(ART_UI.lineup),
|
|
369
407
|
run: async (a) => {
|
|
370
408
|
const root = ctx.root(a.studio);
|
|
371
409
|
const need = needsInstall(ctx, root); if (need) return need;
|
|
372
410
|
const game = gameOf(root, a.game);
|
|
373
|
-
|
|
411
|
+
if (a.use && typeof a.use === 'object') {
|
|
412
|
+
const { setUsage } = await import('./asset-manifest.mjs');
|
|
413
|
+
try { for (const [asset, usage] of Object.entries(a.use)) setUsage(root, game, String(asset), String(usage)); } catch (error) { return fail(error.message); }
|
|
414
|
+
}
|
|
415
|
+
const r = await cli(ctx, root, `assets lineup ${game}`, ['assets', 'lineup', game, ...(a.scope ? ['--scope', String(a.scope)] : [])]);
|
|
374
416
|
if (!r.ended) return stillRunning(r.job, 'The lineup');
|
|
375
417
|
const x = r.result;
|
|
376
418
|
if (!x?.rows) return fail(x?.why ?? `The lineup was not drawn: ${whyOf(r.job)}`);
|
|
377
419
|
writeArtSummary(root, game);
|
|
378
420
|
let check = null;
|
|
379
421
|
try { check = JSON.parse(readFileSync(join(root, '.studio', 'art', game, 'check.json'), 'utf8')); } catch { check = null; }
|
|
380
|
-
const data = { kind: 'lineup', mode: 'lineup', game, rows: x.rows, flagged: x.flagged, palette: x.palette, family: x.family, images: { front: picture(h, join(root, x.images.front), 220 * 1024), quarter: picture(h, join(root, x.images.quarter), 220 * 1024), silhouettes: picture(h, join(root, x.images.silhouettes), 80 * 1024) }, files: x.images, totals: check?.totals ?? null, budgets: check?.budgets ?? null, spend: artSpend(root, game) };
|
|
381
|
-
|
|
422
|
+
const data = { kind: 'lineup', mode: 'lineup', game, rows: x.rows, lineups: x.lineups ?? null, unused: x.unused ?? [], flagged: x.flagged, palette: x.palette, family: x.family, images: { front: picture(h, join(root, x.images.front), 220 * 1024), quarter: picture(h, join(root, x.images.quarter), 220 * 1024), silhouettes: picture(h, join(root, x.images.silhouettes), 80 * 1024) }, files: x.images, totals: check?.totals ?? null, budgets: check?.budgets ?? null, spend: artSpend(root, game) };
|
|
423
|
+
const scoped = ['cast', 'environment'].map((n) => (x.lineups?.[n]?.images ? `${n} lineup (${x.lineups[n].ids.length}): ${x.lineups[n].images.front}, ${x.lineups[n].images.quarter}` : x.lineups?.[n]?.same ? `${n} lineup: the same models as the inventory` : null)).filter(Boolean);
|
|
424
|
+
return ok([`Inventory lineup of ${x.rows.length} asset(s): every recorded model, ${x.flagged} flagged${x.unused?.length ? `, ${x.unused.length} marked unused (not drawn by the game: ${x.unused.join(', ')})` : ''}. Pictures: ${x.images.front}, ${x.images.quarter} (file_read shows them).`, ...scoped, 'A review of how the GAME looks reads the cast and environment lineups; the inventory is the folder.', ...x.rows.map((row) => ` ${row.flags.length ? 'FLAG' : 'ok'} ${row.id} [${row.usage ?? '?'}]${row.size ? ` ${row.size[1]} m` : ''}${row.flags.length ? `: ${row.flags.join('; ')}` : ''}`)].join('\n'), data);
|
|
382
425
|
},
|
|
383
426
|
},
|
|
384
427
|
{
|
|
385
428
|
name: 'asset_rights', title: 'Rights',
|
|
386
|
-
description: 'Every asset\'s licence in plain words (what it is, where it came from, what the licence allows, any credit owed
|
|
429
|
+
description: 'Every asset\'s licence in plain words (what it is, where it came from, what the licence allows, any credit owed) and what the providers\' terms say; writes RIGHTS.md and the credits. Anything that would stop a public game from publishing is named with its fix.',
|
|
387
430
|
inputSchema: { type: 'object', properties: { ...GAME } },
|
|
388
431
|
annotations: { title: 'Rights', ...RW }, _meta: ui(ART_UI.rights),
|
|
389
432
|
run: async (a) => {
|
|
@@ -395,7 +438,7 @@ export function artToolDefs(ctx, h) {
|
|
|
395
438
|
const x = r.result;
|
|
396
439
|
if (!x?.text) return fail(`Not written: ${whyOf(r.job)}`);
|
|
397
440
|
const m = readManifest(root, game);
|
|
398
|
-
const rows = m.assets.map((e) => ({ id: e.id, kind: e.kind, route: e.route, license: e.license?.kind ?? null,
|
|
441
|
+
const rows = m.assets.map((e) => ({ id: e.id, kind: e.kind, route: e.route, license: e.license?.kind ?? null, attribution: e.license?.attribution ?? null, from: e.from?.pack ?? e.from?.item ?? null, placeholder: Boolean(e.placeholder) }));
|
|
399
442
|
return ok(`${x.file}:\n\n${x.text.slice(0, 6000)}`, { kind: 'rights', game, file: x.file, rows, problems: x.licence, text: x.text.slice(0, 20_000) });
|
|
400
443
|
},
|
|
401
444
|
},
|