@homie-rocks/studio 0.20.0 → 0.22.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 +78 -1
- package/README.md +55 -0
- package/assets/assets.ts +500 -0
- package/assets/render-page.ts +629 -0
- package/assets/safety.mjs +194 -0
- package/bin/homie-studio.mjs +52 -3
- package/lib/art-cli.mjs +296 -0
- package/lib/art-tools.mjs +331 -0
- package/lib/asset-check.mjs +189 -0
- package/lib/asset-import.mjs +136 -0
- package/lib/asset-manifest.mjs +318 -0
- package/lib/build.mjs +25 -2
- package/lib/check.mjs +3 -1
- package/lib/codex.mjs +43 -1
- package/lib/colour.mjs +160 -0
- package/lib/decisions.mjs +800 -0
- package/lib/directory.mjs +9 -1
- package/lib/library.mjs +169 -0
- package/lib/mcp-tools.mjs +22 -4
- package/lib/mcp.mjs +6 -3
- package/lib/office.mjs +30 -8
- package/lib/optimise.mjs +380 -0
- package/lib/port-check.mjs +3 -1
- package/lib/progress.mjs +16 -1
- package/lib/remix-assets.mjs +84 -0
- package/lib/render3d.mjs +81 -0
- package/lib/scaffold.mjs +32 -1
- package/lib/site.mjs +6 -1
- package/lib/studio.mjs +136 -9
- package/lib/style-board.mjs +227 -0
- package/lib/style-presets.mjs +121 -0
- package/lib/template-history.json +75 -0
- package/lib/thumb.mjs +82 -0
- package/mcp/ui/card.css +15 -0
- package/mcp/ui/cast.js +62 -0
- package/mcp/ui/decision.js +61 -0
- package/mcp/ui/lineup.js +68 -0
- package/mcp/ui/rights.js +30 -0
- package/mcp/ui/style.js +154 -0
- package/package.json +14 -2
- package/starters/gem-rush-3d/assets/RIGHTS.md +203 -0
- package/starters/gem-rush-3d/assets/manifest.json +2318 -0
- package/starters/gem-rush-3d/credits.json +31 -0
- package/starters/gem-rush-3d/game.json +11 -0
- package/starters/gem-rush-3d/index.html +20 -0
- package/starters/gem-rush-3d/lab.json +19 -0
- package/starters/gem-rush-3d/src/main.ts +2086 -0
- package/starters/gem-rush-3d/style.json +66 -0
- package/starters/gem-rush-3d/tunables.json +13 -0
- package/worker/index.mjs +5 -3
- package/worker/pages.mjs +13 -5
- package/worker/qr.mjs +11 -0
- package/worker/site.mjs +5 -3
- package/worker/version.mjs +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE STYLE VOCABULARY the decisions pick from (lib/decisions.mjs): render styles, named palettes, light and camera
|
|
3
|
+
* set-ups, fonts and the starter library's families, each with the words in a prompt that point at it. Taste, kept
|
|
4
|
+
* as data: the automatic path reads it, the style board mixes it, and a person steers past it with their own values.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** How the world is drawn. `family`: the starter library family that fits; `materials`: the material model. */
|
|
8
|
+
export const RENDERS = Object.freeze({
|
|
9
|
+
'lowpoly-flat': { label: 'Flat low-poly', words: /\b(low[- ]?poly|cozy|cosy|cute|toy|minimal|clean|simple|wholesome|chill)\b/i, materials: 'flat', outline: false, family: 'kenney', fonts: { display: 'Fredoka', body: 'Nunito' }, vfx: 'soft', note: 'faceted shapes in flat palette colours: the cheapest look on a phone, the most coherent, and the richest free library' },
|
|
10
|
+
toon: { label: 'Toon / cel', words: /\b(toon|cel|cartoon|anime|comic|neon|cyber|synth|arcade)\b/i, materials: 'toon', outline: true, family: 'kaykit', fonts: { display: 'Lilita One', body: 'Nunito' }, vfx: 'toon', note: 'banded light and ink outlines: reads at any size, loud on a TV' },
|
|
11
|
+
painted: { label: 'Stylised hand-painted', words: /\b(painterly|painted|storybook|watercolou?r|ghibli-like|fairy ?tale|whimsical|hand[- ]painted)\b/i, materials: 'hand-painted', outline: false, family: 'kaykit', fonts: { display: 'Fraunces', body: 'Lora' }, vfx: 'soft', note: 'soft painted albedo with the light baked gently in: warm and storybook' },
|
|
12
|
+
'stylised-pbr': { label: 'Stylised PBR', words: /\b(realistic|real|photo|gritty|grounded|military|pbr)\b/i, materials: 'pbr', outline: false, family: 'polyhaven', fonts: { display: 'Rajdhani', body: 'Inter' }, vfx: 'soft', note: 'metal and roughness under real light: the most expensive look on a phone (textures and memory)' },
|
|
13
|
+
'pixel-hd2d': { label: 'Pixel / HD-2D', words: /\b(pixel|8[- ]?bit|16[- ]?bit|retro|hd[- ]?2d|sprite)\b/i, materials: 'pixel', outline: false, family: 'kenney', fonts: { display: 'Press Start 2P', body: 'Silkscreen' }, vfx: 'pixel', note: 'pixel sprites in a low-poly world with tilt-shift and bloom (the sprite branch is phase 3; the board shows the world)' },
|
|
14
|
+
voxel: { label: 'Voxel', words: /\b(voxel|blocky|blocks|cube|minecraft-like)\b/i, materials: 'flat', outline: false, family: 'kenney', fonts: { display: 'Silkscreen', body: 'Nunito' }, vfx: 'pixel', note: 'everything built of cubes: crisp, cheap, playful' },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** The codex keys every palette fills (frontmatter `palette`), plus `ramp`: 12 to 16 colours for textures. */
|
|
18
|
+
export const PALETTE_KEYS = Object.freeze(['bg', 'ink', 'accent', 'accent2', 'danger', 'good', 'gold']);
|
|
19
|
+
|
|
20
|
+
export const PALETTES = Object.freeze({
|
|
21
|
+
'autumn-grove': { label: 'Autumn grove: rust, moss and cream', words: /\b(forest|woods?|grove|autumn|fall|fox(es)?|berr(y|ies)|mushroom|acorn|leaf|leaves)\b/i,
|
|
22
|
+
bg: '#1f2a24', ink: '#f6ecd9', accent: '#e9893a', accent2: '#8fbf5a', danger: '#e2553f', good: '#7fcf6a', gold: '#f2c14e',
|
|
23
|
+
ramp: ['#2c3b31', '#3f5640', '#5d7a4a', '#8fbf5a', '#c9d98a', '#6b4a2f', '#9a6a3d', '#c98a4b', '#e9893a', '#b8442e', '#f6ecd9', '#d9c7a3', '#f2c14e', '#7a5a8c'] },
|
|
24
|
+
'meadow-morning': { label: 'Meadow morning: sky blue, grass and butter', words: /\b(meadow|farm|garden|spring|sunny|bright|happy|picnic|village)\b/i,
|
|
25
|
+
bg: '#cfe8f5', ink: '#1d2b3a', accent: '#ff8a5b', accent2: '#5cbf8a', danger: '#e2553f', good: '#45b36b', gold: '#ffd25a',
|
|
26
|
+
ramp: ['#9fd3ef', '#cfe8f5', '#f8f3e6', '#8fd06a', '#5cbf8a', '#3f8f5a', '#ffd25a', '#ffb347', '#ff8a5b', '#e86a6a', '#b07ad9', '#8a6a4a', '#5a4a3a', '#ffffff'] },
|
|
27
|
+
'embers-night': { label: 'Embers at night: warm golds on soot', words: /\b(fire|ember|lava|volcano|forge|torch|lantern|burning|ash(en)?)\b/i,
|
|
28
|
+
bg: '#120c0a', ink: '#f6e7d3', accent: '#f4b23c', accent2: '#7ad38a', danger: '#ff5a4e', good: '#5fdc8b', gold: '#f2c14e',
|
|
29
|
+
ramp: ['#120c0a', '#2b1d16', '#4a2e20', '#6e3b22', '#a4512a', '#e0702f', '#f4b23c', '#ffd98a', '#f6e7d3', '#7ad38a', '#3f6b4a', '#5a4a6e'] },
|
|
30
|
+
'neon-dusk': { label: 'Neon dusk: magenta and cyan on ink', words: /\b(neon|cyber|synth(wave)?|city|night ?club|future|retro[- ]future|laser)\b/i,
|
|
31
|
+
bg: '#0b0a1a', ink: '#eef0ff', accent: '#ff3fa4', accent2: '#2ee6f2', danger: '#ff4d4d', good: '#4dff9a', gold: '#ffd84a',
|
|
32
|
+
ramp: ['#0b0a1a', '#1a1438', '#2c1f5e', '#4b2a8c', '#7a35c4', '#ff3fa4', '#ff7ac8', '#2ee6f2', '#7af3ff', '#ffd84a', '#eef0ff', '#3a3a5e'] },
|
|
33
|
+
'candy-pop': { label: 'Candy pop: bubblegum and mint', words: /\b(candy|sweet|sugar|pastel|toy|kawaii|party|bubble|jelly)\b/i,
|
|
34
|
+
bg: '#fff2f7', ink: '#2a1f3d', accent: '#ff5fa2', accent2: '#4fd1c5', danger: '#ff4f64', good: '#40c07a', gold: '#ffc94d',
|
|
35
|
+
ramp: ['#fff2f7', '#ffd6e8', '#ff9ec7', '#ff5fa2', '#c45fd1', '#8a7cff', '#4fd1c5', '#a6f0d9', '#ffc94d', '#ff9a5a', '#2a1f3d', '#ffffff'] },
|
|
36
|
+
'frost-blue': { label: 'Frost: ice blue and snow', words: /\b(snow|ice|icy|winter|frost|frozen|arctic|polar|penguin)\b/i,
|
|
37
|
+
bg: '#eaf3fb', ink: '#14243a', accent: '#3d8bff', accent2: '#8fe3ff', danger: '#ff5a6a', good: '#41c08a', gold: '#ffcc4d',
|
|
38
|
+
ramp: ['#ffffff', '#eaf3fb', '#cfe2f5', '#a9cbee', '#7fb0e6', '#3d8bff', '#2a5fb0', '#14243a', '#8fe3ff', '#c7f3ff', '#ffcc4d', '#b4a3d9'] },
|
|
39
|
+
'desert-noon': { label: 'Desert noon: sand, terracotta and turquoise', words: /\b(desert|sand|dune|canyon|western|cowboy|egypt|pyramid|savanna)\b/i,
|
|
40
|
+
bg: '#f5e2c0', ink: '#3a2412', accent: '#d9653b', accent2: '#2bb3a8', danger: '#c8322a', good: '#5aa84a', gold: '#f2b632',
|
|
41
|
+
ramp: ['#fff3dc', '#f5e2c0', '#e8c48f', '#d9a35f', '#c47a3f', '#d9653b', '#a8452a', '#6e2e1a', '#3a2412', '#2bb3a8', '#7ad3c8', '#f2b632'] },
|
|
42
|
+
'deep-sea': { label: 'Deep sea: teal dark with coral', words: /\b(ocean|sea|under ?water|reef|coral|fish|submarine|diver?|pirate|island|beach)\b/i,
|
|
43
|
+
bg: '#06222e', ink: '#e6fbff', accent: '#ff7a59', accent2: '#3fe0c5', danger: '#ff5a6a', good: '#5fe08a', gold: '#ffd166',
|
|
44
|
+
ramp: ['#03141b', '#06222e', '#0b3a4a', '#11566a', '#1a7a8c', '#3fe0c5', '#a6f5e6', '#ff7a59', '#ffb08a', '#ffd166', '#e6fbff', '#5a4a8c'] },
|
|
45
|
+
'moonlit-grove': { label: 'Moonlit grove: indigo, silver and firefly', words: /\b(night|moon(lit)?|dark|spooky|haunted|ghost|witch|owl|halloween|grave)\b/i,
|
|
46
|
+
bg: '#0e1430', ink: '#e8ecff', accent: '#c9a8ff', accent2: '#b8f26a', danger: '#ff5e7a', good: '#6fe0a0', gold: '#ffe08a',
|
|
47
|
+
ramp: ['#070a1c', '#0e1430', '#1b2350', '#2c3770', '#47509a', '#7c84c9', '#c9a8ff', '#e8ecff', '#b8f26a', '#ffe08a', '#3a5a4a', '#5e3a5a'] },
|
|
48
|
+
'heroic-dawn': { label: 'Heroic dawn: steel blue and gold', words: /\b(hero(ic)?|epic|fantasy|kingdom|castle|knight|quest|rpg|dungeon|sword|dragon|adventure)\b/i,
|
|
49
|
+
bg: '#1a2238', ink: '#f3efe6', accent: '#e8b84a', accent2: '#6aa8e0', danger: '#e04a3a', good: '#5ac07a', gold: '#f2c14e',
|
|
50
|
+
ramp: ['#0f1526', '#1a2238', '#2c3a5c', '#4a5f8c', '#6aa8e0', '#b8d4f0', '#f3efe6', '#d9c49a', '#e8b84a', '#a8742a', '#8c3a2a', '#4a6a3a'] },
|
|
51
|
+
'space-ink': { label: 'Space: ink black with signal orange', words: /\b(space|galaxy|planet|star(ship)?|rocket|alien|asteroid|sci[- ]?fi|moon ?base|orbit)\b/i,
|
|
52
|
+
bg: '#07080f', ink: '#eef2ff', accent: '#ff8a3d', accent2: '#5ad1ff', danger: '#ff4d5e', good: '#59e08a', gold: '#ffd24d',
|
|
53
|
+
ramp: ['#07080f', '#11142a', '#1e2447', '#2f3a6e', '#4a5aa0', '#5ad1ff', '#b8ecff', '#eef2ff', '#ff8a3d', '#ffb87a', '#ffd24d', '#8c5ad9'] },
|
|
54
|
+
'ash-dawn': { label: 'Ash dawn: muted greys with rose', words: /\b(ruin(s|ed)?|post[- ]?apocalyp\w*|grim|bleak|ash|ashen|wasteland|mist(y)?|fog(gy)?|quiet|melanchol\w*)\b/i,
|
|
55
|
+
bg: '#2a2a2e', ink: '#ece6e0', accent: '#e8927c', accent2: '#9cb8b0', danger: '#d9584a', good: '#86b886', gold: '#d9b86a',
|
|
56
|
+
ramp: ['#1a1a1e', '#2a2a2e', '#3e3d42', '#57555c', '#7a7680', '#a49ea6', '#ece6e0', '#e8927c', '#c26a5a', '#9cb8b0', '#6a8a84', '#d9b86a'] },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Light set-ups (the style.light decision). `key`: the sun's direction (from the light toward the scene). */
|
|
60
|
+
export const LIGHTS = Object.freeze({
|
|
61
|
+
morning: { label: 'Soft morning', words: /\b(morning|dawn|spring|fresh)\b/i, key: [-0.5, -1, -0.35], intensity: 2.2, hardness: 0.35, sky: 'mix:bg>#ffffff:0.35', ground: 'mix:accent2>bg:0.25', fog: 0.35, shadows: 'blob', toonSteps: 3, bloom: 0.1, tiltShift: false },
|
|
62
|
+
noon: { label: 'Bright noon', words: /\b(noon|midday|sunny|bright|desert|beach)\b/i, key: [-0.2, -1, -0.15], intensity: 2.8, hardness: 0.75, sky: 'mix:bg>#ffffff:0.5', ground: 'mix:gold>accent2:0.45', fog: 0.2, shadows: 'blob', toonSteps: 2, bloom: 0.05, tiltShift: false },
|
|
63
|
+
golden: { label: 'Golden hour', words: /\b(golden|sunset|dusk|evening|warm|autumn|fall)\b/i, key: [-0.85, -0.55, -0.25], intensity: 2.4, hardness: 0.55, sky: 'mix:accent>#ffffff:0.45', ground: 'mix:accent2>accent:0.3', fog: 0.45, shadows: 'blob', toonSteps: 3, bloom: 0.25, tiltShift: false },
|
|
64
|
+
night: { label: 'Moonlight and lamps', words: /\b(night|moon(lit)?|dark|spooky|neon|stars?|lantern|ember|fire)\b/i, key: [0.4, -1, 0.3], intensity: 1.1, hardness: 0.4, sky: 'bg', ground: 'mix:accent2>bg:0.6', fog: 0.6, shadows: 'blob', toonSteps: 3, bloom: 0.5, tiltShift: false },
|
|
65
|
+
overcast: { label: 'Soft overcast', words: /\b(overcast|cloudy|grey|gray|misty|fog(gy)?|ash|quiet)\b/i, key: [-0.3, -1, -0.2], intensity: 1.6, hardness: 0.15, sky: 'mix:bg>#ffffff:0.25', ground: 'mix:accent2>bg:0.5', fog: 0.7, shadows: 'blob', toonSteps: 2, bloom: 0.05, tiltShift: false },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** Camera set-ups (the style.camera decision): distance in metres, field of view in degrees, the angle above the ground. */
|
|
69
|
+
export const CAMERAS = Object.freeze({
|
|
70
|
+
'high-3/4': { label: 'High three-quarter', projection: 'perspective', pitch: 52, distance: 22, fov: 38, note: 'the whole arena and every player readable on a phone' },
|
|
71
|
+
'low-3/4': { label: 'Low three-quarter', projection: 'perspective', pitch: 34, distance: 20, fov: 42, note: 'down among the players, the woods beyond the clearing in view: a place, not a board' },
|
|
72
|
+
'close-3/4': { label: 'Close three-quarter', projection: 'perspective', pitch: 40, distance: 11, fov: 42, note: 'nearer the characters: more detail on screen, less of the world' },
|
|
73
|
+
iso: { label: 'Isometric', projection: 'orthographic', pitch: 35.264, distance: 20, fov: 30, note: 'a diorama: tidy and readable, the HD-2D and tactics look' },
|
|
74
|
+
'top-down': { label: 'Top-down', projection: 'perspective', pitch: 80, distance: 24, fov: 35, note: 'the map is the screen: best for a crowd and for aiming' },
|
|
75
|
+
chase: { label: 'Chase', projection: 'perspective', pitch: 18, distance: 7, fov: 60, note: 'behind the player: speed and scale, one body at a time' },
|
|
76
|
+
side: { label: 'Side', projection: 'perspective', pitch: 6, distance: 14, fov: 35, note: 'a platformer\'s view: jumps read exactly' },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** Genres (from the prompt and the codex), with the defaults they bring. */
|
|
80
|
+
export const GENRES = Object.freeze({
|
|
81
|
+
gather: { words: /\b(gather|collect|harvest|forag\w*|berr(y|ies)|gems?|coins?|pick ?up|treasure|loot|farm)\b/i, camera: 'low-3/4', heads: 2.5, heightM: 0.9, clips: ['idle', 'walk', 'run', 'pick-up', 'emote'] },
|
|
82
|
+
brawl: { words: /\b(brawl|fight|arena|knock|bump|smash|wrestl\w*|sumo|battle royale)\b/i, camera: 'high-3/4', heads: 2.5, heightM: 1.0, clips: ['idle', 'run', 'jump', 'attack', 'hit', 'die'] },
|
|
83
|
+
race: { words: /\b(race|racing|kart|car|drive|driving|drift|speed)\b/i, camera: 'chase', heads: 3, heightM: 1.0, clips: ['idle', 'drive', 'boost', 'crash'] },
|
|
84
|
+
rpg: { words: /\b(rpg|quest|dungeon|adventure|mmo|wow|zones?|class(es)?|loot|level up)\b/i, camera: 'iso', heads: 4.5, heightM: 1.7, clips: ['idle', 'walk', 'run', 'attack', 'cast', 'hit', 'die', 'interact'] },
|
|
85
|
+
platformer: { words: /\b(platform(er)?|jump(ing)?|parkour|climb)\b/i, camera: 'side', heads: 3, heightM: 1.1, clips: ['idle', 'run', 'jump', 'fall', 'land'] },
|
|
86
|
+
shooter: { words: /\b(shoot(er|ing)?|blaster|gun|laser|tank|twin[- ]stick)\b/i, camera: 'top-down', heads: 5, heightM: 1.8, clips: ['idle', 'run', 'aim', 'shoot', 'hit', 'die'] },
|
|
87
|
+
party: { words: /\b(party|mini[- ]?games?|silly|chaos|couch)\b/i, camera: 'high-3/4', heads: 2.5, heightM: 1.0, clips: ['idle', 'run', 'jump', 'emote', 'win'] },
|
|
88
|
+
puzzle: { words: /\b(puzzle|match|tiles?|board game|cards?|chess)\b/i, camera: 'top-down', heads: 3, heightM: 1.0, clips: ['idle', 'emote', 'win'] },
|
|
89
|
+
sports: { words: /\b(soccer|football|golf|basketball|hockey|ball|sports?)\b/i, camera: 'high-3/4', heads: 3, heightM: 1.2, clips: ['idle', 'run', 'kick', 'jump', 'win'] },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** Starter library families: which look each belongs to. */
|
|
93
|
+
export const FAMILIES = Object.freeze({
|
|
94
|
+
kenney: { label: 'Kenney (CC0)', renders: ['lowpoly-flat', 'voxel', 'pixel-hd2d'], note: 'clean flat-colour low-poly kits: nature, food, pets, dungeons, cars' },
|
|
95
|
+
kaykit: { label: 'KayKit (CC0)', renders: ['toon', 'painted'], note: 'stylised gradient-atlas low-poly: adventurers, dungeons, props' },
|
|
96
|
+
polyhaven: { label: 'Poly Haven (CC0)', renders: ['stylised-pbr'], note: 'photo-scanned props, materials and skies' },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/** Shape language words. */
|
|
100
|
+
export const SHAPES = Object.freeze({
|
|
101
|
+
round: { label: 'Round and soft', words: /\b(cozy|cosy|cute|soft|friendly|gentle|bubbly|kids?|round)\b/i, bevel: 0.6, note: 'round silhouettes and big soft bevels: friendly at a glance' },
|
|
102
|
+
angular: { label: 'Angular and sharp', words: /\b(tense|dark|grim|aggressive|sharp|edgy|horror|cyber|military)\b/i, bevel: 0.1, note: 'pointed silhouettes and crisp edges: tension and speed' },
|
|
103
|
+
blocky: { label: 'Blocky', words: /\b(blocky|voxel|cube|lego|toy|robot)\b/i, bevel: 0.25, note: 'boxes with small bevels: toy-like and readable' },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** Google Fonts (OFL) by mood, for style.ui. */
|
|
107
|
+
export const FONT_SETS = Object.freeze({
|
|
108
|
+
friendly: { display: 'Fredoka', body: 'Nunito' },
|
|
109
|
+
bold: { display: 'Lilita One', body: 'Nunito' },
|
|
110
|
+
storybook: { display: 'Fraunces', body: 'Lora' },
|
|
111
|
+
clean: { display: 'Rajdhani', body: 'Inter' },
|
|
112
|
+
pixel: { display: 'Press Start 2P', body: 'Silkscreen' },
|
|
113
|
+
future: { display: 'Orbitron', body: 'Inter' },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/** Which words mean which mood (palette, light and shape read them). */
|
|
117
|
+
export const MOODS = Object.freeze({
|
|
118
|
+
cozy: /\b(cozy|cosy|cute|warm|gentle|calm|chill|relax\w*|wholesome|soft|friendly|sweet)\b/i,
|
|
119
|
+
tense: /\b(tense|dark|grim|horror|scary|creepy|spooky|survival|hardcore)\b/i,
|
|
120
|
+
loud: /\b(neon|loud|wild|chaos|crazy|party|arcade|electric)\b/i,
|
|
121
|
+
});
|
|
@@ -887,6 +887,81 @@
|
|
|
887
887
|
}
|
|
888
888
|
}
|
|
889
889
|
}
|
|
890
|
+
},
|
|
891
|
+
"0.21.0": {
|
|
892
|
+
"files": {
|
|
893
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
894
|
+
"README.md": "684b3cca8001673c",
|
|
895
|
+
"HANDOFF.md": "ce7a8fd91c6bbee2",
|
|
896
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
897
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
898
|
+
"videos/README.md": "16babc3437d5545b",
|
|
899
|
+
"posts/README.md": "37841aae476bd540",
|
|
900
|
+
"site/README.md": "ca131cd0472464fb",
|
|
901
|
+
"site/src/worker.mjs": "ca3f1630f2420ad3"
|
|
902
|
+
},
|
|
903
|
+
"sections": {
|
|
904
|
+
"AGENTS.md": {
|
|
905
|
+
"(intro)": "214aa46eb3a895d5",
|
|
906
|
+
"## Layout": "43a4c5bf07db36f9",
|
|
907
|
+
"## Commands (all through the pinned CLI in node_modules)": "d80ec56e4b4fc4c6",
|
|
908
|
+
"## The site": "ab922bb6bde23293",
|
|
909
|
+
"## Making games": "a8a893eeada33bf5",
|
|
910
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
911
|
+
"## Running live games (the back office)": "4aa5f513a7cd2c3b",
|
|
912
|
+
"## Servers and AI seats": "0a332b07e22d3fce",
|
|
913
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
914
|
+
"## Rules": "7e8458adc1b81ece",
|
|
915
|
+
"## Beta": "9d02638207a26e73"
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
"named": {
|
|
919
|
+
"Homie Arcade": {
|
|
920
|
+
"sections": {
|
|
921
|
+
"AGENTS.md": {
|
|
922
|
+
"## Commands (all through the pinned CLI in node_modules)": "b35df2c19cd4b534"
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
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
|
+
}
|
|
890
965
|
}
|
|
891
966
|
}
|
|
892
967
|
}
|
package/lib/thumb.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A build's preview as raw pixels, for Claude Code's Homie mod (the plugin's hooks/homie.mjs): a mod's code has no
|
|
3
|
+
* image decoder, so the latest check frame is also kept, small, as plain RGB beside the feed:
|
|
4
|
+
*
|
|
5
|
+
* .studio/progress/<build>.preview.<width>x<height>.rgb width * height * 3 bytes, rows top to bottom
|
|
6
|
+
*
|
|
7
|
+
* The mod draws it as coloured terminal cells (or hands the file to the terminal as a picture). The feed's own JPEG
|
|
8
|
+
* stays what the Claude app's card shows. Plain Node: zlib inflates the PNG Chrome wrote; nothing else is needed.
|
|
9
|
+
*/
|
|
10
|
+
import { readdirSync, rmSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { inflateSync } from 'node:zlib';
|
|
13
|
+
|
|
14
|
+
/** A PNG (8-bit RGB or RGBA, not interlaced: what Chrome writes) as { w, h, rgba }. */
|
|
15
|
+
export function decodePng(buf) {
|
|
16
|
+
if (!Buffer.isBuffer(buf) || buf.length < 33 || buf.readUInt32BE(0) !== 0x89504e47) throw new Error('not a PNG');
|
|
17
|
+
let at = 8; let w = 0; let h = 0; let type = 0; const idat = [];
|
|
18
|
+
while (at + 8 <= buf.length) {
|
|
19
|
+
const len = buf.readUInt32BE(at);
|
|
20
|
+
const kind = buf.toString('latin1', at + 4, at + 8);
|
|
21
|
+
const data = buf.subarray(at + 8, at + 8 + len);
|
|
22
|
+
if (kind === 'IHDR') {
|
|
23
|
+
w = data.readUInt32BE(0); h = data.readUInt32BE(4); type = data[9];
|
|
24
|
+
if (data[8] !== 8 || data[12] !== 0) throw new Error('only 8-bit, non-interlaced PNGs');
|
|
25
|
+
} else if (kind === 'IDAT') idat.push(data);
|
|
26
|
+
else if (kind === 'IEND') break;
|
|
27
|
+
at += 12 + len;
|
|
28
|
+
}
|
|
29
|
+
const bpp = type === 6 ? 4 : type === 2 ? 3 : 0;
|
|
30
|
+
if (!bpp || !w || !h) throw new Error('only RGB or RGBA PNGs');
|
|
31
|
+
const raw = inflateSync(Buffer.concat(idat));
|
|
32
|
+
const stride = w * bpp;
|
|
33
|
+
const px = Buffer.alloc(stride * h);
|
|
34
|
+
for (let y = 0; y < h; y++) {
|
|
35
|
+
const f = raw[y * (stride + 1)];
|
|
36
|
+
const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
37
|
+
const o = y * stride;
|
|
38
|
+
for (let x = 0; x < stride; x++) {
|
|
39
|
+
const a = x >= bpp ? px[o + x - bpp] : 0;
|
|
40
|
+
const b = y ? px[o - stride + x] : 0;
|
|
41
|
+
const c = x >= bpp && y ? px[o - stride + x - bpp] : 0;
|
|
42
|
+
let v = line[x];
|
|
43
|
+
if (f === 1) v += a;
|
|
44
|
+
else if (f === 2) v += b;
|
|
45
|
+
else if (f === 3) v += (a + b) >> 1;
|
|
46
|
+
else if (f === 4) { const p = a + b - c; const pa = Math.abs(p - a); const pb = Math.abs(p - b); const pc = Math.abs(p - c); v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c; }
|
|
47
|
+
px[o + x] = v & 255;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const rgba = Buffer.alloc(w * h * 4);
|
|
51
|
+
for (let i = 0, j = 0; i < w * h; i++, j += bpp) { rgba[i * 4] = px[j]; rgba[i * 4 + 1] = px[j + 1]; rgba[i * 4 + 2] = px[j + 2]; rgba[i * 4 + 3] = bpp === 4 ? px[j + 3] : 255; }
|
|
52
|
+
return { w, h, rgba };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A PNG scaled down (each pixel the average of the ones it covers) to at most `width` pixels wide, as plain RGB. */
|
|
56
|
+
export function rgbThumb(png, width = 160) {
|
|
57
|
+
const img = decodePng(png);
|
|
58
|
+
const w = Math.max(1, Math.min(width, img.w));
|
|
59
|
+
const h = Math.max(1, Math.round((img.h * w) / img.w));
|
|
60
|
+
const rgb = Buffer.alloc(w * h * 3);
|
|
61
|
+
for (let y = 0; y < h; y++) {
|
|
62
|
+
const y0 = Math.floor((y * img.h) / h); const y1 = Math.max(y0 + 1, Math.floor(((y + 1) * img.h) / h));
|
|
63
|
+
for (let x = 0; x < w; x++) {
|
|
64
|
+
const x0 = Math.floor((x * img.w) / w); const x1 = Math.max(x0 + 1, Math.floor(((x + 1) * img.w) / w));
|
|
65
|
+
let r = 0; let g = 0; let b = 0; let n = 0;
|
|
66
|
+
for (let yy = y0; yy < y1; yy++) for (let xx = x0; xx < x1; xx++) { const i = (yy * img.w + xx) * 4; r += img.rgba[i]; g += img.rgba[i + 1]; b += img.rgba[i + 2]; n++; }
|
|
67
|
+
const o = (y * w + x) * 3;
|
|
68
|
+
rgb[o] = Math.round(r / n); rgb[o + 1] = Math.round(g / n); rgb[o + 2] = Math.round(b / n);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { w, h, rgb };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The file name of a build's raw preview. */
|
|
75
|
+
export const thumbName = (build, w, h) => `${build}.preview.${w}x${h}.rgb`;
|
|
76
|
+
|
|
77
|
+
/** Remove a build's earlier raw previews (one is kept: the newest). */
|
|
78
|
+
export function dropThumbs(dir, build) {
|
|
79
|
+
let names = [];
|
|
80
|
+
try { names = readdirSync(dir); } catch { return; }
|
|
81
|
+
for (const n of names) if (n.startsWith(`${build}.preview.`) && n.endsWith('.rgb')) rmSync(join(dir, n), { force: true });
|
|
82
|
+
}
|
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
|
+
})();
|
package/mcp/ui/lineup.js
ADDED
|
@@ -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
|
+
})();
|
package/mcp/ui/rights.js
ADDED
|
@@ -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
|
+
})();
|