@homie-rocks/studio 0.14.4 → 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 +43 -6
- package/bin/homie-studio.mjs +59 -3
- package/lib/build.mjs +48 -0
- package/lib/office.mjs +7 -6
- package/lib/scaffold.mjs +50 -3
- package/lib/servers.mjs +133 -0
- package/lib/template-history.json +55 -0
- package/netplay/NETPLAY.md +317 -21
- package/netplay/netplay.ts +649 -33
- package/package.json +1 -1
- package/port/bots.ts +25 -2
- package/port/hud.ts +10 -7
- package/port/index.ts +5 -2
- package/port/room.ts +48 -10
- package/port/skill.ts +28 -0
- package/port/touch.ts +4 -1
- package/site/SITE.md +71 -0
- package/starters/ember-vale/src/main.ts +58 -12
- package/starters/gem-rush/src/main.ts +128 -33
- package/worker/account-page.mjs +26 -0
- package/worker/agents.mjs +238 -0
- package/worker/index.mjs +353 -68
- package/worker/office-page.mjs +178 -10
- package/worker/office.mjs +294 -25
- package/worker/pages.mjs +622 -18
- package/worker/players.mjs +11 -3
- package/worker/room.mjs +547 -41
- package/worker/saves.mjs +4 -0
- package/worker/servers.mjs +445 -0
- package/worker/site.mjs +166 -10
- package/worker/stats.mjs +9 -3
- package/worker/version.mjs +1 -1
package/worker/office-page.mjs
CHANGED
|
@@ -93,6 +93,19 @@ button.small { min-height: 30px; padding: 4px 10px; font-size: 13px; border-radi
|
|
|
93
93
|
.toast { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); background: #1f2433; border: 1px solid var(--line2); padding: 10px 16px; border-radius: 12px; font-weight: 600; max-width: calc(100vw - 32px); z-index: 9; }
|
|
94
94
|
.note { color: var(--dim); font-size: 13px; max-width: 75ch; margin-top: 26px; }
|
|
95
95
|
.err { color: var(--bad); }
|
|
96
|
+
.servers { padding: 12px 16px 14px; border-bottom: 1px solid var(--line); }
|
|
97
|
+
.srv { border-top: 1px dashed var(--line); padding: 8px 0; }
|
|
98
|
+
.srv:first-of-type { border-top: 0; }
|
|
99
|
+
.srvhead { display: grid; grid-template-columns: minmax(140px, 1.3fr) minmax(160px, 1.6fr) minmax(150px, 1.3fr) auto; gap: 6px 12px; align-items: center; cursor: pointer; font-size: 14px; }
|
|
100
|
+
.srvhead b { font-size: 15px; }
|
|
101
|
+
.srvbody { padding: 8px 0 4px 14px; display: grid; gap: 8px; font-size: 14px; }
|
|
102
|
+
.srvbody .line { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
103
|
+
.warnbox { margin: 10px 16px; padding: 10px 12px; border-radius: 12px; border: 1px solid rgba(255,179,92,.45); color: var(--warn); font-size: 14px; }
|
|
104
|
+
.secret { font: 600 13px/1.4 ui-monospace, Menlo, monospace; word-break: break-all; padding: 10px; border-radius: 10px; background: var(--bg); border: 1px solid var(--warn); }
|
|
105
|
+
.chip.ai { color: #ffe7a8; border-color: rgba(255,207,90,.6); }
|
|
106
|
+
.newsrv { display: grid; gap: 8px; padding: 10px 0 0; }
|
|
107
|
+
.newsrv .line { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
108
|
+
@media (max-width: 720px) { .srvhead { grid-template-columns: 1fr 1fr; } }
|
|
96
109
|
@media (max-width: 720px) {
|
|
97
110
|
.rhead { grid-template-columns: 1fr 1fr 1fr; }
|
|
98
111
|
.rhead .racts { grid-column: 1 / -1; justify-content: flex-start; }
|
|
@@ -190,6 +203,151 @@ export const OFFICE_SCRIPT = String.raw`(function () {
|
|
|
190
203
|
return box;
|
|
191
204
|
}
|
|
192
205
|
|
|
206
|
+
/* ---------------------------------------------------------------- servers and agent seats (0.16.0) */
|
|
207
|
+
var POLICY = { open: 'Open', 'humans-only': 'Humans only', hybrid: 'Hybrid', beginner: 'Beginner' };
|
|
208
|
+
var LEVELS = ['Rookie', 'Steady', 'Fair', 'Strong', 'Maxed'];
|
|
209
|
+
function sel(options, value, label) { var x = el('select'); options.forEach(function (o) { var op = el('option', '', o[1]); op.value = o[0]; if (String(o[0]) === String(value)) op.selected = true; x.appendChild(op); }); if (label) x.setAttribute('aria-label', label); return x; }
|
|
210
|
+
function num(value, min, max, label) { var x = el('input'); x.type = 'number'; x.min = String(min); x.max = String(max); x.value = String(value); x.setAttribute('aria-label', label); return x; }
|
|
211
|
+
function srvSummary(sv) {
|
|
212
|
+
var bits = [POLICY[sv.policy] + (sv.policy === 'hybrid' ? ' · ' + sv.aiSeats : ''), sv.policy === 'beginner' ? sv.guides + ' guides' : null, sv.kids ? 'kids' : null,
|
|
213
|
+
sv.bots === 'off' ? 'bots off' : 'bots fill', LEVELS[sv.level - 1], sv.door !== 'open' ? 'door: ' + sv.door : null, sv.state !== 'open' ? sv.state.toUpperCase() : null];
|
|
214
|
+
return bits.filter(Boolean).join(' · ');
|
|
215
|
+
}
|
|
216
|
+
/** The owner may narrow a server: a second thought first (a stricter door, humans-only, fewer rooms, closing). */
|
|
217
|
+
function narrowing(sv, f) {
|
|
218
|
+
var rank = { open: 0, accounts: 1, invite: 2 };
|
|
219
|
+
return (f.door && rank[f.door] > rank[sv.door]) || (f.policy && f.policy !== sv.policy && (f.policy === 'humans-only' || f.policy === 'beginner')) || (f.rooms !== undefined && f.rooms < sv.rooms);
|
|
220
|
+
}
|
|
221
|
+
function setServer(g, sv, f, done) {
|
|
222
|
+
if (narrowing(sv, f) && !confirm('This takes something away on ' + sv.name + (f.policy === 'humans-only' ? ': its AI players leave after the current round' : '') + '. Go ahead?')) return;
|
|
223
|
+
act('/_studio/api/servers/set', Object.assign({ game: g.id, server: sv.id }, f), done || sv.name + ' changed.');
|
|
224
|
+
}
|
|
225
|
+
function serverRow(g, sv) {
|
|
226
|
+
var box = el('div', 'srv');
|
|
227
|
+
var key = 'srv:' + g.id + '/' + sv.id;
|
|
228
|
+
var head = el('div', 'srvhead');
|
|
229
|
+
head.onclick = function (e) { if (e.target.closest('button,select,input')) return; S.open[key] = !S.open[key]; render(); };
|
|
230
|
+
var live = sv.live || { rooms: 0, players: 0, ai: 0 };
|
|
231
|
+
add(head, add(el('div'), el('b', '', (S.open[key] ? '▾ ' : '▸ ') + sv.name + (sv.id === 'public' ? ' (public)' : '')), el('div', 'faint', sv.id === 'public' ? 'Quick play' : sv.id)),
|
|
232
|
+
el('div', 'dim', srvSummary(sv)),
|
|
233
|
+
el('div', 'stat', live.rooms ? live.rooms + (live.rooms === 1 ? ' room' : ' rooms') + ' · ' + live.players + ' playing · ' + live.ai + ' AI' : '—'),
|
|
234
|
+
el('div', 'faint', sv.members ? sv.members + ' members' : ''));
|
|
235
|
+
box.appendChild(head);
|
|
236
|
+
if (!S.open[key]) return box;
|
|
237
|
+
var body = el('div', 'srvbody');
|
|
238
|
+
var pol = sel([['open', 'Open'], ['hybrid', 'Hybrid'], ['beginner', 'Beginner'], ['humans-only', 'Humans only']], sv.policy, 'Policy');
|
|
239
|
+
var door = sel([['open', 'Door: anyone'], ['accounts', 'Door: accounts'], ['invite', 'Door: invite']], sv.door, 'Door');
|
|
240
|
+
var bots = sel([['fill', 'Bots fill empty seats'], ['off', 'Bots off']], sv.bots, 'Bots');
|
|
241
|
+
var level = sel(LEVELS.map(function (n, i) { return [i + 1, 'Default ' + n]; }), sv.level, 'Default level');
|
|
242
|
+
var levelMax = sel(LEVELS.map(function (n, i) { return [i + 1, 'Ceiling ' + n]; }), sv.levelMax, 'Ceiling');
|
|
243
|
+
var ai = num(sv.policy === 'beginner' ? sv.guides : sv.aiSeats, 0, 31, sv.policy === 'beginner' ? 'Guides' : 'AI seats');
|
|
244
|
+
add(body, add(el('div', 'line'), pol, door, bots, level, levelMax, el('span', 'faint', sv.policy === 'beginner' ? 'guides' : 'AI seats'), ai,
|
|
245
|
+
btn('Save', 'small', function () {
|
|
246
|
+
var f = {};
|
|
247
|
+
if (pol.value !== sv.policy) f.policy = pol.value;
|
|
248
|
+
if (door.value !== sv.door) f.door = door.value;
|
|
249
|
+
if (bots.value !== sv.bots) f.bots = bots.value;
|
|
250
|
+
if (Number(level.value) !== sv.level) f.level = Number(level.value);
|
|
251
|
+
if (Number(levelMax.value) !== sv.levelMax) f.levelMax = Number(levelMax.value);
|
|
252
|
+
var n = Math.floor(Number(ai.value));
|
|
253
|
+
if ((pol.value === 'beginner' ? 'guides' : 'aiSeats') && n !== (sv.policy === 'beginner' ? sv.guides : sv.aiSeats)) f[pol.value === 'beginner' ? 'guides' : 'aiSeats'] = n;
|
|
254
|
+
if (!Object.keys(f).length) { toast('Nothing changed.'); return; }
|
|
255
|
+
setServer(g, sv, f);
|
|
256
|
+
})));
|
|
257
|
+
add(body, el('div', 'faint', sv.line));
|
|
258
|
+
if (sv.id !== 'public' || sv.state !== 'open') {
|
|
259
|
+
add(body, add(el('div', 'line'), Object.assign(el('a', '', 'Its page'), { href: sv.page, target: '_blank', rel: 'noopener' }),
|
|
260
|
+
sv.state === 'open' ? armed('Close server', 'bad', function () { act('/_studio/api/servers/close', { game: g.id, server: sv.id }, sv.name + ' closes after the current round.'); })
|
|
261
|
+
: btn('Open it again', 'ghost small', function () { act('/_studio/api/servers/close', { game: g.id, server: sv.id, reopen: true }, sv.name + ' is open again.'); })));
|
|
262
|
+
} else add(body, add(el('div', 'line'), armed('Hide Quick play', 'bad', function () { act('/_studio/api/servers/close', { game: g.id, server: 'public' }, 'Quick play is hidden: Play shows the other servers.'); })));
|
|
263
|
+
if (sv.policy !== 'humans-only') {
|
|
264
|
+
var brain = sel([['script', 'Guides: scripted'], ['off', 'Guides: off'], ['workers-ai', 'Guides talk (Workers AI)'], ['owner-key', 'Guides talk (your key)']], sv.brain, 'AI guides');
|
|
265
|
+
add(body, add(el('div', 'line'), el('span', 'dim', 'AI guides\' brain'), brain, btn('Set', 'ghost small', function () {
|
|
266
|
+
if ((brain.value === 'workers-ai' || brain.value === 'owner-key') && !S.data.agentsTalk && !confirm('Let AI guides talk? They speak only the lines the game\'s agents.json gives them, at most one every 8 seconds, never about a person, and any player can quiet them.')) return;
|
|
267
|
+
act('/_studio/api/agents/brain', { game: g.id, server: sv.id, mode: brain.value });
|
|
268
|
+
})));
|
|
269
|
+
}
|
|
270
|
+
if (sv.id !== 'public') {
|
|
271
|
+
var mbox = el('div', 'line'); mbox.appendChild(el('span', 'dim', 'Members:'));
|
|
272
|
+
var showM = btn('Show', 'ghost small', function () {
|
|
273
|
+
fetch('/_studio/api/servers/members?game=' + encodeURIComponent(g.id) + '&server=' + encodeURIComponent(sv.id), { credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (d) {
|
|
274
|
+
mbox.textContent = ''; mbox.appendChild(el('span', 'dim', 'Members:'));
|
|
275
|
+
(d.members || []).forEach(function (m) {
|
|
276
|
+
var who = m.account ? (m.account.handle || m.account.id) : m.player;
|
|
277
|
+
var span = add(el('span', 'chip' + (m.role !== 'member' ? ' owner' : '')), document.createTextNode(who + (m.home ? ' ★' : '') + (m.role !== 'member' ? ' · ' + m.role : '')));
|
|
278
|
+
mbox.appendChild(span);
|
|
279
|
+
if (m.role !== 'mentor') mbox.appendChild(btn('Mentor', 'ghost small', function () { act('/_studio/api/servers/member', { game: g.id, server: sv.id, player: m.player, role: 'mentor', name: who }, who + ' is a mentor of ' + sv.name + '.'); }));
|
|
280
|
+
mbox.appendChild(armed('Remove', 'bad', function () { act('/_studio/api/servers/member', { game: g.id, server: sv.id, player: m.player, remove: true, name: who }, who + ' was removed from ' + sv.name + '.'); }));
|
|
281
|
+
});
|
|
282
|
+
if (!(d.members || []).length) mbox.appendChild(el('span', 'faint', d.ok ? 'nobody yet' : (d.message || 'not available')));
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
mbox.appendChild(showM);
|
|
286
|
+
body.appendChild(mbox);
|
|
287
|
+
}
|
|
288
|
+
box.appendChild(body);
|
|
289
|
+
return box;
|
|
290
|
+
}
|
|
291
|
+
function newServer(g) {
|
|
292
|
+
var key = 'new:' + g.id;
|
|
293
|
+
var wrap = el('div');
|
|
294
|
+
if (!S.open[key]) { wrap.appendChild(btn('+ New server', 'small', function () { S.open[key] = true; render(); })); return wrap; }
|
|
295
|
+
var form = el('div', 'newsrv');
|
|
296
|
+
var name = el('input'); name.type = 'text'; name.placeholder = 'Name (Night Shift)'; name.maxLength = 40;
|
|
297
|
+
var pol = sel([['hybrid', 'Hybrid: AI seats in every room'], ['beginner', 'Beginner: new players, AI guides'], ['humans-only', 'Humans only'], ['open', 'Open']], 'hybrid', 'Policy');
|
|
298
|
+
var n = num(2, 0, Math.max(0, g.maxPlayers - 1), 'AI seats or guides');
|
|
299
|
+
var kids = el('input'); kids.type = 'checkbox'; kids.setAttribute('aria-label', 'Kids');
|
|
300
|
+
var door = sel([['open', 'Door: anyone'], ['accounts', 'Door: accounts'], ['invite', 'Door: invite']], 'open', 'Door');
|
|
301
|
+
var level = sel(LEVELS.map(function (x, i) { return [i + 1, 'Level ' + x]; }), 3, 'Default level');
|
|
302
|
+
var levelMax = sel(LEVELS.map(function (x, i) { return [i + 1, 'Ceiling ' + x]; }), 5, 'Ceiling');
|
|
303
|
+
var speech = sel([['game', 'Chat: the game\'s'], ['lines', 'Chat: quick lines only'], ['off', 'Chat: off']], 'game', 'Speech');
|
|
304
|
+
var rooms = num(4, 1, 16, 'Rooms');
|
|
305
|
+
var listed = el('input'); listed.type = 'checkbox'; listed.checked = true; listed.setAttribute('aria-label', 'Listed');
|
|
306
|
+
add(form, add(el('div', 'line'), name, pol),
|
|
307
|
+
add(el('div', 'line'), el('span', 'dim', 'AI seats / guides'), n, add(el('label'), kids, el('span', '', 'kids (beginner)')), door),
|
|
308
|
+
add(el('div', 'line'), level, levelMax, speech, el('span', 'dim', 'rooms'), rooms, add(el('label'), listed, el('span', '', 'listed'))),
|
|
309
|
+
add(el('div', 'line'), btn('Create', 'small', function () {
|
|
310
|
+
var body = { game: g.id, name: name.value, policy: pol.value, door: door.value, level: Number(level.value), levelMax: Number(levelMax.value), speech: speech.value, rooms: Number(rooms.value), listed: listed.checked };
|
|
311
|
+
if (pol.value === 'hybrid') body.aiSeats = Number(n.value);
|
|
312
|
+
if (pol.value === 'beginner') { body.guides = Number(n.value); body.kids = kids.checked; }
|
|
313
|
+
act('/_studio/api/servers', body, (name.value || 'The server') + ' is open.').then(function (r) { if (r && r.ok) { S.open[key] = false; render(); } });
|
|
314
|
+
}), btn('Cancel', 'ghost small', function () { S.open[key] = false; render(); })));
|
|
315
|
+
wrap.appendChild(form);
|
|
316
|
+
return wrap;
|
|
317
|
+
}
|
|
318
|
+
function passes(g) {
|
|
319
|
+
var box = el('div', 'servers');
|
|
320
|
+
add(box, el('h3', '', 'Agent passes (an AI\'s way into a seat, always marked AI)'));
|
|
321
|
+
(g.passes || []).forEach(function (p) {
|
|
322
|
+
var row = el('div', 'inv' + (p.live ? '' : ' closed'));
|
|
323
|
+
add(row, el('code', '', p.name), el('span', 'faint', p.role + ' · hands ' + p.hands + (p.server ? ' · ' + p.server : ' · any server') + (p.expiresAt ? ' · until ' + new Date(p.expiresAt).toLocaleDateString() : '') + (p.revoked ? ' · revoked' : !p.live ? ' · ended' : '')));
|
|
324
|
+
if (p.live) row.appendChild(armed('Revoke', 'bad', function () { act('/_studio/api/agents/pass', { action: 'revoke', game: g.id, id: p.id }, 'Pass revoked: that AI left.'); }));
|
|
325
|
+
box.appendChild(row);
|
|
326
|
+
});
|
|
327
|
+
var label = el('input'); label.type = 'text'; label.placeholder = 'Its name (Claude)'; label.maxLength = 16;
|
|
328
|
+
var server = sel([['', 'any server that lets AI in']].concat((g.servers || []).filter(function (x) { return x.policy !== 'humans-only'; }).map(function (x) { return [x.id, x.name]; })), '', 'Server');
|
|
329
|
+
var hands = sel([['self', 'runs the game itself'], ['host', 'moved by the host\'s bots']], 'self', 'Hands');
|
|
330
|
+
var out = el('div');
|
|
331
|
+
add(box, add(el('div', 'newinv'), label, server, hands, btn('Issue a pass', 'small', function () {
|
|
332
|
+
post('/_studio/api/agents/pass', { action: 'create', game: g.id, label: label.value, server: server.value || null, hands: hands.value, days: 7 }).then(function (r) {
|
|
333
|
+
out.textContent = '';
|
|
334
|
+
if (!r || !r.ok) { toast((r && r.message) || 'No pass made.', true); return; }
|
|
335
|
+
add(out, el('p', 'dim', 'Shown once. Give it to the AI only; it plays as ' + r.pass.name + ' for 7 days.'), el('div', 'secret', r.secret));
|
|
336
|
+
label.value = ''; S.keep = true;
|
|
337
|
+
});
|
|
338
|
+
})), out);
|
|
339
|
+
var fill = el('input', 'switch'); fill.type = 'checkbox'; fill.disabled = true; fill.setAttribute('aria-label', 'Fill-a-spot service');
|
|
340
|
+
add(box, add(el('label', 'faint'), fill, el('span', '', (S.data.fillSpot && S.data.fillSpot.note) || 'Let a fill-a-spot service seat AI (coming later)')));
|
|
341
|
+
return box;
|
|
342
|
+
}
|
|
343
|
+
function serversBox(g) {
|
|
344
|
+
var box = el('div', 'servers');
|
|
345
|
+
add(box, el('h3', '', 'Servers'));
|
|
346
|
+
(g.servers || []).forEach(function (sv) { box.appendChild(serverRow(g, sv)); });
|
|
347
|
+
box.appendChild(newServer(g));
|
|
348
|
+
return box;
|
|
349
|
+
}
|
|
350
|
+
|
|
193
351
|
function person(g, r, c, now) {
|
|
194
352
|
var row = el('div', 'person');
|
|
195
353
|
var who = el('div', 'who');
|
|
@@ -197,7 +355,7 @@ export const OFFICE_SCRIPT = String.raw`(function () {
|
|
|
197
355
|
c.seat !== null && c.seat !== undefined ? el('span', 'faint', 'seat ' + (c.seat + 1)) : el('span', 'faint', c.waiting ? 'waiting for a seat' : 'watching'),
|
|
198
356
|
el('span', 'chip', c.device === 'phone' ? 'phone' : c.device === 'tv' ? 'big screen' : 'computer'),
|
|
199
357
|
c.role === 'host' ? el('span', 'chip host', 'host') : null,
|
|
200
|
-
c.as === 'owner' ? el('span', 'chip owner', 'you') : c.as === 'invited' ? el('span', 'chip invited', 'invited') : c.account ? el('span', 'chip owner', 'player ' + (c.account.handle || c.account.id)) : el('span', 'chip', 'guest'),
|
|
358
|
+
c.agent ? el('span', 'chip ai', 'AI ' + (c.agent.role === 'guide' ? 'guide' : c.agent.role === 'party' ? 'companion' : 'player')) : c.as === 'owner' ? el('span', 'chip owner', 'you') : c.as === 'invited' ? el('span', 'chip invited', 'invited') : c.account ? el('span', 'chip owner', 'player ' + (c.account.handle || c.account.id)) : el('span', 'chip', 'guest'),
|
|
201
359
|
c.muted ? el('span', 'chip muted', 'muted') : null,
|
|
202
360
|
el('span', 'faint', 'here ' + dur(now - c.joinedAt)),
|
|
203
361
|
c.browser ? el('span', 'faint', 'browser ' + c.browser) : null);
|
|
@@ -215,7 +373,7 @@ export const OFFICE_SCRIPT = String.raw`(function () {
|
|
|
215
373
|
var box = el('div', 'room');
|
|
216
374
|
var key = g.id + '/' + r.room;
|
|
217
375
|
var head = el('div', 'rhead');
|
|
218
|
-
head.onclick = function (e) { if (e.target.closest('button')) return; S.open[key] = !S.open[key]; render(); };
|
|
376
|
+
head.onclick = function (e) { if (e.target.closest('button,select')) return; S.open[key] = !S.open[key]; render(); };
|
|
219
377
|
var round = r.round ? 'Round ' + (r.round.n || '?') + (r.round.phase === 'live' && r.round.endsAt ? ' · ' + clock(r.round.endsAt - now) + ' left' : r.round.phase === 'over' ? ' · results' : '') : 'no round yet';
|
|
220
378
|
var announce = btn('Announce', 'ghost small', function () {
|
|
221
379
|
var text = prompt('Announce to everyone in ' + r.label + ' of ' + g.name + ':');
|
|
@@ -224,10 +382,14 @@ export const OFFICE_SCRIPT = String.raw`(function () {
|
|
|
224
382
|
var acts = add(el('div', 'racts'), announce, r.closedUntil
|
|
225
383
|
? btn('Reopen', 'ghost small', function () { act('/_studio/api/close', { game: g.id, room: r.room, reopen: true }, r.label + ' is open again.'); })
|
|
226
384
|
: armed('Close', 'bad', function () { act('/_studio/api/close', { game: g.id, room: r.room, minutes: hold() }, r.label + ' closed for ' + hold() + ' min.'); }));
|
|
385
|
+
// The room's dial (room_level): the owner sets it at once, as the party's vote does.
|
|
386
|
+
var lvl = r.policy ? sel([1, 2, 3, 4, 5].filter(function (n) { return n <= (r.policy.levelMax || 5); }).map(function (n) { return [n, LEVELS[n - 1]]; }), r.policy.level, 'AI level') : null;
|
|
387
|
+
if (lvl) lvl.onchange = function () { act('/_studio/api/room-level', { game: g.id, room: r.room, level: Number(lvl.value) }, 'AI in ' + r.label + ' set to ' + LEVELS[Number(lvl.value) - 1] + '.'); };
|
|
388
|
+
var srvName = ((g.servers || []).filter(function (x) { return x.id === r.server; })[0] || {}).name;
|
|
227
389
|
add(head,
|
|
228
|
-
add(el('div'), el('b', '', (S.open[key] ? '▾ ' : '▸ ') + r.label), el('div', 'faint', r.public ? 'public room' : 'named room')),
|
|
229
|
-
add(el('div', 'stat'), el('span', '', 'players'), document.createTextNode((r.players) + ' / ' + r.max)),
|
|
230
|
-
add(el('div', 'stat'), el('span', '', 'bots'), document.createTextNode(String(r.bots))),
|
|
390
|
+
add(el('div'), el('b', '', (S.open[key] ? '▾ ' : '▸ ') + r.label), el('div', 'faint', (r.server && r.server !== 'public' ? (srvName || r.server) + ' · ' : '') + (r.public ? 'public room' : r.server !== 'public' ? 'server room' : 'named room'))),
|
|
391
|
+
add(el('div', 'stat'), el('span', '', 'players'), document.createTextNode((r.players) + ' / ' + (r.humanSeats || r.max))),
|
|
392
|
+
add(el('div', 'stat'), el('span', '', 'AI · bots'), document.createTextNode((r.ai || 0) + ' · ' + String(r.bots)), lvl ? add(el('div'), lvl) : null),
|
|
231
393
|
add(el('div', 'stat'), el('span', '', 'round'), document.createTextNode(round)),
|
|
232
394
|
add(el('div', 'stat'), el('span', '', 'up'), document.createTextNode(r.closedUntil ? 'closed ' + dur(r.closedUntil - now) : dur(now - r.openedAt))),
|
|
233
395
|
acts);
|
|
@@ -239,7 +401,8 @@ export const OFFICE_SCRIPT = String.raw`(function () {
|
|
|
239
401
|
var people = el('div', 'people');
|
|
240
402
|
(r.clients || []).forEach(function (c) { people.appendChild(person(g, r, c, now)); });
|
|
241
403
|
var bots = (r.slots || []).filter(function (s) { return s.bot; });
|
|
242
|
-
if (bots.length) people.appendChild(el('div', 'held', '
|
|
404
|
+
if (bots.length) people.appendChild(el('div', 'held', 'AI and bots: ' + bots.map(function (s) { return s.name + (s.agent ? ' (AI ' + (s.agent.role === 'guide' ? 'guide' : 'seat') + ')' : ''); }).join(', ')));
|
|
405
|
+
if (r.vote && r.vote.open) people.appendChild(el('div', 'held', 'The party is voting on the AI\'s level: ' + r.vote.voters + ' of ' + r.vote.of_total + ' voted.'));
|
|
243
406
|
(r.bans || []).forEach(function (b) { people.appendChild(el('div', 'held', 'Kicked: ' + (b.name || 'someone') + ', may come back in ' + dur(b.until - now) + (b.address ? ' (their network too)' : ''))); });
|
|
244
407
|
box.appendChild(people);
|
|
245
408
|
}
|
|
@@ -253,8 +416,8 @@ export const OFFICE_SCRIPT = String.raw`(function () {
|
|
|
253
416
|
liveEl.textContent = d.playing ? d.playing + (d.playing === 1 ? ' playing now' : ' playing now') : 'nobody playing right now';
|
|
254
417
|
document.getElementById('livedot').className = 'dot' + (d.playing ? ' on' : '');
|
|
255
418
|
var focus = document.activeElement && root.contains(document.activeElement) && /INPUT|SELECT/.test(document.activeElement.tagName);
|
|
256
|
-
// Never redraw under a typing owner, or a control waiting for its second tap
|
|
257
|
-
if (focus || root.querySelector('[data-armed="1"]')) return;
|
|
419
|
+
// Never redraw under a typing owner, or a control waiting for its second tap, or a pass secret shown once.
|
|
420
|
+
if (focus || root.querySelector('[data-armed="1"]') || root.querySelector('.secret')) return;
|
|
258
421
|
root.textContent = '';
|
|
259
422
|
if (!d.games.length) { root.appendChild(el('p', 'empty', 'This studio has no games yet.')); return; }
|
|
260
423
|
d.games.forEach(function (g) {
|
|
@@ -263,6 +426,10 @@ export const OFFICE_SCRIPT = String.raw`(function () {
|
|
|
263
426
|
add(head, add(el('div', 'name'), el('h2', '', g.name), el('span', 'faint', g.playing ? g.playing + ' playing' : 'quiet')),
|
|
264
427
|
add(el('div', 'links'), Object.assign(el('a', '', 'Play'), { href: g.play, target: '_blank', rel: 'noopener' }), Object.assign(el('a', '', 'Page'), { href: g.page, target: '_blank', rel: 'noopener' })));
|
|
265
428
|
add(card, head, gameSettings(g));
|
|
429
|
+
// A build from before servers (netplay rev 5 or older): what it cannot do yet, and the fix.
|
|
430
|
+
if (g.build && g.build.predates && (g.servers || []).some(function (x) { return x.id !== 'public' && (x.aiSeats || x.guides); })) card.appendChild(el('div', 'warnbox', 'This build predates servers (netplay rev ' + (g.build.netplayRev || '5 or older') + '): reserved AI seats stay empty and its bots don\'t read the dial. Rebuild with @homie-rocks/studio 0.16.'));
|
|
431
|
+
card.appendChild(serversBox(g));
|
|
432
|
+
card.appendChild(passes(g));
|
|
266
433
|
if (g.launch === 'invite' || (g.invites && g.invites.length)) card.appendChild(invites(g));
|
|
267
434
|
var rooms = el('div', 'rooms');
|
|
268
435
|
if (!g.rooms.length) rooms.appendChild(el('div', 'empty', g.launch === 'private' ? 'Private: only you can open it. Nobody is in a room.' : 'Nobody is in a room right now.'));
|
|
@@ -322,6 +489,7 @@ export async function officePage(cat, headers = {}) {
|
|
|
322
489
|
<p class="dim" id="status" role="status"></p>
|
|
323
490
|
<div id="games" aria-live="polite"><p class="empty">Reading the rooms…</p></div>
|
|
324
491
|
<div class="toast" id="toast" role="status" hidden></div>
|
|
492
|
+
<p class="note">Servers: each game's named room pools. Open: anyone, AI with a pass marked AI. Humans only: no AI at all. Hybrid: some seats in every room are AI companions; the party votes their level. Beginner: new players, AI guides, quick lines only. Changing a server reaches its live rooms at once; AI leave after the round when a server becomes humans-only.</p>
|
|
325
493
|
<p class="note">Live from this studio's own rooms, every few seconds. A kick holds that player out of that room for the time you pick (their browser, and their account once players sign in); a mute stops their chat and emotes; Close sends everyone in a room out with a thank-you. Private: only you can open the game. Invite-only: invited players only (each invite link or code lets a browser in). Public: anyone, and listed. A game that is not public leaves the Homie directory the next time it reads this studio. This page is yours alone, never cached or indexed.</p>`;
|
|
326
494
|
return new Response(shell(cat, `Office · ${name}`, body, { script: OFFICE_SCRIPT }), {
|
|
327
495
|
headers: {
|
|
@@ -341,7 +509,7 @@ export function lockedPage(cat, { what = 'office', ask = null } = {}) {
|
|
|
341
509
|
return new Response(shell(cat, `${what === 'confirm' ? 'Confirm' : 'Office'} · private`, body), { status: 401, headers: { ...PRIVATE, 'content-security-policy': NO_SCRIPT_CSP } });
|
|
342
510
|
}
|
|
343
511
|
|
|
344
|
-
const VERB = { kick: 'Kick', mute: 'Mute', close: 'Close the room', announce: 'Announce', game: 'Change it' };
|
|
512
|
+
const VERB = { kick: 'Kick', mute: 'Mute', close: 'Close the room', announce: 'Announce', game: 'Change it', 'server-set': 'Change it', 'server-close': 'Close it', member: 'Remove', 'agents-brain': 'Let them talk' };
|
|
345
513
|
|
|
346
514
|
/** The owner's one tap for what their AI asked, and what came of it. */
|
|
347
515
|
export function confirmPage(cat, ask, { missing = false } = {}) {
|
|
@@ -353,7 +521,7 @@ export function confirmPage(cat, ask, { missing = false } = {}) {
|
|
|
353
521
|
const verb = ask.action?.op === 'mute' && ask.action.off ? 'Unmute' : ask.action?.op === 'close' && ask.action.reopen ? 'Open it again' : VERB[ask.action?.op] ?? 'Yes';
|
|
354
522
|
body = `<h1>${esc(name)} <small>Your AI asks</small></h1>
|
|
355
523
|
<p style="font-size:19px;line-height:1.45;max-width:46ch">${esc(ask.what)}</p>
|
|
356
|
-
<form method="post" class="bar" style="background:transparent;border:0;padding:0"><button type="submit" name="do" value="yes" class="${['kick', 'close'].includes(ask.action?.op) && !ask.action?.reopen ? 'armed' : ''}">${esc(verb)}</button><button type="submit" name="do" value="no" class="ghost">No</button></form>
|
|
524
|
+
<form method="post" class="bar" style="background:transparent;border:0;padding:0"><button type="submit" name="do" value="yes" class="${['kick', 'close', 'server-close', 'member'].includes(ask.action?.op) && !ask.action?.reopen ? 'armed' : ''}">${esc(verb)}</button><button type="submit" name="do" value="no" class="ghost">No</button></form>
|
|
357
525
|
<p class="note">Only you can confirm this: your AI asked through the studio's office key, which cannot say yes. It lasts until ${esc(new Date(ask.expiresAt).toISOString().slice(11, 16))} UTC.</p>`;
|
|
358
526
|
} else {
|
|
359
527
|
const said = { done: 'Done.', failed: 'That did not work.', cancelled: 'Not done: you said no.', expired: 'This ask ran out (15 minutes). Your AI can ask again.', working: 'Working on it…' }[ask.state] ?? ask.state;
|