@homie-rocks/studio 0.32.1 → 0.33.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 +57 -1
- package/README.md +24 -6
- package/agents/agents.ts +57 -24
- package/bin/homie-studio.mjs +3 -3
- package/lib/build.mjs +64 -9
- package/lib/cf-usage.mjs +76 -0
- package/lib/cloudflare.mjs +25 -2
- package/lib/dev.mjs +45 -4
- package/lib/lab-check.mjs +3 -2
- package/lib/mcp-tools.mjs +1 -1
- package/lib/mcp.mjs +1 -1
- package/lib/office.mjs +7 -0
- package/lib/perf.mjs +8 -3
- package/lib/player-image.mjs +40 -0
- package/lib/rules-build.mjs +184 -0
- package/lib/rules-guard.mjs +770 -0
- package/lib/scaffold.mjs +43 -16
- package/lib/shop-catalog.mjs +10 -9
- package/lib/shop.mjs +32 -15
- package/lib/studio.mjs +21 -1
- package/lib/template-history.json +50 -0
- package/lib/upgrade.mjs +19 -15
- package/lib/version.mjs +2 -7
- package/netplay/NETPLAY.md +249 -4
- package/netplay/netplay.ts +55 -13
- package/package.json +23 -1
- package/rules/core.ts +1250 -0
- package/rules/guard.ts +419 -0
- package/rules/host.ts +707 -0
- package/rules/math.ts +266 -0
- package/rules/pack.ts +292 -0
- package/rules/rules.ts +544 -0
- package/rules/view.ts +447 -0
- package/shop/SELLING.md +14 -16
- package/shop/SHOP.md +119 -30
- package/shop/shop.ts +6 -8
- package/site/SITE.md +119 -2
- package/starters/coin-dash/game.json +11 -0
- package/starters/coin-dash/index.html +17 -0
- package/starters/coin-dash/map/main.json +16 -0
- package/starters/coin-dash/src/move.ts +27 -0
- package/starters/coin-dash/src/rules.ts +86 -0
- package/starters/coin-dash/src/view.ts +106 -0
- package/starters/coin-dash/tunables.json +5 -0
- package/starters/ember-vale/index.html +11 -0
- package/worker/brain.mjs +8 -7
- package/worker/chat-page.mjs +1 -1
- package/worker/embed.mjs +135 -0
- package/worker/hosted.mjs +56 -0
- package/worker/index.mjs +413 -22
- package/worker/office-page.mjs +1 -0
- package/worker/office.mjs +24 -7
- package/worker/pages.mjs +189 -43
- package/worker/players.mjs +10 -6
- package/worker/referrals.mjs +192 -57
- package/worker/room-store.mjs +54 -0
- package/worker/room.mjs +294 -119
- package/worker/saves-shell.mjs +7 -1
- package/worker/schema.mjs +4 -3
- package/worker/shop-page.mjs +103 -37
- package/worker/shop-rules.mjs +156 -117
- package/worker/shop-store.mjs +70 -9
- package/worker/shop.mjs +275 -143
- package/worker/site.mjs +26 -5
- package/worker/stripe.mjs +9 -6
- package/worker/version.mjs +1 -1
package/worker/site.mjs
CHANGED
|
@@ -9,6 +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 { playerProperties } from './embed.mjs';
|
|
12
13
|
import { isLocalOrigin, qrSvg } from './qr.mjs';
|
|
13
14
|
import { STUDIO_VERSION_TAG } from './version.mjs';
|
|
14
15
|
import { basedOnRow, licenseLabel } from './license.mjs';
|
|
@@ -597,13 +598,24 @@ export function footer(cat) {
|
|
|
597
598
|
</div></footer>`;
|
|
598
599
|
}
|
|
599
600
|
|
|
600
|
-
const ogTags = (props) => Object.entries(props).filter(([, v]) => v).map(([k, v]) => `<meta ${k.startsWith('twitter:') ? 'name' : 'property'}="${esc(k)}" content="${esc(v)}">`).join('\n');
|
|
601
|
+
export const ogTags = (props) => Object.entries(props).filter(([, v]) => v).map(([k, v]) => `<meta ${k.startsWith('twitter:') ? 'name' : 'property'}="${esc(k)}" content="${esc(v)}">`).join('\n');
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Play, Watch and the player address say what a post of their address shows only when the game has a player card:
|
|
605
|
+
* the card's tags and the Open Graph ones beside them. Without one these pages carry no social tags, as before.
|
|
606
|
+
*/
|
|
607
|
+
export function gameSocialTags(cat, g, origin, enabled = true) {
|
|
608
|
+
const player = enabled ? playerProperties(cat, g, { origin }) : {};
|
|
609
|
+
if (!player['twitter:card']) return '';
|
|
610
|
+
return ogTags({ 'og:type': 'website', 'og:title': g.name, 'og:description': g.blurb,
|
|
611
|
+
'og:url': origin ? `${origin}/${g.id}/` : null, 'og:image': player['twitter:image'], ...player });
|
|
612
|
+
}
|
|
601
613
|
|
|
602
614
|
/**
|
|
603
615
|
* A whole generated page. `hero` pages draw the top line over the hero; `style` is the game's own accent on its
|
|
604
616
|
* landing (game.json landing.theme).
|
|
605
617
|
*/
|
|
606
|
-
export function layout(cat, { title, description = '', origin = '', path = '/', image = null, type = 'website', active = null, page = 'page', hero = false, head = '', ld = null, main, over = null, status = 200, extraHeaders = {} }) {
|
|
618
|
+
export function layout(cat, { title, description = '', origin = '', path = '/', image = null, type = 'website', active = null, page = 'page', hero = false, head = '', ld = null, main, over = null, status = 200, extraHeaders = {}, playerGame = null }) {
|
|
607
619
|
const theme = cat.studio?.theme ?? {};
|
|
608
620
|
const abs = (u) => (u && u.startsWith('/') ? `${origin}${u}` : u);
|
|
609
621
|
const icon = theme.icon ? `<link rel="icon" href="${esc(theme.icon)}">` : `<link rel="icon" href="data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="3" y="3" width="10" height="10" rx="3" fill="${(over?.accent ?? theme.accent ?? TOKEN_DEFAULTS.accent)}"/></svg>`)}">`;
|
|
@@ -617,7 +629,7 @@ export function layout(cat, { title, description = '', origin = '', path = '/',
|
|
|
617
629
|
${description ? `<meta name="description" content="${esc(description)}">` : ''}
|
|
618
630
|
${origin ? `<link rel="canonical" href="${esc(`${origin}${path}`)}">` : ''}
|
|
619
631
|
<meta name="theme-color" content="${esc(over?.bg ?? theme.bg ?? TOKEN_DEFAULTS.bg)}">
|
|
620
|
-
${ogTags({ 'og:title': title, 'og:description': description, 'og:type': type, 'og:url': origin ? `${origin}${path}` : null, 'og:image': abs(image ?? theme.social ?? null), 'og:site_name': studioName(cat), 'twitter:card': image || theme.social ? 'summary_large_image' : 'summary' })}
|
|
632
|
+
${ogTags({ 'og:title': title, 'og:description': description, 'og:type': type, 'og:url': origin ? `${origin}${path}` : null, 'og:image': abs(image ?? theme.social ?? null), 'og:site_name': studioName(cat), 'twitter:card': image || theme.social ? 'summary_large_image' : 'summary', ...(playerGame ? playerProperties(cat, playerGame, { origin, title, description }) : {}) })}
|
|
621
633
|
${icon}${feeds}${head}${origin && ld ? ldScript(ld) : ''}
|
|
622
634
|
<style>${tokensCss(theme, over)}${BASE_CSS}${cat.site?.css ?? ''}</style>
|
|
623
635
|
${partial(cat, 'head') ?? ''}
|
|
@@ -643,7 +655,16 @@ ${footer(cat)}
|
|
|
643
655
|
* and <!-- homie:home-hero --> or <!-- homie:home-hero <id> --> (Home's hero for the featured game, or for the
|
|
644
656
|
* public game it names: homeHero, below; a game that is not public, or not this studio's, leaves nothing there).
|
|
645
657
|
*/
|
|
646
|
-
export function customPage(cat, html, { active = null, schema = '' } = {}) {
|
|
658
|
+
export function customPage(cat, html, { active = null, schema = '', playerGame = null, origin = '' } = {}) {
|
|
659
|
+
// An owner's hand-written Twitter metadata wins as a set. Do not partially replace it.
|
|
660
|
+
if (playerGame && !/<meta\b[^>]*(?:name|property)\s*=\s*(?:["']twitter:|twitter:)/i.test(html)) {
|
|
661
|
+
const tags = ogTags(playerProperties(cat, playerGame, { origin }));
|
|
662
|
+
if (tags) {
|
|
663
|
+
if (/<\/head\s*>/i.test(html)) html = html.replace(/<\/head\s*>/i, () => tags + '</head>');
|
|
664
|
+
else if (/<html\b[^>]*>/i.test(html)) html = html.replace(/<html\b[^>]*>/i, m => m + '<head>' + tags + '</head>');
|
|
665
|
+
else html = html.replace(/^(<!doctype[^>]*>)?/i, m => m + '<head>' + tags + '</head>');
|
|
666
|
+
}
|
|
667
|
+
}
|
|
647
668
|
const out = String(html)
|
|
648
669
|
.replace(/<!--\s*homie:schema\s*-->/g, () => schema)
|
|
649
670
|
.replace(/<!--\s*homie:home-hero(?:\s+([a-z0-9][a-z0-9-]{0,39}))?\s*-->/g, (m, id) => {
|
|
@@ -1232,7 +1253,7 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
1232
1253
|
return layout(cat, {
|
|
1233
1254
|
title: `${g.name} — play free in your browser`,
|
|
1234
1255
|
description: `${g.name}: ${L.pitch ?? g.blurb ?? ''} Free in your browser, nothing to download; a TV or laptop browser can be the big screen, with phones as controllers.`.replace(/\s+/g, ' ').trim(),
|
|
1235
|
-
origin, path: `/${g.id}/`, image: h.wideImage ?? L.cover ?? null, page: 'landing', hero: true, active: 'games', over: landingTokens(cat, L),
|
|
1256
|
+
playerGame: listed ? g : null, origin, path: `/${g.id}/`, image: h.wideImage ?? L.cover ?? null, page: 'landing', hero: true, active: 'games', over: landingTokens(cat, L),
|
|
1236
1257
|
head: `${h.tallImage || h.wideImage ? `<link rel="preload" as="image" href="${esc(h.tallImage ?? h.wideImage)}"${h.tallImage && h.wideImage ? ' media="(max-aspect-ratio: 3/4)"' : ''}>${h.tallImage && h.wideImage ? `<link rel="preload" as="image" href="${esc(h.wideImage)}" media="(min-aspect-ratio: 3/4)">` : ''}` : ''}`,
|
|
1237
1258
|
// A game that is not public yet (private, or an invite-only beta) is never indexed, and offers nothing.
|
|
1238
1259
|
ld: landingLd(cat, g, origin, { listed, shop }), ...(listed ? {} : { extraHeaders: { 'x-robots-tag': 'noindex' } }),
|
package/worker/stripe.mjs
CHANGED
|
@@ -76,14 +76,17 @@ export const isPermissionError = (error) => error instanceof StripeError && (err
|
|
|
76
76
|
* The catalog Product a shop item is, the same id in a sandbox and in live mode: `homie_<studio slug>_<item id>`
|
|
77
77
|
* (Stripe takes a product id of our choosing). Null without a slug or an item id.
|
|
78
78
|
*/
|
|
79
|
-
export function productIdOf(slug, item) {
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
79
|
+
export async function productIdOf(slug, item) {
|
|
80
|
+
if (!slug || !item) return null;
|
|
81
|
+
const s = String(slug).toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 48);
|
|
82
|
+
if (!s) return null;
|
|
83
|
+
if (/^[a-z0-9][a-z0-9-]{0,39}$/.test(String(item))) return `homie_${s}_${String(item).replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')}`;
|
|
84
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(String(item)));
|
|
85
|
+
return `homie_${s}_item_${[...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('')}`;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
/** One call with the studio's key. `idempotencyKey` makes a retried POST do the work once (Stripe keeps it 24 h). */
|
|
86
|
-
export async function stripeCall(env, method, path, params = null, { idempotencyKey = null, fetcher = fetch } = {}) {
|
|
89
|
+
export async function stripeCall(env, method, path, params = null, { idempotencyKey = null, fetcher = fetch, timeout = 15_000 } = {}) {
|
|
87
90
|
const key = String(env?.STRIPE_KEY ?? '');
|
|
88
91
|
if (!KEY_SHAPE.test(key)) throw new StripeError(0, { error: { message: 'no Stripe key on this Worker', code: 'no-key' } });
|
|
89
92
|
const body = params && method !== 'GET' ? formEncode(params).toString() : null;
|
|
@@ -97,7 +100,7 @@ export async function stripeCall(env, method, path, params = null, { idempotency
|
|
|
97
100
|
...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}),
|
|
98
101
|
},
|
|
99
102
|
...(body !== null ? { body } : {}),
|
|
100
|
-
signal: AbortSignal.timeout(
|
|
103
|
+
signal: AbortSignal.timeout(timeout),
|
|
101
104
|
});
|
|
102
105
|
let json = null;
|
|
103
106
|
try { json = await res.json(); } catch { json = null; }
|
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.33.0';
|