@homie-rocks/studio 0.15.0 → 0.16.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 +34 -4
- package/bin/homie-studio.mjs +59 -3
- package/lib/build.mjs +45 -0
- package/lib/office.mjs +7 -6
- package/lib/scaffold.mjs +41 -2
- package/lib/servers.mjs +133 -0
- package/lib/template-history.json +28 -0
- package/netplay/NETPLAY.md +207 -17
- package/netplay/netplay.ts +360 -30
- package/package.json +1 -1
- package/port/bots.ts +25 -2
- package/port/index.ts +5 -2
- package/port/room.ts +33 -10
- package/port/skill.ts +28 -0
- package/site/SITE.md +43 -0
- package/starters/ember-vale/src/main.ts +21 -7
- package/starters/gem-rush/src/main.ts +84 -19
- package/worker/account-page.mjs +26 -0
- package/worker/agents.mjs +238 -0
- package/worker/index.mjs +307 -74
- package/worker/office-page.mjs +178 -10
- package/worker/office.mjs +294 -25
- package/worker/pages.mjs +206 -15
- package/worker/players.mjs +11 -3
- package/worker/room.mjs +491 -44
- package/worker/saves.mjs +4 -0
- package/worker/servers.mjs +445 -0
- package/worker/site.mjs +138 -7
- package/worker/stats.mjs +6 -1
- package/worker/version.mjs +1 -1
package/worker/site.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { qrSvg } from './qr.mjs';
|
|
13
13
|
import { STUDIO_VERSION_TAG } from './version.mjs';
|
|
14
14
|
import { licenseLabel, licenseOf, remixRow } from './license.mjs';
|
|
15
|
+
import { POLICY_WORDS, policyWords } from './servers.mjs';
|
|
15
16
|
|
|
16
17
|
export const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
17
18
|
const jsonScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c');
|
|
@@ -388,6 +389,27 @@ img,video{display:block;max-width:100%}
|
|
|
388
389
|
.hero-media.drift img,.hero-media.drift::after,.hero-media.bare::after{animation:none}
|
|
389
390
|
.js .reveal{opacity:1;transform:none;transition:none}
|
|
390
391
|
}
|
|
392
|
+
|
|
393
|
+
/* servers (0.16.0): a game's named room pools, their policy badges, AI marks, and their doors */
|
|
394
|
+
.pol{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;border:1px solid color-mix(in srgb,var(--fg) 26%,transparent);font:700 11px/1.4 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--soft);white-space:nowrap}
|
|
395
|
+
.pol.hybrid{border-color:color-mix(in srgb,var(--hot) 60%,transparent);color:var(--fg)}
|
|
396
|
+
.pol.beginner{border-color:color-mix(in srgb,#7dd3fc 70%,transparent);color:#bfe9ff}
|
|
397
|
+
.pol.humans-only{border-color:color-mix(in srgb,#43ff9e 60%,transparent);color:#c6ffe0}
|
|
398
|
+
.ai{display:inline-flex;align-items:center;padding:1px 7px;border-radius:999px;background:color-mix(in srgb,var(--hot) 22%,transparent);color:var(--fg);font:800 11px/1.4 var(--mono);letter-spacing:.06em;vertical-align:1px}
|
|
399
|
+
.scards{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));gap:16px;margin-top:24px}
|
|
400
|
+
.scard{display:flex;flex-direction:column;gap:10px;padding:18px 20px;border-radius:var(--r);background:var(--panel);border:1px solid var(--line)}
|
|
401
|
+
.scard .top{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
|
|
402
|
+
.scard h3{margin:0;font:800 22px/1.1 var(--display);letter-spacing:-.02em}
|
|
403
|
+
.scard h3 a{text-decoration:none}
|
|
404
|
+
.scard p{margin:0;color:var(--soft);font-size:15px}
|
|
405
|
+
.scard .nums{color:var(--dim);font-size:14px}
|
|
406
|
+
.scard .acts{display:flex;gap:10px;align-items:center;margin-top:auto;flex-wrap:wrap}
|
|
407
|
+
.scard .acts .btn{min-height:44px;font-size:16px;padding:0 20px;flex:none}
|
|
408
|
+
.homeb{display:inline-flex;align-items:center;gap:7px;min-height:40px;padding:0 14px;border-radius:12px;border:1px solid color-mix(in srgb,var(--fg) 24%,transparent);background:transparent;color:var(--fg);font:700 14px/1 var(--display);cursor:pointer;text-decoration:none}
|
|
409
|
+
.homeb[aria-pressed="true"]{border-color:var(--hot);color:var(--hot)}
|
|
410
|
+
.quickplay{display:inline-flex;gap:8px;align-items:center;margin-top:18px;color:var(--soft);font-weight:700;text-decoration:none}
|
|
411
|
+
.door-why{margin-top:14px;color:var(--soft);max-width:60ch}
|
|
412
|
+
.room .srv{color:var(--soft);font-weight:700}
|
|
391
413
|
`;
|
|
392
414
|
|
|
393
415
|
/* ------------------------------------------------------------------ the script (served at /_homie/site.js) */
|
|
@@ -431,7 +453,7 @@ d.addEventListener('click',function(ev){var b=ev.target&&ev.target.closest&&ev.t
|
|
|
431
453
|
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
454
|
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>';
|
|
433
455
|
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);
|
|
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'));
|
|
456
|
+
var mid=el('div');mid.appendChild(el('b',null,(r.name?r.name+' · ':'')+(r.server?r.server.name+' · ':'')+r.label));mid.appendChild(el('span',null,r.players+' of '+r.max+' seats taken'+(r.ai?' · '+r.ai+' AI':'')+(r.policy==='humans-only'?' · humans only':' · bots hold the rest')));
|
|
435
457
|
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);
|
|
436
458
|
var acts=el('div','room-acts');
|
|
437
459
|
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)}
|
|
@@ -451,6 +473,11 @@ if(src){
|
|
|
451
473
|
var go=function(){setInterval(ask,15000);d.addEventListener('visibilitychange',ask)};
|
|
452
474
|
if(d.prerendering===true)d.addEventListener('prerenderingchange',function(){ask();go()},{once:true});else go();
|
|
453
475
|
}
|
|
476
|
+
// A server's "Make this my home" and "Leave" (a signed-in player): POST /<game>/s/<id>/home, then the page again.
|
|
477
|
+
[].forEach.call(d.querySelectorAll('[data-server-home]'),function(b){b.addEventListener('click',function(){
|
|
478
|
+
var o={};try{o=JSON.parse(b.getAttribute('data-server-home')||'{}')}catch(e){}b.disabled=true;
|
|
479
|
+
fetch(o.at,{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json'},body:JSON.stringify(o.body||{})}).then(function(r){return r.json()}).then(function(j){
|
|
480
|
+
if(j&&j.ok){location.reload();return}b.disabled=false;b.textContent=(j&&j.message)||'That did not work'}).catch(function(){b.disabled=false})})});
|
|
454
481
|
})();`;
|
|
455
482
|
|
|
456
483
|
/* ------------------------------------------------------------------ the shell */
|
|
@@ -659,19 +686,26 @@ export function watchOf(g) {
|
|
|
659
686
|
return 'follow';
|
|
660
687
|
}
|
|
661
688
|
|
|
662
|
-
/**
|
|
663
|
-
|
|
664
|
-
|
|
689
|
+
/**
|
|
690
|
+
* A room as the live list draws it (the same fields /api/rooms and /<game>/live give the script). A server's room
|
|
691
|
+
* (0.16.0) names its server and policy, and how many AI are in it (always marked AI).
|
|
692
|
+
*/
|
|
693
|
+
export function roomView(g, r, max, srv = null) {
|
|
665
694
|
const room = r.name ?? r.room;
|
|
695
|
+
const m = /^(?:pub|s-[a-z0-9-]+)-(\d+)$/.exec(String(room ?? ''));
|
|
696
|
+
const n = m ? Number(m[1]) : null;
|
|
666
697
|
return {
|
|
667
698
|
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
699
|
// Watch this room from any player's view (section 16); a game that cannot be watched has no link.
|
|
669
700
|
watch: watchOf(g) === 'off' ? null : `/${g.id}/watch?room=${encodeURIComponent(room)}`,
|
|
701
|
+
server: srv && srv.id !== 'public' ? { id: srv.id, name: srv.name, badge: policyWords(srv).badge } : null,
|
|
702
|
+
policy: srv?.policy ?? 'open',
|
|
703
|
+
ai: Number(r.ai ?? r.agents ?? 0) || 0,
|
|
670
704
|
};
|
|
671
705
|
}
|
|
672
706
|
|
|
673
707
|
function roomRows(rows, { names = true } = {}) {
|
|
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('');
|
|
708
|
+
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)} · ` : ''}${r.server ? `${esc(r.server.name)} · ` : ''}${esc(r.label)}</b><span>${esc(r.players)} of ${esc(r.max)} seats taken${r.ai ? ` · ${esc(r.ai)} AI` : ''}${r.policy === 'humans-only' ? ' · humans only' : ' · 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('');
|
|
675
709
|
}
|
|
676
710
|
|
|
677
711
|
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' }) : ''; };
|
|
@@ -825,6 +859,103 @@ export function roomsPage(cat, { origin = '', rooms = [] } = {}) {
|
|
|
825
859
|
});
|
|
826
860
|
}
|
|
827
861
|
|
|
862
|
+
/* ------------------------------------------------------------------ servers (0.16.0) */
|
|
863
|
+
|
|
864
|
+
const SERVERS_LEAD = 'Rooms on a server only ever match players from that server.';
|
|
865
|
+
|
|
866
|
+
/** One server's card: its name, policy badge and line, who is on it now, and Play (and its page). */
|
|
867
|
+
function serverCard(g, sv) {
|
|
868
|
+
const words = policyWords(sv);
|
|
869
|
+
const nums = [`${sv.playing} playing`, sv.ai ? `${sv.ai} AI` : '', sv.members ? `${sv.members} ${sv.members === 1 ? 'member' : 'members'}` : ''].filter(Boolean).join(' · ');
|
|
870
|
+
const page = sv.id === 'public' ? `/${g.id}/` : `/${g.id}/s/${sv.id}/`;
|
|
871
|
+
const play = sv.id === 'public' ? `/${g.id}/play` : `/${g.id}/s/${sv.id}/play`;
|
|
872
|
+
return `<article class="scard reveal" data-server="${esc(sv.id)}"><div class="top"><h3><a href="${esc(page)}">${esc(sv.name)}</a></h3><span class="pol ${esc(sv.policy)}">${esc(words.badge)}</span></div>
|
|
873
|
+
<p>${esc(sv.blurb || words.line)}</p><p class="nums">${esc(nums)}</p>
|
|
874
|
+
<div class="acts"><a class="btn" href="${esc(play)}" data-play>${PLAY_ICON}Play</a>${sv.id === 'public' ? '' : `<a class="ghost" href="${esc(page)}">About</a>`}</div></article>`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Servers worth showing to everyone: open, listed, and not the public one (Quick play is its own link). */
|
|
878
|
+
const shownServers = (list) => (list ?? []).filter((x) => x.id !== 'public' && x.listed && x.state === 'open');
|
|
879
|
+
|
|
880
|
+
/** The landing's Servers band: there when the game has a server besides Quick play. */
|
|
881
|
+
export function serversBand(g, list) {
|
|
882
|
+
const shown = shownServers(list);
|
|
883
|
+
if (!shown.length) return '';
|
|
884
|
+
const pub = (list ?? []).find((x) => x.id === 'public');
|
|
885
|
+
return `<section class="band tight" aria-labelledby="servers-title" data-servers-band><div class="band-in">
|
|
886
|
+
<div class="head-row reveal"><div><p class="kicker">Servers</p><h2 class="small-h" id="servers-title">${esc(SERVERS_LEAD)}</h2></div><a class="more" href="/${esc(g.id)}/servers/">Every server ${icon('arrow')}</a></div>
|
|
887
|
+
<div class="scards">${shown.map((sv) => serverCard(g, sv)).join('')}</div>
|
|
888
|
+
${pub && pub.state === 'open' ? `<a class="quickplay" href="/${esc(g.id)}/play" data-play>Quick play ${icon('arrow')} <span class="dim">(the public server)</span></a>` : ''}
|
|
889
|
+
</div></section>`;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** /<game>/servers/: every server anyone can join (and, with `pick`, the page Play shows when Quick play is off). */
|
|
893
|
+
export function serversPage(cat, g, { origin = '', servers = [], hidden = false, pick = false } = {}) {
|
|
894
|
+
const shown = shownServers(servers);
|
|
895
|
+
const pub = servers.find((x) => x.id === 'public');
|
|
896
|
+
const cards = [...(pub && !hidden ? [pub] : []), ...shown];
|
|
897
|
+
return layout(cat, {
|
|
898
|
+
title: `Servers · ${g.name}`, description: `${g.name}'s servers: ${SERVERS_LEAD}`, origin, path: `/${g.id}/servers/`, page: 'servers', active: 'games',
|
|
899
|
+
main: `${pageHead(g.name, pick ? 'Pick a server' : 'Servers', SERVERS_LEAD)}
|
|
900
|
+
<div class="wrap">${cards.length ? `<div class="scards">${cards.map((sv) => serverCard(g, sv)).join('')}</div>` : `<p class="quiet">No server is open right now.</p>`}
|
|
901
|
+
<p class="sec">Every policy</p><ul class="howto">${Object.values(POLICY_WORDS).map((w) => `<li>${esc(w.line.replace('{n}', 'N'))}</li>`).join('')}</ul></div>`,
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* /<game>/s/<id>/: a server's page. Its name, blurb and badge with its line; its live rooms (AI marked); how many
|
|
907
|
+
* belong to it and this player's membership ("Make this my home"); and a big Play (or what its door asks for).
|
|
908
|
+
*/
|
|
909
|
+
export function serverPage(cat, g, sv, { origin = '', rooms = [], door = { ok: true }, member = null, signedIn = false } = {}) {
|
|
910
|
+
const words = policyWords(sv);
|
|
911
|
+
const play = `/${g.id}/s/${sv.id}/play`;
|
|
912
|
+
const at = `/${g.id}/s/${sv.id}/home`;
|
|
913
|
+
const homeAct = (body, label, pressed = false) => `<button class="homeb" type="button" aria-pressed="${pressed ? 'true' : 'false'}" data-server-home="${esc(JSON.stringify({ at, body }))}">${esc(label)}</button>`;
|
|
914
|
+
const membership = !signedIn ? `<a class="homeb" href="/account/?next=${encodeURIComponent(`/${g.id}/s/${sv.id}/`)}">Sign in to make it your home</a>`
|
|
915
|
+
: member ? `${member.home ? homeAct({ join: true, home: false }, '★ Your home', true) : homeAct({ join: true, home: true }, '☆ Make this my home')}${member.role === 'mentor' ? ' <span class="pol beginner">Mentor</span>' : member.role === 'mod' ? ' <span class="pol">Mod</span>' : ''}${member.role === 'member' ? ` ${homeAct({ join: false }, 'Leave')}` : ''}`
|
|
916
|
+
: homeAct({ join: true, home: true }, '☆ Make this my home');
|
|
917
|
+
const blocked = !door.ok ? `<p class="door-why">${esc(doorWords(g, sv, door).line)}</p>` : '';
|
|
918
|
+
return layout(cat, {
|
|
919
|
+
title: `${sv.name} · ${g.name}`, description: `${sv.name}, a ${words.badge.toLowerCase()} server of ${g.name}. ${words.line}`, origin, path: `/${g.id}/s/${sv.id}/`, page: 'server', active: 'games',
|
|
920
|
+
main: `<header class="head"><p class="kicker">${esc(g.name)} · server</p><h1>${esc(sv.name)}</h1>
|
|
921
|
+
<p class="lead"><span class="pol ${esc(sv.policy)}">${esc(words.badge)}</span> ${esc(words.line)}</p>${sv.blurb ? `<p class="lead">${esc(sv.blurb)}</p>` : ''}
|
|
922
|
+
${blocked}<div class="keys">${door.ok ? `<a class="btn" href="${esc(play)}" data-play>${PLAY_ICON}<span>Play on ${esc(sv.name)}</span></a>` : ''}${membership}</div></header>
|
|
923
|
+
<div class="wrap"><p class="sec">Live rooms · ${esc(sv.playing)} playing${sv.ai ? ` · ${esc(sv.ai)} AI` : ''} · ${esc(sv.members)} ${sv.members === 1 ? 'member' : 'members'}</p>
|
|
924
|
+
${rooms.length ? `<ol class="rooms">${roomRows(rooms, { names: false })}</ol>` : `<p class="quiet">Nobody is on ${esc(sv.name)} this minute. Press Play and you open its first room.</p>`}
|
|
925
|
+
<p class="sec"><a href="/${esc(g.id)}/servers/">Every server of ${esc(g.name)}</a></p></div>`,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** What a server's door says to someone it does not let in (DESIGN section 7.1). */
|
|
930
|
+
function doorWords(g, sv, { why, days = null } = {}) {
|
|
931
|
+
switch (why) {
|
|
932
|
+
case 'account': return { title: `Sign in to play on ${sv.name}`, line: `${sv.name} is for players with an account on this studio (a passkey: no password, nothing to remember).` };
|
|
933
|
+
case 'invite': return { title: `${sv.name} is invite-only`, line: `${sv.name} lets in players with an invite. Have a code? Enter it to play.` };
|
|
934
|
+
case 'veteran': return { title: `${sv.name} is for new players`, line: `${sv.name} is for new players.${days ? ` You've been playing ${g.name} for ${days} days:` : ''} try another server or Quick play.` };
|
|
935
|
+
default: return { title: `${sv.name} is closed`, line: `${sv.name} is not open right now.` };
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** A door that does not let this visitor in: said plainly, with the way on (sign in, an invite code, another server). */
|
|
940
|
+
export function doorPage(cat, g, sv, { why = 'closed', days = null, next = '', watch = false, code = '' } = {}) {
|
|
941
|
+
const shown = String(code ?? '').toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 8);
|
|
942
|
+
const pretty = shown.length === 8 ? `${shown.slice(0, 4)}-${shown.slice(4)}` : shown;
|
|
943
|
+
const w = doorWords(g, sv, { why, days });
|
|
944
|
+
const quick = `<a class="ghost" href="/${esc(g.id)}/play" data-play>Quick play</a>`;
|
|
945
|
+
const others = `<a class="ghost" href="/${esc(g.id)}/servers/">Other servers</a>`;
|
|
946
|
+
const acts = why === 'account'
|
|
947
|
+
? `<a class="btn" href="/account/?next=${encodeURIComponent(next || `/${g.id}/s/${sv.id}/play`)}">Sign in to play on ${esc(sv.name)}</a>${others}`
|
|
948
|
+
: why === 'invite'
|
|
949
|
+
? `<form method="post" action="/${esc(g.id)}/invite" class="keys" style="flex-wrap:wrap;gap:10px;margin-top:0"><input name="code" value="${esc(pretty)}" placeholder="XXXX-XXXX" autocomplete="one-time-code" autocapitalize="characters" spellcheck="false" maxlength="12" required aria-label="Invite code" style="font:600 18px/1 ui-monospace,Menlo,monospace;letter-spacing:.08em;padding:12px 14px;border-radius:12px;border:1px solid rgba(127,127,127,.35);background:transparent;color:inherit;min-width:12ch"><button class="btn" type="submit">Join ${esc(sv.name)}</button></form>${others}`
|
|
950
|
+
: `${quick}${others}`;
|
|
951
|
+
return layout(cat, {
|
|
952
|
+
title: `${w.title} · ${g.name}`, page: 'door', status: 403, active: 'games', extraHeaders: { 'cache-control': 'no-store, private', 'x-robots-tag': 'noindex' },
|
|
953
|
+
main: `<header class="head" data-door="${esc(why)}"><p class="kicker">${esc(g.name)} · ${esc(sv.name)}${watch ? ' · watch' : ''}</p><h1>${esc(w.title)}</h1>
|
|
954
|
+
<p class="lead">${esc(w.line)}</p><p class="door-why"><span class="pol ${esc(sv.policy)}">${esc(policyWords(sv).badge)}</span> ${esc(policyWords(sv).line)}</p>
|
|
955
|
+
<div class="keys">${acts}</div></header>`,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
828
959
|
export function postsPage(cat, { origin = '' } = {}) {
|
|
829
960
|
const posts = cat.posts ?? [];
|
|
830
961
|
const name = studioName(cat);
|
|
@@ -936,7 +1067,7 @@ export function jsonFeed(cat, posts, origin) {
|
|
|
936
1067
|
* public room, phone / computer / TV (with the join code), live rooms, how to play, credits, and "Make a game like
|
|
937
1068
|
* this". A studio's site/pages/<id>/index.html replaces it; site/partials/game.html and game-<id>.html add a band.
|
|
938
1069
|
*/
|
|
939
|
-
export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week = null, remix = true } = {}) {
|
|
1070
|
+
export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week = null, remix = true, servers = null } = {}) {
|
|
940
1071
|
const name = studioName(cat);
|
|
941
1072
|
const L = g.landing ?? {};
|
|
942
1073
|
// A remix says what it is a remix of, linked, under its name and in its credits (game.json `remixOf`).
|
|
@@ -1047,7 +1178,7 @@ export function gameLanding(cat, g, { origin = '', rooms = [], playing = 0, week
|
|
|
1047
1178
|
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(),
|
|
1048
1179
|
origin, path: `/${g.id}/`, image: h.wideImage ?? L.cover ?? null, page: 'landing', hero: true, active: 'games', over: landingTokens(cat, L),
|
|
1049
1180
|
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)">` : ''}` : ''}<script type="application/ld+json">${jsonScript(ld)}</script>`,
|
|
1050
|
-
main: `${hero}${ways}${liveBand}${howBand}${band(partial(cat, 'game', vars))}${band(partial(cat, `game-${g.id}`, vars))}${creditsBand}${makeBand}`,
|
|
1181
|
+
main: `${hero}${ways}${liveBand}${servers ? serversBand(g, servers) : ''}${howBand}${band(partial(cat, 'game', vars))}${band(partial(cat, `game-${g.id}`, vars))}${creditsBand}${makeBand}`,
|
|
1051
1182
|
});
|
|
1052
1183
|
}
|
|
1053
1184
|
|
package/worker/stats.mjs
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* humans the people in finished rounds, summed
|
|
16
16
|
* peak the most people playing one game at once that day (all its rooms), and peak-room in one room
|
|
17
17
|
* song a song's player started; video: a video's player started
|
|
18
|
+
* agent-minutes minutes an AI sat in a seat (an agent pass, NETPLAY.md section 17; 0.16.0), counted as it leaves
|
|
19
|
+
* brain-calls, brain-neurons reserved for AI guides' brains (a later version): calls made and Workers AI used
|
|
20
|
+
* A round's rows count agents as AI, never as people: `humans` is people only.
|
|
18
21
|
* Each visit, play and screen row carries WHERE FROM: '' (typed, bookmarked, or this site's own link), the
|
|
19
22
|
* referring site's host (homie.rocks, another studio, anywhere on the web), or `via:<tag>` from a ?via= link.
|
|
20
23
|
* Concurrent players now come from the Lobby at read time; nothing stores them.
|
|
@@ -54,7 +57,7 @@ CREATE TABLE IF NOT EXISTS stats_keys (
|
|
|
54
57
|
) WITHOUT ROWID;
|
|
55
58
|
`;
|
|
56
59
|
|
|
57
|
-
export const METRICS = Object.freeze(['visit', 'play', 'screen', 'watch', 'room', 'round', 'humans', 'peak', 'peak-room', 'song', 'video']);
|
|
60
|
+
export const METRICS = Object.freeze(['visit', 'play', 'screen', 'watch', 'room', 'round', 'humans', 'peak', 'peak-room', 'song', 'video', 'agent-minutes', 'brain-calls', 'brain-neurons']);
|
|
58
61
|
const MAX_METRICS = new Set(['peak', 'peak-room']);
|
|
59
62
|
const SLUG = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
60
63
|
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -292,6 +295,7 @@ export async function readStats(env, cat, { range, only = null, studios = new Se
|
|
|
292
295
|
visits: of('visit'), plays: of('play'), screens: of('screen'), watches: of('watch'), rooms: of('room'),
|
|
293
296
|
rounds: of('round'), roundsWithPeople: of('round', (r) => r.source === 'people'), peopleInRounds: of('humans'),
|
|
294
297
|
peakPlayers: maxOf((r) => r.metric === 'peak' && r.subject === g.id), peakInOneRoom: maxOf((r) => r.metric === 'peak-room' && r.subject === g.id),
|
|
298
|
+
agentMinutes: of('agent-minutes'),
|
|
295
299
|
playingNow: now[g.id]?.players ?? 0, roomsNow: now[g.id]?.rooms ?? 0,
|
|
296
300
|
};
|
|
297
301
|
});
|
|
@@ -320,6 +324,7 @@ export async function readStats(env, cat, { range, only = null, studios = new Se
|
|
|
320
324
|
visits: sum((r) => r.metric === 'visit'), plays: sum((r) => r.metric === 'play'), screens: sum((r) => r.metric === 'screen'), watches: sum((r) => r.metric === 'watch'),
|
|
321
325
|
rooms: sum((r) => r.metric === 'room'), rounds: sum((r) => r.metric === 'round'), roundsWithPeople: sum((r) => r.metric === 'round' && r.source === 'people'),
|
|
322
326
|
peopleInRounds: sum((r) => r.metric === 'humans'), songPlays: sum((r) => r.metric === 'song'), videoViews: sum((r) => r.metric === 'video'),
|
|
327
|
+
agentMinutes: sum((r) => r.metric === 'agent-minutes'),
|
|
323
328
|
peakPlayers: Math.max(0, ...perGame.map((g) => g.peakPlayers)), peakInOneRoom: Math.max(0, ...perGame.map((g) => g.peakInOneRoom)),
|
|
324
329
|
playingNow: perGame.reduce((n, g) => n + g.playingNow, 0),
|
|
325
330
|
},
|
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.16.0';
|