@homie-rocks/studio 0.14.3 → 0.15.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/README.md +16 -7
- package/bin/homie-studio.mjs +1 -1
- package/lib/build.mjs +15 -4
- package/lib/mcp-tools.mjs +3 -2
- package/lib/scaffold.mjs +15 -1
- package/lib/studio.mjs +23 -3
- package/lib/template-history.json +54 -0
- package/netplay/NETPLAY.md +119 -13
- package/netplay/netplay.ts +291 -5
- package/package.json +1 -1
- package/port/hud.ts +10 -7
- package/port/room.ts +15 -0
- package/port/touch.ts +4 -1
- package/site/SITE.md +41 -5
- package/starters/ember-vale/src/main.ts +37 -5
- package/starters/gem-rush/src/main.ts +45 -15
- package/worker/index.mjs +83 -12
- package/worker/license.mjs +74 -0
- package/worker/office-page.mjs +1 -1
- package/worker/office.mjs +2 -1
- package/worker/pages.mjs +417 -4
- package/worker/room.mjs +65 -6
- package/worker/site.mjs +50 -12
- package/worker/stats.mjs +4 -3
- package/worker/version.mjs +1 -1
package/worker/site.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { qrSvg } from './qr.mjs';
|
|
13
13
|
import { STUDIO_VERSION_TAG } from './version.mjs';
|
|
14
|
+
import { licenseLabel, licenseOf, remixRow } from './license.mjs';
|
|
14
15
|
|
|
15
16
|
export const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
16
17
|
const jsonScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c');
|
|
@@ -50,6 +51,7 @@ const I = {
|
|
|
50
51
|
arrow: '<path d="M5 12h13M13 6l6 6-6 6"/>',
|
|
51
52
|
spark: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.5 2.5M15.5 15.5 18 18M6 18l2.5-2.5M15.5 8.5 18 6"/>',
|
|
52
53
|
home: '<path d="M4 11.5 12 5l8 6.5V20H4z"/><path d="M10 20v-5h4v5"/>',
|
|
54
|
+
eye: '<path d="M2 12s3.6-6.5 10-6.5S22 12 22 12s-3.6 6.5-10 6.5S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/>',
|
|
53
55
|
};
|
|
54
56
|
const icon = (name, cls = 'ico') => `<svg class="${cls}" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${I[name] ?? ''}</svg>`;
|
|
55
57
|
const PLAY_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 4.8v14.4a1 1 0 0 0 1.5.86l12-7.2a1 1 0 0 0 0-1.72l-12-7.2A1 1 0 0 0 7 4.8Z" fill="currentColor"/></svg>';
|
|
@@ -122,6 +124,8 @@ img,video{display:block;max-width:100%}
|
|
|
122
124
|
text-shadow:0 0 38px color-mix(in srgb,var(--hot) 50%,transparent),0 2px 0 color-mix(in srgb,var(--bg) 60%,transparent)}
|
|
123
125
|
.title.t-long{font-size:clamp(44px,12vw,64px)}
|
|
124
126
|
.title.t-xlong{font-size:clamp(38px,10vw,54px)}
|
|
127
|
+
.remix-of{margin:12px 0 0;font:600 14px/1.4 var(--text);color:var(--soft)}
|
|
128
|
+
.remix-of a{color:inherit;text-decoration:underline;text-underline-offset:3px}
|
|
125
129
|
.line{margin:14px 0 22px;max-width:36ch;font:500 17px/1.45 var(--text);color:var(--soft);text-wrap:pretty}
|
|
126
130
|
.play{display:flex;width:100%;align-items:center;justify-content:center;gap:12px;min-height:62px;padding:0 30px;border-radius:18px;
|
|
127
131
|
background:var(--hot);color:var(--hot-ink);text-decoration:none;font:800 20px/1 var(--display);letter-spacing:-.01em;
|
|
@@ -233,6 +237,11 @@ img,video{display:block;max-width:100%}
|
|
|
233
237
|
.pips i{width:8px;height:8px;border-radius:50%;background:color-mix(in srgb,var(--fg) 18%,transparent)}
|
|
234
238
|
.pips i.on{background:#43ff9e;box-shadow:0 0 8px rgba(67,255,158,.55)}
|
|
235
239
|
.join{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 18px;border-radius:12px;background:var(--hot);color:var(--hot-ink);font:800 15px/1 var(--display);text-decoration:none}
|
|
240
|
+
.room-acts{display:flex;gap:8px;align-items:center}
|
|
241
|
+
.watchb{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:44px;padding:0 14px;border-radius:12px;border:1px solid color-mix(in srgb,var(--fg) 24%,transparent);color:var(--fg);font:700 15px/1 var(--display);text-decoration:none}
|
|
242
|
+
.watchb:hover{border-color:var(--fg);background:color-mix(in srgb,var(--fg) 6%,transparent)}
|
|
243
|
+
.watchb svg{width:18px;height:18px;flex:none}
|
|
244
|
+
@media (max-width:379px){.watchb span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}.watchb{padding:0 12px}}
|
|
236
245
|
.quiet{margin:22px 0 0;padding:18px 20px;border-radius:var(--r);border:1px dashed color-mix(in srgb,var(--fg) 22%,transparent);color:var(--soft)}
|
|
237
246
|
.stat{display:flex;flex-wrap:wrap;gap:10px;margin-top:18px}
|
|
238
247
|
.stat span{padding:8px 12px;border-radius:999px;background:var(--panel);border:1px solid var(--line);font:600 14px/1.2 var(--text);color:var(--soft)}
|
|
@@ -420,10 +429,13 @@ d.addEventListener('click',function(ev){var b=ev.target&&ev.target.closest&&ev.t
|
|
|
420
429
|
if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(t).then(done,function(){})});
|
|
421
430
|
// Live: the line under Play and the rooms list follow the site's own room counts.
|
|
422
431
|
var el=function(tag,cls,text){var e=d.createElement(tag);if(cls)e.className=cls;if(text!=null)e.textContent=text;return e};
|
|
432
|
+
var EYE='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-6.5 10-6.5S22 12 22 12s-3.6 6.5-10 6.5S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
423
433
|
var roomRow=function(r){var li=el('li','room');var th=el('span','thumb');if(r.cover){var im=el('img');im.src=r.cover;im.alt='';im.loading='lazy';th.appendChild(im)}li.appendChild(th);
|
|
424
434
|
var mid=el('div');mid.appendChild(el('b',null,(r.name?r.name+' · ':'')+r.label));mid.appendChild(el('span',null,r.players+' of '+r.max+' seats taken · bots hold the rest'));
|
|
425
435
|
var pips=el('div','pips');for(var i=0;i<Math.min(r.max,32);i++)pips.appendChild(el('i',i<r.players?'on':''));mid.appendChild(pips);li.appendChild(mid);
|
|
426
|
-
var
|
|
436
|
+
var acts=el('div','room-acts');
|
|
437
|
+
if(r.watch){var w=el('a','watchb');w.href=r.watch;w.setAttribute('aria-label','Watch '+(r.name?r.name+' · ':'')+r.label);w.innerHTML=EYE;w.appendChild(el('span',null,'Watch'));acts.appendChild(w)}
|
|
438
|
+
var a=el('a','join','Join');a.href=r.play;a.setAttribute('data-play','');acts.appendChild(a);li.appendChild(acts);return li};
|
|
427
439
|
var paintRooms=function(list,rooms){if(!list)return;var quiet=d.querySelector('[data-rooms-quiet]');list.textContent='';
|
|
428
440
|
rooms.forEach(function(r){list.appendChild(roomRow(r))});list.hidden=!rooms.length;if(quiet)quiet.hidden=!!rooms.length;hand()};
|
|
429
441
|
var live=d.querySelector('[data-live]'),list=d.querySelector('[data-rooms]');
|
|
@@ -636,14 +648,30 @@ function gameCard(g, playing = 0) {
|
|
|
636
648
|
</article>`;
|
|
637
649
|
}
|
|
638
650
|
|
|
651
|
+
/**
|
|
652
|
+
* What a game lets its watchers see (game.json "watch", NETPLAY.md section 16): `follow` (any player's view; the
|
|
653
|
+
* default), `overview` (the whole room only: a game with hidden hands or roles), or `off` (no watch door at all).
|
|
654
|
+
*/
|
|
655
|
+
export function watchOf(g) {
|
|
656
|
+
const w = g?.watch;
|
|
657
|
+
if (w === false || w === 'off') return 'off';
|
|
658
|
+
if (w === 'overview') return 'overview';
|
|
659
|
+
return 'follow';
|
|
660
|
+
}
|
|
661
|
+
|
|
639
662
|
/** A room as the live list draws it (the same fields /api/rooms and /<game>/live give the script). */
|
|
640
663
|
export function roomView(g, r, max) {
|
|
641
664
|
const n = Number(String(r.name ?? r.room ?? '').replace(/^pub-/, '')) || null;
|
|
642
|
-
|
|
665
|
+
const room = r.name ?? r.room;
|
|
666
|
+
return {
|
|
667
|
+
game: g.id, name: g.name, label: n ? `Room ${n}` : 'A room', room, players: r.players, max, cover: coverOf(g), play: `/${g.id}/play?room=${encodeURIComponent(room)}`,
|
|
668
|
+
// Watch this room from any player's view (section 16); a game that cannot be watched has no link.
|
|
669
|
+
watch: watchOf(g) === 'off' ? null : `/${g.id}/watch?room=${encodeURIComponent(room)}`,
|
|
670
|
+
};
|
|
643
671
|
}
|
|
644
672
|
|
|
645
673
|
function roomRows(rows, { names = true } = {}) {
|
|
646
|
-
return rows.map((r) => `<li class="room"><span class="thumb">${r.cover ? `<img src="${esc(r.cover)}" alt="" loading="lazy">` : ''}</span><div><b>${names ? `${esc(r.name)} · ` : ''}${esc(r.label)}</b><span>${esc(r.players)} of ${esc(r.max)} seats taken · bots hold the rest</span><div class="pips">${Array.from({ length: Math.min(r.max, 32) }, (_, i) => `<i${i < r.players ? ' class="on"' : ''}></i>`).join('')}</div></div><a class="join" href="${esc(r.play)}" data-play>Join</a></li>`).join('');
|
|
674
|
+
return rows.map((r) => `<li class="room"><span class="thumb">${r.cover ? `<img src="${esc(r.cover)}" alt="" loading="lazy">` : ''}</span><div><b>${names ? `${esc(r.name)} · ` : ''}${esc(r.label)}</b><span>${esc(r.players)} of ${esc(r.max)} seats taken · bots hold the rest</span><div class="pips">${Array.from({ length: Math.min(r.max, 32) }, (_, i) => `<i${i < r.players ? ' class="on"' : ''}></i>`).join('')}</div></div><div class="room-acts">${r.watch ? `<a class="watchb" href="${esc(r.watch)}" aria-label="Watch ${names ? `${esc(r.name)} · ` : ''}${esc(r.label)}">${icon('eye', 'ico')}<span>Watch</span></a>` : ''}<a class="join" href="${esc(r.play)}" data-play>Join</a></div></li>`).join('');
|
|
647
675
|
}
|
|
648
676
|
|
|
649
677
|
const fmtDay = (iso) => { const d = new Date(iso); return Number.isFinite(d.getTime()) ? d.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' }) : ''; };
|
|
@@ -908,9 +936,15 @@ export function jsonFeed(cat, posts, origin) {
|
|
|
908
936
|
* public room, phone / computer / TV (with the join code), live rooms, how to play, credits, and "Make a game like
|
|
909
937
|
* this". A studio's site/pages/<id>/index.html replaces it; site/partials/game.html and game-<id>.html add a band.
|
|
910
938
|
*/
|
|
911
|
-
export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week = null } = {}) {
|
|
939
|
+
export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week = null, remix = true } = {}) {
|
|
912
940
|
const name = studioName(cat);
|
|
913
941
|
const L = g.landing ?? {};
|
|
942
|
+
// A remix says what it is a remix of, linked, under its name and in its credits (game.json `remixOf`).
|
|
943
|
+
const lineage = remixRow(g.remixOf);
|
|
944
|
+
const ofWhat = lineage ? `${lineage.page ? `<a href="${esc(lineage.page)}" rel="noopener">${esc(lineage.name)}</a>` : `<strong>${esc(lineage.name)}</strong>`}${lineage.studio ? ` by ${esc(lineage.studio)}` : ''}` : '';
|
|
945
|
+
// Open to remix: the source is in the build, the owner's switch is on, and the licence they picked allows it.
|
|
946
|
+
const license = licenseOf(g.license);
|
|
947
|
+
const remixable = Boolean(L.source) && remix !== false && license.kind !== 'no-remix';
|
|
914
948
|
const h = L.hero ?? {};
|
|
915
949
|
const host = origin.replace(/^https?:\/\//, '');
|
|
916
950
|
const playUrl = `${origin}/${g.id}/play`;
|
|
@@ -929,12 +963,13 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
929
963
|
<div class="hero-copy">
|
|
930
964
|
<p class="kicker">${kickerHtml}</p>
|
|
931
965
|
<h1 class="title ${titleClass(g.name)}" id="game-title">${esc(g.name)}</h1>
|
|
966
|
+
${lineage ? `<p class="remix-of">Remix of ${ofWhat}</p>` : ''}
|
|
932
967
|
<p class="line">${esc(L.pitch ?? g.blurb)}</p>
|
|
933
968
|
<div class="row">
|
|
934
969
|
<a class="play" href="/${esc(g.id)}/play" data-play>${PLAY_ICON}<span>Play now — free</span></a>
|
|
935
970
|
${liveLine(g, playing, { idle: `Bots hold every empty seat · one tap and you are in` })}
|
|
936
971
|
</div>
|
|
937
|
-
${tvOn ? `<a class="also" href="#anywhere">${icon('tv')}<span>Or put it on the big screen</span></a>` : ''}${trailer ? `<a class="also" href="/videos/${esc(trailer.slug)}/">${icon('videos')}<span>Watch the trailer</span></a>` : ''}${g.saves ? `<a class="also" href="/account/?next=${encodeURIComponent(`/${g.id}/play`)}" data-account>${icon('spark')}<span>Your progress follows you: sign in</span></a>` : ''}
|
|
972
|
+
${tvOn ? `<a class="also" href="#anywhere">${icon('tv')}<span>Or put it on the big screen</span></a>` : ''}${playing > 0 && rooms.length && watchOf(g) !== 'off' ? `<a class="also" href="/${esc(g.id)}/watch">${icon('eye')}<span>Watch a live room</span></a>` : ''}${trailer ? `<a class="also" href="/videos/${esc(trailer.slug)}/">${icon('videos')}<span>Watch the trailer</span></a>` : ''}${g.saves ? `<a class="also" href="/account/?next=${encodeURIComponent(`/${g.id}/play`)}" data-account>${icon('spark')}<span>Your progress follows you: sign in</span></a>` : ''}
|
|
938
973
|
</div>
|
|
939
974
|
</section>`;
|
|
940
975
|
|
|
@@ -979,27 +1014,28 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
979
1014
|
const o = C.original;
|
|
980
1015
|
const creditCards = [
|
|
981
1016
|
`<div class="credit"><h3>Made by</h3><p><strong><a href="/">${esc(name)}</a></strong>${cat.studio?.tagline ? ` · ${esc(cat.studio.tagline)}` : ''}</p>${(C.people ?? []).length ? `<ul>${C.people.map((c) => `<li>${c.role ? `${esc(c.role)}: ` : ''}${c.url ? `<a href="${esc(c.url)}" rel="noopener">${esc(c.name)}</a>` : `<strong>${esc(c.name)}</strong>`}</li>`).join('')}</ul>` : ''}</div>`,
|
|
1017
|
+
lineage ? `<div class="credit"><h3>Remix of</h3><p>${ofWhat}.</p>${lineage.page ? `<p><a href="${esc(lineage.page)}" rel="noopener">${esc(lineage.page.replace(/^https:\/\//, ''))}</a></p>` : ''}</div>` : '',
|
|
982
1018
|
o && !o.house ? `<div class="credit"><h3>Based on</h3><p><strong>${esc(o.title)}</strong>${o.author ? ` by <strong>${esc(o.author)}</strong>` : ''}${o.year ? ` (${esc(o.year)})` : ''}${o.licence ? `, ${esc(o.licence)} licence` : ''}.</p>${o.url ? `<p><a href="${esc(o.url)}" rel="noopener">${esc(o.url.replace(/^https:\/\//, ''))}</a></p>` : ''}<p>Made multiplayer with Homie: public rooms, bots, join-in-progress, a new host when one leaves, touch controls and the big screen.</p></div>` : '',
|
|
983
1019
|
(C.parts ?? []).length ? `<div class="credit"><h3>Also inside</h3><ul>${C.parts.map((p) => `<li>${esc(p.what)}${p.author ? `: ${esc(p.author)}` : ''}${p.url ? ` (<a href="${esc(p.url)}" rel="noopener">source</a>)` : ''}${p.licence ? `, ${p.licenceUrl ? `<a href="${esc(p.licenceUrl)}" rel="noopener">${esc(p.licence)}</a>` : esc(p.licence)}` : ''}</li>`).join('')}</ul></div>` : '',
|
|
984
|
-
`<div class="credit"><h3>Built with</h3><p>Homie’s open engine: <a href="https://github.com/homie-rocks/homie" rel="noopener">@homie-rocks/studio</a> (rooms, netplay, the big screen), Apache-2.0.</p>${C.texts ? `<p><a href="/${esc(g.id)}/credits">Licences and full credits</a></p>` : ''}</div>`,
|
|
1020
|
+
`<div class="credit"><h3>Built with</h3><p>Homie’s open engine: <a href="https://github.com/homie-rocks/homie" rel="noopener">@homie-rocks/studio</a> (rooms, netplay, the big screen), Apache-2.0.</p>${L.source ? `<p>Its source: ${esc(licenseLabel(license))}.</p>` : ''}${C.texts ? `<p><a href="/${esc(g.id)}/credits">Licences and full credits</a></p>` : ''}</div>`,
|
|
985
1021
|
].filter(Boolean).join('');
|
|
986
1022
|
const creditsBand = `<section class="band tight" aria-labelledby="credits-title"><div class="band-in"><p class="kicker reveal">Credits</p><h2 class="small-h reveal" id="credits-title">Who made ${esc(g.name)}</h2><div class="credits reveal">${creditCards}</div></div></section>`;
|
|
987
1023
|
|
|
988
1024
|
const source = `${origin}/games/${g.id}/source.json`;
|
|
989
|
-
const say =
|
|
1025
|
+
const say = remixable
|
|
990
1026
|
? `Remix ${g.name} from ${source} into a game of my own in my Homie studio`
|
|
991
1027
|
: `Make a multiplayer game like ${g.name} in my Homie studio`;
|
|
992
|
-
const makeHref =
|
|
1028
|
+
const makeHref = remixable ? `https://homie.rocks/studio/?remix=${encodeURIComponent(source)}` : 'https://homie.rocks/studio/';
|
|
993
1029
|
const makeBand = `<section class="band" aria-labelledby="make-title"><div class="band-in"><div class="make reveal">
|
|
994
|
-
<p class="kicker">${
|
|
1030
|
+
<p class="kicker">${remixable ? 'Open to remix' : 'Your turn'}</p>
|
|
995
1031
|
<h2 id="make-title">Make a game like this</h2>
|
|
996
|
-
<p class="lead">${
|
|
1032
|
+
<p class="lead">${remixable ? `${esc(g.name)}’s source is shared. Homie is a plugin for Claude Code and Codex: your AI copies this game into a studio of your own, makes it yours, and puts it online with public rooms like these, on your own free Cloudflare account.` : `Homie is a plugin for Claude Code and Codex: your AI sets up a studio of your own and makes a multiplayer game like ${esc(g.name)}, with public rooms like these, on your own free Cloudflare account.`}</p>
|
|
997
1033
|
<ol class="say">
|
|
998
1034
|
<li><div><span class="label">1 · In Claude Code</span><code>/plugin marketplace add homie-rocks/homie</code></div><button class="copy" type="button" data-copy="/plugin marketplace add homie-rocks/homie">${icon('copy')}<span data-copy-word>Copy</span></button></li>
|
|
999
1035
|
<li><div><span class="label">2</span><code>/plugin install homie@homie</code></div><button class="copy" type="button" data-copy="/plugin install homie@homie">${icon('copy')}<span data-copy-word>Copy</span></button></li>
|
|
1000
1036
|
<li><div><span class="label">3 · Then say</span><code>${esc(say)}</code></div><button class="copy" type="button" data-copy="${esc(say)}">${icon('copy')}<span data-copy-word>Copy</span></button></li>
|
|
1001
1037
|
</ol>
|
|
1002
|
-
<div class="keys"><a class="btn" href="${esc(makeHref)}">${icon('spark')}<span>Make a game like this</span></a>${
|
|
1038
|
+
<div class="keys"><a class="btn" href="${esc(makeHref)}">${icon('spark')}<span>Make a game like this</span></a>${remixable ? `<a class="ghost" href="/games/${esc(g.id)}/source.json">See the source</a>` : ''}</div>
|
|
1003
1039
|
</div></div></section>`;
|
|
1004
1040
|
|
|
1005
1041
|
// A partial that is its own <section> stands as it is; anything else sits in one of the page's bands.
|
|
@@ -1018,10 +1054,12 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
1018
1054
|
/** /<game>/credits: the licence texts the game ships with (CREDITS.md and every licence file credits.json names). */
|
|
1019
1055
|
export function creditsPage(cat, g, texts, { origin = '' } = {}) {
|
|
1020
1056
|
const name = studioName(cat);
|
|
1057
|
+
const lineage = remixRow(g.remixOf);
|
|
1058
|
+
const of = lineage ? `<p class="sec">Remix of</p><p>${lineage.page ? `<a href="${esc(lineage.page)}" rel="noopener">${esc(lineage.name)}</a>` : esc(lineage.name)}${lineage.studio ? ` by ${esc(lineage.studio)}` : ''}</p>` : '';
|
|
1021
1059
|
return layout(cat, {
|
|
1022
1060
|
title: `Credits · ${g.name} · ${name}`, description: `Credits and licences for ${g.name}.`, origin, path: `/${g.id}/credits`, page: 'credits', active: 'games',
|
|
1023
1061
|
main: `${pageHead(g.name, 'Credits and licences', null, `<div class="keys"><a class="ghost" href="/${esc(g.id)}/">${icon('arrow')}<span>Back to ${esc(g.name)}</span></a></div>`)}
|
|
1024
|
-
<div class="wrap">${texts.map(({ file, text }) => `<p class="sec">${esc(file)}</p><pre class="licence">${esc(text)}</pre>`).join('')}</div>`,
|
|
1062
|
+
<div class="wrap">${of}${texts.map(({ file, text }) => `<p class="sec">${esc(file)}</p><pre class="licence">${esc(text)}</pre>`).join('')}</div>`,
|
|
1025
1063
|
});
|
|
1026
1064
|
}
|
|
1027
1065
|
|
package/worker/stats.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* visit a page opened (home, a game's page, /music/, a song's page, /videos/, a video's page)
|
|
10
10
|
* play a game's play page opened: somebody pressed Play (or followed a Play link or QR code)
|
|
11
11
|
* screen a big screen opened (/<game>/tv)
|
|
12
|
+
* watch a watch page opened (/<game>/watch: a live room from any player's view; 0.15.0)
|
|
12
13
|
* room a room opened: the first seat taken in an empty room (public, or a named one)
|
|
13
14
|
* round a round finished (source: `people` when a person was in it, `bots` when only bots were)
|
|
14
15
|
* humans the people in finished rounds, summed
|
|
@@ -53,7 +54,7 @@ CREATE TABLE IF NOT EXISTS stats_keys (
|
|
|
53
54
|
) WITHOUT ROWID;
|
|
54
55
|
`;
|
|
55
56
|
|
|
56
|
-
export const METRICS = Object.freeze(['visit', 'play', 'screen', 'room', 'round', 'humans', 'peak', 'peak-room', 'song', 'video']);
|
|
57
|
+
export const METRICS = Object.freeze(['visit', 'play', 'screen', 'watch', 'room', 'round', 'humans', 'peak', 'peak-room', 'song', 'video']);
|
|
57
58
|
const MAX_METRICS = new Set(['peak', 'peak-room']);
|
|
58
59
|
const SLUG = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
59
60
|
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -288,7 +289,7 @@ export async function readStats(env, cat, { range, only = null, studios = new Se
|
|
|
288
289
|
const of = (m, extra = () => true) => sum((r) => r.metric === m && r.subject === g.id && extra(r));
|
|
289
290
|
return {
|
|
290
291
|
id: g.id, name: g.name, seats: g.players?.max ?? null,
|
|
291
|
-
visits: of('visit'), plays: of('play'), screens: of('screen'), rooms: of('room'),
|
|
292
|
+
visits: of('visit'), plays: of('play'), screens: of('screen'), watches: of('watch'), rooms: of('room'),
|
|
292
293
|
rounds: of('round'), roundsWithPeople: of('round', (r) => r.source === 'people'), peopleInRounds: of('humans'),
|
|
293
294
|
peakPlayers: maxOf((r) => r.metric === 'peak' && r.subject === g.id), peakInOneRoom: maxOf((r) => r.metric === 'peak-room' && r.subject === g.id),
|
|
294
295
|
playingNow: now[g.id]?.players ?? 0, roomsNow: now[g.id]?.rooms ?? 0,
|
|
@@ -316,7 +317,7 @@ export async function readStats(env, cat, { range, only = null, studios = new Se
|
|
|
316
317
|
return {
|
|
317
318
|
v: 1, kind: 'homie-studio-stats', studio: cat.studio?.name ?? null, range, ...(only ? { only } : {}),
|
|
318
319
|
totals: {
|
|
319
|
-
visits: sum((r) => r.metric === 'visit'), plays: sum((r) => r.metric === 'play'), screens: sum((r) => r.metric === 'screen'),
|
|
320
|
+
visits: sum((r) => r.metric === 'visit'), plays: sum((r) => r.metric === 'play'), screens: sum((r) => r.metric === 'screen'), watches: sum((r) => r.metric === 'watch'),
|
|
320
321
|
rooms: sum((r) => r.metric === 'room'), rounds: sum((r) => r.metric === 'round'), roundsWithPeople: sum((r) => r.metric === 'round' && r.source === 'people'),
|
|
321
322
|
peopleInRounds: sum((r) => r.metric === 'humans'), songPlays: sum((r) => r.metric === 'song'), videoViews: sum((r) => r.metric === 'video'),
|
|
322
323
|
peakPlayers: Math.max(0, ...perGame.map((g) => g.peakPlayers)), peakInOneRoom: Math.max(0, ...perGame.map((g) => g.peakInOneRoom)),
|
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.15.0';
|