@homie-rocks/studio 0.22.0 → 0.24.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 +61 -1
- package/README.md +59 -0
- package/bin/homie-studio.mjs +68 -2
- package/chat/CHAT.md +101 -0
- package/chat/OWNERS.md +78 -0
- package/lib/build.mjs +20 -2
- package/lib/chat-cli.mjs +89 -0
- package/lib/cloudflare.mjs +5 -2
- package/lib/scaffold.mjs +59 -2
- package/lib/shop.mjs +270 -0
- package/lib/template-history.json +77 -0
- package/netplay/NETPLAY.md +194 -4
- package/netplay/netplay.ts +156 -3
- package/package.json +7 -1
- package/port/index.ts +3 -2
- package/port/view.ts +238 -0
- package/shop/SELLING.md +83 -0
- package/shop/SHOP.md +154 -0
- package/shop/shop.ts +130 -0
- package/site/SITE.md +22 -1
- package/starters/ember-vale/src/main.ts +22 -2
- package/starters/gem-rush/src/main.ts +21 -1
- package/starters/gem-rush-3d/src/main.ts +22 -1
- package/worker/account-page.mjs +14 -2
- package/worker/agents.mjs +3 -3
- package/worker/chat-page.mjs +556 -0
- package/worker/chat-store.mjs +161 -0
- package/worker/chat-words.mjs +46 -0
- package/worker/chat.mjs +419 -0
- package/worker/index.mjs +189 -7
- package/worker/office-page.mjs +120 -5
- package/worker/office.mjs +173 -6
- package/worker/pages.mjs +25 -8
- package/worker/players.mjs +8 -0
- package/worker/referrals.mjs +328 -0
- package/worker/room.mjs +291 -11
- package/worker/servers.mjs +5 -1
- package/worker/shop-page.mjs +532 -0
- package/worker/shop-rules.mjs +277 -0
- package/worker/shop-store.mjs +217 -0
- package/worker/shop.mjs +686 -0
- package/worker/site.mjs +4 -1
- package/worker/stats.mjs +4 -1
- package/worker/stripe.mjs +149 -0
- package/worker/version.mjs +1 -1
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* The take "strike" (lab.json) stands a big training slime in front of the hero (lab.stage "dummy"). Outside the lab
|
|
44
44
|
* every lab call is a no-op.
|
|
45
45
|
*/
|
|
46
|
-
import { AI_MARK, createControls, createLabels, createRoom, createSaves, easeView, exposePort, fitView, jitter, q, standoff, stripAi, type BodyBase, type Fit, type LabelIn, type LabelOut, type NetEvent, type Skill } from '@homie-rocks/studio/port';
|
|
46
|
+
import { AI_MARK, BUBBLE_FONT, createBubbles, createControls, createLabels, createRoom, paintBubbles, type BubbleIn, type BubbleOut, createSaves, easeView, exposePort, fitView, jitter, q, standoff, stripAi, type BodyBase, type Fit, type LabelIn, type LabelOut, type NetEvent, type Skill } from '@homie-rocks/studio/port';
|
|
47
47
|
import { useAgents, type Goal, type Vocabulary } from '@homie-rocks/studio/agents';
|
|
48
48
|
import vocabulary from '../agents.json';
|
|
49
49
|
// The Game Lab: tunables, phases, tracks and overlays (no-ops outside the lab).
|
|
@@ -712,8 +712,14 @@ const INSET = { top: 124, bottom: 100 }; // the hero panel and its pills; STRIKE
|
|
|
712
712
|
let cam: Fit | null = null;
|
|
713
713
|
let lastDraw = 0;
|
|
714
714
|
// Names keep clear of the guides' bubbles and of the ask panel (a page element over the canvas).
|
|
715
|
-
const labels = createLabels({ screen: () => ({ w: vw, h: vh }), avoid: () => { const p = document.getElementById('asks'); return p && !p.hidden ? [...
|
|
715
|
+
const labels = createLabels({ screen: () => ({ w: vw, h: vh }), avoid: () => { const p = document.getElementById('asks'); const all = [...bubbleBoxes, ...chatBubbles.boxes()]; return p && !p.hidden ? [...all, p.getBoundingClientRect()] : all; } });
|
|
716
716
|
let bubbleBoxes: { left: number; top: number; right: number; bottom: number }[] = [];
|
|
717
|
+
// Room chat (NETPLAY.md section 19): what a hero's player says in the room's chat shows over the hero for a few seconds,
|
|
718
|
+
// clear of the guides' own bubbles. The studio taking a message down takes its bubble too.
|
|
719
|
+
const chatBubbles = createBubbles({ measure: (t) => { ctx.font = BUBBLE_FONT; return ctx.measureText(t).width; }, screen: () => ({ w: vw, h: vh }), avoid: () => bubbleBoxes });
|
|
720
|
+
net.on('say', (e) => chatBubbles.say(e.seat, e.text, { id: e.id, kind: e.kind }));
|
|
721
|
+
net.on('unchat', (e) => { for (const id of e.ids) chatBubbles.remove(id); });
|
|
722
|
+
let shownChat: BubbleOut[] = [];
|
|
717
723
|
let shownLabels: LabelOut[] = [];
|
|
718
724
|
function aim(): Fit {
|
|
719
725
|
const v = lookOnly ? room.viewBody() : null;
|
|
@@ -772,6 +778,7 @@ function draw(t: number): void {
|
|
|
772
778
|
ctx.font = `600 ${fs}px ui-sans-serif, system-ui, sans-serif`;
|
|
773
779
|
const names: LabelIn[] = [];
|
|
774
780
|
const said: { x: number; y: number; head: number; text: string }[] = [];
|
|
781
|
+
const heads = new Map<number, { x: number; y: number; seat: number; self: boolean }>();
|
|
775
782
|
const mine: ScreenBox[] = []; // your own hero and its name: no bubble covers them
|
|
776
783
|
const meAt = (() => { const v = room.view().find((b) => !b.bot && b.seat === viewS); return v ? { x: v.x, y: v.y } : null; })();
|
|
777
784
|
for (const b of room.view()) {
|
|
@@ -801,6 +808,7 @@ function draw(t: number): void {
|
|
|
801
808
|
const body = { left: sx(x - 24), top: sy(y - R - 12), right: sx(x + 24), bottom: sy(y + R) };
|
|
802
809
|
if (body.right < 0 || body.left > vw || body.bottom < 0 || body.top > vh) continue; // off screen: no name at the edge
|
|
803
810
|
if (self) { const w = ctx.measureText(text).width; mine.push(body, { left: sx(x) - w / 2 - 6, top: body.top - 4 - fs * 1.2, right: sx(x) + w / 2 + 6, bottom: body.top }); }
|
|
811
|
+
if (!b.bot && b.seat !== null && b.seat !== undefined) heads.set(b.slot, { x: sx(x), y: body.top - 3, seat: b.seat, self });
|
|
804
812
|
names.push({ key: b.slot, text, x: sx(x), y: body.top - 3, w: ctx.measureText(text).width, h: fs * 1.2, below: body.bottom + 4 + fs * 1.2, body, self, rank: (b.bot ? (isGuide ? 1 : 2) : 0) * 10_000 + near });
|
|
805
813
|
const line = bubbles.get(b.slot);
|
|
806
814
|
// The bubble's tail stops just above the guide's own name, so the name keeps its spot while it speaks.
|
|
@@ -830,6 +838,16 @@ function draw(t: number): void {
|
|
|
830
838
|
ctx.fillText(l.text, l.cx, l.cy);
|
|
831
839
|
}
|
|
832
840
|
ctx.globalAlpha = 1; ctx.textBaseline = 'alphabetic';
|
|
841
|
+
// Room chat's bubbles: over each speaking hero's name (your own placed first), clear of the guides' bubbles.
|
|
842
|
+
if (chatBubbles.size) {
|
|
843
|
+
const anchors: BubbleIn[] = [];
|
|
844
|
+
for (const [slot, h] of heads) {
|
|
845
|
+
const l = shownLabels.find((x) => x.key === slot && x.alpha > 0 && !x.moved);
|
|
846
|
+
anchors.push({ key: h.seat, x: l ? l.cx : h.x, y: l ? l.top - 1 : h.y, self: h.self });
|
|
847
|
+
}
|
|
848
|
+
shownChat = chatBubbles.place(anchors);
|
|
849
|
+
paintBubbles(ctx, shownChat, { paper: '#f6efdc', ink: '#1a1710', edge: 'rgba(40,30,10,.35)' });
|
|
850
|
+
} else shownChat = [];
|
|
833
851
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
834
852
|
hud(t);
|
|
835
853
|
}
|
|
@@ -1116,6 +1134,8 @@ function frame(t: number): void {
|
|
|
1116
1134
|
|
|
1117
1135
|
// Tonight's experience per hero: the watch page's live scores, and who Auto follows when nobody is slaying.
|
|
1118
1136
|
net.expose({
|
|
1137
|
+
// Room chat's bubbles on this screen now: whose, what, and where (an end-to-end test reads them).
|
|
1138
|
+
chatBubbles: () => shownChat.map((b) => ({ seat: b.key, text: b.lines.join(' '), alpha: b.alpha, left: Math.round(b.left), top: Math.round(b.top), right: Math.round(b.right), bottom: Math.round(b.bottom) })),
|
|
1119
1139
|
scores: () => room.view().map((b) => ({ slot: b.slot, seat: b.seat, bot: b.bot, score: b.score })),
|
|
1120
1140
|
// The guides, for the e2e probe: each guide's goal now, and the log of goals, asks and lines (seats and ids only).
|
|
1121
1141
|
guides: () => ({
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
import { createNetplay, Roster, q, lerp, capMove, PALETTE, AI_MARK, type RoleChange, type RoundInfo, type RoundResult, type Skill, type Slot, type Snapshot } from '@homie-rocks/studio/netplay';
|
|
40
40
|
// The port toolkit: its probe (what `homie-studio port check` reads for the owner tests, and sandbox + audio shims),
|
|
41
41
|
// and a flat world on every screen (port/view.ts: the camera, and name labels that never pile up).
|
|
42
|
-
import { createLabels, exposePort, fitView, type LabelIn, type LabelOut } from '@homie-rocks/studio/port';
|
|
42
|
+
import { BUBBLE_FONT, createBubbles, createLabels, exposePort, fitView, paintBubbles, type BubbleIn, type BubbleOut, type LabelIn, type LabelOut } from '@homie-rocks/studio/port';
|
|
43
43
|
// The Game Lab: tunables, phases, tracks and overlays (no-ops outside the lab).
|
|
44
44
|
import { lab } from '@homie-rocks/studio/lab';
|
|
45
45
|
import tuning from '../tunables.json';
|
|
@@ -768,6 +768,12 @@ resize();
|
|
|
768
768
|
|
|
769
769
|
const cam = { x: W / 2, y: H / 2, scale: 0 };
|
|
770
770
|
const labels = createLabels({ screen: () => ({ w: innerWidth, h: innerHeight }) });
|
|
771
|
+
// Room chat (NETPLAY.md section 19): what a player says in the room's chat (the play page's Chat, or a quick line) shows
|
|
772
|
+
// over their body for a few seconds, when they want it there. The studio taking a message down takes its bubble too.
|
|
773
|
+
const bubbles = createBubbles({ measure: (t) => { ctx.font = BUBBLE_FONT; return ctx.measureText(t).width; }, screen: () => ({ w: innerWidth, h: innerHeight }) });
|
|
774
|
+
net.on('say', (s) => bubbles.say(s.seat, s.text, { id: s.id, kind: s.kind }));
|
|
775
|
+
net.on('unchat', (e) => { for (const id of e.ids) bubbles.remove(id); });
|
|
776
|
+
let shownBubbles: BubbleOut[] = [];
|
|
771
777
|
let shownLabels: LabelOut[] = [];
|
|
772
778
|
let lastDraw = 0;
|
|
773
779
|
/** Where a seat's body is drawn now (host: the real body; replica: interpolated), or null. */
|
|
@@ -868,6 +874,7 @@ function draw(t: number): void {
|
|
|
868
874
|
const fs = Math.round(Math.max(15, Math.min(22, (15 * Math.min(cw, ch)) / 720)));
|
|
869
875
|
ctx.font = `600 ${fs}px ui-sans-serif, system-ui, sans-serif`;
|
|
870
876
|
const tags: LabelIn[] = [];
|
|
877
|
+
const heads = new Map<number, { x: number; y: number; seat: number; mine: boolean }>();
|
|
871
878
|
const viewAt = list.find((a) => a.mine) ?? null;
|
|
872
879
|
for (const a of list) {
|
|
873
880
|
const colour = colourOf(a.slot, a.bot ? null : a.seat);
|
|
@@ -884,6 +891,7 @@ function draw(t: number): void {
|
|
|
884
891
|
const text = a.mine && !net.watching ? 'You' : label(a.name, a.bot);
|
|
885
892
|
const sx = cw / 2 + (a.x - cam.x) * scale; const sy = ch / 2 + (a.y - cam.y) * scale; const r = (R_AV + (a.mine ? 7 : 0)) * scale;
|
|
886
893
|
if (sx + r < 0 || sx - r > cw || sy + r < 0 || sy - r > ch) continue; // off screen: no name at the edge
|
|
894
|
+
if (a.seat !== null && !a.bot) heads.set(a.slot, { x: sx, y: sy - r - 5, seat: a.seat, mine: a.mine });
|
|
887
895
|
// People before bots, nearer the view's body first; each keeps off the others' bodies when it can.
|
|
888
896
|
tags.push({ key: a.slot, text, x: sx, y: sy - r - 5, w: ctx.measureText(text).width, h: fs * 1.2, below: sy + r + 4 + fs * 1.2, body: { left: sx - r, top: sy - r, right: sx + r, bottom: sy + r }, self: a.mine, rank: (a.bot ? 10_000 : 0) + (viewAt ? Math.hypot(a.x - viewAt.x, a.y - viewAt.y) : 0) });
|
|
889
897
|
}
|
|
@@ -909,6 +917,16 @@ function draw(t: number): void {
|
|
|
909
917
|
ctx.fillText(l.text, l.cx, l.cy);
|
|
910
918
|
}
|
|
911
919
|
ctx.globalAlpha = 1; ctx.textBaseline = 'alphabetic';
|
|
920
|
+
// Speech bubbles (room chat): over each speaker's name, the view's own player first.
|
|
921
|
+
if (bubbles.size) {
|
|
922
|
+
const anchors: BubbleIn[] = [];
|
|
923
|
+
for (const [slot, h] of heads) {
|
|
924
|
+
const l = shownLabels.find((x) => x.key === slot && x.alpha > 0 && !x.moved);
|
|
925
|
+
anchors.push({ key: h.seat, x: l ? l.cx : h.x, y: l ? l.top - 1 : h.y, self: h.mine });
|
|
926
|
+
}
|
|
927
|
+
shownBubbles = bubbles.place(anchors);
|
|
928
|
+
paintBubbles(ctx, shownBubbles);
|
|
929
|
+
} else shownBubbles = [];
|
|
912
930
|
|
|
913
931
|
hud(cw, ch, phone, list);
|
|
914
932
|
if (stick.active) {
|
|
@@ -985,6 +1003,8 @@ function frame(t: number): void {
|
|
|
985
1003
|
}
|
|
986
1004
|
|
|
987
1005
|
net.expose({
|
|
1006
|
+
// Room chat's bubbles on this screen now: whose, what, and where (an end-to-end test reads them).
|
|
1007
|
+
bubbles: () => shownBubbles.map((b) => ({ seat: b.key, text: b.lines.join(' '), alpha: b.alpha, left: Math.round(b.left), top: Math.round(b.top), right: Math.round(b.right), bottom: Math.round(b.bottom) })),
|
|
988
1008
|
self: () => (me.has ? { x: me.x, y: me.y } : null),
|
|
989
1009
|
peer: (seat: number) => {
|
|
990
1010
|
if (hosting) { const b = [...bodies.values()].find((x) => x.seat === seat); return b ? { x: b.x, y: b.y } : null; }
|
|
@@ -45,7 +45,7 @@ import { createModels, instancedCopies, placeCopy, repaint, stylize, type Copies
|
|
|
45
45
|
import { createNetplay, Roster, q, lerp, capMove, PALETTE, AI_MARK, type RoleChange, type RoundInfo, type RoundResult, type Skill, type Slot, type Snapshot } from '@homie-rocks/studio/netplay';
|
|
46
46
|
// The port toolkit: its probe (what `homie-studio port check` and `perf` read, and sandbox + audio shims), and name
|
|
47
47
|
// labels that never pile up (port/view.ts).
|
|
48
|
-
import { createLabels, exposePort, type LabelIn, type LabelOut } from '@homie-rocks/studio/port';
|
|
48
|
+
import { createBubbles, createLabels, exposePort, paintBubbles, type BubbleIn, type BubbleOut, type LabelIn, type LabelOut } from '@homie-rocks/studio/port';
|
|
49
49
|
// The Game Lab: tunables, phases, tracks and overlays (no-ops outside the lab).
|
|
50
50
|
import { lab } from '@homie-rocks/studio/lab';
|
|
51
51
|
import tuning from '../tunables.json';
|
|
@@ -1659,6 +1659,13 @@ resize();
|
|
|
1659
1659
|
let zoneMark: { left: number; top: number; right: number; bottom: number } | null = null;
|
|
1660
1660
|
const labels = createLabels({ screen: () => ({ w: innerWidth, h: innerHeight }), avoid: () => (zoneMark ? [zoneMark] : []) });
|
|
1661
1661
|
let shownLabels: LabelOut[] = [];
|
|
1662
|
+
// Room chat (NETPLAY.md section 19): what a player says in the room's chat shows over their animal for a few seconds,
|
|
1663
|
+
// on the HUD's own paper (one UI with the names, the clock and the board). The studio taking a message down takes it too.
|
|
1664
|
+
const BUBBLE_TYPE = `600 14px ${FONT_BODY}`;
|
|
1665
|
+
const bubbles = createBubbles({ measure: (t) => { ctx.font = BUBBLE_TYPE; return ctx.measureText(t).width; }, screen: () => ({ w: innerWidth, h: innerHeight }), avoid: () => (zoneMark ? [zoneMark] : []) });
|
|
1666
|
+
net.on('say', (s) => bubbles.say(s.seat, s.text, { id: s.id, kind: s.kind }));
|
|
1667
|
+
net.on('unchat', (e) => { for (const id of e.ids) bubbles.remove(id); });
|
|
1668
|
+
let shownBubbles: BubbleOut[] = [];
|
|
1662
1669
|
let lastDraw = 0;
|
|
1663
1670
|
/** Where a seat's body is drawn now (host: the real body; replica: interpolated), or null. */
|
|
1664
1671
|
function seatPos(seat: number): { x: number; y: number } | null {
|
|
@@ -1823,6 +1830,7 @@ function draw(t: number, dt: number): void {
|
|
|
1823
1830
|
const fs = Math.round(Math.max(15, Math.min(22, (15 * Math.min(cw, ch)) / 720)));
|
|
1824
1831
|
ctx.font = `700 ${fs}px ${FONT_BODY}`;
|
|
1825
1832
|
const tags: LabelIn[] = [];
|
|
1833
|
+
const heads = new Map<number, { x: number; y: number; seat: number; mine: boolean }>();
|
|
1826
1834
|
const viewAt = list.find((a) => a.mine) ?? null;
|
|
1827
1835
|
for (const a of list) {
|
|
1828
1836
|
const text = a.mine && !net.watching ? 'You' : label(a.name, a.bot);
|
|
@@ -1833,6 +1841,7 @@ function draw(t: number, dt: number): void {
|
|
|
1833
1841
|
const r = Math.max(6, Math.hypot(side.x - mid.x, side.y - mid.y));
|
|
1834
1842
|
if (mid.x + r < 0 || mid.x - r > cw || head.y > ch || mid.y + r < 0) continue; // off screen: no name at the edge
|
|
1835
1843
|
const foot = onScreen(a.x, a.y, 0, cw, ch);
|
|
1844
|
+
if (a.seat !== null && !a.bot) heads.set(a.slot, { x: head.x, y: head.y - 3, seat: a.seat, mine: a.mine });
|
|
1836
1845
|
// People before bots, nearer the view's body first; each keeps off the others' bodies when it can.
|
|
1837
1846
|
tags.push({ key: a.slot, text, x: head.x, y: head.y - 3, w: ctx.measureText(text).width, h: fs * 1.2, below: foot.y + r * 0.6 + 4 + fs * 1.2, body: { left: mid.x - r, top: head.y, right: mid.x + r, bottom: foot.y + r * 0.4 }, self: a.mine, rank: (a.bot ? 10_000 : 0) + (viewAt ? Math.hypot(a.x - viewAt.x, a.y - viewAt.y) * 50 : 0) });
|
|
1838
1847
|
}
|
|
@@ -1855,6 +1864,16 @@ function draw(t: number, dt: number): void {
|
|
|
1855
1864
|
ctx.fillText(l.text, l.cx, l.cy);
|
|
1856
1865
|
}
|
|
1857
1866
|
ctx.globalAlpha = 1; ctx.textBaseline = 'alphabetic';
|
|
1867
|
+
// Speech bubbles (room chat): over each speaker's name pill, the view's own player first.
|
|
1868
|
+
if (bubbles.size) {
|
|
1869
|
+
const anchors: BubbleIn[] = [];
|
|
1870
|
+
for (const [slot, h] of heads) {
|
|
1871
|
+
const l = shownLabels.find((x) => x.key === slot && x.alpha > 0 && !x.moved);
|
|
1872
|
+
anchors.push({ key: h.seat, x: l ? l.cx : h.x, y: l ? l.top - 3 : h.y, self: h.mine });
|
|
1873
|
+
}
|
|
1874
|
+
shownBubbles = bubbles.place(anchors);
|
|
1875
|
+
paintBubbles(ctx, shownBubbles, { font: BUBBLE_TYPE, paper: PAPER, ink: TEXT, edge: mixHex(TEXT, PAPER, 0.7) });
|
|
1876
|
+
} else shownBubbles = [];
|
|
1858
1877
|
|
|
1859
1878
|
hud(cw, ch, phone, list);
|
|
1860
1879
|
if (stick.active) {
|
|
@@ -2028,6 +2047,8 @@ function frame(t: number): void {
|
|
|
2028
2047
|
}
|
|
2029
2048
|
|
|
2030
2049
|
net.expose({
|
|
2050
|
+
// Room chat's bubbles on this screen now: whose, what, and where (an end-to-end test reads them).
|
|
2051
|
+
bubbles: () => shownBubbles.map((b) => ({ seat: b.key, text: b.lines.join(' '), alpha: b.alpha, left: Math.round(b.left), top: Math.round(b.top), right: Math.round(b.right), bottom: Math.round(b.bottom) })),
|
|
2031
2052
|
self: () => (me.has ? { x: me.x, y: me.y } : null),
|
|
2032
2053
|
peer: (seat: number) => {
|
|
2033
2054
|
if (hosting) { const b = [...bodies.values()].find((x) => x.seat === seat); return b ? { x: b.x, y: b.y } : null; }
|
package/worker/account-page.mjs
CHANGED
|
@@ -160,6 +160,10 @@ export function accountPage(cat, { origin = '', next = '/account/', owner = null
|
|
|
160
160
|
<label for="acct-email">Email</label><input id="acct-email" data-email type="email" autocomplete="email">
|
|
161
161
|
<div class="row"><button class="ghost" type="button" data-email-save>Send a confirmation</button><button class="ghost" type="button" data-email-remove hidden>Remove it</button></div>
|
|
162
162
|
</div>` : ''}
|
|
163
|
+
<div class="card shop-card" data-shop-card hidden>
|
|
164
|
+
<h2>Your purchases</h2>
|
|
165
|
+
<div data-shop-body></div>
|
|
166
|
+
</div>
|
|
163
167
|
<div class="card" data-servers-card hidden>
|
|
164
168
|
<h2>Your servers</h2>
|
|
165
169
|
<p>The servers you belong to in this studio's games. Your home server is where Play takes you.</p>
|
|
@@ -193,8 +197,9 @@ ${back}
|
|
|
193
197
|
</details>
|
|
194
198
|
</section>
|
|
195
199
|
<script type="application/json" id="account-boot">${JSON.stringify(boot).replace(/</g, '\\u003c')}</script>
|
|
196
|
-
<script src="/_homie/account.js" defer></script
|
|
197
|
-
|
|
200
|
+
<script src="/_homie/account.js" defer></script>
|
|
201
|
+
<script src="/_homie/shop.js" defer></script>`;
|
|
202
|
+
return layout(cat, { title: `Your account · ${studio}`, description: `Your player account on ${studio}.`, origin, path: '/account/', page: 'account', head: `<style>${ACCOUNT_CSS}.shop-card .badge{display:inline-block;padding:3px 10px;border-radius:999px;background:var(--hot,#ffcf5a);color:var(--hot-ink,#0b0b10);font:800 12px/1.3 ui-sans-serif,system-ui,sans-serif;margin-right:6px}.shop-card ul{margin:8px 0 0;padding-left:18px}.shop-card li{margin:4px 0}</style><meta name="robots" content="noindex">`, main });
|
|
198
203
|
}
|
|
199
204
|
|
|
200
205
|
/** /_homie/account.js */
|
|
@@ -343,6 +348,13 @@ export const ACCOUNT_JS = `${PASSKEY_JS}
|
|
|
343
348
|
var typed = prompt('This deletes your account and everything it kept on ' + boot.studio + ', in every game. It cannot be undone. Type DELETE to go ahead.');
|
|
344
349
|
if (!typed || typed.trim().toUpperCase() !== 'DELETE') { say('Nothing was deleted.'); return; }
|
|
345
350
|
P.api('POST', '/api/player/delete', { confirm: 'delete' }).then(function (r) {
|
|
351
|
+
if (!r.ok && r.error === 'purchases') {
|
|
352
|
+
var again = prompt(r.message + ' Type DELETE again to give them up and delete the account.');
|
|
353
|
+
if (!again || again.trim().toUpperCase() !== 'DELETE') return { ok: false, message: 'Nothing was deleted.' };
|
|
354
|
+
return P.api('POST', '/api/player/delete', { confirm: 'delete', purchases: 'forfeit' });
|
|
355
|
+
}
|
|
356
|
+
return r;
|
|
357
|
+
}).then(function (r) {
|
|
346
358
|
if (!r.ok) throw new Error(r.message);
|
|
347
359
|
me = null; P.remember(null); render();
|
|
348
360
|
say('Your account and everything it kept are deleted. You can also remove the passkey from your device\\'s passwords.', 'good');
|
package/worker/agents.mjs
CHANGED
|
@@ -74,10 +74,10 @@ export function encodeFacts(value) {
|
|
|
74
74
|
for (const b of bytes) s += String.fromCharCode(b);
|
|
75
75
|
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
76
76
|
}
|
|
77
|
-
/** The reverse, or null for anything that is not one (at most 4 KB). */
|
|
78
|
-
export function decodeFacts(text) {
|
|
77
|
+
/** The reverse, or null for anything that is not one (at most 4 KB, or `max`: a room's chat rules may be longer). */
|
|
78
|
+
export function decodeFacts(text, max = 4096) {
|
|
79
79
|
const t = String(text ?? '');
|
|
80
|
-
if (!t || t.length >
|
|
80
|
+
if (!t || t.length > max || !/^[A-Za-z0-9_-]+$/.test(t)) return null;
|
|
81
81
|
try {
|
|
82
82
|
const s = atob(t.replace(/-/g, '+').replace(/_/g, '/'));
|
|
83
83
|
const bytes = Uint8Array.from(s, (c) => c.charCodeAt(0));
|