@homie-rocks/studio 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/README.md +92 -0
  3. package/assets/assets.ts +500 -0
  4. package/assets/render-page.ts +629 -0
  5. package/assets/safety.mjs +194 -0
  6. package/bin/homie-studio.mjs +81 -2
  7. package/chat/CHAT.md +101 -0
  8. package/chat/OWNERS.md +78 -0
  9. package/lib/art-cli.mjs +296 -0
  10. package/lib/art-tools.mjs +331 -0
  11. package/lib/asset-check.mjs +189 -0
  12. package/lib/asset-import.mjs +136 -0
  13. package/lib/asset-manifest.mjs +318 -0
  14. package/lib/build.mjs +32 -2
  15. package/lib/chat-cli.mjs +89 -0
  16. package/lib/cloudflare.mjs +5 -2
  17. package/lib/codex.mjs +43 -1
  18. package/lib/colour.mjs +160 -0
  19. package/lib/decisions.mjs +800 -0
  20. package/lib/directory.mjs +9 -1
  21. package/lib/library.mjs +169 -0
  22. package/lib/mcp-tools.mjs +22 -4
  23. package/lib/mcp.mjs +6 -3
  24. package/lib/optimise.mjs +380 -0
  25. package/lib/remix-assets.mjs +84 -0
  26. package/lib/render3d.mjs +81 -0
  27. package/lib/scaffold.mjs +39 -2
  28. package/lib/site.mjs +6 -1
  29. package/lib/studio.mjs +136 -9
  30. package/lib/style-board.mjs +227 -0
  31. package/lib/style-presets.mjs +121 -0
  32. package/lib/template-history.json +76 -0
  33. package/mcp/ui/card.css +15 -0
  34. package/mcp/ui/cast.js +62 -0
  35. package/mcp/ui/decision.js +61 -0
  36. package/mcp/ui/lineup.js +68 -0
  37. package/mcp/ui/rights.js +30 -0
  38. package/mcp/ui/style.js +154 -0
  39. package/netplay/NETPLAY.md +191 -3
  40. package/netplay/netplay.ts +150 -3
  41. package/package.json +15 -2
  42. package/port/index.ts +3 -2
  43. package/port/view.ts +238 -0
  44. package/site/SITE.md +11 -1
  45. package/starters/ember-vale/src/main.ts +22 -2
  46. package/starters/gem-rush/src/main.ts +21 -1
  47. package/starters/gem-rush-3d/assets/RIGHTS.md +203 -0
  48. package/starters/gem-rush-3d/assets/manifest.json +2318 -0
  49. package/starters/gem-rush-3d/credits.json +31 -0
  50. package/starters/gem-rush-3d/game.json +11 -0
  51. package/starters/gem-rush-3d/index.html +20 -0
  52. package/starters/gem-rush-3d/lab.json +19 -0
  53. package/starters/gem-rush-3d/src/main.ts +2107 -0
  54. package/starters/gem-rush-3d/style.json +66 -0
  55. package/starters/gem-rush-3d/tunables.json +13 -0
  56. package/worker/agents.mjs +3 -3
  57. package/worker/chat-page.mjs +556 -0
  58. package/worker/chat-store.mjs +161 -0
  59. package/worker/chat-words.mjs +46 -0
  60. package/worker/chat.mjs +419 -0
  61. package/worker/index.mjs +153 -8
  62. package/worker/office-page.mjs +115 -0
  63. package/worker/office.mjs +137 -6
  64. package/worker/pages.mjs +32 -9
  65. package/worker/qr.mjs +11 -0
  66. package/worker/room.mjs +288 -10
  67. package/worker/servers.mjs +5 -1
  68. package/worker/site.mjs +9 -4
  69. package/worker/stats.mjs +4 -1
  70. package/worker/version.mjs +1 -1
@@ -924,6 +924,82 @@
924
924
  }
925
925
  }
926
926
  }
927
+ },
928
+ "0.22.0": {
929
+ "files": {
930
+ "CLAUDE.md": "ddf2b2a0376202f6",
931
+ "README.md": "684b3cca8001673c",
932
+ "HANDOFF.md": "ce7a8fd91c6bbee2",
933
+ "games/README.md": "5ad32fe80c61f117",
934
+ "music/README.md": "a3dc042dadd39df8",
935
+ "videos/README.md": "16babc3437d5545b",
936
+ "posts/README.md": "37841aae476bd540",
937
+ "site/README.md": "ca131cd0472464fb",
938
+ "site/src/worker.mjs": "ca3f1630f2420ad3"
939
+ },
940
+ "sections": {
941
+ "AGENTS.md": {
942
+ "(intro)": "214aa46eb3a895d5",
943
+ "## Layout": "8baf49e4291e7e88",
944
+ "## Commands (all through the pinned CLI in node_modules)": "d80ec56e4b4fc4c6",
945
+ "## The site": "ab922bb6bde23293",
946
+ "## Making games": "a8a893eeada33bf5",
947
+ "## The Game Codex and progress": "0738ef719cef3a4c",
948
+ "## Art direction and models": "124772c374de7390",
949
+ "## Running live games (the back office)": "4aa5f513a7cd2c3b",
950
+ "## Servers and AI seats": "0a332b07e22d3fce",
951
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
952
+ "## Rules": "7e8458adc1b81ece",
953
+ "## Beta": "9d02638207a26e73"
954
+ }
955
+ },
956
+ "named": {
957
+ "Homie Arcade": {
958
+ "sections": {
959
+ "AGENTS.md": {
960
+ "## Commands (all through the pinned CLI in node_modules)": "b35df2c19cd4b534"
961
+ }
962
+ }
963
+ }
964
+ }
965
+ },
966
+ "0.23.0": {
967
+ "files": {
968
+ "CLAUDE.md": "ddf2b2a0376202f6",
969
+ "README.md": "684b3cca8001673c",
970
+ "HANDOFF.md": "ce7a8fd91c6bbee2",
971
+ "games/README.md": "5ad32fe80c61f117",
972
+ "music/README.md": "a3dc042dadd39df8",
973
+ "videos/README.md": "16babc3437d5545b",
974
+ "posts/README.md": "37841aae476bd540",
975
+ "site/README.md": "ca131cd0472464fb",
976
+ "site/src/worker.mjs": "ca3f1630f2420ad3"
977
+ },
978
+ "sections": {
979
+ "AGENTS.md": {
980
+ "(intro)": "214aa46eb3a895d5",
981
+ "## Layout": "8baf49e4291e7e88",
982
+ "## Commands (all through the pinned CLI in node_modules)": "d80ec56e4b4fc4c6",
983
+ "## The site": "ab922bb6bde23293",
984
+ "## Making games": "a8a893eeada33bf5",
985
+ "## The Game Codex and progress": "0738ef719cef3a4c",
986
+ "## Art direction and models": "124772c374de7390",
987
+ "## Running live games (the back office)": "4aa5f513a7cd2c3b",
988
+ "## Servers and AI seats": "0a332b07e22d3fce",
989
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
990
+ "## Rules": "7e8458adc1b81ece",
991
+ "## Beta": "9d02638207a26e73"
992
+ }
993
+ },
994
+ "named": {
995
+ "Homie Arcade": {
996
+ "sections": {
997
+ "AGENTS.md": {
998
+ "## Commands (all through the pinned CLI in node_modules)": "b35df2c19cd4b534"
999
+ }
1000
+ }
1001
+ }
1002
+ }
927
1003
  }
