@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,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"v": 1,
|
|
3
|
+
"palette": {
|
|
4
|
+
"bg": "#cfe8f5",
|
|
5
|
+
"ink": "#1d2b3a",
|
|
6
|
+
"accent": "#ff8a5b",
|
|
7
|
+
"accent2": "#5cbf8a",
|
|
8
|
+
"danger": "#e2553f",
|
|
9
|
+
"good": "#45b36b",
|
|
10
|
+
"gold": "#ffd25a",
|
|
11
|
+
"ramp": [
|
|
12
|
+
"#9fd3ef",
|
|
13
|
+
"#cfe8f5",
|
|
14
|
+
"#f8f3e6",
|
|
15
|
+
"#8fd06a",
|
|
16
|
+
"#5cbf8a",
|
|
17
|
+
"#3f8f5a",
|
|
18
|
+
"#ffd25a",
|
|
19
|
+
"#ffb347",
|
|
20
|
+
"#ff8a5b",
|
|
21
|
+
"#e86a6a",
|
|
22
|
+
"#b07ad9",
|
|
23
|
+
"#8a6a4a",
|
|
24
|
+
"#5a4a3a",
|
|
25
|
+
"#ffffff"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"fonts": {
|
|
29
|
+
"display": "Fredoka",
|
|
30
|
+
"body": "Nunito"
|
|
31
|
+
},
|
|
32
|
+
"light": {
|
|
33
|
+
"time": "morning",
|
|
34
|
+
"key": [
|
|
35
|
+
-0.5,
|
|
36
|
+
-1,
|
|
37
|
+
-0.35
|
|
38
|
+
],
|
|
39
|
+
"intensity": 2.2,
|
|
40
|
+
"hardness": 0.35,
|
|
41
|
+
"sky": "#e0f0f9",
|
|
42
|
+
"ground": "#79c9a5",
|
|
43
|
+
"fog": 0.35,
|
|
44
|
+
"bloom": 0.1
|
|
45
|
+
},
|
|
46
|
+
"camera": {
|
|
47
|
+
"angle": "low-3/4",
|
|
48
|
+
"projection": "perspective",
|
|
49
|
+
"pitch": 34,
|
|
50
|
+
"distance": 20,
|
|
51
|
+
"fov": 42
|
|
52
|
+
},
|
|
53
|
+
"render": "lowpoly-flat",
|
|
54
|
+
"materials": {
|
|
55
|
+
"model": "flat",
|
|
56
|
+
"outline": false,
|
|
57
|
+
"texelDensity": null,
|
|
58
|
+
"atlas": true
|
|
59
|
+
},
|
|
60
|
+
"rev": {
|
|
61
|
+
"style.palette": 1,
|
|
62
|
+
"style.ui": 1,
|
|
63
|
+
"style.light": 1,
|
|
64
|
+
"style.camera": 1
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"knockRange": { "value": 2.2, "min": 1.2, "max": 4, "step": 0.02, "unit": "m", "group": "Knock", "note": "How near a body must be for a wave to bump it" },
|
|
3
|
+
"hitStopMs": { "value": 70, "min": 0, "max": 200, "step": 5, "unit": "ms", "group": "Knock", "note": "The hit lands: the bumped body holds, squashed and shaking, before it flies" },
|
|
4
|
+
"knockDistance": { "value": 3.7, "min": 0.8, "max": 8, "step": 0.1, "unit": "m", "group": "Knock", "note": "How far a bump sends a body, the same at any frame rate" },
|
|
5
|
+
"knockMs": { "value": 420, "min": 120, "max": 1000, "step": 10, "unit": "ms", "group": "Knock", "note": "How long the slide takes" },
|
|
6
|
+
"knockEase": { "value": 4, "min": 1, "max": 6, "step": 0.1, "group": "Knock", "note": "1 slides at one speed; higher leaves faster and eases into the stop" },
|
|
7
|
+
"squash": { "value": 0.35, "min": 0, "max": 0.8, "step": 0.01, "group": "Body", "note": "How much a flying body stretches along the hit, and squashes when it stops" },
|
|
8
|
+
"settleMs": { "value": 260, "min": 0, "max": 700, "step": 10, "unit": "ms", "group": "Body", "note": "The wobble after the stop (overlap)" },
|
|
9
|
+
"flashMs": { "value": 110, "min": 0, "max": 300, "step": 5, "unit": "ms", "group": "Hit", "note": "How long the bumped body flashes white" },
|
|
10
|
+
"shake": { "value": 0.16, "min": 0, "max": 0.6, "step": 0.01, "unit": "m", "group": "Hit", "note": "The camera's kick when you bump or are bumped" },
|
|
11
|
+
"sparks": { "value": 9, "min": 0, "max": 24, "step": 1, "group": "Hit", "note": "Sparks thrown off the hit" },
|
|
12
|
+
"ringMs": { "value": 900, "min": 200, "max": 2000, "step": 10, "unit": "ms", "group": "Wave", "note": "How long a wave's ring lasts" }
|
|
13
|
+
}
|
package/worker/index.mjs
CHANGED
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
import { BRAIN_BUDGET, HouseAgents, agentFacts, aiName, decodeFacts, encodeFacts, passById, passRefusal, sitRoute } from './agents.mjs';
|
|
82
82
|
import { talks } from './brain.mjs';
|
|
83
83
|
import { doorPage, serverPage, serversPage } from './site.mjs';
|
|
84
|
-
import { qrSvg } from './qr.mjs';
|
|
84
|
+
import { isLocalOrigin, qrSvg } from './qr.mjs';
|
|
85
85
|
import { SEAT_MAX, perAddress, seatsOf } from './seats.mjs';
|
|
86
86
|
import {
|
|
87
87
|
SITE_JS, atomFeed, creditsPage, customPage, gameCover, gameLanding, gamesPage, homePage, jsonFeed, mediaArt, mediaIndexPage,
|
|
@@ -846,9 +846,11 @@ async function route(request, env, ctx) {
|
|
|
846
846
|
try { room = (await (await lobby().fetch(`https://lobby/join?max=${humanSeats(pol)}&server=${srv.id}&rooms=${srv.roomsMax}`, { method: 'POST' })).json()).room ?? null; } catch { room = null; }
|
|
847
847
|
}
|
|
848
848
|
const joinUrl = `${url.origin}/${game}/play${room ? `?room=${encodeURIComponent(room)}` : ''}`;
|
|
849
|
+
// Local development: no phone can open this computer's own address, so no code for it; the card says to deploy.
|
|
850
|
+
const local = isLocalOrigin(url.origin);
|
|
849
851
|
let qr = null;
|
|
850
|
-
try { qr = qrSvg(joinUrl, { title: `Join ${meta.name ?? game}` }); } catch { /* too long for a QR: the address shows as text */ }
|
|
851
|
-
return playPage(cat, meta, { screen: true, joinUrl, qr, room, ticket, owner: d.owner, launch, server });
|
|
852
|
+
if (!local) try { qr = qrSvg(joinUrl, { title: `Join ${meta.name ?? game}` }); } catch { /* too long for a QR: the address shows as text */ }
|
|
853
|
+
return playPage(cat, meta, { screen: true, joinUrl, qr, local, room, ticket, owner: d.owner, launch, server });
|
|
852
854
|
}
|
|
853
855
|
await countVisit(request, env, ctx, game, 'play');
|
|
854
856
|
shareDaily(cat, url, ctx);
|
package/worker/pages.mjs
CHANGED
|
@@ -84,12 +84,16 @@ export function sharePlaces(value) {
|
|
|
84
84
|
* the edge (game.json `screen.share`, per device: sharePlaces) opens Invite, Big screen and the room code; nothing
|
|
85
85
|
* covers the middle of the screen or a thumb.
|
|
86
86
|
*/
|
|
87
|
-
export function playPage(cat, g, { screen = false, joinUrl = null, qr = null, room = null, ticket = null, owner = false, launch = 'public', server = null } = {}) {
|
|
87
|
+
export function playPage(cat, g, { screen = false, joinUrl = null, qr = null, local = false, room = null, ticket = null, owner = false, launch = 'public', server = null } = {}) {
|
|
88
88
|
const accent = cat?.studio?.theme?.accent ?? '#ffcf5a';
|
|
89
89
|
const corner = (name, fallback) => (['top-left', 'top-right', 'bottom-left', 'bottom-right'].includes(g.screen?.[name]) ? g.screen[name] : fallback);
|
|
90
90
|
const places = sharePlaces(g.screen?.share);
|
|
91
91
|
// The phone's glass belongs to the game: no page pan, pinch-zoom, text selection or callout under a thumb
|
|
92
92
|
// (a pinch between a stick thumb and a button thumb made the browser cancel both touches).
|
|
93
|
+
// A game with an art direction (games.json `ui`, from its style.json): the pills wear its paper and text, like its HUD.
|
|
94
|
+
const okHex = (v) => /^#[0-9a-f]{6}$/i.test(String(v ?? ''));
|
|
95
|
+
const ui = okHex(g.ui?.paper) && okHex(g.ui?.text) ? g.ui : null;
|
|
96
|
+
const pillUi = ui ? `\n.pill, .chip, .pill.dim { background: ${ui.paper}e6; color: ${ui.text}; border-color: ${ui.text}22; }\n.pill svg { color: ${ui.text}; }` : '';
|
|
93
97
|
const css = `:root{--hot:${/^#[0-9a-f]{3,8}$/i.test(accent) ? accent : '#ffcf5a'}}
|
|
94
98
|
html, body { height: 100%; margin: 0; overflow: hidden; overscroll-behavior: none; background: #04060c; touch-action: none; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; }
|
|
95
99
|
iframe.game { position: fixed; inset: 0; width: 100%; height: 100%; border: 0; display: block; background: #04060c; touch-action: none; }
|
|
@@ -155,7 +159,7 @@ iframe.game { position: fixed; inset: 0; width: 100%; height: 100%; border: 0; d
|
|
|
155
159
|
.notice .acts a { display: inline-flex; align-items: center; min-height: 44px; padding: 0 16px; border-radius: 12px; text-decoration: none; font-weight: 700; color: #eef1f8; border: 1px solid rgba(255,255,255,.18); }
|
|
156
160
|
.notice .acts a.primary { background: var(--hot); color: #0b0b10; border-color: transparent; }
|
|
157
161
|
[hidden] { display: none !important; }
|
|
158
|
-
${SERVER_CSS}${g.saves && !screen ? SAVES_SHELL_CSS : ''}`;
|
|
162
|
+
${SERVER_CSS}${g.saves && !screen ? SAVES_SHELL_CSS : ''}${pillUi}`;
|
|
159
163
|
// The room button's place on each device (sharePlaces); the shell moves it to this browser's once it knows the device.
|
|
160
164
|
// A ticket (a game that is not public) and the owner's overlay ride along only for the browser they are for.
|
|
161
165
|
// The server this page plays on (0.16.0): its pool (the Lobby), its badge and line for the chip, its ceiling.
|
|
@@ -166,7 +170,9 @@ ${SERVER_CSS}${g.saves && !screen ? SAVES_SHELL_CSS : ''}`;
|
|
|
166
170
|
// game.json "screen": { "join": "top-left" | "top-right" | "bottom-left" | "bottom-right" } keeps the card off the game's own HUD.
|
|
167
171
|
const joinCorner = corner('join', 'bottom-right');
|
|
168
172
|
const first = places.desk;
|
|
169
|
-
|
|
173
|
+
// Local development: no join card at all on the big screen (no phone can open this computer's address); the room
|
|
174
|
+
// button's sheet says to deploy to share, for whoever looks for a link.
|
|
175
|
+
const joinCard = screen && joinUrl && !local ? `<div class="join join-${joinCorner}" data-join>${qr ? `<div class="qr">${qr}</div>` : ''}<div><b>Scan to play</b><span>${esc(joinUrl.replace(/^https?:\/\//, ''))}</span></div></div>` : '';
|
|
170
176
|
const share = screen ? '' : `<div class="room at-${first.at}" style="--dx:${first.x}px;--dy:${first.y}px" data-room-ui>
|
|
171
177
|
<div class="pills"><button class="pill${first.label ? '' : ' icon'}" type="button" data-share-toggle aria-expanded="false" aria-controls="share-sheet" aria-label="Room, invite and big screen"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5.5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="18.5" r="2.5"/><path d="m8.2 10.8 7.6-4.1M8.2 13.2l7.6 4.1"/></svg><span data-room-code>Room</span></button>
|
|
172
178
|
<button class="pill spill" type="button" data-server-toggle aria-expanded="false" aria-controls="server-sheet" hidden><span class="sdot" aria-hidden="true"></span><span data-server-label></span></button></div>
|
|
@@ -267,6 +273,8 @@ const SHELL_JS = String.raw`(function () {
|
|
|
267
273
|
var boot = window.__HOMIE_PLAY;
|
|
268
274
|
var params = new URLSearchParams(location.search);
|
|
269
275
|
var screenMode = params.get('screen') === '1' || boot.screen === true;
|
|
276
|
+
// Local development (this computer's own address): never hand a phone a link it cannot open.
|
|
277
|
+
var LOCAL = /^(localhost|.*\.localhost|127\..*|\[::1\]|0\.0\.0\.0)$/.test(location.hostname);
|
|
270
278
|
var asked = params.get('hand');
|
|
271
279
|
var device = asked === 'phone' || asked === 'desk' || asked === 'tv' ? asked : Math.min(innerWidth, innerHeight) <= 540 ? 'phone' : 'desk';
|
|
272
280
|
var want = screenMode ? 'screen' : 'play';
|
|
@@ -352,7 +360,7 @@ const SHELL_JS = String.raw`(function () {
|
|
|
352
360
|
state.link = link;
|
|
353
361
|
ui.querySelector('[data-room-code]').textContent = labelOf(room);
|
|
354
362
|
ui.querySelector('[data-room-label]').textContent = labelOf(room);
|
|
355
|
-
ui.querySelector('[data-room-link]').textContent = link.replace(/^https?:\/\//, '');
|
|
363
|
+
ui.querySelector('[data-room-link]').textContent = LOCAL ? 'Deploy to share: this preview address works on this computer only.' : link.replace(/^https?:\/\//, '');
|
|
356
364
|
ui.querySelector('[data-bigscreen]').href = '/' + boot.game + '/tv?room=' + encodeURIComponent(room);
|
|
357
365
|
toggle.addEventListener('click', function (e) { e.stopPropagation(); open(sheet.hidden); });
|
|
358
366
|
ui.querySelector('[data-invite]').addEventListener('click', function () {
|
|
@@ -412,7 +420,7 @@ const SHELL_JS = String.raw`(function () {
|
|
|
412
420
|
});
|
|
413
421
|
watch(room);
|
|
414
422
|
shareReady(room);
|
|
415
|
-
if (screenMode && !document.querySelector('[data-join]')) {
|
|
423
|
+
if (screenMode && !document.querySelector('[data-join]') && !LOCAL) {
|
|
416
424
|
var h2 = document.createElement('h2'); h2.textContent = 'Join on your phone';
|
|
417
425
|
var div = document.createElement('div'); div.textContent = location.origin + '/' + boot.game + '/play?room=' + encodeURIComponent(room);
|
|
418
426
|
screenCard.append(h2, div); screenCard.hidden = false;
|
package/worker/qr.mjs
CHANGED
|
@@ -489,6 +489,17 @@ export function encodeQr(text, { minLevel = 'M' } = {}) {
|
|
|
489
489
|
|
|
490
490
|
|
|
491
491
|
/** The symbol as an SVG string: one path, dark modules on a light square with the quiet zone. */
|
|
492
|
+
/**
|
|
493
|
+
* An address only this computer can open (local development: localhost, 127.x, ::1, 0.0.0.0). A page never puts one in
|
|
494
|
+
* a QR code or tells a phone to open it: a phone cannot reach it. It says "deploy to share" instead.
|
|
495
|
+
*/
|
|
496
|
+
export function isLocalOrigin(origin) {
|
|
497
|
+
let host = '';
|
|
498
|
+
try { host = new URL(String(origin)).hostname; } catch { return false; }
|
|
499
|
+
host = host.replace(/^\[|\]$/g, '');
|
|
500
|
+
return host === 'localhost' || host.endsWith('.localhost') || /^127\./.test(host) || host === '::1' || host === '0.0.0.0';
|
|
501
|
+
}
|
|
502
|
+
|
|
492
503
|
export function qrSvg(text, { minLevel = 'M', quiet = 4, dark = '#000000', light = '#ffffff', title = '' } = {}) {
|
|
493
504
|
const q = encodeQr(text, { minLevel });
|
|
494
505
|
const total = q.size + quiet * 2;
|
package/worker/site.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* What a studio puts in site/ wins: a whole page (site/pages), a partial (site/partials), its tokens
|
|
10
10
|
* (site/theme.json) and CSS (site/theme.css). The build gathers them into games.json and site/dist/_site/.
|
|
11
11
|
*/
|
|
12
|
-
import { qrSvg } from './qr.mjs';
|
|
12
|
+
import { isLocalOrigin, qrSvg } from './qr.mjs';
|
|
13
13
|
import { STUDIO_VERSION_TAG } from './version.mjs';
|
|
14
14
|
import { licenseLabel, licenseOf, remixRow } from './license.mjs';
|
|
15
15
|
import { POLICY_WORDS, policyWords } from './servers.mjs';
|
|
@@ -1080,7 +1080,9 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
1080
1080
|
const host = origin.replace(/^https?:\/\//, '');
|
|
1081
1081
|
const playUrl = `${origin}/${g.id}/play`;
|
|
1082
1082
|
let qr = '';
|
|
1083
|
-
|
|
1083
|
+
// Local development: no code a phone cannot open; the landing says to deploy instead of naming this computer's address.
|
|
1084
|
+
const local = isLocalOrigin(origin);
|
|
1085
|
+
if (!local) try { qr = qrSvg(playUrl, { title: `Play ${g.name} on your phone` }); } catch { qr = ''; }
|
|
1084
1086
|
const tvOn = L.tv !== false;
|
|
1085
1087
|
const max = g.players?.max ?? 8;
|
|
1086
1088
|
const words = L.words ?? {};
|
|
@@ -1111,7 +1113,7 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
1111
1113
|
<p class="lead">Nothing to download and nothing to install: ${esc(g.name)} runs in the browser you already have, and everyone who presses Play meets in the same public room.</p>
|
|
1112
1114
|
<ol class="ways">
|
|
1113
1115
|
<li class="way"><span class="n">${icon('phone')}</span><div><h3>On your phone</h3><p>${esc(L.controls?.phone ?? 'Touch controls appear under your thumbs; the middle of the screen stays clear.')}</p>
|
|
1114
|
-
<div class="scan desk-only">${qr ? `<span class="qr" role="img" aria-label="A code for your phone’s camera: ${esc(host)}/${esc(g.id)}/play">${qr}</span>` : ''}<p
|
|
1116
|
+
<div class="scan desk-only">${qr ? `<span class="qr" role="img" aria-label="A code for your phone’s camera: ${esc(host)}/${esc(g.id)}/play">${qr}</span>` : ''}<p>${local ? 'Deploy to share: once the studio is online, a code here takes a phone straight to the game.' : `Point your phone’s camera at the code, or open <span class="addr">${esc(host)}/${esc(g.id)}/play</span>.`}</p></div>
|
|
1115
1117
|
<p class="phone-only" style="margin-top:8px"><strong>You are on it:</strong> press Play above.</p></div></li>
|
|
1116
1118
|
<li class="way"><span class="n">${icon('computer')}</span><div><h3>On your computer</h3><p>${esc(L.controls?.computer ?? 'Keys and mouse, in any modern browser; the game shows you which keys.')}</p></div></li>
|
|
1117
1119
|
${tvOn ? `<li class="way"><span class="n">${icon('tv')}</span><div><h3>On the TV</h3><ol><li>Open <span class="addr">${esc(host)}/${esc(g.id)}/tv</span> in the TV’s browser, or on a laptop plugged into it.</li><li>Scan the code it shows with your phone’s camera.</li><li>Play on your phone. Everyone on the couch joins the same way, up to ${esc(max)} ${esc(many)}, and the TV shows the whole room.</li></ol>${L.controls?.tv ? `<p style="margin-top:8px">${esc(L.controls.tv)}</p>` : ''}</div></li>` : ''}
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.22.0';
|