@homie-rocks/studio 0.48.4 → 0.50.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 +46 -0
- package/app-starters/gathering/README.md +4 -0
- package/app-starters/gathering/app.json +162 -0
- package/app-starters/gathering/index.html +3 -0
- package/app-starters/gathering/map/main.json +1 -0
- package/app-starters/gathering/src/move.ts +3 -0
- package/app-starters/gathering/src/rules.ts +21 -0
- package/app-starters/gathering/src/view.ts +74 -0
- package/app-starters/giving/README.md +5 -0
- package/app-starters/giving/app.json +162 -0
- package/app-starters/giving/index.html +3 -0
- package/app-starters/giving/map/main.json +1 -0
- package/app-starters/giving/src/move.ts +3 -0
- package/app-starters/giving/src/rules.ts +21 -0
- package/app-starters/giving/src/view.ts +74 -0
- package/app-starters/jobs/README.md +4 -0
- package/app-starters/jobs/app.json +162 -0
- package/app-starters/jobs/index.html +3 -0
- package/app-starters/jobs/map/main.json +1 -0
- package/app-starters/jobs/src/move.ts +3 -0
- package/app-starters/jobs/src/rules.ts +21 -0
- package/app-starters/jobs/src/view.ts +74 -0
- package/app-starters/quiz/README.md +4 -0
- package/app-starters/quiz/app.json +162 -0
- package/app-starters/quiz/index.html +3 -0
- package/app-starters/quiz/map/main.json +1 -0
- package/app-starters/quiz/src/move.ts +3 -0
- package/app-starters/quiz/src/rules.ts +21 -0
- package/app-starters/quiz/src/view.ts +74 -0
- package/app-starters/welcome/app.json +25 -2
- package/app-starters/welcome/index.html +1 -1
- package/app-starters/welcome/src/view.ts +20 -8
- package/bin/homie-studio.mjs +20 -16
- package/functions/FUNCTIONS.md +1 -1
- package/guides/app/SKILL.md +178 -0
- package/guides/art/SKILL.md +154 -0
- package/guides/art/references/RIGHTS.md +20 -0
- package/guides/art/scripts/art.mjs +308 -0
- package/guides/models/SKILL.md +198 -0
- package/guides/models/references/RIGHTS.md +46 -0
- package/guides/models/references/models.json +175 -0
- package/guides/models/scripts/models.mjs +483 -0
- package/guides/music/SKILL.md +197 -0
- package/guides/music/references/COMPOSITION.md +59 -0
- package/guides/music/references/GAME-AUDIO.md +50 -0
- package/guides/music/references/RIGHTS.md +43 -0
- package/guides/music/scripts/lib/audio.mjs +258 -0
- package/guides/music/scripts/lib/eleven.mjs +260 -0
- package/guides/music/scripts/lib/studio.mjs +147 -0
- package/guides/music/scripts/music.mjs +644 -0
- package/guides/parts/SKILL.md +158 -0
- package/guides/plan/SKILL.md +126 -0
- package/guides/plan/references/CODEX.md +105 -0
- package/guides/plan/references/INTERVIEW.md +86 -0
- package/guides/playtest/SKILL.md +205 -0
- package/guides/playtest/references/METHOD.md +124 -0
- package/guides/playtest/references/REVIEWER.md +78 -0
- package/guides/playtest/scripts/lib/exit.mjs +31 -0
- package/guides/playtest/scripts/lib/judge.mjs +502 -0
- package/guides/playtest/scripts/lib/pixels.mjs +128 -0
- package/guides/playtest/scripts/lib/preflight.mjs +69 -0
- package/guides/playtest/scripts/playtest.mjs +870 -0
- package/guides/shop/SKILL.md +153 -0
- package/guides/sound/SKILL.md +156 -0
- package/guides/sound/references/GAME-SOUND.md +100 -0
- package/guides/sound/references/SCORE.md +131 -0
- package/guides/sound/references/TRAPS.md +93 -0
- package/guides/sound/scripts/lib/measure.mjs +252 -0
- package/guides/sound/scripts/lib/score.mjs +415 -0
- package/guides/sound/scripts/lib/sfx.mjs +222 -0
- package/guides/sound/scripts/lib/synth.mjs +509 -0
- package/guides/sound/scripts/sound.mjs +520 -0
- package/guides/studio-setup/SKILL.md +419 -0
- package/guides/style/SKILL.md +105 -0
- package/lib/app-needs.mjs +63 -0
- package/lib/authoring-guides.mjs +5 -4
- package/lib/build.mjs +5 -4
- package/lib/check-site.mjs +6 -6
- package/lib/check.mjs +18 -5
- package/lib/codex.mjs +95 -18
- package/lib/decisions.mjs +9 -7
- package/lib/dev.mjs +4 -2
- package/lib/doctor.mjs +1 -1
- package/lib/handoff.mjs +13 -8
- package/lib/mcp-tools.mjs +19 -17
- package/lib/mcp.mjs +1 -1
- package/lib/parts.mjs +6 -6
- package/lib/scaffold.mjs +45 -11
- package/lib/shop-links.mjs +2 -2
- package/lib/shop.mjs +5 -3
- package/lib/studio.mjs +7 -6
- package/lib/template-history.json +84 -0
- package/package.json +4 -4
- package/shop/SHOP.md +4 -0
- package/shop/shop.ts +4 -4
- package/worker/app-format.mjs +1 -1
- package/worker/build-id.mjs +1 -1
- package/worker/functions-schema.mjs +15 -0
- package/worker/index.mjs +2 -1
- package/worker/parts.mjs +4 -4
- package/worker/shop-page.mjs +22 -16
- package/worker/shop-rules.mjs +4 -3
- package/worker/shop.mjs +6 -2
- package/worker/site.mjs +4 -2
- package/worker/version.mjs +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
+
import app from '../app.json';
|
|
3
|
+
const words: Record<string, string> = app.words;
|
|
4
|
+
const word = (key: string, values: Record<string, string | number> = {}) => String(words[key] ?? '').replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? ''));
|
|
2
5
|
import { damp1 } from '@homie-rocks/camera/spring.js';
|
|
3
6
|
import { chamferBox } from '@homie-rocks/geom/chamfer.js';
|
|
4
7
|
import { openRoom } from '@homie-rocks/studio/rules/view';
|
|
@@ -22,23 +25,28 @@ let loaded = false;
|
|
|
22
25
|
let light = 0;
|
|
23
26
|
let refreshing = false;
|
|
24
27
|
join.hidden = role !== 'customer'; next.hidden = role !== 'staff';
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
$('h1').textContent = role === 'wall' ? words.wallTitle || app.name : role === 'staff' ? words.staffTitle || app.name : app.name;
|
|
29
|
+
$('#intro').textContent = app.blurb;
|
|
30
|
+
$('.eyebrow').textContent = words.eyebrow;
|
|
31
|
+
$('footer').textContent = words.footer;
|
|
32
|
+
join.textContent = words.joinAction; next.textContent = words.nextAction;
|
|
33
|
+
$('#waiting-word').textContent = words.waiting;
|
|
34
|
+
$('.panel').setAttribute('aria-label', words.panel);
|
|
27
35
|
function paint() {
|
|
28
36
|
const waiting = rows.filter((r) => r.data.status === 'waiting');
|
|
29
37
|
const called = rows.filter((r) => r.data.status === 'called').at(-1);
|
|
30
38
|
const own = rows.find((r) => r.id === mine);
|
|
31
39
|
$('[data-count]').textContent = String(waiting.length);
|
|
32
|
-
$('#panel-label').textContent = role === 'customer' ? (own ?
|
|
33
|
-
$('#ticket').textContent = role === 'customer' ? own ?
|
|
34
|
-
$('#detail').textContent = own?.data.status === 'called' ?
|
|
40
|
+
$('#panel-label').textContent = role === 'customer' ? (own ? words.yourTicket : words.welcome) : words.panel;
|
|
41
|
+
$('#ticket').textContent = role === 'customer' ? own ? word('ticket', { label: own.data.label }) : words.home : called ? word('ticket', { label: called.data.label }) : words.ready;
|
|
42
|
+
$('#detail').textContent = own?.data.status === 'called' ? words.called : own ? word('ahead', { n: Math.max(0, waiting.findIndex((r) => r.id === mine)) }) : role === 'customer' ? words.guest : waiting.length ? word('waitingDetail', { n: waiting.length, people: waiting.length === 1 ? words.one : words.many }) : words.empty;
|
|
35
43
|
$('.panel').classList.toggle('called', own?.data.status === 'called' || (role !== 'customer' && !!called));
|
|
36
44
|
join.hidden = role !== 'customer' || !!own;
|
|
37
45
|
join.disabled = pending || !loaded; next.disabled = pending || !waiting.length;
|
|
38
46
|
}
|
|
39
47
|
async function refresh() {
|
|
40
48
|
if (refreshing) return; refreshing = true;
|
|
41
|
-
try { rows = await records.list(); loaded = true; $('#status').textContent = ''; paint(); }
|
|
49
|
+
try { rows = await records.list(); loaded = true; net.playable(); $('#status').textContent = ''; paint(); }
|
|
42
50
|
catch (e) { $('#status').textContent = (e as Error).message; }
|
|
43
51
|
finally { refreshing = false; }
|
|
44
52
|
}
|
|
@@ -55,7 +63,7 @@ join.onclick = () => action(async () => {
|
|
|
55
63
|
// Build a customer link from a clean address, never from a private staff URL.
|
|
56
64
|
const origin = new URL((window as any).HOMIE_NET.url.replace(/^ws/, 'http')).origin;
|
|
57
65
|
const url = appLink(`/${(window as any).HOMIE_NET.url.split('/')[3]}/open`, { ticket: mine, room: (window as any).HOMIE_NET.room }, origin);
|
|
58
|
-
$('#ticket-qr').innerHTML = qrSvg(url, { title:
|
|
66
|
+
$('#ticket-qr').innerHTML = qrSvg(url, { title: words.keepTicket });
|
|
59
67
|
parent.postMessage({ type: 'homie-app-ticket', ticket: mine }, '*');
|
|
60
68
|
});
|
|
61
69
|
next.onclick = () => action(async () => {
|
|
@@ -65,11 +73,12 @@ next.onclick = () => action(async () => {
|
|
|
65
73
|
// The room carries scene state and change notices; records always come from the authorized database.
|
|
66
74
|
room.on('refresh', () => { void refresh(); });
|
|
67
75
|
net.on('link', () => { void refresh(); });
|
|
68
|
-
setInterval(() => { $('#connection').textContent = net.link === 'online' ?
|
|
76
|
+
setInterval(() => { $('#connection').textContent = net.link === 'online' ? words.live : words.reconnecting; }, 500);
|
|
69
77
|
setInterval(() => void refresh(), 2000); // recover a missed notice; no dependency on a trusted browser host
|
|
70
78
|
void refresh(); paint();
|
|
71
79
|
|
|
72
80
|
// A single sculptural scene. Queue arrivals unfold the lanterns; the camera follows your context.
|
|
81
|
+
function sceneStart() {
|
|
73
82
|
const renderer = new THREE.WebGLRenderer({ canvas: $('canvas'), antialias: true, alpha: false });
|
|
74
83
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5)); renderer.setClearColor('#122f38');
|
|
75
84
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
@@ -107,3 +116,6 @@ function draw(now: number) {
|
|
|
107
116
|
renderer.render(scene, camera);
|
|
108
117
|
}
|
|
109
118
|
requestAnimationFrame(draw);
|
|
119
|
+
|
|
120
|
+
}
|
|
121
|
+
try { sceneStart(); } catch { $('canvas').hidden = true; const fallback = document.createElement('p'); fallback.dataset.webglFallback = ''; fallback.textContent = words.fallback; $('header').append(fallback); net.playable(); }
|
package/bin/homie-studio.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* this toolkit as a local MCP server (stdio): the Claude desktop app's Homie
|
|
14
14
|
* extension, Claude Code or any MCP client builds in the same chat that shows
|
|
15
15
|
* the cards (lib/mcp.mjs); --studios: the folder the studios live in
|
|
16
|
-
* homie-studio app new <id> [--name "<App Name>"]
|
|
16
|
+
* homie-studio app new <id> [--from welcome|gathering|giving|quiz|jobs] [--name "<App Name>"]
|
|
17
17
|
* homie-studio app role <id> <role> [--player <account>] [--revoke] [--url <site>]
|
|
18
18
|
* homie-studio game new <id> [--from gem-rush] [--name "<Game Name>"]
|
|
19
19
|
* homie-studio games
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
* (what the guides' brain would decide in that moment, with no seat taken;
|
|
179
179
|
* spent from the guides' day; --model compares a model before setting it)
|
|
180
180
|
* homie-studio shop is the studio's shop selling (its own Stripe), and if not, what is missing
|
|
181
|
-
* homie-studio shop init [--supporter] [--currency
|
|
181
|
+
* homie-studio shop init [--supporter|--giving] [--currency <code>] [--price 500] [--managed] shop.json and SELLING.md
|
|
182
182
|
* homie-studio shop check shop.json against studio settings and provider requirements
|
|
183
183
|
* homie-studio shop connect [--live] [--renew] [--manual] sync Payment Links and webhook through Stripe browser approval; --manual chooses the local page for the studio's
|
|
184
184
|
* restricted Stripe key; with it this makes the webhook (0.24.3), and the key
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
* also says whether Homie's holds are on (the plugin's hooks ran just now), and
|
|
244
244
|
* when another MCP server named homie has taken the connector's name there.
|
|
245
245
|
*
|
|
246
|
-
* homie-studio codex new <id> [--name "<Name>"]
|
|
246
|
+
* homie-studio codex new <id> [--kind app|game] [--name "<Name>"]
|
|
247
247
|
* games/<id>/CODEX.md: the Game Codex, every section, in the game's colours (a game
|
|
248
248
|
* is planned before it is made: with no game <id> yet, it starts its folder)
|
|
249
249
|
* homie-studio codex <id> [--artifact] [--open]
|
|
@@ -326,7 +326,7 @@
|
|
|
326
326
|
// FIRST, before any other file of the toolkit is evaluated: a Node.js that is too old ends here, with the reason.
|
|
327
327
|
import '../lib/node-version.mjs';
|
|
328
328
|
import { spawn, spawnSync } from 'node:child_process';
|
|
329
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
329
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync } from 'node:fs';
|
|
330
330
|
import { basename, join, relative, resolve } from 'node:path';
|
|
331
331
|
import { build } from '../lib/build.mjs';
|
|
332
332
|
import { PREVIEW_PORT, previewServer } from '../lib/preview.mjs';
|
|
@@ -347,7 +347,7 @@ import { R2_COST, lineOf, mediaPlan, r2OverOf, recordUpload, resolveMedia, sizeO
|
|
|
347
347
|
import { newStudio } from '../lib/scaffold.mjs';
|
|
348
348
|
import { lineDiff, upgradeApply, upgradePlan } from '../lib/upgrade.mjs';
|
|
349
349
|
import { whatsNewLines } from '../lib/changelog.mjs';
|
|
350
|
-
import { listGames, newGame, newApp, readStudio, requireStudio, siteUrl, starters, workerDir } from '../lib/studio.mjs';
|
|
350
|
+
import { listExperiences, listGames, newGame, newApp, readStudio, requireStudio, siteUrl, starters, workerDir } from '../lib/studio.mjs';
|
|
351
351
|
import { STUDIO_VERSION } from '../lib/version.mjs';
|
|
352
352
|
import { statsKey, statsLink, statsRevoke, statsShare, statsShow } from '../lib/stats.mjs';
|
|
353
353
|
import { playersOwner, playersShow } from '../lib/players.mjs';
|
|
@@ -382,7 +382,7 @@ import { standaloneDeployNotes } from '../lib/standalone.mjs';
|
|
|
382
382
|
|
|
383
383
|
const argv = process.argv.slice(2);
|
|
384
384
|
const flags = new Map();
|
|
385
|
-
const BOOL_FLAGS = ['public', 'lan', 'off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'no-rig', 'supporter', 'managed', 'live', 'manual', 'renew', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release', 'device'];
|
|
385
|
+
const BOOL_FLAGS = ['public', 'lan', 'off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile', 'hands-on', 'automatic', 'unlock', 'confirm', 'no-library', 'no-validate', 'rigged', 'no-rig', 'supporter', 'giving', 'managed', 'live', 'manual', 'renew', 'send', 'accept-tos', 'quiet', 'no-local-ai', 'timestamps', 'overwrite', 'own-route', 'before', 'release', 'device'];
|
|
386
386
|
const positional = [];
|
|
387
387
|
for (let i = 0; i < argv.length; i++) {
|
|
388
388
|
const a = argv[i];
|
|
@@ -436,7 +436,7 @@ function print(result) {
|
|
|
436
436
|
lines.push(`Landing /${l.id}/: hero from ${l.hero === 'footage' ? 'its footage' : l.hero === 'art' ? 'its art (moving)' : 'the studio\'s colours (it has no picture or footage yet)'}${l.credits ? ', credits' : ''}`);
|
|
437
437
|
// Which files, and what shape: the names the landing looks for (lib/site.mjs HERO_NAMES). Any size is shown,
|
|
438
438
|
// cropped to fill; these are the sizes that fill a computer and an upright phone without waste.
|
|
439
|
-
if (l.hero === 'colours') lines.push(` give it one: games/${l.id}/hero/wide.jpg (16:9, 1280×720 or larger) and games/${l.id}/hero/tall.jpg (9:16, 720×1280, for an upright phone); footage beside them as hero/wide.mp4 and hero/tall.mp4 (the same shapes, a silent loop of 8 to 15 s, under 3 MB each); or "cover" in its game.json`);
|
|
439
|
+
if (l.hero === 'colours') lines.push(` give it one: ${l.kind === 'app' ? 'apps' : 'games'}/${l.id}/hero/wide.jpg (16:9, 1280×720 or larger) and ${l.kind === 'app' ? 'apps' : 'games'}/${l.id}/hero/tall.jpg (9:16, 720×1280, for an upright phone); footage beside them as hero/wide.mp4 and hero/tall.mp4 (the same shapes, a silent loop of 8 to 15 s, under 3 MB each); or "cover" in its ${l.kind === 'app' ? 'app' : 'game'}.json`);
|
|
440
440
|
}
|
|
441
441
|
if (result.posts?.length) lines.push(`Posts: ${result.posts.map((x) => `/posts/${x}/`).join(', ')} (feeds: /posts/feed.xml, /posts/feed.json)`);
|
|
442
442
|
if (result.pages?.length || result.partials?.length) lines.push(`The studio's own: ${[...(result.pages ?? []).map((x) => `page ${x}`), ...(result.partials ?? []).map((x) => `partial ${x}`)].join(', ')}`);
|
|
@@ -680,7 +680,7 @@ function print(result) {
|
|
|
680
680
|
lines.push(formatHandoff(result));
|
|
681
681
|
break;
|
|
682
682
|
case 'codex':
|
|
683
|
-
lines.push(`The Game Codex for ${result.title}: ${result.file}${result.opened ? ' (opened in the browser)' : ''}`,
|
|
683
|
+
lines.push(`The ${result.kind === 'app' ? 'App' : 'Game'} Codex for ${result.title}: ${result.file}${result.opened ? ' (opened in the browser)' : ''}`,
|
|
684
684
|
` tabs: ${result.sections.map((x) => x.title).join(' · ')} · Build status${result.build ? ` (${result.build.state}, ${result.build.percent}%)` : ''}`,
|
|
685
685
|
...(result.missing.length ? [` not decided yet: ${result.missing.join(', ')}`] : [' every section has something in it']),
|
|
686
686
|
` open questions: ${result.openQuestions}`,
|
|
@@ -785,6 +785,7 @@ function print(result) {
|
|
|
785
785
|
`Written: ${result.file}`);
|
|
786
786
|
break;
|
|
787
787
|
case 'check':
|
|
788
|
+
if (result.kind === 'app') { lines.push(`PASS: app ${result.game ?? ''}: wall and two phones shared an action; reconnect ${result.reconnect ? 'passed' : 'failed'}.`, ` ${result.before} → ${result.after}`); break; }
|
|
788
789
|
if (result.results) { for (const r of result.results) lines.push(`${r.game}: PASS at ${r.site}: room ${r.room}, round ${r.round?.n} (${r.round?.humans} humans, ${r.round?.bots} bots), connection ${r.uninterrupted === true ? 'uninterrupted' : r.uninterrupted === false ? 'INTERRUPTED' : 'unknown'}`); break; }
|
|
789
790
|
if (result.site) lines.push(`Checked ${result.local ? 'this studio\'s own build' : 'the site'} at ${result.site}.`);
|
|
790
791
|
lines.push(`PASS: two fresh browsers in room ${result.room} finished round ${result.round.n} (${result.round.humans} humans, ${result.round.bots} bots) in ${Math.round(result.totalMs / 1000)} s.`,
|
|
@@ -874,7 +875,7 @@ async function main() {
|
|
|
874
875
|
if (cmd === 'version' || flags.has('version')) return { ok: true, command: 'version', version: STUDIO_VERSION };
|
|
875
876
|
const strange = unknownFlags(positional, flags);
|
|
876
877
|
if (strange) return strange;
|
|
877
|
-
if (cmd === 'new') return newStudio(positional[1], { name: flags.get('name'), homie: flags.get('homie'), slug: flags.get('slug'), install: !flags.has('no-install'), template: flags.has('template') });
|
|
878
|
+
if (cmd === 'new') return newStudio(positional[1], { name: flags.get('name'), homie: flags.get('homie'), slug: flags.get('slug'), install: !flags.has('no-install'), template: flags.has('template'), kind: flags.get('kind') ?? 'game' });
|
|
878
879
|
if (cmd === 'starters') return { ok: true, command: 'starters', starters: starters() };
|
|
879
880
|
// The floor's built-in words need no studio: anyone can read what a studio's chat always holds.
|
|
880
881
|
if (cmd === 'chat' && sub === 'words') return chatWords();
|
|
@@ -905,7 +906,7 @@ async function main() {
|
|
|
905
906
|
const root = requireStudio();
|
|
906
907
|
// Keep guidance visible to AI callers using --json as well as human output.
|
|
907
908
|
if (['build', 'check', 'upgrade'].includes(cmd)) {
|
|
908
|
-
for (const line of (await import('../lib/authoring-guides.mjs')).authoringWords()) process.stderr.write(`${line}\n`);
|
|
909
|
+
for (const line of (await import('../lib/authoring-guides.mjs')).authoringWords({ kind: ['app', 'business', 'cause'].includes(readStudio(root).kind) || listExperiences(root).some(g => g.kind === 'app') ? 'app' : 'game' })) process.stderr.write(`${line}\n`);
|
|
909
910
|
}
|
|
910
911
|
if (cmd === 'function') return (await import('../lib/functions-cli.mjs')).functionCommand(root, sub, positional, flags, { log });
|
|
911
912
|
if (cmd === 'tool') return (await import('../lib/tools-cli.mjs')).toolCommand(root, sub, positional, flags, { log });
|
|
@@ -946,7 +947,7 @@ async function main() {
|
|
|
946
947
|
if (!/^[a-z0-9][a-z0-9-]{0,39}$/.test(id ?? '') || !/^[a-z][a-z0-9-]{0,39}$/.test(role ?? '')) throw new Error('app role <id> <role> [--player <account>] [--revoke] [--url <site>]');
|
|
947
948
|
return withKey(root, flags.get('url'), (call) => call(`/${id}/api/app/roles`, { role, ...(flags.get('player') ? { player: flags.get('player') } : {}), revoke: flags.has('revoke') }));
|
|
948
949
|
}
|
|
949
|
-
if (cmd === 'app' && sub === 'new') return newApp(root, positional[2], { name: flags.get('name') });
|
|
950
|
+
if (cmd === 'app' && sub === 'new') return newApp(root, positional[2], { name: flags.get('name'), from: flags.get('from') ?? 'welcome' });
|
|
950
951
|
if (cmd === 'game' && sub === 'new') return newGame(root, positional[2], { from: flags.get('from') ?? 'gem-rush', name: flags.get('name') });
|
|
951
952
|
// Remix was retired (a game is never handed over whole): the command is refused in a sentence that says where to
|
|
952
953
|
// go, so an old note, an old card or an agent's memory of the toolkit does not end at "unknown command".
|
|
@@ -994,8 +995,11 @@ async function main() {
|
|
|
994
995
|
const results = [];
|
|
995
996
|
for (const game of games) {
|
|
996
997
|
const shots = flags.get('shots') ? resolve(flags.get('shots'), ...(games.length > 1 ? [game] : [])) : null;
|
|
997
|
-
const r = await tracked(root, 'checks', (report) => check({ url: site.url, game, shots, log, report }), 'check');
|
|
998
|
+
const r = await tracked(root, 'checks', (report) => check({ root, url: site.url, game, shots, log, report }), 'check');
|
|
998
999
|
results.push({ ...r, game, site: site.url, local: site.local });
|
|
1000
|
+
const receiptDir = join(root, '.studio', 'checks');
|
|
1001
|
+
mkdirSync(receiptDir, { recursive: true });
|
|
1002
|
+
writeFileSync(join(receiptDir, 'latest.json'), JSON.stringify({ ...r, game, site: site.url, at: new Date().toISOString() }, null, 2));
|
|
999
1003
|
if (!r.ok) break;
|
|
1000
1004
|
}
|
|
1001
1005
|
return results.length === 1 ? results[0] : { ok: results.every((r) => r.ok), command: 'check', site: site.url, local: site.local, why: results.find((r) => !r.ok)?.why, results };
|
|
@@ -1099,7 +1103,7 @@ async function main() {
|
|
|
1099
1103
|
if (cmd === 'shop') {
|
|
1100
1104
|
const url = flags.get('url');
|
|
1101
1105
|
if (!sub) return shopStatus(root, { url });
|
|
1102
|
-
if (sub === 'init') return shopInit(root, { supporter: flags.has('supporter'), currency: flags.get('currency') ??
|
|
1106
|
+
if (sub === 'init') return shopInit(root, { supporter: flags.has('supporter'), giving: flags.has('giving'), currency: flags.get('currency') ?? null, price: flags.get('price') ?? 500, managed: flags.has('managed') });
|
|
1103
1107
|
if (sub === 'check') return shopCheck(root);
|
|
1104
1108
|
if (sub === 'connect') return shopConnect(root, { managed: flags.has('managed') ? true : null, live: flags.has('live'), manual: flags.has('manual'), fromClipboard: flags.has('from-clipboard'), renew: flags.has('renew'), log: (line) => process.stderr.write(`${line}\n`) });
|
|
1105
1109
|
if (sub === 'disconnect') return shopDisconnect(root);
|
|
@@ -1168,8 +1172,8 @@ async function labCommand(root, sub) {
|
|
|
1168
1172
|
|
|
1169
1173
|
/** `homie-studio codex …` (lib/codex.mjs): the Game Codex of one game. */
|
|
1170
1174
|
function codexCommand(root, sub) {
|
|
1171
|
-
const games =
|
|
1172
|
-
if (sub === 'new') return newCodex(root, positional[2] ?? (games.length === 1 ? games[0].id : undefined), { name: flags.get('name') ?? null });
|
|
1175
|
+
const games = listExperiences(root);
|
|
1176
|
+
if (sub === 'new') return newCodex(root, positional[2] ?? (games.length === 1 ? games[0].id : undefined), { name: flags.get('name') ?? null, kind: flags.get('kind') ?? null });
|
|
1173
1177
|
if (sub === 'link') {
|
|
1174
1178
|
const id = positional[2] ?? (games.length === 1 ? games[0].id : null);
|
|
1175
1179
|
if (!id || !games.some((g) => g.id === id)) return { ok: false, command: 'codex link', why: 'name the game: homie-studio codex link <id>' };
|
|
@@ -1222,7 +1226,7 @@ async function tracked(root, stage, run, command) {
|
|
|
1222
1226
|
const note = stage === 'build' ? `Built ${(result.games ?? []).map((g) => `${g.id} (${Math.round(g.bytes / 1024)} KB)`).join(', ') || 'the site'}`
|
|
1223
1227
|
: stage === 'deploy' ? `Live: ${result.url ?? 'deployed'}`
|
|
1224
1228
|
: result.command === 'port check' ? `${result.passed.length} passed${result.skipped.length ? `, ${result.skipped.length} skipped` : ''} in ${Math.round(result.totalMs / 1000)} s`
|
|
1225
|
-
: `Room ${result.room}: round ${result.round?.n} finished with ${result.round?.humans} people in ${Math.round(result.totalMs / 1000)} s`;
|
|
1229
|
+
: result.kind === 'app' ? `App ${result.game ?? ''}: wall and two phones, shared action and reconnect passed` : `Room ${result.room}: round ${result.round?.n} finished with ${result.round?.humans} people in ${Math.round(result.totalMs / 1000)} s`;
|
|
1226
1230
|
feed.stage(stage, 'done', note);
|
|
1227
1231
|
if (stage === 'deploy') {
|
|
1228
1232
|
const doc = feed.doc;
|
package/functions/FUNCTIONS.md
CHANGED
|
@@ -31,7 +31,7 @@ another business workflow. Treat event data as data, not instructions.
|
|
|
31
31
|
|
|
32
32
|
| Event | Data |
|
|
33
33
|
| --- | --- |
|
|
34
|
-
| `order.paid` | `order`, `source`, `amount`, `currency`, `mode` |
|
|
34
|
+
| `order.paid` | `order`, `source`, `amount`, `currency`, `mode`; shop orders also carry `app` and buyer-supplied `reference` (null when omitted) |
|
|
35
35
|
| `payment.refunded` | `refund`, `payment`, `amount`, `currency` (each successful Stripe refund, including partial refunds) |
|
|
36
36
|
| `order.refunded` | `order`, `source`, `refund`, `mode` (full refund) |
|
|
37
37
|
| `player.joined` | `game`, `room`, `player` (nullable), `seat` |
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app
|
|
3
|
+
description: "Make or change a Homie app: one morphing screen, roles and reusable parts, live across phones, wall screens, kiosks and staff tablets. Use for customer apps, venue screens, queues, workshop boards and cause dashboards in a studio with studio.json. Apps use the same engines, rooms, parts and standalone builds as games, without compulsory rounds, scores, bots or players."
|
|
4
|
+
compatibility: Node 22 and Chrome; the studio's pinned toolkit and Wrangler.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Make an app
|
|
8
|
+
|
|
9
|
+
An app is **one screen, its roles, and its parts**. The screen transforms with the person and the work: a camera moves, a ticket unfolds, controls appear in context. Do not build page-to-page navigation, a sidebar of links, or a conventional admin website inside it. A studio's public website can still have pages around the app.
|
|
10
|
+
|
|
11
|
+
Read the person's brief and existing `apps/<id>/` first. Choose sensible engineering defaults from the brief; never ask the person how to implement shared state. A business or cause needs no game demo, mandatory entertainment, invented customers, scores or rounds. Never impose a shop, directory listing, sign-in for public visitors, or paid media. Make the fewest human steps possible; run commands yourself.
|
|
12
|
+
|
|
13
|
+
## The person's dream is the brief
|
|
14
|
+
|
|
15
|
+
Before scaffolding the app, read the shared `plan` skill. Run `codex new <id> --kind app --name "<Name>"` and fill `apps/<id>/CODEX.md` from their request. This is the same Codex mechanism as games, with the sections an app needs. Draw its page with `codex <id>`. Keep it open as the person's way to steer the work. A request to decide and continue authorizes you to make sensible decisions; do not wait for approval of a plan they did not ask to approve.
|
|
16
|
+
|
|
17
|
+
Capture these before implementation:
|
|
18
|
+
|
|
19
|
+
- **Their words:** quote the request, including its scene and emotional intent. Do not replace “my skis moving through the workshop” with “a kanban dashboard.”
|
|
20
|
+
- **Scene:** describe walking into the place. What occupies the wall? What moves, arrives, fills or lights up? What can someone understand from across the room? What feels different about this place?
|
|
21
|
+
- **People and surfaces:** owner, staff, customer, visitor, volunteer or donor as appropriate. For each: what they need to accomplish and where. Include the person who arrives with no account and the person returning to their own work.
|
|
22
|
+
- **Offers:** names, prices, currency, once/monthly, what is included, and what purchase or gift pays for. Preserve “from” prices; a base repair estimate is not a fixed final charge. Record assumptions plainly.
|
|
23
|
+
- **Needs:** short statements that can be shown true or false on a real wall and phone. Give each a stable ID. A request to book, follow, give monthly or log a send is a separate need. The app existing is not a need.
|
|
24
|
+
|
|
25
|
+
The AI chooses records, room structure, packages, camera, interaction, animation and build strategy. Never interview a nontechnical owner about hosting, databases, engines or synchronization. Ask only for indispensable real-world information; continue independent work. If a request names their *actual* place and there is no layout/photo, build an editable, clearly labelled provisional layout and report that exact need as partly met until it is supplied. Never invent a real floor plan and call it actual.
|
|
26
|
+
|
|
27
|
+
## Turn the work into a living scene
|
|
28
|
+
|
|
29
|
+
A starter is plumbing: role permissions, records, room connection and a real check action. Nobody is commissioning the starter. Choose the closest plumbing, install its reported dependencies, then replace its scene and workflow with the Codex. A starter painted in brand colours is not completion.
|
|
30
|
+
|
|
31
|
+
Design a spatial or pictorial model of the business process. Make the things the owner cares about the largest objects, not equal-sized cards with labels. A service has physical objects, locations and transitions; a cause has a visible shared outcome; a venue has an actual place and people doing things there. Stages belong in the scene as destinations, work areas or changes to the objects. They are not automatically columns. Compose the wall first in a sketch, then implement a useful phone view of the same world.
|
|
32
|
+
|
|
33
|
+
For example, a workshop may have recognisable skis and snowboards riding a rail between a wax bench, an edge station and a repair press. Wax can shine, repair marks can disappear, and completed gear can settle into a collection rack. A kitchen may lay places at a long communal table as verified gifts fund meals; an unconfirmed pledge must look distinct. A climbing wall may carry routes in their real positions; completion switches their lighting, while a climber's send briefly traces that route. These are examples of translating verbs into pictures, not new templates to copy.
|
|
34
|
+
|
|
35
|
+
Use scene geometry, illustration or layered SVG when it serves the request; 3D is available, not compulsory. A beautifully composed 2D place can work. A flat board of boxes with counters does not satisfy a request for things moving through a place. Avoid generic dashboards, stock admin layouts and navigation as the primary experience. Native controls, readable text and clear labels remain essential.
|
|
36
|
+
|
|
37
|
+
Before writing scene components:
|
|
38
|
+
|
|
39
|
+
1. Run `style init <id> --prompt "<the person's own scene words>"`. Record the direction in the Codex: composition, camera, materials, colour, typography and motion. Use the same style machinery as games, not the starter's default theme.
|
|
40
|
+
2. Inspect the installed `@homie-rocks/*` packages for mechanisms. `geom`, `props`, `render`, `camera`, `postfx` and `fx` can make the place; `input`, `ui`, `ui-world`, `device`, `audio` and `loop` make it usable. Read the package exports and examples rather than guessing imports. Install every dependency reported by the starter or parts before building.
|
|
41
|
+
3. Search `parts_find` for each meaningful piece; shell equivalent: `homie-studio parts find "<piece>"`. Read its contract, source studio and licence. Add fitting pieces with `part_add` / `parts add`; write the missing parts yourself. The example studios share real app pieces: search for venue, workshop, giving, arrivals, scene, queue and sound, not just game characters. If local example studios are available, read their shared `parts/` and manifests without modifying the source studios. A failed catalogue request is not proof that nothing exists.
|
|
42
|
+
4. Use the art/models/sound/music skills with connected providers and an authorized budget. Generate distinctive assets when useful. If no provider is connected, continue with original geometry, locally drawn SVG, CC0 assets and local synthesized sound. Report the means honestly. Provider absence never justifies replacing the scene with a board.
|
|
43
|
+
5. Record packages, parts with attribution, original work and media routes under **Built from**. Keep costs and provenance visible.
|
|
44
|
+
|
|
45
|
+
Make a small vertical slice before dressing every corner: a customer performs one real action; its durable record appears as a recognisable object in the wall scene; staff can advance it; the customer's own view updates. Then complete the remaining needs. If an engine import, type check or function build fails, repair the dependency or source error. Do not remove the designed scene, permissions or durable workflow to make a build pass. Diagnose the reported error, read the package contract, and build again.
|
|
46
|
+
|
|
47
|
+
## Records are the scene's truth
|
|
48
|
+
|
|
49
|
+
Write a mapping from each durable entity to its visual representation. Use a stable record ID as the object key. Its stage, progress, position, amount or completion time drives the picture; an animation interpolates from the last confirmed state to the next. Reloading reconstructs the same scene from records. A customer action shows a pending acknowledgement immediately, then confirmed state or an actionable failure. Do not animate a successful booking, paid gift or staff transition before it is confirmed.
|
|
50
|
+
|
|
51
|
+
Choose a coherent transition vocabulary for that place. An arrival enters the scene, stage changes move an object, completion changes material/light, and removal retires it. Use elapsed time for local easing, not frame-count increments. A late subscriber goes directly to current truth rather than replaying every historical event. Prevent a reconnect from duplicating objects or counting the same gift twice. Reduced motion preserves the information with a quiet state change.
|
|
52
|
+
|
|
53
|
+
Separate records by who may read them. Public wall records contain safe display information and stable IDs. Contact details, private booking notes and staff-only operations belong in protected collections. UI filtering is not access control. Never put a customer email or private token on the wall. Read `apps/APPS.md` for collection permissions and versioned updates. Do not weaken permissions to make a local staff demo easy.
|
|
54
|
+
|
|
55
|
+
Keep scene state derived from the records service. Rooms may relay transient effects and live presence, but a room emptying must not erase bookings, gifts, routes or shifts. On a version conflict, reload the record and show the current state; do not silently overwrite another staff member. Validate allowed transitions and meaningful inputs. No invented capacity guarantee: a booking request is a request unless an implemented reservation system confirms capacity.
|
|
56
|
+
|
|
57
|
+
## Let each person find their own thing
|
|
58
|
+
|
|
59
|
+
On submission, give the customer a stable receipt or tracking URL for the exact created record. Use `randomId`, `appLink` and `qrSvg` from the links package. Keep the link on the success screen, with an explicit copy/save action and meaningful short reference. Persist their own reference locally so reloads return to it; also allow entering a reference or opening the saved link. Another phone using that link should follow the same public-safe item. Never make the customer search a wall full of other people's work to discover whether their booking succeeded.
|
|
60
|
+
|
|
61
|
+
A public tracking ID identifies a public-safe record; it is not an authorization secret. Private customer information needs authenticated access. Use clean public URLs, never staff links. Test returning later, unknown reference, an empty workshop, a failed submission and a reconnect. Clearly distinguish pending payment, paid, in progress and ready to collect where those states matter.
|
|
62
|
+
|
|
63
|
+
Design each surface intentionally:
|
|
64
|
+
|
|
65
|
+
| Surface | The job of the scene and controls |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| Wall | Legible across the room; the whole place and its changing work; little prose; no personal data; no dependence on hovering or tapping; a clear QR to the public phone action. |
|
|
68
|
+
| Phone | A close view of *my* booking, gift, route or shift; one obvious next action, native fields, receipt and return link; responsive portrait and landscape. |
|
|
69
|
+
| Staff tablet | Operational controls at the thing being worked on, current record/version, failure and conflict recovery; authenticated privilege. |
|
|
70
|
+
| Kiosk | Large touch targets, public actions, a clear finish/reset, no previous person's private details left behind. |
|
|
71
|
+
|
|
72
|
+
These can share a renderer and records while changing framing and controls. Do not shrink the wall to a phone or show staff controls to customers. Test touch, keyboard focus, text contrast, 44 px targets and reduced motion. A QR is only useful if its address is reachable from the phone: use the printed LAN address for a physical phone. Capture at actual viewport sizes in Chrome, not resized desktop screenshots.
|
|
73
|
+
|
|
74
|
+
## Acceptance belongs to the owner
|
|
75
|
+
|
|
76
|
+
The Codex's `needs` fenced block is a JSON array, one entry per need from the request. It is the check's source, not a second parallel list in app.json. Example (replace selectors and actions with this app's actual workflow):
|
|
77
|
+
|
|
78
|
+
```needs
|
|
79
|
+
[
|
|
80
|
+
{
|
|
81
|
+
"id": "track-gear",
|
|
82
|
+
"statement": "Customers book a tune from their phone and watch their own gear on the workshop wall and their phone.",
|
|
83
|
+
"steps": [
|
|
84
|
+
{"surface":"phone", "selector":"#gear-name", "fill":"Blue board"},
|
|
85
|
+
{"surface":"phone", "selector":"#book-tune", "click":true}
|
|
86
|
+
],
|
|
87
|
+
"assertions": [
|
|
88
|
+
{"surface":"phone", "selector":"#my-ticket", "text":"Blue board"},
|
|
89
|
+
{"surface":"wall", "selector":"[data-gear]", "text":"Blue board"}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Steps support `fill`, `select`, `click: true` and `reload: true`; surfaces are `wall`, `phone`, `phone-two`. Assertions require a visible selector and optionally contained text. Both wall and phone must have assertions. An honestly unavailable capability remains in the block with no assertions and a plain explanation in the prose; check reports it not met. Do not delete, weaken or replace a need to get green. Static needs such as accurate prices may have no action steps. Use semantic attributes on real visible scene elements for testability, never hidden QA-only counters.
|
|
96
|
+
|
|
97
|
+
Run build and the shared-action check, then the needs step runs on those real browsers. Save captures with `--shots .checks`. The result contains each statement, met/partly/not, the assertion result and wall/phone captures; `.studio/checks/<id>-needs.json` drives the Codex's Needs status. Redraw `codex <id>` after checking. Passing a selector assertion is evidence, not a visual judgement: read every capture and perform the full flows as owner and customer using the playtest skill. If the picture fails the words, it is not done even with green assertions.
|
|
98
|
+
|
|
99
|
+
For staff flows, sign in through the existing local owner link and prove the transition in real Chrome. Keep evidence in the Codex alongside the automated flows. Confirm unauthorized writes are refused and records remain after all screens close. Never call an untested privileged path passed. Before final handover, show the need table with evidence and explicit remaining gaps. If the starter or a board of boxes is still visible, return to the scene design and build it.
|
|
100
|
+
|
|
101
|
+
## Tie payment to the work
|
|
102
|
+
|
|
103
|
+
Create the booking, gift intention or ticket first and pass its ID as the application's reference:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
await shop.buy('full-tune', 1, undefined, { reference: booking.id });
|
|
107
|
+
// Or a cart:
|
|
108
|
+
await shop.checkout({ reference: booking.id });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
These retain their existing boolean result: true means checkout opened, not payment received. The durable `order.paid` function event includes `data.order` (the order ID), `data.reference` (your reference or null), and `data.app` (the app ID). A verified provider payment emits it. Validate the referenced record, expected amount/currency and app, deduplicate by event/order ID, then fulfil that booking or add that gift to the confirmed meal table. A buyer supplies the reference: it is correlation, never proof they own a private record or paid the expected price. Do not put personal data in it.
|
|
112
|
+
|
|
113
|
+
For each offer, build the promised action and fulfilment, not just a price label: a naming purchase needs the proposed name and the staff/customer outcome; a ticket needs its receipt. Keep an unpaid request distinct when charging is unavailable.
|
|
114
|
+
|
|
115
|
+
When selling or giving is a stated need, implement and locally exercise the reference-to-record `order.paid` handler before calling payment provider-blocked. A disconnected provider blocks charging; it is not a reason to omit fulfilment code. Use `function fire` with an explicitly simulated event and a separate test record, confirm the right record changes once, and keep test money out of real totals.
|
|
116
|
+
|
|
117
|
+
Without Stripe, keep real services and prices visible and say payment is not connected; allow a clearly labelled unpaid request/intention if useful. Never mark it paid or add it to the paid total. Test event handlers locally with explicit test events, labelled as simulations. Report actual charging and recurring collection as not yet met until connected and verified. Do not build a new payment sandbox or pretend recurring intentions are subscriptions.
|
|
118
|
+
|
|
119
|
+
## Start and compose
|
|
120
|
+
|
|
121
|
+
- Run `homie-studio app new <id> --from <starter> --name "<Name>"`, or `app_make`. Starters: `welcome` (queue), `gathering` (check-in/task choice), `giving` (goal and giving intentions linked to the shop), `quiz` (casual quiz night), `jobs` (service booking requests). The welcome starter is one 3D scene with customer, wall and staff roles. Install its reported `needsAdded`, then build.
|
|
122
|
+
- `apps/<id>/app.json` describes roles (`signIn`, `can`), surfaces (`phone`, `wall`, `kiosk`, `tablet`), wording, record collections, parts and the real UI action used by check. The app's entry and assets live beside it, just like a game.
|
|
123
|
+
- Before inventing each component, inspect engine packages for the mechanism and `parts_find` for the piece. A queue, quiz, menu board, live total, music loop or video intro can be a part. Record its licence and credit. Sharing happens only when requested; selling parts is not part of this workflow.
|
|
124
|
+
- Use the same engine packages as a game: `@homie-rocks/geom` and `props` for original scenery; `render`, `camera`, `postfx`, `fx` for light, composition and motion; `input`, `ui`, `ui-world`, `audio`, `device`, `loop` for interaction and feel. An app can be fully 3D and wild. Legibility and the actual task come first.
|
|
125
|
+
- Use the `style`, `art`, `models`, `animate`, `sound`, `music`, `video` and `perf` skills where they help. It should look like a game studio made it: a coherent world with a useful HUD, not generic cards and navigation. Media cost and licensing decisions remain the person's.
|
|
126
|
+
- Keep native buttons at least 44 px, keyboard focus visible, text as text, motion reducible, and the wall QR clear of useful information. Pointer gesture guards belong on the canvas, not native click buttons.
|
|
127
|
+
|
|
128
|
+
## Roles and records
|
|
129
|
+
|
|
130
|
+
For transient shared live state, use the same server rules rooms as games. Read
|
|
131
|
+
[the rules reference](../game/RULES.md). Declare `src/rules.ts`, `src/move.ts`, `src/view.ts`, and app.json `room: { host: 'server' }`
|
|
132
|
+
with `entry: 'src/view.ts'`. Each interactive visitor has a player entity containing
|
|
133
|
+
its cursor/selection; commands validate actions in that entity, then sendRoom to
|
|
134
|
+
update shared bounded board/poll/scene state. Walls watch without a seat. Omit bots,
|
|
135
|
+
scores and automatic rounds; use `rounds: { seconds: 0, breakSeconds: 0 }`. The current contract requires a body for each participant: use a circle with radius 0.1,
|
|
136
|
+
maxSpeed 0 and a no-op defineMove handler for a stationary board or poll. This is a
|
|
137
|
+
logical seat anchor, not a visible avatar; never add fake movement to pass a test. Include a small map/main.json with bounds even for stationary seats. Draw through openRoom, and acknowledge a button
|
|
138
|
+
locally while waiting for shared outcomes. Keep animations and camera in the view.
|
|
139
|
+
|
|
140
|
+
Do not add rules to an app that only needs the records service and local UI.
|
|
141
|
+
The welcome starter demonstrates authorized records plus a server-rules scene with
|
|
142
|
+
stationary participants, change effects and a shared revision. Existing apps keep working. Use the manual extraction steps in [REWRITE.md](../game/REWRITE.md)
|
|
143
|
+
for existing live state, adapting game-only tests to the app's workflow.
|
|
144
|
+
|
|
145
|
+
Transport host/replica/screen roles are not staff permission. Never trust a role
|
|
146
|
+
parameter or hidden button to authorize an operational change. Rules snapshots
|
|
147
|
+
are public, rooms have at most 32 seats, and end after the last person leaves;
|
|
148
|
+
they are neither private business storage nor an authorization service.
|
|
149
|
+
|
|
150
|
+
Use `createAppRecords` from `@homie-rocks/studio/apps` for lasting records. Declare each collection's fields and every role's read/create/update/delete capabilities. The Worker validates writes and checks the signed-in account on every privileged request. Updates use a version; reload and resolve a conflict instead of overwriting somebody else's edit. A room may empty without deleting these records. Transient effects and camera state remain in netplay.
|
|
151
|
+
|
|
152
|
+
Public collections reach everyone allowed to read them. A ticket number or filtering the HUD is not privacy. Keep personal/private records in a different collection whose read permission excludes customers and walls. Do not put contact details in the starter's public queue.
|
|
153
|
+
|
|
154
|
+
`homie-studio app role <id> staff --player <account-id> --url <site>` grants an existing signed-in account and returns its private role address. Without `--player` it returns the owner's role address. The existing owner check protects issuance; the link alone grants nothing. `--revoke` removes a named account's grant. Staff use the studio's existing account/passkey sign-in. Keep these links private.
|
|
155
|
+
|
|
156
|
+
The complete helper and limits are in `node_modules/@homie-rocks/studio/apps/APPS.md`. Import `appLink`, `qrSvg`, and `randomId` from `@homie-rocks/studio/links` for a ticket URL with parameters. Build customer links from a clean public address, never by copying a staff URL. `randomId` works on plain HTTP.
|
|
157
|
+
|
|
158
|
+
## Prove and deliver
|
|
159
|
+
|
|
160
|
+
1. `homie-studio build --types` and `homie-studio dev --lan`. Open the printed Wi-Fi address for phones and the wall. Localhost QR codes cannot reach a phone. Passkeys need HTTPS or localhost; do not bypass staff authorization for HTTP.
|
|
161
|
+
2. `homie-studio check <id> --url <origin> --shots .checks`: a wall and two phones connect, a declared real UI action changes all screens, and a reload retains it. In `app.json`, `check.action` is its CSS selector and `check.observe` is a shared text value that changes as a consequence. Use an actual workflow, no fake QA-only action.
|
|
162
|
+
3. Also prove the app's own role workflow with staff signed in, unauthorized writes refused, records after all screens close, and layout in phone portrait/landscape, tablet and wall. Use the `playtest` skill for visual and interaction review, adapting game-only criteria to the app's purpose.
|
|
163
|
+
4. Customer store builds use the existing `standalone` skill/command with the app id. The same bundle and public records work through the wrapper; its existing account, billing and store-upload limitations still apply. Never promise store approval.
|
|
164
|
+
5. Give the person `/<id>/open`, `/<id>/tv`, a real screenshot and the measured proof. Publish only within their authorization through the existing publish workflow. Do not turn a request for an app into an unwanted deployment or directory listing.
|
|
165
|
+
|
|
166
|
+
## Connect an AI to this studio
|
|
167
|
+
|
|
168
|
+
Use Studio functions (`functions/FUNCTIONS.md` in the toolkit) for durable reactions to `record.changed`, `order.paid`, refunds, schedules and tool calls. `homie-studio function new <name> --event <type>` creates a plain TypeScript file; local `function fire` and the office delivery table make it testable. Deduplicate external effects by event ID. Public or priced tools can expose app services to customer AIs using the same purchase engine.
|
|
169
|
+
|
|
170
|
+
Every studio serves its own MCP at `<site>/mcp`. Use the `tools` skill for studio-defined business actions, signed integrations and staff AI access. Reuse office and app-record built-ins first. After an authorized deploy, hand over the MCP address and `<site>/_studio/office/connections`: the person approves with the studio account they already have, with no key to copy. Staff connect their own account and retain only their app grants. Client connector setup is separate from the studio approval. Never deploy merely to set up a local tool test.
|
|
171
|
+
|
|
172
|
+
## Public pages and giving
|
|
173
|
+
|
|
174
|
+
Scaffold businesses with `new <folder> --kind business`; causes with `--kind cause` (MCP `studio_scaffold` uses `kind`). The header links custom `site/pages/<slug>/index.html` pages and an existing Shop. A page at `site/pages/<app-id>/index.html` takes over only the landing: use `/<id>/open` for phones and `/<id>/tv` for the wall. Build says when this happens. In-app links to public pages should use `target="_blank" rel="noopener"` because the app is sandboxed. App assets and hero files belong under `apps/<id>/`, including `apps/<id>/hero/`.
|
|
175
|
+
|
|
176
|
+
For giving, set the studio currency and run `shop init --giving --currency CAD` (use the organisation’s currency). It creates one-time/monthly suggested gifts. The owner supplies `giving.receiptText` in shop.json; Homie supplies no legal or tax text. Before Stripe is connected, local /shop/ previews items and prices with checkout disabled. Run the connection step yourself; the owner approves their Stripe account in the browser.
|
|
177
|
+
|
|
178
|
+
Booking requests are not capacity reservations. Calendar availability and seat capacity are the next app capability; do not promise them.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: art
|
|
3
|
+
description: Make a studio's game look like something at build time — a cover from a real frame of the game (free), painted covers, backdrops, textures and character plates from image models through the creator's OWN fal account under a hard budget with a receipt for every call, checked (does the texture tile, is the file small enough for a phone, does the cover promise only what the game contains) and loaded by the game — plus the art-direction method (a style sheet and paintovers of real frames as the target the game's own rendering is changed to reach). Use when someone asks for cover art, key art, a thumbnail, a hero image, backgrounds, textures, sprites, or says the game looks rough, flat or unfinished.
|
|
4
|
+
compatibility: Node 22, ffmpeg and Chrome. Generated images use the creator's own fal account (FAL_KEY); fal's own MCP server (https://mcp.fal.ai/mcp-relay) finds models and reads prices.
|
|
5
|
+
metadata:
|
|
6
|
+
providers: fal
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Art for a studio's game
|
|
10
|
+
|
|
11
|
+
**Build time, never a frame of play.** Everything here happens once, before anybody plays, and costs
|
|
12
|
+
nothing at runtime: never call an image model from a game's loop or from anything a person waits on.
|
|
13
|
+
If you want one there, you want a baked asset instead.
|
|
14
|
+
|
|
15
|
+
One script: `scripts/art.mjs` in this skill's folder (Claude Code:
|
|
16
|
+
`node "${CLAUDE_PLUGIN_ROOT}/skills/art/scripts/art.mjs" <command>`), from inside the studio. Node 22,
|
|
17
|
+
ffmpeg and Chrome; `--json` on every command. Art jobs live in `art/<slug>/` (the images, the
|
|
18
|
+
`budget.json`, the `.request` and `.json` receipts beside each generated file); every paid call is
|
|
19
|
+
also a line in `art/receipts.jsonl`.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
node <art.mjs> check # ffmpeg, Chrome, the studio; the fal key checked for free (only generated images need it)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 1. The cover, first, always
|
|
26
|
+
|
|
27
|
+
The cover is what a stranger sees before pressing anything: the studio's game page, the directory
|
|
28
|
+
card, a link preview. Start from the game itself:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm run dev # background task
|
|
32
|
+
node <art.mjs> frame <game> --url http://127.0.0.1:8787 # real game frames, the most detailed picked
|
|
33
|
+
node <art.mjs> cover <game> --from art/frames/frame-04.png [--focus 0.6,0.4]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`frame` uses the play view for server rules so the camera's seat keeps the room
|
|
37
|
+
running, and the big screen for older games. It hides the platform's furniture
|
|
38
|
+
(join QR, status chip, result cards) and keeps the frame with the most visible detail.
|
|
39
|
+
For `--view tv` in a rules game, keep a real player in that same room: a TV alone
|
|
40
|
+
does not keep a server match running. A paused-room message means use `--view play`
|
|
41
|
+
or open a player, not change hosting mode.
|
|
42
|
+
`cover` crops it to 16:9 around the focus, writes `cover.jpg` (1600x900, under 400 KB) into the game
|
|
43
|
+
and names it in `game.json` (`"cover": "cover.jpg"`): the site and the directory use it until the game's
|
|
44
|
+
landing has a still of its own (`hero/wide.jpg`), which then is its picture everywhere.
|
|
45
|
+
|
|
46
|
+
**Open the cover and look.** A cover is a picture of the GAME: no QR code, no share panel, no debug
|
|
47
|
+
text, not the waiting screen (a picture of nothing happening). Dark or featureless frames get a
|
|
48
|
+
warning: a black card on a page reads as a black box.
|
|
49
|
+
|
|
50
|
+
**A painted cover** from that frame, when the person wants one (paid, below): write the prompt from
|
|
51
|
+
the game you just played, never from its title. Describe what is on screen: the palette, the time of
|
|
52
|
+
day, what the player is doing, the shapes. A cover that promises something the game does not contain
|
|
53
|
+
is the one failure here that matters: a person presses Play expecting that picture and meets
|
|
54
|
+
something else. Use the real frame as the image reference (`"@file:art/frames/frame-04.png"` in the
|
|
55
|
+
input) so the painting keeps the game's layout.
|
|
56
|
+
|
|
57
|
+
## 2. Paid images: price, budget, one call at a time
|
|
58
|
+
|
|
59
|
+
Use an already connected fal account for paid generation; never ask the person to find or set a key.
|
|
60
|
+
The local paid script discovers an existing fal key using the lookup below. If none is available, continue
|
|
61
|
+
with local assets or captured footage and explain that this paid-script connection is not ready.
|
|
62
|
+
Do not make account configuration a prerequisite for a live studio. **fal's own MCP server** is how you find a
|
|
63
|
+
model and read its input schema and price (`search_models`, `recommend_model`, `get_model_schema`,
|
|
64
|
+
`get_pricing`): use it rather than memory, because endpoints retire and reprice. Not connected? Offer
|
|
65
|
+
it, and the person signs in on fal's own page (no key): in Claude Code `claude mcp add --transport http
|
|
66
|
+
--scope user fal https://mcp.fal.ai/mcp-relay`, then `/mcp`; in Codex `codex mcp add fal --url
|
|
67
|
+
https://mcp.fal.ai/mcp-relay`, then `codex mcp login fal`. **Paid calls go through `art.mjs gen`,
|
|
68
|
+
never the MCP's `run_model` or `submit_job`**: `gen` prices, caps, receipts and resumes; the MCP's run
|
|
69
|
+
does none of that (the Homie mod in Claude Code, and Homie's hooks in Codex, hold one as a call whose cost cannot be read first).
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
node <art.mjs> price --model fal-ai/flux/dev --input art/cover/input.json # free: fal's unit price x this input
|
|
73
|
+
node <art.mjs> budget cover --cap <dollars the person agreed to>
|
|
74
|
+
node <art.mjs> gen cover --model fal-ai/flux/dev --input art/cover/input.json --out cover-painted.jpg --dry-run
|
|
75
|
+
node <art.mjs> gen cover --model fal-ai/flux/dev --input art/cover/input.json --out cover-painted.jpg --yes
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- Tell the person the price and ask for a cap before the first call. `gen` refuses with no budget,
|
|
79
|
+
refuses past the cap, and asks again without `--yes`.
|
|
80
|
+
- The receipt is written the moment fal accepts the job. Running the same `gen` again resumes it from
|
|
81
|
+
`<out>.request`: it never pays twice. A cut-off call is resumed, not re-sent.
|
|
82
|
+
- **Never run paid generation in a loop** to "try until it is good". Look at what came back, change
|
|
83
|
+
the prompt, run it once more. Spend the riskiest image first.
|
|
84
|
+
- No real people, no real brands, no copyrighted characters, no "in the style of" a living artist, in
|
|
85
|
+
prompts or references.
|
|
86
|
+
- **Every prompt starts from the game's derived style prompt** (`homie-studio style prompt <id>`: medium,
|
|
87
|
+
shape, palette hexes, light, framing; computed from the decisions, never edited by hand) and uses the golden
|
|
88
|
+
images as references on a multi-reference model. A picture made under an older palette is listed stale when
|
|
89
|
+
the palette changes (the `style` skill's blast radius).
|
|
90
|
+
- What the provider's terms say about the pictures, with read dates: `references/RIGHTS.md`. Read the live page
|
|
91
|
+
again before anything commercial is published.
|
|
92
|
+
- 3D models (props, characters) are the `models` skill's; mood images for the style board are too
|
|
93
|
+
(`models.mjs mood`).
|
|
94
|
+
|
|
95
|
+
## 3. Backdrops, textures, plates
|
|
96
|
+
|
|
97
|
+
A procedural gradient sky is the tell that a game was generated; a painted one costs one image at build
|
|
98
|
+
time and loads like any other asset. Good candidates: a skybox or far matte, a ground or wall texture,
|
|
99
|
+
a character plate, a title card. Bad: anything the game generates procedurally for a reason.
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
node <art.mjs> tile art/floor/floor.png # does it tile? seam numbers and a 2x2 preview
|
|
103
|
+
node <art.mjs> fit art/floor/floor.png --out games/<id>/public/art/floor.jpg --width 1024 --max-kb 250
|
|
104
|
+
node <art.mjs> sheet art/floor # everything in a job, labelled, to look at
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- **Models are bad at tiling.** Ask for a texture, then check it: a seam over 2 shows as a grid of
|
|
108
|
+
lines in the game. Mirror-blend the edges or make it tile yourself before it ships.
|
|
109
|
+
- **Size is a feature.** A phone downloads every byte before the first round. Fit backdrops to the
|
|
110
|
+
size they are drawn at, JPEG (or WebP where ffmpeg has it) for pictures, PNG only for hard-edged
|
|
111
|
+
sprites with transparency. Then play it again: the `playtest` skill's first row times the load; an
|
|
112
|
+
asset that doubled the load time made the game worse.
|
|
113
|
+
- Put art where the game's own build puts assets (`games/<id>/public/` for a bundled game, the
|
|
114
|
+
game's folder for a static one) and load it with a relative path.
|
|
115
|
+
|
|
116
|
+
## 4. Art direction: paintovers, then change the game
|
|
117
|
+
|
|
118
|
+
Images do not fix a game that looks unfinished; the best-loved browser games often ship no texture
|
|
119
|
+
files at all. What makes a frame look finished is light, contrast between the subject and the ground,
|
|
120
|
+
detail at more than one scale, and a clear horizon or depth. The method:
|
|
121
|
+
|
|
122
|
+
1. **The style bible is the game's decisions** (the `style` skill): render style, palette with its jobs,
|
|
123
|
+
light, shape language, camera, fonts, drawn in the codex's Art direction tab. An older studio's
|
|
124
|
+
`art/STYLE.md` retires into it: carry what it says into `style set` / `style steer`, then keep only the
|
|
125
|
+
decisions. Paint with the LOCKED palette and light.
|
|
126
|
+
2. **Paintovers**: real frames of the game (`frame`), painted over by an image model with the derived style
|
|
127
|
+
prompt (`npx --no-install homie-studio style prompt <id>`) and the frame as the reference, plus the golden
|
|
128
|
+
images (`style golden`) where there are any. These are the TARGET, never shipped as the game.
|
|
129
|
+
3. **Change the game's own rendering** (lights, materials, fog, the floor, the camera) until frames of
|
|
130
|
+
the running game match the paintover; compare them side by side.
|
|
131
|
+
4. **Judge blind**: frames of the old build, the new one and a reference game the person admires,
|
|
132
|
+
shuffled under random labels, scored by a fresh reviewer before the labels are revealed (the
|
|
133
|
+
`playtest` skill's review). A reference is a bar, not a template.
|
|
134
|
+
|
|
135
|
+
## Tell the person
|
|
136
|
+
|
|
137
|
+
What was made and where it is used, the file sizes, what it cost (from the receipts) against the cap,
|
|
138
|
+
what is a real frame and what is generated, and the pictures themselves.
|
|
139
|
+
|
|
140
|
+
## Never
|
|
141
|
+
|
|
142
|
+
- Never ship generated art as a screenshot of gameplay, or a cover that shows what the game does not have.
|
|
143
|
+
- Never spend without a budget, past it, or without a receipt; never print or store a key.
|
|
144
|
+
- Never replace art a person made without asking.
|
|
145
|
+
|
|
146
|
+
### Existing provider credentials
|
|
147
|
+
|
|
148
|
+
The tools read `FAL_KEY` or `ELEVENLABS_API_KEY` first, then the plain single-line
|
|
149
|
+
key in `~/.homie/keys/fal-key` or `~/.homie/keys/elevenlabs-key`. They also accept
|
|
150
|
+
`~/.homie/workspace/fal-key` and `~/.homie/workspace/elevenlabs-key` from earlier
|
|
151
|
+
installs. Checks name the source, never the key. Keep these files private (mode
|
|
152
|
+
0600); never copy them into a studio. ElevenLabs also accepts its CLI browser
|
|
153
|
+
sign-in and the existing Stripe Projects connection. Run the tool’s `check`
|
|
154
|
+
before declaring a provider disconnected.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Rights for painted art
|
|
2
|
+
|
|
3
|
+
What fal's terms say about the pictures this skill makes on the person's own account, with the date the page was
|
|
4
|
+
read. Read the live page again before anything commercial is published, and update the date here.
|
|
5
|
+
|
|
6
|
+
- **fal** (https://fal.ai/terms, read 2026-10-02, "Last Updated: September 8, 2026"): the customer "owns and
|
|
7
|
+
retains all right, title, and interest in and to the Customer Input" (the prompt, a frame, a reference). There
|
|
8
|
+
is no matching clause that assigns the Output to the customer, and fal "does not represent, warrant, or
|
|
9
|
+
covenant that any Output Content will be original, will not infringe rights of any third party". Third-party
|
|
10
|
+
model outputs may not be used to train models that compete with them.
|
|
11
|
+
- **The models this skill uses** carry fal's "Commercial use" label (fal's FAQ: models marked "Research only" are
|
|
12
|
+
non-commercial; never use one of those for a game).
|
|
13
|
+
- **FLUX [dev]** outputs through fal may be used commercially ("You may use Output for any purpose (including for
|
|
14
|
+
commercial purposes)", the FLUX [dev] licence); its weights are non-commercial, so never run them locally for a
|
|
15
|
+
commercial game.
|
|
16
|
+
- **Nano Banana** pictures carry an invisible SynthID watermark.
|
|
17
|
+
- **Copyright in purely AI-made pictures** may be thin: never claim more than the terms say. A cover from a real
|
|
18
|
+
frame of the game (`frame`, `cover`) is the studio's own work.
|
|
19
|
+
|
|
20
|
+
The `models` skill's `references/RIGHTS.md` covers 3D models and the starter library.
|