928
1004
  }
929
1005
  }
package/mcp/ui/card.css CHANGED
@@ -72,4 +72,19 @@ ol.list,ul.list{list-style:none;margin:0;padding:0;display:grid;gap:6px}
72
72
  .ready{margin:0 0 8px;font-size:13px;color:var(--c-fg2)}.ready .ok{color:var(--c-ok);font-weight:600}
73
73
  .tabs{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}
74
74
  .tabs span{padding:4px 9px;border-radius:var(--r-full);background:var(--c-bg2);font-size:12px;color:var(--c-fg2)}
75
+ .dirs{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,240px),1fr));gap:10px;margin:8px 0}
76
+ .dir{display:grid;gap:6px;align-content:start;padding:8px;border-radius:var(--r-md);background:var(--c-bg2)}.dir.chosen{box-shadow:inset 0 0 0 2px var(--accent)}
77
+ .dir img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:var(--r-sm);display:block;background:var(--c-bg3)}
78
+ .dirhead{display:flex;gap:8px;align-items:baseline}.dirhead b{font-size:15px}.dirhead span{font-size:13px;color:var(--c-fg2)}
79
+ .dir .type{margin:0;font-size:20px;line-height:1.2;color:var(--c-fg)}.dir .fine{margin:0}
80
+ .chips{display:inline-flex;flex-wrap:wrap;gap:4px;margin-top:4px}.swatch{width:18px;height:18px;border-radius:5px;box-shadow:inset 0 0 0 1px var(--c-line2);display:inline-block}
81
+ .list.decisions li{flex-wrap:wrap}.list.decisions li.locked{box-shadow:inset 3px 0 0 var(--c-ok)}.list li .actions{margin-left:auto}
82
+ .steer{flex-basis:100%;display:flex;gap:8px;margin-top:6px}
83
+ input[type=text]{flex:1;min-width:0;font:14px/1.2 var(--f-sans);padding:8px 10px;border-radius:var(--r-sm);border:1px solid var(--c-line2);background:var(--c-bg);color:var(--c-fg)}
84
+ .mix{display:grid;gap:6px;margin:10px 0}.mixrow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mixname{min-width:84px;font:600 12px/1.2 var(--f-mono);color:var(--c-fg2)}
85
+ .list.cast li{align-items:center}.thumb{flex:none;width:52px;height:52px;border-radius:var(--r-sm);background:var(--c-bg3);display:grid;place-items:center;overflow:hidden}.thumb img{width:100%;height:100%;object-fit:cover}
86
+ .sil{display:block;max-width:100%;margin-top:8px;border-radius:var(--r-sm);background:#fff}
87
+ .budget{display:grid;grid-template-columns:110px 1fr auto;gap:10px;align-items:center;font-size:13px;color:var(--c-fg2);margin:6px 0}.budget .n{font:12px/1 var(--f-mono)}
88
+ .budget .track{height:8px;border-radius:4px;background:var(--c-bg3);overflow:hidden}.budget .track i{display:block;height:100%;background:var(--c-ok)}.budget .track i.over{background:var(--c-bad)}
89
+ details summary{cursor:pointer;color:var(--c-fg2);font-size:13px;margin-top:10px}
75
90
  @media (max-width:420px){.card{padding:14px 12px 12px;border-radius:var(--r-md)}h1{font-size:18px}.stages{grid-auto-flow:row;grid-auto-columns:auto}.frame{height:520px}}
package/mcp/ui/cast.js ADDED
@@ -0,0 +1,62 @@
1
+ /* The cast card: the assets a game needs with their route (the engine, the free CC0 starter library, generated, the
2
+ * person's own), a library match with its thumbnail or a price, against the art budget (assets_plan); or the starter
3
+ * library's matches for some words (assets_find), each with Add. Adding a library item is free; anything paid goes
4
+ * through the person's yes in the chat (asset_make prices it first). */
5
+ (function () {
6
+ var C = Card;
7
+ var busy = {};
8
+ function thumb(src, alt) { var t = C.el('div', 'thumb'); var i = src ? C.img(src, alt) : null; if (i) t.appendChild(i); else t.appendChild(C.el('span', '', '·')); return t; }
9
+ function addItem(sc, item, extra) {
10
+ if (busy[item.id] || !sc.game) return;
11
+ busy[item.id] = 'Adding…'; C.render();
12
+ var a = { game: sc.game, item: item.id };
13
+ for (var k in (extra || {})) if (extra[k] !== null && extra[k] !== undefined) a[k] = extra[k];
14
+ C.call('asset_add', a).then(function (r) {
15
+ busy[item.id] = r && r.isError ? C.textOf(r) : 'Added';
16
+ if (!(r && r.isError)) C.tell('add:' + item.id, 'The person added ' + item.id + ' (CC0, free) from the starter library to ' + sc.game + ' on the cast card.');
17
+ if (sc.mode === 'plan') C.call('assets_plan', { game: sc.game }).then(C.take).catch(C.render); else C.render();
18
+ }).catch(function () { busy[item.id] = 'Not added'; C.render(); });
19
+ }
20
+ function plan(card, sc) {
21
+ var list = C.el('ul', 'list cast');
22
+ (sc.rows || []).forEach(function (r) {
23
+ var li = C.el('li', r.state === 'made' ? 'done' : r.state === 'stale' ? 'now' : 'todo');
24
+ li.appendChild(thumb(r.match && r.match.thumb, r.match ? r.match.name : r.id));
25
+ li.appendChild(C.add(C.el('div', 'cb'), C.el('b', '', r.id + (r.card ? ' · ' + r.card : '')), C.el('span', '', r.kind + ', ' + r.tier + ' · ' + (r.state === 'planned' ? (r.match ? 'library match: ' + r.match.name + ' (' + r.match.tris + ' triangles, CC0)' : r.route === 'procedural' ? 'drawn by the game\'s code (free)' : 'no library match: generated, about US$' + r.usd) : r.state + ' (' + r.route + ')'))));
26
+ var side = C.el('div', 'actions');
27
+ if (r.state === 'planned' && r.match) side.appendChild(C.btn(busy[r.match.id] || 'Use it (free)', 'small', function () { addItem(sc, r.match, { as: r.id, card: r.card, height: r.heightM || null }); }));
28
+ else if (r.state === 'planned' && r.usd) side.appendChild(C.btn('Generate', 'ghost small', function () { C.tell('gen:' + r.id, 'The person wants ' + r.id + ' (' + (r.card || r.kind) + ') generated: price it with asset_make (a dry run) and ask before paying.'); busy[r.id] = 'Asked your AI to price it'; C.render(); }));
29
+ else side.appendChild(C.pill(r.state === 'made' ? 'Made' : r.state === 'stale' ? 'Stale' : r.state, r.state === 'made' ? 'ok' : r.state === 'stale' ? 'warn' : ''));
30
+ if (busy[r.id]) side.appendChild(C.el('span', 'fine', busy[r.id]));
31
+ li.appendChild(side);
32
+ list.appendChild(li);
33
+ });
34
+ card.appendChild(list);
35
+ card.appendChild(C.el('p', 'fine', 'To generate what the library cannot cover: about US$' + (sc.total || 0) + (sc.budget !== null && sc.budget !== undefined ? ' against an art budget of US$' + sc.budget : ' (no art budget: free routes only until you set one)') + '. Spent so far: US$' + (sc.spend ? sc.spend.used.toFixed(2) : '0.00') + '.'));
36
+ }
37
+ function find(card, sc) {
38
+ var grid = C.el('div', 'games');
39
+ (sc.items || []).forEach(function (it) {
40
+ var g = C.el('div', 'game');
41
+ var art = C.el('div', 'art'); var i = it.thumb ? C.img(it.thumb, it.name) : null; if (i) art.appendChild(i); else art.appendChild(document.createTextNode(it.name.slice(0, 1)));
42
+ g.appendChild(art);
43
+ g.appendChild(C.el('b', '', it.name));
44
+ g.appendChild(C.el('span', '', it.kind + ' · ' + it.tris + ' triangles · ' + it.kb + ' KB · ' + it.family + (it.rigged ? ' · rigged' : '')));
45
+ if (sc.game) g.appendChild(C.btn(busy[it.id] || 'Add (free)', 'small', function () { addItem(sc, it); }));
46
+ grid.appendChild(g);
47
+ });
48
+ card.appendChild(grid);
49
+ card.appendChild(C.el('p', 'fine', 'Every item is CC0 (free for any use, no credit required), already phone-sized. Adding one copies it into the game: it never loads from anywhere else.'));
50
+ }
51
+ function render(sc) {
52
+ var card = C.el('main', 'card');
53
+ var title = sc.mode === 'find' ? 'Free models for “' + sc.query + '”' : sc.mode === 'added' ? 'Added ' + (sc.added && sc.added.asset) : 'The cast of ' + sc.game;
54
+ C.add(card, C.top('Cast', sc.mode === 'plan' ? C.pill((sc.rows || []).filter(function (r) { return r.state === 'made'; }).length + ' of ' + (sc.rows || []).length + ' made', '') : null), C.el('h1', '', title));
55
+ if (sc.library && sc.library.error) card.appendChild(C.el('p', 'note', 'Starter library: ' + sc.library.error));
56
+ if (sc.mode === 'plan') plan(card, sc);
57
+ else if (sc.mode === 'find') find(card, sc);
58
+ else if (sc.added) card.appendChild(C.el('p', 'sub', sc.added.asset + ' is in games/' + sc.game + (sc.added.after ? ': ' + sc.added.after.tris + ' triangles, ' + sc.added.after.kb + ' KB' : '') + '. Licence: ' + (sc.added.license || 'CC0') + '.'));
59
+ return card;
60
+ }
61
+ C.start({ kind: 'cast', label: 'Cast', fullscreen: true, render: render, poll: null });
62
+ })();
@@ -0,0 +1,61 @@
1
+ /* The look-decision card: what one decision_set did. For a LOCKED decision about to change, the blast radius first:
2
+ * every asset made under it that would go stale, what remaking each would cost, and what a free palette re-tint fixes.
3
+ * Nothing changes until the person presses "Change it"; nothing is ever remade by itself. */
4
+ (function () {
5
+ var C = Card;
6
+ var STATE = { auto: 'Auto', steered: 'Steered', pinned: 'Pinned by use', locked: 'Locked' };
7
+ var done = null;
8
+ function chips(colours) {
9
+ var row = C.el('span', 'chips');
10
+ (colours || []).forEach(function (c) { var s = C.el('span', 'swatch'); s.style.background = c; s.title = c; row.appendChild(s); });
11
+ return row;
12
+ }
13
+ function blast(card, b) {
14
+ var list = C.el('ul', 'list');
15
+ (b.assets || []).forEach(function (a) {
16
+ var li = C.el('li', a.free ? 'done' : '');
17
+ li.appendChild(C.add(C.el('div', 'cb'), C.el('b', '', a.id + (a.card ? ' · ' + a.card : '')), C.el('span', '', a.route + ', made under its ' + a.why)));
18
+ li.appendChild(a.free ? C.pill('Free: ' + a.free.replace(/ \(.*\)$/, ''), 'ok') : a.remake.usd ? C.pill('US$' + a.remake.usd.toFixed(2) + ' to remake', 'warn') : C.pill(a.remake.how, ''));
19
+ list.appendChild(li);
20
+ });
21
+ card.appendChild(list);
22
+ card.appendChild(C.el('p', 'fine', (b.assets || []).length + ' asset' + ((b.assets || []).length === 1 ? '' : 's') + ' would go stale. Remaking those with no free fix: about US$' + (b.totals ? b.totals.usd.toFixed(2) : '0.00') + '. ' + (b.note || '')));
23
+ }
24
+ function render(sc) {
25
+ var card = C.el('main', 'card');
26
+ var r = sc.record;
27
+ var head = sc.pending ? C.pill('Not changed yet', 'warn') : sc.ok ? C.pill(r ? (STATE[r.state] || r.state) : 'Done', r && r.state === 'locked' ? 'ok' : '') : C.pill('Not done', 'bad');
28
+ C.add(card, C.top('Look decision', head), C.el('h1', '', sc.pending ? 'Change the locked ' + (sc.decision || '').replace('style.', '') + '?' : r ? r.id + ': ' + r.label : (sc.action === 'pick' ? 'Picked from the style board' : 'Done')));
29
+ if (r && r.colours) card.appendChild(chips(r.colours));
30
+ if (sc.why) card.appendChild(C.el('p', sc.ok ? 'sub' : 'note bad', sc.why));
31
+ if (sc.pending && sc.blast) {
32
+ card.appendChild(C.el('p', 'sub', 'From ' + (sc.blast.from || '?') + (sc.blast.to ? ' to ' + sc.blast.to : '') + '. This is what it would make stale:'));
33
+ blast(card, sc.blast);
34
+ if (done) card.appendChild(C.el('p', 'note ' + (done === 'changed' ? 'ok' : ''), done === 'changed' ? 'Changed. The stale assets are listed; nothing was remade.' : 'Kept as it was.'));
35
+ else {
36
+ var acts = C.el('div', 'actions');
37
+ acts.appendChild(C.btn('Change it', 'danger', function () {
38
+ var a = sc.ask || {};
39
+ C.call('decision_set', { game: sc.game, decision: a.decision, value: a.value, unlock: true, reason: a.reason || 'the person confirmed on the card', confirm: true, by: 'person' }).then(function (res) {
40
+ done = 'changed';
41
+ C.tell('changed:' + Date.now(), 'The person confirmed changing the locked ' + a.decision + ' on the card. ' + ((sc.blast.assets || []).length) + ' asset(s) are now stale; nothing was remade. Ask before remaking any.');
42
+ C.take(res);
43
+ });
44
+ }));
45
+ acts.appendChild(C.btn('Keep it', 'ghost', function () { done = 'kept'; C.tell('kept:' + Date.now(), 'The person kept the locked ' + (sc.ask && sc.ask.decision) + ' as it is.'); C.render(); }));
46
+ card.appendChild(acts);
47
+ }
48
+ } else if (sc.blast) {
49
+ card.appendChild(C.el('p', 'sub', 'Made under the old one (stale now, nothing remade):'));
50
+ blast(card, sc.blast);
51
+ }
52
+ if (sc.changed && sc.changed.length && sc.action !== 'set') card.appendChild(C.el('p', 'fine', 'Changed: ' + sc.changed.join(', ')));
53
+ if (sc.refused && sc.refused.length) card.appendChild(C.el('p', 'fine', 'Left as locked: ' + sc.refused.join(', ')));
54
+ if (!sc.pending && sc.stale && sc.stale.length) card.appendChild(C.el('p', 'note', 'Stale: ' + sc.stale.join(', ') + '. Remaking any is the person\'s call, priced first.'));
55
+ var foot = C.el('div', 'foot');
56
+ foot.appendChild(C.el('span', 'when', 'Spent US$' + (sc.spend ? sc.spend.used.toFixed(2) : '0.00') + (sc.spend && sc.spend.cap !== null ? ' of US$' + sc.spend.cap : '')));
57
+ card.appendChild(foot);
58
+ return card;
59
+ }
60
+ C.start({ kind: 'decision', label: 'Look decision', render: render, poll: null });
61
+ })();
@@ -0,0 +1,68 @@
1
+ /* The lineup card: every made asset at true scale on a 1 m grid under the game's light, front and three-quarter, their
2
+ * silhouettes at phone size, each asset's flags (over budget, palette drift, an unreadable silhouette, another library
3
+ * family, stale) and the scene's budgets as bars (asset_lineup). Also a generated prop on its way (asset_make): the price
4
+ * before anything is paid, the concept to look at, then the model; each paid step only on the person's press. */
5
+ (function () {
6
+ var C = Card;
7
+ var state = { busy: null };
8
+ function bar(label, value, budget, unit) {
9
+ if (value === null || value === undefined || !budget) return null;
10
+ var row = C.el('div', 'budget');
11
+ var pct = Math.min(100, Math.round((value / budget) * 100));
12
+ var b = C.el('div', 'track'); var f = C.el('i'); f.style.width = pct + '%'; if (value > budget) f.className = 'over'; b.appendChild(f);
13
+ C.add(row, C.el('span', '', label), b, C.el('span', 'n', (typeof value === 'number' ? value.toLocaleString('en-US') : value) + ' / ' + budget.toLocaleString('en-US') + (unit || '')));
14
+ return row;
15
+ }
16
+ function lineup(card, sc) {
17
+ var pics = C.el('div', 'preview');
18
+ [['front', 'Front, true scale on a 1 m grid'], ['quarter', 'Three-quarter, under the game\'s light']].forEach(function (p) { var i = sc.images && C.img(sc.images[p[0]], p[1]); if (i) { i.style.maxHeight = 'none'; i.style.objectFit = 'contain'; pics.appendChild(i); } });
19
+ card.appendChild(pics);
20
+ if (sc.images && sc.images.silhouettes) { var s = C.img(sc.images.silhouettes, 'Silhouettes at 64 px'); if (s) { s.className = 'sil'; card.appendChild(s); } }
21
+ var list = C.el('ul', 'list');
22
+ (sc.rows || []).forEach(function (r) {
23
+ var li = C.el('li', r.flags.length ? 'now' : 'done');
24
+ li.appendChild(C.add(C.el('div', 'cb'), C.el('b', '', r.label + (r.size ? ' · ' + r.size[1] + ' m' : '')), C.el('span', '', r.kind + ', ' + r.route + (r.drift ? (r.drift.repaint ? ' · repainted from style.json in the game' : ' · palette distance ' + r.drift.mean) : '')), r.flags.length ? C.el('span', 'fix', r.flags.join(' · ')) : null));
25
+ li.appendChild(C.pill(r.flags.length ? r.flags.length + ' flag' + (r.flags.length === 1 ? '' : 's') : 'Fits', r.flags.length ? 'warn' : 'ok'));
26
+ list.appendChild(li);
27
+ });
28
+ card.appendChild(list);
29
+ if (sc.totals && sc.budgets) {
30
+ card.appendChild(C.el('h2', '', 'Phone budgets, the whole scene'));
31
+ [bar('Draw calls', sc.totals.drawCalls, sc.budgets.drawCalls), bar('Triangles', sc.totals.triangles, sc.budgets.triangles), bar('Picture memory', sc.totals.textureMB, sc.budgets.textureMB, ' MB'), bar('First play', sc.totals.firstPlayMB, sc.budgets.firstPlayMB, ' MB')].forEach(function (b) { if (b) card.appendChild(b); });
32
+ }
33
+ }
34
+ function make(card, sc) {
35
+ var run = function (args, note) {
36
+ state.busy = note; C.render();
37
+ var a = { game: sc.game, asset: sc.asset };
38
+ for (var q in (sc.request || {})) a[q] = sc.request[q];
39
+ for (var k in args) a[k] = args[k];
40
+ C.call('asset_make', a).then(function (r) { state.busy = null; C.take(r); }).catch(function () { state.busy = 'That did not reach the studio.'; C.render(); });
41
+ };
42
+ if (sc.concept) { var i = C.img(sc.concept, 'The concept for ' + sc.asset); if (i) { i.style.maxHeight = '360px'; i.style.objectFit = 'contain'; card.appendChild(C.add(C.el('div', 'preview'), i)); } }
43
+ if (sc.dryRun) {
44
+ card.appendChild(C.el('p', 'sub', (sc.step === 'mesh' ? 'The 3D model' : 'The concept image') + ' costs about US$' + (sc.price ? sc.price.usd : '?') + (sc.total ? '; the whole prop about US$' + sc.total : '') + ', on your own fal account.'));
45
+ card.appendChild(C.add(C.el('div', 'actions'), C.btn('Make it (US$' + (sc.price ? sc.price.usd : '?') + ')', '', function () { C.tell('approve:' + sc.asset + sc.step, 'The person approved US$' + (sc.price && sc.price.usd) + ' for the ' + sc.step + ' of ' + sc.asset + ' on the card.'); run({ step: sc.step, approve: true }, 'Making it…'); })));
46
+ } else if (sc.needs === 'approval' || sc.why) card.appendChild(C.el('p', 'note', sc.why || 'Needs the person\'s yes.'));
47
+ else if ((sc.step === 'concept' || sc.step === 'look') && sc.concept) {
48
+ card.appendChild(C.el('p', 'sub', 'Look at it: the thing, whole, in the game\'s style, on a plain background? Then the 3D model (about US$' + (sc.then ? sc.then.usd : '0.50') + ').'));
49
+ card.appendChild(C.add(C.el('div', 'actions'), C.btn('Make the 3D model', '', function () { C.tell('mesh:' + sc.asset, 'The person approved the 3D model of ' + sc.asset + ' (about US$' + (sc.then ? sc.then.usd : '0.50') + ') on the card.'); run({ step: 'mesh', approve: true }, 'Making the model (1 to 3 minutes)…'); })));
50
+ } else if (sc.step === 'mesh' && sc.after) {
51
+ card.appendChild(C.el('p', 'note ok', sc.asset + ': ' + sc.before.tris + ' → ' + sc.after.tris + ' triangles, ' + sc.after.kb + ' KB, ' + sc.after.heightM + ' m. Recorded with its receipts and licence.'));
52
+ card.appendChild(C.add(C.el('div', 'actions'), C.btn('Show the lineup', 'ghost', function () { state.busy = 'Drawing the lineup…'; C.render(); C.call('asset_lineup', { game: sc.game }).then(function (r) { state.busy = null; C.take(r); }); })));
53
+ }
54
+ }
55
+ function render(sc) {
56
+ var card = C.el('main', 'card');
57
+ var flagged = sc.mode === 'lineup' ? sc.flagged : null;
58
+ C.add(card, C.top(sc.mode === 'make' ? 'Make a prop' : 'Lineup', C.add(C.el('div', 'actions'), flagged !== null && flagged !== undefined ? C.pill(flagged ? flagged + ' flagged' : 'One look', flagged ? 'warn' : 'ok') : null, C.fullscreenButton())),
59
+ C.el('h1', '', sc.mode === 'make' ? sc.asset : 'The cast of ' + sc.game + ', at true scale'));
60
+ if (state.busy) card.appendChild(C.el('p', 'note', state.busy));
61
+ if (sc.mode === 'make') make(card, sc); else lineup(card, sc);
62
+ var foot = C.el('div', 'foot');
63
+ foot.appendChild(C.el('span', 'when', 'Spent US$' + (sc.spend ? sc.spend.used.toFixed(2) : '0.00') + (sc.spend && sc.spend.cap !== null ? ' of US$' + sc.spend.cap : '') + ' · receipts in art/receipts.jsonl'));
64
+ card.appendChild(foot);
65
+ return card;
66
+ }
67
+ C.start({ kind: 'lineup', label: 'Lineup', fullscreen: true, render: render, poll: null });
68
+ })();
@@ -0,0 +1,30 @@
1
+ /* The rights card: every asset's licence in plain words (asset_rights): where it came from, its licence, the credit it
2
+ * owes, what a remixer gets; what would stop a public game from publishing, with its fix; and RIGHTS.md itself. */
3
+ (function () {
4
+ var C = Card;
5
+ var REMIX = { include: 'A remix gets it', reference: 'A remix fetches it from its origin', none: 'A remix gets a placeholder' };
6
+ var LICENCE = { cc0: 'CC0 (public domain)', 'cc-by-4.0': 'CC BY 4.0', 'cc-by-3.0': 'CC BY 3.0', own: 'Your own work', generated: 'Generated on your account', qal: 'Quaternius licence', mixamo: 'Mixamo', other: 'Other' };
7
+ function render(sc) {
8
+ var card = C.el('main', 'card');
9
+ var refuse = (sc.problems || []).filter(function (p) { return p.level === 'refuse'; });
10
+ C.add(card, C.top('Rights', C.pill(refuse.length ? refuse.length + ' to fix' : 'Publishable', refuse.length ? 'bad' : 'ok')), C.el('h1', '', 'Who owns what in ' + sc.game));
11
+ var list = C.el('ul', 'list');
12
+ (sc.rows || []).forEach(function (r) {
13
+ var li = C.el('li', r.license ? 'done' : 'now');
14
+ li.appendChild(C.add(C.el('div', 'cb'), C.el('b', '', r.id + ' · ' + r.kind), C.el('span', '', (LICENCE[r.license] || r.license || 'NO LICENCE RECORDED') + ' · ' + r.route + (r.from ? ' (' + r.from + ')' : '') + (r.placeholder ? ' · a placeholder' : '')), r.attribution ? C.el('span', 'fix', 'Credit: ' + r.attribution) : null));
15
+ li.appendChild(C.pill(REMIX[r.remix] || 'No remix rule', r.remix === 'include' ? 'ok' : 'warn'));
16
+ list.appendChild(li);
17
+ });
18
+ card.appendChild(list);
19
+ (sc.problems || []).forEach(function (p) { card.appendChild(C.el('p', 'note' + (p.level === 'refuse' ? ' bad' : ''), p.asset + ': ' + p.problem + (p.fix ? ' (' + p.fix + ')' : ''))); });
20
+ if (sc.text) {
21
+ var d = C.el('details'); d.appendChild(C.el('summary', '', sc.file || 'RIGHTS.md'));
22
+ d.appendChild(C.el('pre', 'log', sc.text));
23
+ d.ontoggle = function () { C.size(); };
24
+ card.appendChild(d);
25
+ }
26
+ card.appendChild(C.el('p', 'fine', 'Terms change: read the providers\' live pages again before publishing anything commercial.'));
27
+ return card;
28
+ }
29
+ C.start({ kind: 'rights', label: 'Rights', render: render, poll: null });
30
+ })();
@@ -0,0 +1,154 @@
1
+ /* The style board card: a game's three directions, each drawn by the game engine (a painted mood image beside it only as
2
+ * a labelled target), with Pick, Mix, Steer and Lock; and the style decisions with their state (auto, steered, pinned
3
+ * by use, locked). Every press is the person's: decision_set with by "person", then style_board redraws the card, and
4
+ * the model is told once what changed. */
5
+ (function () {
6
+ var C = Card;
7
+ var STATE = { auto: 'Auto', steered: 'Steered', pinned: 'Pinned', locked: 'Locked' };
8
+ var STATE_CLS = { auto: '', steered: '', pinned: 'live', locked: 'ok' };
9
+ var ui = { mix: false, steer: null, busy: false, msg: null };
10
+ var fontsAsked = {};
11
+ function loadFont(f) {
12
+ if (!f || fontsAsked[f] || !/^[A-Za-z0-9 ]{1,40}$/.test(f)) return;
13
+ fontsAsked[f] = true;
14
+ var l = document.createElement('link'); l.rel = 'stylesheet';
15
+ l.href = 'https://fonts.googleapis.com/css2?family=' + encodeURIComponent(f).replace(/%20/g, '+') + ':wght@400;700&display=swap';
16
+ l.onload = function () { C.size(); };
17
+ document.head.appendChild(l);
18
+ }
19
+ function chips(colours) {
20
+ var row = C.el('span', 'chips');
21
+ (colours || []).forEach(function (c) { if (!/^#[0-9a-f]{3,8}$/i.test(c)) return; var s = C.el('span', 'swatch'); s.style.background = c; s.title = c; row.appendChild(s); });
22
+ return row;
23
+ }
24
+ function refresh(sc, note) {
25
+ return C.call('style_board', { game: sc.game }).then(function (r) { ui.busy = false; ui.msg = note || null; C.take(r); }).catch(function () { ui.busy = false; C.render(); });
26
+ }
27
+ function act(sc, args, tell, note) {
28
+ if (ui.busy) return;
29
+ ui.busy = true; ui.msg = 'Working…'; C.render();
30
+ var a = { game: sc.game, by: 'person' };
31
+ for (var k in args) a[k] = args[k];
32
+ C.call('decision_set', a).then(function (r) {
33
+ var why = r && r.isError ? C.textOf(r) : null;
34
+ if (!why && tell) C.tell(tell.key + ':' + Date.now(), tell.text);
35
+ refresh(sc, why || note);
36
+ }).catch(function () { ui.busy = false; ui.msg = 'That did not reach the studio.'; C.render(); });
37
+ }
38
+ function direction(sc, d) {
39
+ var box = C.el('div', 'dir' + (d.chosen ? ' chosen' : ''));
40
+ if (d.swatch) { var i = C.img(d.swatch, 'Direction ' + d.id.toUpperCase() + ', drawn by the game engine'); if (i) box.appendChild(i); }
41
+ if (d.mood && d.mood.image) {
42
+ var m = C.img(d.mood.image, 'A painted mood image (a target)');
43
+ if (m) { box.appendChild(m); box.appendChild(C.el('p', 'fine', 'Painted target, not what the game draws' + (d.mood.usd ? ' (US$' + d.mood.usd + ')' : ''))); }
44
+ }
45
+ var head = C.add(C.el('div', 'dirhead'), C.el('b', '', d.id.toUpperCase()), C.el('span', '', d.label));
46
+ box.appendChild(head);
47
+ box.appendChild(chips(d.colours));
48
+ if (d.fonts && d.fonts.display) {
49
+ loadFont(d.fonts.display); loadFont(d.fonts.body);
50
+ var t = C.el('p', 'type', sc.title);
51
+ t.style.fontFamily = '"' + d.fonts.display + '", var(--f-sans)';
52
+ var b = C.el('p', 'fine', d.fonts.display + ' and ' + d.fonts.body);
53
+ if (d.fonts.body) b.style.fontFamily = '"' + d.fonts.body + '", var(--f-sans)';
54
+ box.appendChild(t); box.appendChild(b);
55
+ }
56
+ var acts = C.el('div', 'actions');
57
+ acts.appendChild(C.btn(d.chosen ? 'Picked' : 'Pick ' + d.id.toUpperCase(), d.chosen ? 'ghost small' : 'small', function () {
58
+ act(sc, { pick: d.id }, { key: 'pick', text: 'The person picked direction ' + d.id.toUpperCase() + ' (' + d.label + ') on the style board.' }, 'Picked ' + d.id.toUpperCase() + '.');
59
+ }));
60
+ box.appendChild(acts);
61
+ return box;
62
+ }
63
+ function mixTable(sc) {
64
+ var rows = {};
65
+ sc.directions.forEach(function (d) { (d.rows || []).forEach(function (r) { (rows[r.decision] = rows[r.decision] || {})[d.id] = r; }); });
66
+ var t = C.el('div', 'mix');
67
+ Object.keys(rows).forEach(function (dec) {
68
+ var line = C.el('div', 'mixrow');
69
+ line.appendChild(C.el('span', 'mixname', dec.replace('style.', '')));
70
+ sc.directions.forEach(function (d) {
71
+ var r = rows[dec][d.id]; if (!r) return;
72
+ var b = C.btn(d.id.toUpperCase() + ' · ' + r.label, r.same ? 'small' : 'ghost small', function () {
73
+ var m = {}; m[dec] = d.id;
74
+ act(sc, { mix: m }, { key: 'mix', text: 'The person took the ' + dec + ' of direction ' + d.id.toUpperCase() + ' (' + r.label + ') on the style board.' }, 'Mixed in ' + dec + ' from ' + d.id.toUpperCase() + '.');
75
+ });
76
+ b.title = r.same ? 'the game has this now' : 'take this one';
77
+ line.appendChild(b);
78
+ });
79
+ t.appendChild(line);
80
+ });
81
+ return t;
82
+ }
83
+ function decisions(sc) {
84
+ var style = (sc.phases || []).filter(function (p) { return p.id === 'style'; })[0];
85
+ if (!style) return null;
86
+ var list = C.el('ul', 'list decisions');
87
+ style.rows.forEach(function (r) {
88
+ var li = C.el('li', r.state);
89
+ var body = C.add(C.el('div', 'cb'), C.add(C.el('b'), document.createTextNode(r.name + ' ')), C.el('span', '', r.label + (r.steer ? ' (“' + r.steer + '”)' : '')));
90
+ if (r.colours) body.appendChild(chips(r.colours));
91
+ li.appendChild(body);
92
+ var side = C.el('div', 'actions');
93
+ side.appendChild(C.pill(STATE[r.state] || r.state, STATE_CLS[r.state] || ''));
94
+ if (r.state !== 'locked') {
95
+ side.appendChild(C.btn('Steer', 'ghost small', function () { ui.steer = r.id; C.render(); }));
96
+ side.appendChild(C.btn('Lock', 'ghost small', function () {
97
+ act(sc, { decision: r.id, lock: true, words: 'tapped Lock on the style board' }, { key: 'lock', text: 'The person locked ' + r.id + ' (' + r.label + ') on the style board.' }, 'Locked ' + r.name + '.');
98
+ }));
99
+ }
100
+ li.appendChild(side);
101
+ if (ui.steer === r.id) {
102
+ var form = C.el('div', 'steer');
103
+ var input = C.el('input'); input.type = 'text'; input.placeholder = r.id === 'style.palette' ? 'warmer, less saturated, #ff8a3d accent…' : r.id === 'style.camera' ? 'closer, higher, top-down…' : r.id === 'style.light' ? 'golden hour, harder shadows…' : 'in your words';
104
+ input.maxLength = 100;
105
+ var go = function () { var w = input.value.trim(); if (!w) return; ui.steer = null; act(sc, { decision: r.id, steer: w }, { key: 'steer', text: 'The person steered ' + r.id + ': "' + w + '".' }, 'Steered ' + r.name + ': ' + w + '.'); };
106
+ input.onkeydown = function (e) { if (e.key === 'Enter') go(); if (e.key === 'Escape') { ui.steer = null; C.render(); } };
107
+ form.appendChild(input); form.appendChild(C.btn('Steer', 'small', go));
108
+ li.appendChild(form);
109
+ setTimeout(function () { input.focus(); }, 0);
110
+ }
111
+ list.appendChild(li);
112
+ });
113
+ return list;
114
+ }
115
+ function render(sc) {
116
+ var card = C.el('main', 'card');
117
+ var locked = 0; var total = 0;
118
+ (sc.phases || []).forEach(function (p) { if (p.id !== 'style') return; p.rows.forEach(function (r) { total++; if (r.state === 'locked') locked++; }); });
119
+ C.add(card, C.top('Style board', C.add(C.el('div', 'actions'), C.pill(locked === total && total ? 'Style locked' : locked + ' of ' + total + ' locked', locked === total && total ? 'ok' : ''), C.fullscreenButton())),
120
+ C.el('h1', '', sc.title), C.el('p', 'sub', sc.line || 'No decisions yet.'));
121
+ if (ui.msg) card.appendChild(C.el('p', 'note', ui.msg));
122
+ if (!sc.directions || !sc.directions.length) {
123
+ card.appendChild(C.el('p', 'note', 'No board yet: three directions, drawn by the game engine, free.'));
124
+ card.appendChild(C.add(C.el('div', 'actions'), C.btn('Draw the board', '', function () { ui.msg = 'Drawing (about 20 s)…'; C.render(); C.call('style_explore', { game: sc.game }).then(function (r) { ui.msg = null; C.take(r); }); })));
125
+ } else {
126
+ var grid = C.el('div', 'dirs');
127
+ sc.directions.forEach(function (d) { grid.appendChild(direction(sc, d)); });
128
+ card.appendChild(grid);
129
+ card.appendChild(C.el('p', 'fine', 'Each swatch is what the game will look like: drawn by its own engine, with the free starter library\'s pieces re-tinted into the palette.'));
130
+ var bar = C.el('div', 'actions');
131
+ bar.appendChild(C.btn(ui.mix ? 'Done mixing' : 'Mix', 'ghost small', function () { ui.mix = !ui.mix; C.render(); }));
132
+ bar.appendChild(C.btn('Lock the style', 'small', function () {
133
+ act(sc, { decision: 'style.render', lock: 'style', words: 'tapped Lock the style on the style board' }, { key: 'lockall', text: 'The person locked the whole style (every style decision) on the style board.' }, 'The style is locked.');
134
+ }));
135
+ if (!sc.directions.some(function (d) { return d.mood; })) {
136
+ bar.appendChild(C.btn(sc.quote && sc.quote.usd ? 'Paint mood images (US$' + sc.quote.usd + ')' : 'Price mood images', 'ghost small', function () {
137
+ if (sc.quote && sc.quote.usd) {
138
+ C.tell('mood:' + Date.now(), 'The person asked for the three painted mood images at about US$' + sc.quote.usd + ' on their own fal account (a target, labelled so). Make them with style_explore paid and approve.');
139
+ ui.msg = 'Asked your AI to paint them (about US$' + sc.quote.usd + ', your fal account).'; C.render();
140
+ } else { ui.msg = 'Pricing…'; C.render(); C.call('style_explore', { game: sc.game, paid: true }).then(function (r) { ui.msg = null; C.take(r); }); }
141
+ }));
142
+ }
143
+ card.appendChild(bar);
144
+ if (ui.mix) card.appendChild(mixTable(sc));
145
+ }
146
+ var dl = decisions(sc);
147
+ if (dl) { card.appendChild(C.el('h2', '', 'Style decisions')); card.appendChild(dl); }
148
+ var foot = C.el('div', 'foot');
149
+ foot.appendChild(C.el('span', 'when', (sc.stale && sc.stale.length ? sc.stale.length + ' stale asset' + (sc.stale.length === 1 ? '' : 's') + ' · ' : '') + 'Spent US$' + (sc.spend ? sc.spend.used.toFixed(2) : '0.00') + (sc.spend && sc.spend.cap !== null ? ' of US$' + sc.spend.cap : '')));
150
+ card.appendChild(foot);
151
+ return card;
152
+ }
153
+ C.start({ kind: 'style', label: 'Style board', fullscreen: true, render: render, poll: null });
154
+ })();