@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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GAME PARTS AS CHAT TOOLS (parts/PARTS.md): parts_find, part_add, part_new and part_share, named and worded like
|
|
3
|
+
* the asset tools beside them (assets_find, asset_add). Four tools are the whole surface: a newer version of a part
|
|
4
|
+
* is part_add again, stopping is part_share with share false, and checking is part_share with nothing to change.
|
|
5
|
+
*
|
|
6
|
+
* The person never types a command: every result here says what happened and what to do next in words, and names a
|
|
7
|
+
* tool, never a terminal line. Tests pass `ctx.partsNet = { fetch, npm }` so nothing reaches the network or npm.
|
|
8
|
+
*/
|
|
9
|
+
import { PART_KINDS, newPart, partDir, readPart, writePart } from './parts.mjs';
|
|
10
|
+
import { addPart, checkParts, findParts, sharePart } from './parts-store.mjs';
|
|
11
|
+
import { partsLines } from './parts-cli.mjs';
|
|
12
|
+
|
|
13
|
+
/** What the planning and game-making tools say about parts, in one place so a test can hold them to it. */
|
|
14
|
+
export const PARTS_FIRST = 'Before writing any system from scratch (camera, movement, bots, pickups, effects, sound, UI, environment), look for what exists, two different things: (1) the @homie-rocks/* packages on npm have the general mechanism (camera, input, audio, effects): use them; (2) `parts find` (the parts_find tool) searches the pieces of real games other studios shared: a creature from one game, a level from another, a bot brain from a third, each brought in with part_add. Record in the game\'s CODEX what came from where: which packages, which parts and the game and studio each came from, and what was written here and why. After building a piece another game could use, offer the person to make it a part (part_new lifts it out of the game) and, only if they ask, to share it (part_share).';
|
|
15
|
+
|
|
16
|
+
/** A fix-it line that names the plumbing, said as the chat tool that does the same. */
|
|
17
|
+
export const inChat = (t) => String(t).replace(/homie-studio parts add (\S+?)(?:@(\S+))?(?: --game (\S+))?(?=[\s).,;]|$)/g, (m, ref, v, g) => `part_add { "part": "${ref}${v ? `@${v}` : ''}"${g ? `, "game": "${g}"` : ''} }`);
|
|
18
|
+
const text = (r) => inChat(partsLines(r).join('\n'));
|
|
19
|
+
|
|
20
|
+
export function partsToolDefs(ctx, h) {
|
|
21
|
+
const { ok, fail, str, STUDIO_ARG, RO, RW } = h;
|
|
22
|
+
const net = () => ctx.partsNet ?? {};
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
name: 'parts_find', title: 'Find game parts',
|
|
26
|
+
description: 'Search the pieces of real games that studios shared (a creature, a level generator, a chase camera, a bot brain, a pickup mechanic, an audio pack), and this studio\'s own parts. The first move when planning or building a game: run it for each system the game needs before writing one from scratch, so a new game can be made with parts from several games. Each result has its licence, the game and studio it came from, its costs, what it builds on and what part_add takes. If the catalogue cannot be reached it says so (that is not "nothing exists"). Homie\'s @homie-rocks/* packages are not parts and are not listed here: they come from npm.',
|
|
27
|
+
inputSchema: { type: 'object', properties: { query: str('Words for what the game needs: "chase camera", "pickups", "cave"'), kind: { type: 'string', enum: [...PART_KINDS], description: 'Only this kind of part' }, tag: str('Only parts with this tag'), license: str('Only this licence (an SPDX identifier, e.g. CC0-1.0)'), builds: str('Only parts that build on this: a package ("@homie-rocks/camera") or a skeleton ("humanoid-v1")'), ...STUDIO_ARG } },
|
|
28
|
+
annotations: { title: 'Find game parts', ...RO, openWorldHint: true },
|
|
29
|
+
run: async (a) => {
|
|
30
|
+
const root = ctx.root(a.studio, { need: false });
|
|
31
|
+
const r = await findParts(root, String(a.query ?? ''), { kind: a.kind ?? null, tag: a.tag ?? null, license: a.license ?? null, builds: a.builds ?? null, fetch: net().fetch, hub: ctx.directory ?? null });
|
|
32
|
+
return ok([text(r), '', r.results.length ? 'Tell the person what you found and which game each piece is from, with your pick; part_add brings one in. Write in the game\'s CODEX which parts it uses and what you wrote yourself and why.' : 'Nothing fits: write it, and say so in the game\'s CODEX. If it turns out reusable, offer to make it a part (part_new).'].join('\n'), { kind: 'parts', ...r });
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'part_add', title: 'Add a game part',
|
|
37
|
+
description: 'Bring a part another studio shared into this studio, for a game: one exact version is fetched (the latest unless the part names one, "<studio site>/<part id>@1.2.0"), every file is checked against its SHA-256, and it is copied into parts/ where the studio owns and tunes it. The game credits it (its licence and who made it, on the game\'s credits page), and the packages it builds on are installed by npm. For a part that is already here it brings the newer version: what changed is shown, the studio\'s tuning is kept, and a file the studio edited is never replaced unless overwrite is true (ask the person first). The result says plainly when the licences of the game\'s parts cannot be combined, and gives the one import line.',
|
|
38
|
+
inputSchema: { type: 'object', properties: { part: str('"<studio site>/<part id>", as parts_find gives it; "@<version>" for one version'), game: str('The game that will use it (its credits get the line)'), overwrite: { type: 'boolean', description: 'For a newer version: also replace files this studio edited. Only after the person agreed' }, ...STUDIO_ARG }, required: ['part'] },
|
|
39
|
+
annotations: { title: 'Add a game part', ...RW, openWorldHint: true },
|
|
40
|
+
run: async (a) => {
|
|
41
|
+
const root = ctx.root(a.studio);
|
|
42
|
+
const said = [];
|
|
43
|
+
const r = await addPart(root, String(a.part ?? ''), { game: a.game ? String(a.game) : null, overwrite: a.overwrite === true, say: (l) => said.push(l), ...net() });
|
|
44
|
+
if (!r.ok) return fail(`Not added: ${inChat(r.why)}`, { kind: 'part', ...r });
|
|
45
|
+
const next = [];
|
|
46
|
+
if (r.import) next.push(`In the game: import … from '${r.import}', then build. Its part.json says what it takes ("contract") and its tuning.json holds the values to tune.`);
|
|
47
|
+
for (const n of r.needs) next.push(`It needs another part, ${n.ref}: part_add { "part": "${n.ref}"${a.game ? `, "game": "${a.game}"` : ''} }.`);
|
|
48
|
+
if (r.licences.some((c) => c.level === 'conflict')) next.push('The licences cannot be combined (above): tell the person plainly before building on it.');
|
|
49
|
+
if (r.packages.some((p) => p.state === 'pinned' || p.state === 'failed')) next.push('A package it builds on is not in place (above): the part will not build until it is.');
|
|
50
|
+
return ok([...said, text(r), ...next, 'Write in the game\'s CODEX that it uses this part and which game and studio it came from.'].join('\n'), { kind: 'part', ...r });
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'part_new', title: 'Make a game part',
|
|
55
|
+
description: 'Make a part of this studio: a piece of one of its games that another game can use. With from (a game\'s id) and files (paths inside that game: a module or a folder), the files are LIFTED out of the game into parts/<id>/ and the game imports them from there, so it builds and behaves as before; a file that still reaches into the rest of the game is refused by name (pass what it needs in as an argument first). Without from, an empty part to write. A part is a piece of a game, never the whole game, in whatever form suits it (code, assets, data, a tuned config). A new part is private. Offer this after building something reusable.',
|
|
56
|
+
inputSchema: { type: 'object', properties: { id: str('The part\'s id (lowercase, digits, hyphens): "chase-camera"'), kind: { type: 'string', enum: [...PART_KINDS], description: 'What kind of part (default mechanic)' }, name: str('Its name'), from: str('The game to lift it out of'), files: { type: 'array', items: { type: 'string' }, description: 'With from: paths inside games/<from>/ that become the part ("src/creature.ts", "src/cave")' }, ...STUDIO_ARG }, required: ['id'] },
|
|
57
|
+
annotations: { title: 'Make a game part', ...RW },
|
|
58
|
+
run: async (a) => {
|
|
59
|
+
const root = ctx.root(a.studio);
|
|
60
|
+
const r = newPart(root, String(a.id ?? ''), { kind: a.kind ?? 'mechanic', name: a.name ?? null, from: a.from ? String(a.from) : null, paths: Array.isArray(a.files) ? a.files.map(String) : [] });
|
|
61
|
+
if (!r.ok) return fail(`No part was made: ${r.why}`, { kind: 'part', ...r });
|
|
62
|
+
return ok([text(r), r.lifted ? `Build ${r.from} now and check it still plays the same.` : '', 'Fill in parts/' + r.id + '/part.json: a one-sentence summary, what it takes from a game ("contract"), and its costs if measured. It stays private until the person asks to share it (part_share).'].filter(Boolean).join('\n'), { kind: 'part', ...r });
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'part_share', title: 'Share a game part',
|
|
67
|
+
description: 'Share one of this studio\'s parts with other studios, or stop sharing it. Only when the person asked: a part is private until they say otherwise. It needs a licence the person picked (an SPDX identifier: CC0-1.0, CC-BY-4.0, MIT…; with license it is set here), who to credit when the licence asks, and known rights to every file: anything in the way is listed in plain words and nothing is shared. It is live after the studio\'s next deploy (studio_deploy), not before; stopping takes effect then too. With share left out it only checks and reports.',
|
|
68
|
+
inputSchema: { type: 'object', properties: { id: str('The part\'s id'), share: { type: 'boolean', description: 'true: share it; false: stop sharing it; left out: only check what stands in the way' }, license: str('The licence the person picked, an SPDX identifier (set before sharing)'), attribution: str('Who a game using it should credit'), ...STUDIO_ARG }, required: ['id'] },
|
|
69
|
+
annotations: { title: 'Share a game part', ...RW },
|
|
70
|
+
run: async (a) => {
|
|
71
|
+
const root = ctx.root(a.studio);
|
|
72
|
+
const id = String(a.id ?? '');
|
|
73
|
+
if (a.license !== undefined || a.attribution !== undefined) {
|
|
74
|
+
let part;
|
|
75
|
+
try { part = readPart(partDir(root, id)); } catch { return fail(`There is no part "${id}" in this studio.`); }
|
|
76
|
+
if (a.license !== undefined) part.license = String(a.license).trim();
|
|
77
|
+
if (a.attribution !== undefined) part.attribution = String(a.attribution).slice(0, 200);
|
|
78
|
+
writePart(partDir(root, id), part);
|
|
79
|
+
}
|
|
80
|
+
if (a.share === undefined) {
|
|
81
|
+
const r = checkParts(root, id, { write: true });
|
|
82
|
+
if (!r.rows?.length) return fail(r.why ?? `There is no part "${id}" in this studio.`);
|
|
83
|
+
const row = r.rows[0];
|
|
84
|
+
return ok([text(r), row.shareable ? 'Nothing stands in the way. It is still private: share it only when the person asks (part_share with share: true).' : 'It cannot be shared until those are settled. It is private.'].join('\n'), { kind: 'part', ...r });
|
|
85
|
+
}
|
|
86
|
+
const r = sharePart(root, id, a.share === true);
|
|
87
|
+
if (!r.ok) return fail(`Not shared: ${inChat(r.why)}`, { kind: 'part', ...r });
|
|
88
|
+
return ok([text(r), a.share === true ? 'Tell the person it is live after the studio\'s next deploy (studio_deploy), and under which licence.' : ''].filter(Boolean).join('\n'), { kind: 'part', ...r });
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|