@zaisma/game-server 1.9.0 → 1.10.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/bench/glass.js +403 -13
- package/bin/local-play.js +65 -0
- package/package.json +1 -1
- package/sdk/zaisma.d.ts +27 -0
- package/sdk/zaisma.js +49 -1
- package/src/room.d.ts +11 -0
- package/src/room.js +16 -0
package/bench/glass.js
CHANGED
|
@@ -6273,6 +6273,7 @@ var ERROR_CODES = {
|
|
|
6273
6273
|
confirmation_needed: { httpStatus: 403, meaning: "This needs the developer's passkey on zaisma.com (turning spending on or up, or connecting a Stripe account): confirm_url is where they do it." },
|
|
6274
6274
|
bad_monthly_budget_cents: { httpStatus: 400, meaning: "monthly_budget_cents isn't a whole number of cents of at least 100 ($1), or null for no paid hosting." },
|
|
6275
6275
|
bad_top_up_amount: { httpStatus: 400, meaning: "amount_cents isn't a whole number of cents from 1,000 ($10) to 100,000 ($1,000)." },
|
|
6276
|
+
account_not_protected: { httpStatus: 403, meaning: "The account has no passkey and no email yet, and a top-up keeps a card, so it needs one of them first: add a passkey or an email on zaisma.com/account." },
|
|
6276
6277
|
no_top_up: { httpStatus: 404, meaning: "You have no top-up with that ID." },
|
|
6277
6278
|
no_spending_request: { httpStatus: 404, meaning: "You have no spending request with that ID." },
|
|
6278
6279
|
top_up_not_refundable: { httpStatus: 409, meaning: "The top-up has nothing left to refund: it's spent, refunded, or over 180 days old." },
|
|
@@ -6437,6 +6438,62 @@ var MATROSKA_HEAD_BYTES = 64 * 1024;
|
|
|
6437
6438
|
var MAXIMUM_EMBEDDED_CHARACTER_COUNT = 40 * 1024 * 1024;
|
|
6438
6439
|
var PACK_WINDOW_SIZE_BYTES = 32 * 1024 * 1024;
|
|
6439
6440
|
|
|
6441
|
+
// ../core/src/scan/textures.ts
|
|
6442
|
+
var digitsOf = (digits) => [...digits].map(Number);
|
|
6443
|
+
var TRITS = [
|
|
6444
|
+
"0120012101220122012001210122012001200121012201220120012101220121012001210122012201200121012201220120012101220122012001210122012201200121012201220120012101220120012001210122012201200121012201210120012101220122012001210122012201200121012201220120012101220122",
|
|
6445
|
+
"0000111022202220000111112221000000001110222022200001111122211110000011102220222000011111222122200000111022202220000111112221222000001110222022200001111122210001000011102220222000011111222111110000111022202220000111112221222100001110222022200001111122212221",
|
|
6446
|
+
"0002000200022222111211121112000200020002000222221112111211120002000200020002222211121112111200020002000200022222111211121112222200020002000222221112111211121112000200020002222211121112111211120002000200022222111211121112111200020002000222221112111211122222",
|
|
6447
|
+
"0000000000000000000000000000222211111111111111111111111111112222222222222222222222222222222222220000000000000000000000000000222200000000000000000000000000002222111111111111111111111111111122222222222222222222222222222222222211111111111111111111111111112222",
|
|
6448
|
+
"0000000000000000000000000000222200000000000000000000000000002222000000000000000000000000000022222222222222222222222222222222222211111111111111111111111111112222111111111111111111111111111122221111111111111111111111111111222222222222222222222222222222222222"
|
|
6449
|
+
].map(digitsOf);
|
|
6450
|
+
var QUINTS = [
|
|
6451
|
+
"01234044012341440123424401234344012340400123414101234242012343430123402301234123012342230123432301234001012341010123420101234301",
|
|
6452
|
+
"00000444111114442222244433333444000004041111141422222424333334340000040011111411222224223333343300000400111114112222242233333433",
|
|
6453
|
+
"00000004000000140000002400000034111111441111114411111144111111442222224422222244222222442222224433333344333333443333334433333344"
|
|
6454
|
+
].map(digitsOf);
|
|
6455
|
+
|
|
6456
|
+
// ../core/src/scan/png.ts
|
|
6457
|
+
var CRC_TABLE = (() => {
|
|
6458
|
+
const table2 = new Uint32Array(256);
|
|
6459
|
+
for (let byteIndex = 0; byteIndex < 256; byteIndex++) {
|
|
6460
|
+
let remainderCode = byteIndex;
|
|
6461
|
+
for (let bitIndex = 0; bitIndex < 8; bitIndex++) remainderCode = remainderCode & 1 ? 3988292384 ^ remainderCode >>> 1 : remainderCode >>> 1;
|
|
6462
|
+
table2[byteIndex] = remainderCode >>> 0;
|
|
6463
|
+
}
|
|
6464
|
+
return table2;
|
|
6465
|
+
})();
|
|
6466
|
+
|
|
6467
|
+
// ../core/src/scan/unity.ts
|
|
6468
|
+
var MAXIMUM_BUNDLE_BLOCK_SIZE_BYTES = 32 * 1024 * 1024;
|
|
6469
|
+
var MAXIMUM_READ_INSIDE_SIZE_BYTES = 16 * 1024 * 1024;
|
|
6470
|
+
var READ_WINDOW_SIZE_BYTES = 8 * 1024 * 1024;
|
|
6471
|
+
var BLOCK_CACHE_SIZE_BYTES = 32 * 1024 * 1024;
|
|
6472
|
+
var UNITY_TEXTURE_FORMATS = {
|
|
6473
|
+
2: "argb4444",
|
|
6474
|
+
3: "rgb8",
|
|
6475
|
+
4: "rgba8",
|
|
6476
|
+
5: "argb8",
|
|
6477
|
+
7: "rgb565",
|
|
6478
|
+
10: "bc1",
|
|
6479
|
+
11: "bc2",
|
|
6480
|
+
12: "bc3",
|
|
6481
|
+
13: "rgba4444",
|
|
6482
|
+
14: "bgra8",
|
|
6483
|
+
25: "bc7",
|
|
6484
|
+
34: "etc1",
|
|
6485
|
+
45: "etc2_rgb",
|
|
6486
|
+
46: "etc2_rgba1",
|
|
6487
|
+
47: "etc2_rgba8",
|
|
6488
|
+
60: "etc1",
|
|
6489
|
+
61: "etc2_rgba8",
|
|
6490
|
+
...Object.fromEntries([4, 5, 6, 8, 10, 12].flatMap((sidePixelCount, i) => [[48 + i, `astc_${sidePixelCount}x${sidePixelCount}`], [54 + i, `astc_${sidePixelCount}x${sidePixelCount}`]]))
|
|
6491
|
+
};
|
|
6492
|
+
|
|
6493
|
+
// ../core/src/scan/godot.ts
|
|
6494
|
+
var GODOT_3_FILE_FORMAT_CODE = 1 << 20 | 1 << 21;
|
|
6495
|
+
var WINDOW_SIZE_BYTES = 8 * 1024 * 1024;
|
|
6496
|
+
|
|
6440
6497
|
// ../core/src/plans.ts
|
|
6441
6498
|
var PLAN_LIMITS = {
|
|
6442
6499
|
free: { maximumLargeReviewsPerDay: 5, maximumSmallReviewsPerDay: 30, maximumDailyCheckCount: 20, maximumVersionsPerDay: 20, maximumGameSizeBytes: 2 * 1024 ** 3 },
|
|
@@ -6790,6 +6847,18 @@ var REVIEW_CODES = {
|
|
|
6790
6847
|
fix: `Send the background without words or logos (add_image, or add_shader for a shader); words that are part of the scene, like a sign on a building, are fine. ${COVER_GUIDE} ${IF_THE_CHECK_IS_WRONG}`,
|
|
6791
6848
|
hasOwnMessage: true
|
|
6792
6849
|
},
|
|
6850
|
+
overlay_on_background: {
|
|
6851
|
+
meaning: "Something is laid over a background instead of being part of its world, like a button, badge or sticker, so it doesn't show; the message says what.",
|
|
6852
|
+
contentRule: null,
|
|
6853
|
+
fix: `Send the background without it (add_image, or add_shader for a shader). ${COVER_GUIDE} ${IF_THE_CHECK_IS_WRONG}`,
|
|
6854
|
+
hasOwnMessage: true
|
|
6855
|
+
},
|
|
6856
|
+
famous_name_on_background: {
|
|
6857
|
+
meaning: "A background shows a famous character, logo or name, from a film, show, game, brand or club, so it doesn't show; the message names it.",
|
|
6858
|
+
contentRule: null,
|
|
6859
|
+
fix: `Send a background of your game's own world (add_image, or add_shader for a shader). Only obvious ones count. If you own it or have permission, message_zaisma with the game_slug. ${COVER_GUIDE}`,
|
|
6860
|
+
hasOwnMessage: true
|
|
6861
|
+
},
|
|
6793
6862
|
// The shader check (Backgrounds in docs/DEVELOPER-API.md): a shader is drawn before anyone sees it
|
|
6794
6863
|
shader_does_not_compile: {
|
|
6795
6864
|
meaning: "The shader didn't compile; the message gives the first compiler error.",
|
|
@@ -6832,18 +6901,19 @@ var SOCIAL_SETTING_CHOICES = {
|
|
|
6832
6901
|
who_sees_playing_now: ["nobody", "friends", "anyone"],
|
|
6833
6902
|
who_sees_played_games: ["nobody", "friends", "anyone"],
|
|
6834
6903
|
who_sees_online: ["nobody", "friends"],
|
|
6835
|
-
who_can_send_friend_requests: ["nobody", "anyone"],
|
|
6904
|
+
who_can_send_friend_requests: ["nobody", "played_with", "anyone"],
|
|
6836
6905
|
who_sees_profile: ["friends", "anyone"]
|
|
6837
6906
|
};
|
|
6838
6907
|
var SOCIAL_SETTINGS = Object.keys(SOCIAL_SETTING_CHOICES);
|
|
6839
6908
|
var SOCIAL_MODES = ["solo_focused", "friends_focused", "open_and_social"];
|
|
6840
6909
|
var SOCIAL_MODE_SETTINGS = {
|
|
6841
6910
|
solo_focused: { who_can_chat: "nobody", who_can_join: "friends", who_sees_playing_now: "friends", who_sees_played_games: "friends", who_sees_online: "friends", who_can_send_friend_requests: "nobody", who_sees_profile: "friends" },
|
|
6842
|
-
|
|
6911
|
+
// Friends-focused keeps every way to reach the player with friends, but anyone can offer friendship (Julius, Oct 27, 2026).
|
|
6912
|
+
friends_focused: { who_can_chat: "friends", who_can_join: "friends", who_sees_playing_now: "friends", who_sees_played_games: "friends", who_sees_online: "friends", who_can_send_friend_requests: "anyone", who_sees_profile: "friends" },
|
|
6843
6913
|
open_and_social: { who_can_chat: "anyone", who_can_join: "anyone", who_sees_playing_now: "anyone", who_sees_played_games: "anyone", who_sees_online: "friends", who_can_send_friend_requests: "anyone", who_sees_profile: "anyone" }
|
|
6844
6914
|
};
|
|
6845
6915
|
var AGE_LIMITS = {
|
|
6846
|
-
"13-15": { who_can_chat: "friends", who_can_join: "friends", who_sees_playing_now: "friends", who_sees_played_games: "friends", who_sees_online: "friends", who_can_send_friend_requests: "
|
|
6916
|
+
"13-15": { who_can_chat: "friends", who_can_join: "friends", who_sees_playing_now: "friends", who_sees_played_games: "friends", who_sees_online: "friends", who_can_send_friend_requests: "played_with", who_sees_profile: "anyone" },
|
|
6847
6917
|
"16-17": { who_can_chat: "anyone", who_can_join: "friends", who_sees_playing_now: "friends", who_sees_played_games: "friends", who_sees_online: "friends", who_can_send_friend_requests: "anyone", who_sees_profile: "anyone" },
|
|
6848
6918
|
"18+": { who_can_chat: "anyone", who_can_join: "anyone", who_sees_playing_now: "anyone", who_sees_played_games: "anyone", who_sees_online: "friends", who_can_send_friend_requests: "anyone", who_sees_profile: "anyone" }
|
|
6849
6919
|
};
|
|
@@ -6863,7 +6933,8 @@ function defaultSocialMode(band) {
|
|
|
6863
6933
|
return band === "13-15" ? "solo_focused" : "friends_focused";
|
|
6864
6934
|
}
|
|
6865
6935
|
function socialModeSettings(socialMode, band) {
|
|
6866
|
-
|
|
6936
|
+
const settings = withinAgeLimits(SOCIAL_MODE_SETTINGS[socialMode], band);
|
|
6937
|
+
return band === "13-15" ? { ...settings, who_can_send_friend_requests: "nobody" } : settings;
|
|
6867
6938
|
}
|
|
6868
6939
|
function chatBandGroup(account) {
|
|
6869
6940
|
if (account.who_can_chat === "nobody") return null;
|
|
@@ -14782,8 +14853,10 @@ component({
|
|
|
14782
14853
|
.choice-cards { --choice-row-least: calc(var(--choice-count) * var(--size-card)); display: flex; flex-wrap: wrap; align-items: stretch; gap: var(--gap-control-stack) 0; }
|
|
14783
14854
|
.choice-cards > .choice-card { flex: 1 1 calc((var(--choice-row-least) - 100%) * 999); }
|
|
14784
14855
|
.choice-cards > .choice-card:not(:last-child) { margin-inline-end: max(var(--gap-control-row-boxes), min(0px, (var(--choice-row-least) - 100%) * 999)); }
|
|
14785
|
-
.choice-card { position: relative; display: flex; align-items: flex-start; gap: var(--gap-words-words); min-height:
|
|
14856
|
+
.choice-card { position: relative; display: flex; align-items: flex-start; gap: var(--gap-words-words); min-height: 44px; padding: 14px var(--pad-inside-words); cursor: pointer; -webkit-tap-highlight-color: transparent;
|
|
14786
14857
|
color: var(--muted); --face-color: var(--well); background: var(--face-color); box-shadow: var(--well-shade); clip-path: var(--cut-shape); transition: --face-color 0.2s, box-shadow 0.2s, color 0.2s; }
|
|
14858
|
+
/* A card with a line on what it means has room for it; one with only its name (an amount) is as tall as its name, never taller than its content. */
|
|
14859
|
+
.choice-card:has(.choice-text) { min-height: 76px; }
|
|
14787
14860
|
.choice-card > :not(.cut-line):not(.choice-input) { position: relative; z-index: 2; }
|
|
14788
14861
|
.choice-input { position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
|
|
14789
14862
|
.choice-card > .ico { flex: none; margin-top: 1px; color: var(--muted); }
|
|
@@ -16779,7 +16852,7 @@ component({
|
|
|
16779
16852
|
function notice(options) {
|
|
16780
16853
|
const isProblem = options.tone === "problem";
|
|
16781
16854
|
const undo = options.undoAction === void 0 ? "" : html`<form class="notice-undo" method="post" action="${options.undoAction}">${textButton("Undo", { type: "submit" })}</form>`;
|
|
16782
|
-
return html`<div class="notice glass ${options.tone}${options.isInPlace ? " in-place" : ""}" role="${isProblem ? "alert" : "status"}" data-notice="${options.tone}"${undo ? raw(" data-notice-undo") : ""} data-glass data-glass-corners="menu">${EDGE}${icon(isProblem ? "warning" : "check")}<span class="notice-text">${options.text}</span>${undo}${isProblem ? cutButton("Close", { type: "button", icon: "close", isLabelHidden: true, isSmall: true }) : ""}</div>`;
|
|
16855
|
+
return html`<div class="notice glass ${options.tone}${options.isInPlace ? " in-place" : ""}" role="${isProblem ? "alert" : "status"}" data-notice="${options.tone}"${undo ? raw(" data-notice-undo") : ""} data-glass data-glass-corners="menu">${EDGE}${icon(isProblem ? "warning" : "check")}<span class="notice-text">${options.text}${options.link ? html` ${textLink(options.link.words, options.link.href)}` : ""}</span>${undo}${isProblem ? cutButton("Close", { type: "button", icon: "close", isLabelHidden: true, isSmall: true }) : ""}</div>`;
|
|
16783
16856
|
}
|
|
16784
16857
|
var STARTED_LOW_SCRIPT = `/* panes started low */(function () {
|
|
16785
16858
|
function place(pane) {
|
|
@@ -16856,7 +16929,7 @@ ${touchReach(".notice")}
|
|
|
16856
16929
|
examples: [
|
|
16857
16930
|
{ label: "Done", markup: notice({ text: "Saved", tone: "done", isInPlace: true }) },
|
|
16858
16931
|
{ label: "Done, longer", markup: notice({ text: "Version 14 is live", tone: "done", isInPlace: true }) },
|
|
16859
|
-
{ label: "A problem", markup: notice({ text: "Couldn't save: you're offline. Your changes are kept here.", tone: "problem", isInPlace: true }) },
|
|
16932
|
+
{ label: "A problem", markup: notice({ text: "Couldn't save: you're offline. Your changes are kept here.", tone: "problem", link: { words: "Try again", href: "#notice" }, isInPlace: true }) },
|
|
16860
16933
|
{ label: "Done, with Undo", markup: notice({ text: "Removed Ashen Crown from your games", tone: "done", undoAction: "#notice", isInPlace: true }) },
|
|
16861
16934
|
{ label: "Two at once", markup: html`<div class="notice-stack">${notice({ text: "Copied", tone: "done", isInPlace: true })}${notice({ text: "Couldn't reach Zaisma. Check your connection.", tone: "problem", isInPlace: true })}</div>` }
|
|
16862
16935
|
]
|
|
@@ -23278,7 +23351,7 @@ function securityHeaders(env2, extra = {}, hasTurnstileCheck = false, frames = [
|
|
|
23278
23351
|
var PRESENCE_SCRIPT = pageScript("presence", `(()=>{if(!/(^|; )(__Host-)?zaisma_in=1/.test(document.cookie))return;let last=0,off=false;const b=document.body,KEY="zaisma:checked-in-at-ms";
|
|
23279
23352
|
try{last=Number(sessionStorage.getItem(KEY))||0}catch{}if(last>Date.now())last=0;
|
|
23280
23353
|
const beat=()=>{if(off||document.visibilityState!=="visible")return;last=Date.now();try{sessionStorage.setItem(KEY,String(last))}catch{}const g=b.dataset.presenceGame;
|
|
23281
|
-
fetch("/api/presence",{method:"POST",body:JSON.stringify(g?{game_slug:g}:{}),keepalive:true}).then(r=>{if(r.status===401)off=true}).catch(()=>{})};
|
|
23354
|
+
fetch("/api/presence",{method:"POST",body:JSON.stringify(g?{game_slug:g}:{}),keepalive:true}).then(r=>{if(r.status===401)off=true;else if(r.status===200)r.json().then(a=>dispatchEvent(new CustomEvent("zaisma:presence",{detail:a}))).catch(()=>{})}).catch(()=>{})};
|
|
23282
23355
|
const start=()=>{beat();setInterval(beat,${PRESENCE_CHECK_IN_MS})};if(b.dataset.presenceGame||Date.now()-last>60000)start();else setTimeout(start,last+${PRESENCE_CHECK_IN_MS}-Date.now());document.addEventListener("visibilitychange",()=>{if(Date.now()-last>60000)beat()});
|
|
23283
23356
|
new MutationObserver(beat).observe(b,{attributes:true,attributeFilter:["data-presence-game"]});
|
|
23284
23357
|
addEventListener("pagehide",()=>{const g=b.dataset.presenceGame;if(g&&!off)navigator.sendBeacon("/api/presence",JSON.stringify({left_game_slug:g}))})})();`);
|
|
@@ -43574,7 +43647,7 @@ var PICTURE_FIELDS = {
|
|
|
43574
43647
|
},
|
|
43575
43648
|
overlay: {
|
|
43576
43649
|
type: "string",
|
|
43577
|
-
description: `Covers
|
|
43650
|
+
description: `Covers and backgrounds ("" for screenshots): what is laid over the picture instead of being part of the scene: a button, menu, shop panel, row of rewards, badge, sticker, starburst, icon bubble, or an arrow or circle pointing at something. A few words naming each. Lighting, sparks and effects in the scene aren't overlays. "" when there's nothing.`
|
|
43578
43651
|
},
|
|
43579
43652
|
fake_reward: {
|
|
43580
43653
|
type: "string",
|
|
@@ -43582,7 +43655,7 @@ var PICTURE_FIELDS = {
|
|
|
43582
43655
|
},
|
|
43583
43656
|
famous_name: {
|
|
43584
43657
|
type: "string",
|
|
43585
|
-
description: `Covers
|
|
43658
|
+
description: `Covers and backgrounds ("" for screenshots): a famous character, logo or name from a film, show, game, brand or sports club that a typical teenager would name at a glance, like Mario, Naruto, Goku, Lightning McQueen, the Batmobile, Pikachu, a football club's crest, the Roblox logo or Robux. Name it. Generic blocky, pixel or cartoon figures, and lookalikes nobody would name for sure, are not famous: when unsure, "". "" when there's none.`
|
|
43586
43659
|
},
|
|
43587
43660
|
main_subject: {
|
|
43588
43661
|
type: "string",
|
|
@@ -43598,7 +43671,7 @@ var PICTURE_FIELDS = {
|
|
|
43598
43671
|
},
|
|
43599
43672
|
background_text: {
|
|
43600
43673
|
type: "string",
|
|
43601
|
-
description: `Backgrounds only ("" for covers and screenshots): every word, number, symbol or logo written on the picture, the game's own name and logo included, quoted exactly or named
|
|
43674
|
+
description: `Backgrounds only ("" for covers and screenshots): every word, number, symbol or logo written on the picture, the game's own name and logo included, quoted exactly or named, separated by " / ". Overlays go in overlay and famous characters, logos and names in famous_name. Words that are part of the scene (a sign on a building, a neon shop sign, writing on a shirt) don't count. "" when there's none.`
|
|
43602
43675
|
},
|
|
43603
43676
|
has_safety_concern: {
|
|
43604
43677
|
type: "boolean",
|
|
@@ -44216,6 +44289,13 @@ function playersSheet(options) {
|
|
|
44216
44289
|
const words3 = (markup) => ({ kind: "words", markup });
|
|
44217
44290
|
const body = stack({
|
|
44218
44291
|
items: [
|
|
44292
|
+
// Friend requests waiting for the player, with Accept and Turn down (friend-requests.ts brings them).
|
|
44293
|
+
{
|
|
44294
|
+
kind: "block",
|
|
44295
|
+
id: "players-requests",
|
|
44296
|
+
isHidden: true,
|
|
44297
|
+
markup: stack({ items: [words3(html`<h3>Friend requests</h3>`), { kind: "block", markup: listRows({ whenEmpty: html`<p hidden></p>`, rows: [], id: "players-requests-list", label: "Friend requests" }) }] })
|
|
44298
|
+
},
|
|
44219
44299
|
words3(html`<h3 id="players-room-title">In this room<span id="players-count"></span></h3>`),
|
|
44220
44300
|
// While nobody's listed, the note says why (the page's script writes it): the list's empty state.
|
|
44221
44301
|
{ kind: "block", markup: listRows({ whenEmpty: html`<p id="players-note" hidden></p>`, rows: [], id: "players-list", label: "In this room" }) },
|
|
@@ -44232,6 +44312,10 @@ function playersSheet(options) {
|
|
|
44232
44312
|
const playerBody = stack({
|
|
44233
44313
|
items: [
|
|
44234
44314
|
{ kind: "control", markup: cutButton("Profile", { href: "/", id: "players-player-profile", isFullWidth: true, isHidden: true }) },
|
|
44315
|
+
// Only when they take friend requests from this player (friend-requests.ts asks), with no hint why otherwise.
|
|
44316
|
+
{ kind: "control", markup: cutButton("Add friend", { type: "button", id: "players-player-friend", isFullWidth: true, isHidden: true }) },
|
|
44317
|
+
words3(html`<p id="players-player-sent" hidden>Friend request sent.</p>`),
|
|
44318
|
+
words3(html`<p id="players-player-sign-in" hidden>${textLink("Sign in", options.signInPath)} to add friends.</p>`),
|
|
44235
44319
|
{ kind: "control", markup: cutButton("Block", { type: "button", id: "players-player-block", isFullWidth: true }) },
|
|
44236
44320
|
{ kind: "control", markup: cutButton("Report", { type: "button", id: "players-player-report", isFullWidth: true }) }
|
|
44237
44321
|
]
|
|
@@ -44241,6 +44325,7 @@ function playersSheet(options) {
|
|
|
44241
44325
|
return html`${sheet({ id: "players", title: "Players", body, closeLabel: "Close", placement: "foot" })}
|
|
44242
44326
|
${sheet({ id: "players-player", title: "Player", body: playerBody, closeLabel: "Close", placement: "foot" })}
|
|
44243
44327
|
<template id="players-row">${rowTemplate}</template>
|
|
44328
|
+
<template id="players-request-row">${listRows({ whenEmpty: html`<p hidden></p>`, rows: [{ lead: pictureFrame("?", "md"), title: "?", action: [runButton("Accept", { ...rowButton, dataAttributes: { "request-action": "accept" } }), cutButton("Turn down", { ...rowButton, dataAttributes: { "request-action": "turn-down" } })] }] })}</template>
|
|
44244
44329
|
${pageData("players-data", { gameOrigin: options.gameOrigin, slug: options.slug, hasZaismaRooms: options.hasZaismaRooms, canSignIn: options.canSignIn })}
|
|
44245
44330
|
${scriptTags(PLAYERS_SHEET_SCRIPT)}`;
|
|
44246
44331
|
}
|
|
@@ -44279,6 +44364,33 @@ var PLAYERS_SHEET_SCRIPT = pageScript("players-sheet", String.raw`(() => {
|
|
|
44279
44364
|
}
|
|
44280
44365
|
// A button's words: the last span in its face (cutButton).
|
|
44281
44366
|
const wordsOf = (button) => button.querySelector(".face > span:last-child");
|
|
44367
|
+
// Friend requests waiting for this player, and the ones they sent from here this session.
|
|
44368
|
+
let requests = [];
|
|
44369
|
+
const sentTo = new Set();
|
|
44370
|
+
addEventListener("zaisma:friend-requests-waiting", (e) => { requests = e.detail && Array.isArray(e.detail.requests) ? e.detail.requests : []; if (sheet.open) render(); });
|
|
44371
|
+
addEventListener("zaisma:friend-request-sent", (e) => { if (e.detail) sentTo.add(e.detail.id); });
|
|
44372
|
+
// Friends now (an accept either way): the row says Friend, and the menu has nothing to add.
|
|
44373
|
+
addEventListener("zaisma:friend-added", (e) => {
|
|
44374
|
+
if (!e.detail) return;
|
|
44375
|
+
sentTo.delete(e.detail.id);
|
|
44376
|
+
const known = accounts.get(e.detail.id);
|
|
44377
|
+
if (known && !known.isFriend) { known.isFriend = true; if (sheet.open) render(); }
|
|
44378
|
+
});
|
|
44379
|
+
function renderRequests() {
|
|
44380
|
+
const list = $("players-requests-list");
|
|
44381
|
+
list.textContent = "";
|
|
44382
|
+
$("players-requests").hidden = !requests.length;
|
|
44383
|
+
list.hidden = !requests.length;
|
|
44384
|
+
requests.slice(0, 20).forEach((waiting) => {
|
|
44385
|
+
const item = $("players-request-row").content.querySelector("li").cloneNode(true);
|
|
44386
|
+
paint(item.querySelector(".av"), waiting.name);
|
|
44387
|
+
item.querySelector(".list-row-title").textContent = waiting.name;
|
|
44388
|
+
item.querySelectorAll("[data-request-action]").forEach((button) => button.addEventListener("click", () => {
|
|
44389
|
+
dispatchEvent(new CustomEvent("zaisma:answer-friend-request", { detail: { id: waiting.player_id, verb: button.dataset.requestAction } }));
|
|
44390
|
+
}));
|
|
44391
|
+
list.appendChild(item);
|
|
44392
|
+
});
|
|
44393
|
+
}
|
|
44282
44394
|
|
|
44283
44395
|
addEventListener("message", (e) => {
|
|
44284
44396
|
const f = frame();
|
|
@@ -44321,6 +44433,7 @@ var PLAYERS_SHEET_SCRIPT = pageScript("players-sheet", String.raw`(() => {
|
|
|
44321
44433
|
}
|
|
44322
44434
|
|
|
44323
44435
|
function render() {
|
|
44436
|
+
renderRequests();
|
|
44324
44437
|
$("players-count").textContent = room ? " · " + room.players.length : "";
|
|
44325
44438
|
const list = $("players-list");
|
|
44326
44439
|
list.textContent = "";
|
|
@@ -44369,11 +44482,43 @@ var PLAYERS_SHEET_SCRIPT = pageScript("players-sheet", String.raw`(() => {
|
|
|
44369
44482
|
profile.rel = "noopener";
|
|
44370
44483
|
}
|
|
44371
44484
|
$("players-player-block").hidden = isBlocked;
|
|
44485
|
+
showFriendChoice(other, known, isBlocked);
|
|
44372
44486
|
if (!playerSheet.open) playerSheet.showModal();
|
|
44373
44487
|
});
|
|
44374
44488
|
return item;
|
|
44375
44489
|
}
|
|
44376
44490
|
|
|
44491
|
+
// Add friend, Friend request sent, or Sign in to add friends: asked of Zaisma as the player's sheet opens.
|
|
44492
|
+
function showFriendChoice(other, known, isBlocked) {
|
|
44493
|
+
const add = $("players-player-friend"), sent = $("players-player-sent"), signIn = $("players-player-sign-in");
|
|
44494
|
+
add.hidden = true;
|
|
44495
|
+
sent.hidden = true;
|
|
44496
|
+
signIn.hidden = !(other.kind === "account" && !isSignedIn() && CAN_SIGN_IN);
|
|
44497
|
+
if (!known || known.isFriend || isBlocked || !isSignedIn()) return;
|
|
44498
|
+
// Sent from here: say so at once, then ask Zaisma, which knows when it was accepted since.
|
|
44499
|
+
sent.hidden = !sentTo.has(other.id);
|
|
44500
|
+
const asking = other.id;
|
|
44501
|
+
fetch("/friends/requests/about/" + other.id)
|
|
44502
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
44503
|
+
.then((about) => {
|
|
44504
|
+
if (!about || !chosen || chosen.id !== asking) return;
|
|
44505
|
+
if (about.is_friend) {
|
|
44506
|
+
sentTo.delete(asking);
|
|
44507
|
+
known.isFriend = true;
|
|
44508
|
+
render();
|
|
44509
|
+
}
|
|
44510
|
+
add.hidden = !about.can_send;
|
|
44511
|
+
sent.hidden = !about.has_sent;
|
|
44512
|
+
})
|
|
44513
|
+
.catch(() => {});
|
|
44514
|
+
}
|
|
44515
|
+
$("players-player-friend").addEventListener("click", () => {
|
|
44516
|
+
if (!chosen) return;
|
|
44517
|
+
const playerId = chosen.id;
|
|
44518
|
+
closeBoth();
|
|
44519
|
+
dispatchEvent(new CustomEvent("zaisma:ask-friend-request", { detail: { id: playerId } }));
|
|
44520
|
+
});
|
|
44521
|
+
|
|
44377
44522
|
function closeBoth() {
|
|
44378
44523
|
if (playerSheet.open) playerSheet.close();
|
|
44379
44524
|
if (sheet.open) sheet.close();
|
|
@@ -44406,7 +44551,7 @@ var PLAYERS_SHEET_SCRIPT = pageScript("players-sheet", String.raw`(() => {
|
|
|
44406
44551
|
|
|
44407
44552
|
// The bar's button opens it (SHEET_SCRIPT does too, by its aria-controls; this runs first and fills it in).
|
|
44408
44553
|
opener.addEventListener("click", () => {
|
|
44409
|
-
["report", "block"].forEach((id) => { const other = $(id); if (other && other.open) other.close(); });
|
|
44554
|
+
["report", "block", "friend-request"].forEach((id) => { const other = $(id); if (other && other.open) other.close(); });
|
|
44410
44555
|
render();
|
|
44411
44556
|
loadFriends();
|
|
44412
44557
|
if (!sheet.open) sheet.showModal();
|
|
@@ -44416,6 +44561,238 @@ var PLAYERS_SHEET_SCRIPT = pageScript("players-sheet", String.raw`(() => {
|
|
|
44416
44561
|
update();
|
|
44417
44562
|
})();`);
|
|
44418
44563
|
|
|
44564
|
+
// ../../apps/site/src/friend-requests.ts
|
|
44565
|
+
function friendRequestParts(options) {
|
|
44566
|
+
const body = stack({
|
|
44567
|
+
items: [
|
|
44568
|
+
{ kind: "block", markup: listRows({ whenEmpty: emptyState({ title: "Nobody picked", text: "Who you're adding shows here." }), rows: [{ lead: pictureFrame("?", "md"), title: "?" }], id: "friend-request-who" }) },
|
|
44569
|
+
{ kind: "words", markup: html`<p><span id="friend-request-told"></span> can accept it from their game or their friends page. Once you're friends, you can chat and join each other in games that allow it.</p>` },
|
|
44570
|
+
{ kind: "block", markup: html`<div id="friend-request-error" role="alert" hidden>${banner({ text: "Couldn't send it. Check your connection and try again.", icon: "warning", tone: "warning" })}</div>` }
|
|
44571
|
+
]
|
|
44572
|
+
});
|
|
44573
|
+
const card = playingCard({
|
|
44574
|
+
id: "friend-request-card",
|
|
44575
|
+
title: "Friend request",
|
|
44576
|
+
body: stack({
|
|
44577
|
+
items: [
|
|
44578
|
+
{ kind: "words", markup: html`<p><strong id="friend-request-card-name"></strong> wants to be friends.</p>` },
|
|
44579
|
+
{
|
|
44580
|
+
kind: "block",
|
|
44581
|
+
markup: row({ widths: "fit", label: "Answer", items: [{ kind: "control", markup: runButton("Accept", { type: "button", id: "friend-request-accept" }) }, { kind: "control", markup: cutButton("Not now", { type: "button", id: "friend-request-later" }) }] })
|
|
44582
|
+
}
|
|
44583
|
+
]
|
|
44584
|
+
})
|
|
44585
|
+
});
|
|
44586
|
+
const acceptedCard = playingCard({ id: "friend-accepted-card", body: stack({ items: [{ kind: "words", markup: html`<p>You and <strong id="friend-accepted-name"></strong> are friends now.</p>` }] }) });
|
|
44587
|
+
return html`${sheet({
|
|
44588
|
+
id: "friend-request",
|
|
44589
|
+
title: "Send a friend request?",
|
|
44590
|
+
body,
|
|
44591
|
+
closeLabel: "Close",
|
|
44592
|
+
placement: "foot",
|
|
44593
|
+
buttons: [cutButton("Cancel", { type: "submit", formMethod: "dialog" }), runButton("Send request", { type: "button", id: "friend-request-yes" })]
|
|
44594
|
+
})}
|
|
44595
|
+
${card}
|
|
44596
|
+
${acceptedCard}
|
|
44597
|
+
${pageData("friend-request-data", { gameOrigin: options.gameOrigin, slug: options.slug })}
|
|
44598
|
+
${scriptTags(FRIEND_REQUEST_SCRIPT)}`;
|
|
44599
|
+
}
|
|
44600
|
+
var CARD_SHOWN_MS = 8e3;
|
|
44601
|
+
var NEWS_SHOWN_MS = 5e3;
|
|
44602
|
+
var FRIEND_REQUEST_SCRIPT = pageScript("friend-requests", String.raw`(() => {
|
|
44603
|
+
const sheet = document.getElementById("friend-request");
|
|
44604
|
+
const card = document.getElementById("friend-request-card");
|
|
44605
|
+
const data = document.getElementById("friend-request-data");
|
|
44606
|
+
if (!sheet || !card || !data) return;
|
|
44607
|
+
const { gameOrigin: GAME_ORIGIN, slug: SLUG } = JSON.parse(data.textContent);
|
|
44608
|
+
const PLAYER_ID = /^player_[a-z0-9]{1,40}$/;
|
|
44609
|
+
const $ = (id) => document.getElementById(id);
|
|
44610
|
+
const yes = $("friend-request-yes");
|
|
44611
|
+
const frame = () => document.getElementById("game");
|
|
44612
|
+
const tell = (message) => { const f = frame(); if (f && f.contentWindow) f.contentWindow.postMessage(message, GAME_ORIGIN); };
|
|
44613
|
+
const isSignedIn = () => /(^|; )(__Host-)?zaisma_in=1/.test(document.cookie);
|
|
44614
|
+
const post = (path, body) => fetch(path, { method: "POST", headers: body ? { "Content-Type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined }).catch(() => null);
|
|
44615
|
+
|
|
44616
|
+
// The request the sheet is asking about: { id, name }.
|
|
44617
|
+
let asking = null;
|
|
44618
|
+
function finish(status) {
|
|
44619
|
+
if (!asking) return;
|
|
44620
|
+
tell({ type: "zaisma:friend-request", playerId: asking.id, status });
|
|
44621
|
+
if (status === "sent") dispatchEvent(new CustomEvent("zaisma:friend-request-sent", { detail: { id: asking.id } }));
|
|
44622
|
+
asking = null;
|
|
44623
|
+
if (sheet.open) sheet.close();
|
|
44624
|
+
}
|
|
44625
|
+
// Who it's for, as the page draws an account (accountPicture): initials in its own color, from the name.
|
|
44626
|
+
function showWho(name) {
|
|
44627
|
+
const who = $("friend-request-who");
|
|
44628
|
+
let hue = 7;
|
|
44629
|
+
for (const character of name) hue = (hue * 31 + character.codePointAt(0)) % 360;
|
|
44630
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
44631
|
+
const letters = words.length > 1 ? [Array.from(words[0])[0], Array.from(words[1])[0]] : Array.from(words[0] || "?").slice(0, 2);
|
|
44632
|
+
const picture = who.querySelector(".av");
|
|
44633
|
+
picture.style.setProperty("--h", String(hue));
|
|
44634
|
+
picture.textContent = letters.join("").toUpperCase();
|
|
44635
|
+
who.querySelector(".list-row-title").textContent = name;
|
|
44636
|
+
}
|
|
44637
|
+
async function ask(playerId) {
|
|
44638
|
+
// One question at a time: another while the sheet is up is a no.
|
|
44639
|
+
if (asking) return tell({ type: "zaisma:friend-request", playerId, status: "cancelled" });
|
|
44640
|
+
if (!isSignedIn()) return tell({ type: "zaisma:friend-request", playerId, status: "cancelled" });
|
|
44641
|
+
tell({ type: "zaisma:friend-request", playerId, status: "asking" });
|
|
44642
|
+
asking = { id: playerId, name: "" };
|
|
44643
|
+
const about = await fetch("/friends/requests/about/" + playerId).then((res) => (res.ok ? res.json() : null)).catch(() => null);
|
|
44644
|
+
if (!about || !about.player) { asking = { id: playerId }; return finish("not_taking_requests"); }
|
|
44645
|
+
if (about.is_friend) return finish("already_friends");
|
|
44646
|
+
// Already waiting looks the same as just sent, whatever became of it.
|
|
44647
|
+
if (about.has_sent) return finish("sent");
|
|
44648
|
+
if (!about.can_send) return finish("not_taking_requests");
|
|
44649
|
+
asking.name = about.player.name;
|
|
44650
|
+
$("friend-request-title").textContent = "Send " + asking.name + " a friend request?";
|
|
44651
|
+
showWho(asking.name);
|
|
44652
|
+
$("friend-request-told").textContent = asking.name;
|
|
44653
|
+
$("friend-request-error").hidden = true;
|
|
44654
|
+
yes.removeAttribute("aria-busy");
|
|
44655
|
+
if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
|
|
44656
|
+
sheet.showModal();
|
|
44657
|
+
yes.focus();
|
|
44658
|
+
}
|
|
44659
|
+
yes.addEventListener("click", async () => {
|
|
44660
|
+
if (!asking || yes.getAttribute("aria-busy") === "true") return;
|
|
44661
|
+
yes.setAttribute("aria-busy", "true");
|
|
44662
|
+
$("friend-request-error").hidden = true;
|
|
44663
|
+
const res = await post("/friends/requests", { player_id: asking.id, game_slug: SLUG });
|
|
44664
|
+
yes.removeAttribute("aria-busy");
|
|
44665
|
+
const answer = res ? await res.json().catch(() => null) : null;
|
|
44666
|
+
// One back to someone whose request waited: friends at once, and they hear of it like an accept.
|
|
44667
|
+
if (res && res.ok && answer && answer.status === "friends") {
|
|
44668
|
+
dispatchEvent(new CustomEvent("zaisma:friend-added", { detail: { id: asking.id } }));
|
|
44669
|
+
tell({ type: "zaisma:friend-request", playerId: asking.id, status: "accepted" });
|
|
44670
|
+
waiting = waiting.filter((each) => each.player_id !== asking.id);
|
|
44671
|
+
dispatchEvent(new CustomEvent("zaisma:friend-requests-waiting", { detail: { requests: waiting } }));
|
|
44672
|
+
}
|
|
44673
|
+
if (res && res.ok) return finish("sent");
|
|
44674
|
+
const code = answer && answer.error ? answer.error.code : "";
|
|
44675
|
+
if (code === "already_friends") return finish("already_friends");
|
|
44676
|
+
if (code === "already_sent") return finish("sent");
|
|
44677
|
+
if (code === "not_taking_requests" || code === "not_an_account") return finish("not_taking_requests");
|
|
44678
|
+
const error = $("friend-request-error");
|
|
44679
|
+
error.querySelector(".banner-text").textContent = answer && answer.error ? answer.error.message : "Couldn't send it. Check your connection and try again.";
|
|
44680
|
+
error.hidden = false;
|
|
44681
|
+
});
|
|
44682
|
+
sheet.addEventListener("close", () => { finish("cancelled"); const f = frame(); if (f) f.focus(); });
|
|
44683
|
+
// The game asks (Zaisma.friends.sendRequest); only its own frame.
|
|
44684
|
+
addEventListener("message", (e) => {
|
|
44685
|
+
const f = frame();
|
|
44686
|
+
const d = e.data;
|
|
44687
|
+
if (!f || e.source !== f.contentWindow || e.origin !== GAME_ORIGIN || !d) return;
|
|
44688
|
+
if (d.type === "zaisma:friend-request" && typeof d.playerId === "string" && PLAYER_ID.test(d.playerId) && !d.status) ask(d.playerId);
|
|
44689
|
+
// Someone in the room sent this player one: look now.
|
|
44690
|
+
if (d.type === "zaisma:friend-requests") look();
|
|
44691
|
+
});
|
|
44692
|
+
// Players' Add friend.
|
|
44693
|
+
addEventListener("zaisma:ask-friend-request", (e) => { if (e.detail && PLAYER_ID.test(e.detail.id)) ask(e.detail.id); });
|
|
44694
|
+
|
|
44695
|
+
// Requests waiting for this player, newest first; the card brings each one once a tab.
|
|
44696
|
+
const SHOWN_KEY = "zaisma:friend-requests-shown";
|
|
44697
|
+
const shown = () => { try { return new Set(JSON.parse(sessionStorage.getItem(SHOWN_KEY) || "[]")); } catch { return new Set(); } };
|
|
44698
|
+
const keepShown = (ids) => { try { sessionStorage.setItem(SHOWN_KEY, JSON.stringify([...ids].slice(-200))); } catch {} };
|
|
44699
|
+
let waiting = [];
|
|
44700
|
+
let showing = null;
|
|
44701
|
+
let cardTimer = null;
|
|
44702
|
+
let isLooking = false;
|
|
44703
|
+
let hasLooked = false;
|
|
44704
|
+
// The game page while its game runs (pages.ts swaps is-leaving for is-playing).
|
|
44705
|
+
const isPlaying = () => document.documentElement.classList.contains("is-playing");
|
|
44706
|
+
async function look() {
|
|
44707
|
+
if (!isSignedIn() || isLooking) return;
|
|
44708
|
+
isLooking = true;
|
|
44709
|
+
hasLooked = true;
|
|
44710
|
+
const answer = await fetch("/friends/requests").then((res) => (res.ok ? res.json() : null)).catch(() => null);
|
|
44711
|
+
isLooking = false;
|
|
44712
|
+
if (!answer || !Array.isArray(answer.requests)) return;
|
|
44713
|
+
waiting = answer.requests;
|
|
44714
|
+
dispatchEvent(new CustomEvent("zaisma:friend-requests-waiting", { detail: { requests: waiting } }));
|
|
44715
|
+
// Requests this player sent that were accepted: Players marks them Friend, and the news shows once a tab.
|
|
44716
|
+
const seen = shown();
|
|
44717
|
+
(Array.isArray(answer.accepted) ? answer.accepted : []).forEach((each) => {
|
|
44718
|
+
dispatchEvent(new CustomEvent("zaisma:friend-added", { detail: { id: each.player_id } }));
|
|
44719
|
+
const key = "accepted:" + each.player_id + ":" + each.answered_at_ms;
|
|
44720
|
+
if (seen.has(key)) return;
|
|
44721
|
+
seen.add(key);
|
|
44722
|
+
news.push(each.name);
|
|
44723
|
+
});
|
|
44724
|
+
keepShown(seen);
|
|
44725
|
+
showNext();
|
|
44726
|
+
}
|
|
44727
|
+
// Names whose accept this player hasn't seen yet, shown one at a time.
|
|
44728
|
+
const news = [];
|
|
44729
|
+
const newsCard = $("friend-accepted-card");
|
|
44730
|
+
let newsTimer = null;
|
|
44731
|
+
function showNews() {
|
|
44732
|
+
if (!newsCard || !news.length || !newsCard.hidden || !isPlaying()) return;
|
|
44733
|
+
$("friend-accepted-name").textContent = news.shift();
|
|
44734
|
+
newsCard.hidden = false;
|
|
44735
|
+
clearTimeout(newsTimer);
|
|
44736
|
+
newsTimer = setTimeout(() => { newsCard.hidden = true; setTimeout(showNews, 600); }, ${NEWS_SHOWN_MS});
|
|
44737
|
+
}
|
|
44738
|
+
function showNext() {
|
|
44739
|
+
showNews();
|
|
44740
|
+
if (showing || !isPlaying()) return;
|
|
44741
|
+
const seen = shown();
|
|
44742
|
+
const next = waiting.find((each) => !seen.has(each.player_id + ":" + each.created_at_ms));
|
|
44743
|
+
if (!next) return;
|
|
44744
|
+
seen.add(next.player_id + ":" + next.created_at_ms);
|
|
44745
|
+
keepShown(seen);
|
|
44746
|
+
showing = next;
|
|
44747
|
+
$("friend-request-card-name").textContent = next.name;
|
|
44748
|
+
card.hidden = false;
|
|
44749
|
+
clearTimeout(cardTimer);
|
|
44750
|
+
cardTimer = setTimeout(hideCard, ${CARD_SHOWN_MS});
|
|
44751
|
+
}
|
|
44752
|
+
function hideCard() {
|
|
44753
|
+
clearTimeout(cardTimer);
|
|
44754
|
+
card.hidden = true;
|
|
44755
|
+
showing = null;
|
|
44756
|
+
setTimeout(showNext, 600);
|
|
44757
|
+
}
|
|
44758
|
+
// A hand on the card keeps it up.
|
|
44759
|
+
card.addEventListener("pointerenter", () => clearTimeout(cardTimer));
|
|
44760
|
+
card.addEventListener("pointerleave", () => { if (showing) cardTimer = setTimeout(hideCard, 3000); });
|
|
44761
|
+
async function answerRequest(playerId, verb) {
|
|
44762
|
+
const res = await post("/friends/requests/" + playerId + "/" + verb);
|
|
44763
|
+
waiting = waiting.filter((each) => each.player_id !== playerId);
|
|
44764
|
+
dispatchEvent(new CustomEvent("zaisma:friend-requests-waiting", { detail: { requests: waiting } }));
|
|
44765
|
+
const isAccepted = Boolean(res && res.ok) && verb === "accept";
|
|
44766
|
+
// Friends now: Players marks them, and the rooms they share tell the sender's game to have their page look.
|
|
44767
|
+
if (isAccepted) {
|
|
44768
|
+
dispatchEvent(new CustomEvent("zaisma:friend-added", { detail: { id: playerId } }));
|
|
44769
|
+
tell({ type: "zaisma:friend-request", playerId, status: "accepted" });
|
|
44770
|
+
}
|
|
44771
|
+
return Boolean(res && res.ok);
|
|
44772
|
+
}
|
|
44773
|
+
$("friend-request-accept").addEventListener("click", async () => {
|
|
44774
|
+
if (!showing) return;
|
|
44775
|
+
const playerId = showing.player_id;
|
|
44776
|
+
hideCard();
|
|
44777
|
+
await answerRequest(playerId, "accept");
|
|
44778
|
+
});
|
|
44779
|
+
$("friend-request-later").addEventListener("click", hideCard);
|
|
44780
|
+
// Players answers from its list too.
|
|
44781
|
+
addEventListener("zaisma:answer-friend-request", (e) => {
|
|
44782
|
+
if (!e.detail || !PLAYER_ID.test(e.detail.id) || (e.detail.verb !== "accept" && e.detail.verb !== "turn-down")) return;
|
|
44783
|
+
if (showing && showing.player_id === e.detail.id) hideCard();
|
|
44784
|
+
answerRequest(e.detail.id, e.detail.verb);
|
|
44785
|
+
});
|
|
44786
|
+
// The presence check-in says how many wait and how many of this player's were accepted; a change means something new to look at.
|
|
44787
|
+
let lastCounts = "";
|
|
44788
|
+
addEventListener("zaisma:presence", (e) => {
|
|
44789
|
+
const counts = e.detail ? [e.detail.waiting_friend_request_count, e.detail.accepted_friend_request_count].map((count) => (typeof count === "number" ? count : 0)) : [0, 0];
|
|
44790
|
+
if (counts.join() !== lastCounts) { lastCounts = counts.join(); if (counts[0] || counts[1] || waiting.length) look(); }
|
|
44791
|
+
});
|
|
44792
|
+
// Once the game is running.
|
|
44793
|
+
new MutationObserver(() => { if (isPlaying()) { showNext(); if (!hasLooked) look(); } }).observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
44794
|
+
})();`);
|
|
44795
|
+
|
|
44419
44796
|
// ../../apps/site/src/game-updates.ts
|
|
44420
44797
|
var LIVE_VERSION_CHECK_MS = 2 * 6e4;
|
|
44421
44798
|
function updateBarButtons() {
|
|
@@ -45842,6 +46219,7 @@ ${multiplayer ? playersSheet({ gameOrigin, slug: game.slug, title: game.title, h
|
|
|
45842
46219
|
${keepCard}
|
|
45843
46220
|
${saves ? playingCard({ id: "save-toast", body: stack({ items: [{ kind: "words", markup: html`<p></p>` }] }) }) : ""}
|
|
45844
46221
|
${signInCard}
|
|
46222
|
+
${isClosed || !authConfigured(env2) ? "" : friendRequestParts({ gameOrigin, slug: game.slug })}
|
|
45845
46223
|
${multiplayer ? stillThereCard() : ""}
|
|
45846
46224
|
${leavingCard(game.title)}
|
|
45847
46225
|
${isClosed ? "" : buySheet({ joinHref: joinHere, signInHref: signinHere, coverUrl })}
|
|
@@ -46502,6 +46880,16 @@ var USERNAME_SCRIPT = pageScript("username-check", `(() => {
|
|
|
46502
46880
|
check();
|
|
46503
46881
|
});
|
|
46504
46882
|
})();`);
|
|
46883
|
+
var JOIN_TODAY_SCRIPT = pageScript("join-today", `(() => {
|
|
46884
|
+
const form = document.getElementById("age-form");
|
|
46885
|
+
if (!form) return;
|
|
46886
|
+
const setToday = () => {
|
|
46887
|
+
const now = new Date();
|
|
46888
|
+
form.elements.today.value = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0") + "-" + String(now.getDate()).padStart(2, "0");
|
|
46889
|
+
};
|
|
46890
|
+
setToday();
|
|
46891
|
+
form.addEventListener("submit", setToday);
|
|
46892
|
+
})();`);
|
|
46505
46893
|
var ENTRY_SWITCH_SCRIPT = pageScript("entry-switch", `(() => {
|
|
46506
46894
|
const sw = document.querySelector(".city-pane .tab-switch");
|
|
46507
46895
|
const views = [...document.querySelectorAll("[data-entry-view]")];
|
|
@@ -46891,6 +47279,7 @@ function devSheetBody(name, view) {
|
|
|
46891
47279
|
var AGE_BAND_WORDS = { "13-15": "13\u201315", "16-17": "16\u201317", "18+": "18+" };
|
|
46892
47280
|
var isSocialModeAllowed = (ageBand2, socialMode) => socialModesFor(ageBand2).includes(socialMode);
|
|
46893
47281
|
var startingSocialMode = (ageBand2) => defaultSocialMode(ageBand2);
|
|
47282
|
+
var takesFriendRequests = (ageBand2, socialMode) => socialModeSettings(socialMode, ageBand2).who_can_send_friend_requests !== "nobody";
|
|
46894
47283
|
function chatOf(ageBand2, socialMode) {
|
|
46895
47284
|
const whoCanChat = socialModeSettings(socialMode, ageBand2).who_can_chat;
|
|
46896
47285
|
return { whoCanChat, bandGroup: chatBandGroup({ who_can_chat: whoCanChat, age_band: ageBand2 }) };
|
|
@@ -46987,5 +47376,6 @@ export {
|
|
|
46987
47376
|
playerPanelHtml,
|
|
46988
47377
|
playersPanelHtml,
|
|
46989
47378
|
siteAsset,
|
|
46990
|
-
startingSocialMode
|
|
47379
|
+
startingSocialMode,
|
|
47380
|
+
takesFriendRequests
|
|
46991
47381
|
};
|
package/bin/local-play.js
CHANGED
|
@@ -145,6 +145,8 @@ function defaultState() {
|
|
|
145
145
|
// Each player's guest, while they play as one: { id, name }.
|
|
146
146
|
guests: {},
|
|
147
147
|
friends: [],
|
|
148
|
+
// Friend requests waiting between bench accounts: { from, to, createdAtMs }.
|
|
149
|
+
friendRequests: [],
|
|
148
150
|
blocks: [],
|
|
149
151
|
ownedItems: {},
|
|
150
152
|
items: [],
|
|
@@ -629,6 +631,8 @@ export async function startLocalPlay(givenOptions) {
|
|
|
629
631
|
if (rest === "" && method === "DELETE") {
|
|
630
632
|
state.accounts = state.accounts.filter((each) => each.id !== account.id);
|
|
631
633
|
state.friends = state.friends.filter((pair) => !pair.includes(account.id));
|
|
634
|
+
state.friendRequests = (state.friendRequests ?? []).filter((each) => each.from !== account.id && each.to !== account.id);
|
|
635
|
+
state.acceptedFriendRequests = (state.acceptedFriendRequests ?? []).filter((each) => each.from !== account.id && each.to !== account.id);
|
|
632
636
|
state.blocks = state.blocks.filter((block) => block.blocker !== account.id && block.blocked !== account.id);
|
|
633
637
|
state.purchases = state.purchases.filter((purchase) => purchase.accountId !== account.id);
|
|
634
638
|
delete state.ownedItems[account.id];
|
|
@@ -951,6 +955,67 @@ export async function startLocalPlay(givenOptions) {
|
|
|
951
955
|
}
|
|
952
956
|
return reply(200, { player: { player_id: target.playerId, name: target.username }, is_friend: friendIdsOf(account.id).includes(target.id), is_blocked: state.blocks.some((block) => block.blocker === account.id && block.blocked === target.id) });
|
|
953
957
|
}
|
|
958
|
+
// Friend requests between bench accounts, as Zaisma's game page sends and answers them (apps/site/src/friend-requests.ts).
|
|
959
|
+
if (path === "/friends/requests" || path.startsWith("/friends/requests/")) {
|
|
960
|
+
if (!account) return reply(401, { error: { code: "not_signed_in", message: "Sign in to add friends." } });
|
|
961
|
+
const requests = (state.friendRequests ??= []);
|
|
962
|
+
const isBlockedPair = (other) => state.blocks.some((block) => isPair([block.blocker, block.blocked], account.id, other.id));
|
|
963
|
+
const isFriend = (other) => friendIdsOf(account.id).includes(other.id);
|
|
964
|
+
const waitingFrom = (from, to) => requests.find((each) => each.from === from && each.to === to);
|
|
965
|
+
const makeFriends = (other) => {
|
|
966
|
+
// The sender hears of the accept on their next look, for a day, as on Zaisma.
|
|
967
|
+
(state.acceptedFriendRequests ??= []).push({ from: other.id, to: account.id, answeredAtMs: Date.now() });
|
|
968
|
+
state.friendRequests = requests.filter((each) => !isPair([each.from, each.to], account.id, other.id));
|
|
969
|
+
if (!isFriend(other)) state.friends.push([account.id, other.id]);
|
|
970
|
+
saveState();
|
|
971
|
+
log(`${account.username} and ${other.username} are friends now.`);
|
|
972
|
+
};
|
|
973
|
+
if (request.method === "GET" && path === "/friends/requests") {
|
|
974
|
+
const accepted = (state.acceptedFriendRequests ?? []).filter((each) => each.from === account.id && each.answeredAtMs > Date.now() - 86400000 && isFriend({ id: each.to })).map((each) => ({ to: accountOf(each.to), answeredAtMs: each.answeredAtMs })).filter((each) => each.to);
|
|
975
|
+
return reply(200, {
|
|
976
|
+
requests: requests.filter((each) => each.to === account.id).map((each) => ({ from: accountOf(each.from), createdAtMs: each.createdAtMs })).filter((each) => each.from).map((each) => ({ player_id: each.from.playerId, name: each.from.username, created_at_ms: each.createdAtMs, game_slug: gameSlug })).reverse(),
|
|
977
|
+
accepted: accepted.map((each) => ({ player_id: each.to.playerId, name: each.to.username, answered_at_ms: each.answeredAtMs })).reverse(),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
const aboutMatch = /^\/friends\/requests\/about\/(player_[a-z0-9]{1,40})$/.exec(path);
|
|
981
|
+
if (request.method === "GET" && aboutMatch) {
|
|
982
|
+
const other = accountOfPlayer(aboutMatch[1]);
|
|
983
|
+
if (!other || other.id === account.id) return reply(200, { player: null, can_send: false, has_sent: false, is_friend: false });
|
|
984
|
+
const hasSent = Boolean(waitingFrom(account.id, other.id));
|
|
985
|
+
const canSend = !isFriend(other) && !hasSent && !isBlockedPair(other) && other.ageBand === account.ageBand && (Boolean(waitingFrom(other.id, account.id)) || glass.takesFriendRequests(other.ageBand, other.socialMode));
|
|
986
|
+
return reply(200, { player: { player_id: other.playerId, name: other.username }, can_send: canSend, has_sent: hasSent, is_friend: isFriend(other) });
|
|
987
|
+
}
|
|
988
|
+
if (isPost && path === "/friends/requests") {
|
|
989
|
+
const body = await readJson(request);
|
|
990
|
+
const other = typeof body.player_id === "string" ? accountOfPlayer(body.player_id) : null;
|
|
991
|
+
if (!other || other.id === account.id) return reply(409, { error: { code: "not_an_account", message: "Only players signed in to Zaisma take friend requests." } });
|
|
992
|
+
if (isFriend(other)) return reply(409, { error: { code: "already_friends", message: "You're friends already." } });
|
|
993
|
+
if (waitingFrom(other.id, account.id)) {
|
|
994
|
+
makeFriends(other);
|
|
995
|
+
return reply(200, { status: "friends" });
|
|
996
|
+
}
|
|
997
|
+
if (waitingFrom(account.id, other.id)) return reply(409, { error: { code: "already_sent", message: "Your request is waiting for them." } });
|
|
998
|
+
if (isBlockedPair(other) || other.ageBand !== account.ageBand || !glass.takesFriendRequests(other.ageBand, other.socialMode)) return reply(409, { error: { code: "not_taking_requests", message: "They aren't taking friend requests from you." } });
|
|
999
|
+
requests.push({ from: account.id, to: other.id, createdAtMs: Date.now() });
|
|
1000
|
+
saveState();
|
|
1001
|
+
log(`${account.username} sent ${other.username} a friend request.`);
|
|
1002
|
+
return reply(200, { status: "sent" });
|
|
1003
|
+
}
|
|
1004
|
+
const answerMatch = /^\/friends\/requests\/(player_[a-z0-9]{1,40})\/(accept|turn-down)$/.exec(path);
|
|
1005
|
+
if (isPost && answerMatch) {
|
|
1006
|
+
await readText(request);
|
|
1007
|
+
const other = accountOfPlayer(answerMatch[1]);
|
|
1008
|
+
if (!other || !waitingFrom(other.id, account.id)) return reply(404, { error: { code: "request_not_found", message: "That request isn't waiting any more." } });
|
|
1009
|
+
if (answerMatch[2] === "accept") makeFriends(other);
|
|
1010
|
+
else {
|
|
1011
|
+
state.friendRequests = requests.filter((each) => !(each.from === other.id && each.to === account.id));
|
|
1012
|
+
saveState();
|
|
1013
|
+
log(`${account.username} turned down ${other.username}'s friend request.`);
|
|
1014
|
+
}
|
|
1015
|
+
return reply(200, { answer: answerMatch[2] === "accept" ? "accepted" : "turned_down" });
|
|
1016
|
+
}
|
|
1017
|
+
return reply(404, { error: { code: "not_found", message: "Nothing here." } });
|
|
1018
|
+
}
|
|
954
1019
|
// A report goes to Zaisma's moderators, or the game's developer: here the terminal says what it would send.
|
|
955
1020
|
if (isPost && path === `${game}/report`) {
|
|
956
1021
|
const body = await readJson(request);
|
package/package.json
CHANGED
package/sdk/zaisma.d.ts
CHANGED
|
@@ -240,8 +240,35 @@ declare namespace Zaisma {
|
|
|
240
240
|
* await Zaisma.friends.setRoom(null); // left the match
|
|
241
241
|
*/
|
|
242
242
|
setRoom(room: OwnServerRoom | null): Promise<void>;
|
|
243
|
+
/**
|
|
244
|
+
* Offer the player to send another player a friend request, from your own scoreboard, lobby or menu.
|
|
245
|
+
* Zaisma asks the player first on its page ("Send Mia a friend request?", with the name Zaisma knows
|
|
246
|
+
* them by), so nothing is sent without them, and the other player gets it over their game's corner and
|
|
247
|
+
* on zaisma.com/friends. Players can also send one from Players in Zaisma's bar, so a button of your
|
|
248
|
+
* own is optional. Resolves with what happened: "sent"; "cancelled" when the player said no or another
|
|
249
|
+
* question was up; "already_friends"; or "not_taking_requests" when the other player doesn't take them
|
|
250
|
+
* from this one (their settings, their age group, a block, or a guest), which never says which. Your game
|
|
251
|
+
* never hears of requests themselves, only that two players are friends (room.areFriends on your game server, and the
|
|
252
|
+
* passes' tags, from their next join). Fails with error.code "account_needed" for a guest (ask them to
|
|
253
|
+
* sign in with {@link player.signIn}), "bad_player_id" for something that isn't a player's id, and
|
|
254
|
+
* "not_on_zaisma" when the game isn't played on Zaisma.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* // A button beside each player on your end-of-match screen.
|
|
258
|
+
* const { status } = await Zaisma.friends.sendRequest(player.id);
|
|
259
|
+
* if (status === "sent") button.textContent = "Request sent";
|
|
260
|
+
*/
|
|
261
|
+
sendRequest(playerId: string): Promise<FriendRequestAnswer>;
|
|
243
262
|
}
|
|
244
263
|
|
|
264
|
+
/** What became of a friend request the game offered ({@link Friends.sendRequest}). */
|
|
265
|
+
interface FriendRequestAnswer {
|
|
266
|
+
status: FriendRequestStatus;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** "sent", "cancelled" (the player said no), "already_friends", or "not_taking_requests" (never why). */
|
|
270
|
+
type FriendRequestStatus = "sent" | "cancelled" | "already_friends" | "not_taking_requests";
|
|
271
|
+
|
|
245
272
|
/** The player's friends playing this game ({@link Friends.list}). */
|
|
246
273
|
interface FriendList {
|
|
247
274
|
/** By name. Empty when none of them are playing it. */
|
package/sdk/zaisma.js
CHANGED
|
@@ -617,6 +617,9 @@
|
|
|
617
617
|
} else if (message.type === "blocks") {
|
|
618
618
|
room._blocks = new Set(Array.isArray(message.playerIds) ? message.playerIds : []);
|
|
619
619
|
room._mutedChanged();
|
|
620
|
+
} else if (message.type === "friend_request") {
|
|
621
|
+
// Someone here sent this player a friend request, or accepted theirs: the Zaisma page looks now (never who).
|
|
622
|
+
if (onZaismaPage()) window.parent.postMessage({ type: "zaisma:friend-requests" }, config.site);
|
|
620
623
|
} else if (message.type === "message") {
|
|
621
624
|
room._emit("message", message.data, typeof message.fromPlayerId === "string" ? message.fromPlayerId : null);
|
|
622
625
|
} else if (message.type === "chat" || message.type === "chat_sent" || message.type === "chat_stopped" || (message.type === "error" && typeof message.chatNumber === "number")) {
|
|
@@ -2457,6 +2460,51 @@
|
|
|
2457
2460
|
}
|
|
2458
2461
|
|
|
2459
2462
|
var rooms = { list: listRooms, quickJoin: quickJoin, linkedRoomCode: roomCodeFromPage(), linkedServerUrl: roomCodeFromPage() ? serverUrlFromPage() : null, isLinkedAsSpectator: watchFromPage() };
|
|
2460
|
-
|
|
2463
|
+
/*
|
|
2464
|
+
* Friend requests (zaisma.d.ts: Friends.sendRequest): the Zaisma page asks the player before anything is
|
|
2465
|
+
* sent, with the name Zaisma knows the other player by, so no game sends one on a player's behalf. Once one
|
|
2466
|
+
* is sent, from here or the page's Players, the rooms the other player is in tell their game to have their
|
|
2467
|
+
* page look now; Zaisma's record is the request itself.
|
|
2468
|
+
*/
|
|
2469
|
+
var friendRequestAsk = null;
|
|
2470
|
+
var FRIEND_REQUEST_STATUSES = ["sent", "cancelled", "already_friends", "not_taking_requests"];
|
|
2471
|
+
function sendFriendRequest(playerId) {
|
|
2472
|
+
if (!onZaismaPage() || !config.rooms) return Promise.reject(notOnZaisma("Friend requests come from Zaisma, so they work only in games played on Zaisma."));
|
|
2473
|
+
if (typeof playerId !== "string" || !/^player_[a-z0-9]{1,40}$/.test(playerId)) return Promise.reject(codedError("bad_player_id", "Zaisma.friends.sendRequest takes a player's id from room.players or Zaisma.friends.list."));
|
|
2474
|
+
return getPass().then(function (mine) {
|
|
2475
|
+
if (mine.kind !== "account") throw codedError("account_needed", "Friend requests are for players signed in to Zaisma: ask the player to sign in first (Zaisma.player.signIn).");
|
|
2476
|
+
// One question at a time: another request while the page is asking is a no.
|
|
2477
|
+
if (friendRequestAsk) return { status: "cancelled" };
|
|
2478
|
+
friendRequestAsk = new Promise(function (resolve) {
|
|
2479
|
+
var isAsking = false;
|
|
2480
|
+
var timer = setTimeout(function () { if (!isAsking) settle("cancelled"); }, 3000);
|
|
2481
|
+
function settle(status) {
|
|
2482
|
+
removeEventListener("message", onMessage);
|
|
2483
|
+
clearTimeout(timer);
|
|
2484
|
+
friendRequestAsk = null;
|
|
2485
|
+
resolve({ status: status });
|
|
2486
|
+
}
|
|
2487
|
+
function onMessage(event) {
|
|
2488
|
+
var told = event.data;
|
|
2489
|
+
if (event.origin !== config.site || event.source !== window.parent || !told || told.type !== "zaisma:friend-request" || told.playerId !== playerId) return;
|
|
2490
|
+
if (told.status === "asking") isAsking = true;
|
|
2491
|
+
else if (FRIEND_REQUEST_STATUSES.indexOf(told.status) !== -1) settle(told.status);
|
|
2492
|
+
}
|
|
2493
|
+
addEventListener("message", onMessage);
|
|
2494
|
+
window.parent.postMessage({ type: "zaisma:friend-request", playerId: playerId }, config.site);
|
|
2495
|
+
});
|
|
2496
|
+
return friendRequestAsk;
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
// A request sent (by the game's own button or the page's Players), or one accepted: the rooms the other player is in nudge their game.
|
|
2500
|
+
window.addEventListener("message", function (event) {
|
|
2501
|
+
var told = event.data;
|
|
2502
|
+
if (!config.site || event.origin !== config.site || event.source !== window.parent || !told || told.type !== "zaisma:friend-request" || (told.status !== "sent" && told.status !== "accepted") || typeof told.playerId !== "string") return;
|
|
2503
|
+
joined.forEach(function (room) {
|
|
2504
|
+
if (!room._done && !room.isSpectator && room.players.some(function (each) { return each.id === told.playerId; })) room._say({ type: "friend_request", playerId: told.playerId });
|
|
2505
|
+
});
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
var friends = { list: listFriends, setRoom: setOwnRoom, sendRequest: sendFriendRequest };
|
|
2461
2509
|
window.Zaisma = { sdkVersionNumber: 1, gameSlug: config.game || null, player: player, joinRoom: joinRoom, rooms: rooms, server: server, invite: invite, checkChat: checkChat, openPlayersPanel: openPlayersPanel, links: links, items: items, friends: friends, saves: saves, versions: versions };
|
|
2462
2510
|
})();
|
package/src/room.d.ts
CHANGED
|
@@ -892,6 +892,8 @@ export type ServerMessage = {
|
|
|
892
892
|
} | {
|
|
893
893
|
type: "blocks";
|
|
894
894
|
playerIds: string[];
|
|
895
|
+
} | {
|
|
896
|
+
type: "friend_request";
|
|
895
897
|
} | {
|
|
896
898
|
type: "chat";
|
|
897
899
|
fromPlayerId: string;
|
|
@@ -959,6 +961,9 @@ export type ClientMessage = {
|
|
|
959
961
|
} | {
|
|
960
962
|
type: "unblock";
|
|
961
963
|
playerId: string;
|
|
964
|
+
} | {
|
|
965
|
+
type: "friend_request";
|
|
966
|
+
playerId: string;
|
|
962
967
|
} | {
|
|
963
968
|
type: "probe_answer";
|
|
964
969
|
probeNumber: number;
|
|
@@ -1427,6 +1432,12 @@ export declare class RoomCore<S = any> {
|
|
|
1427
1432
|
private tellBlocks;
|
|
1428
1433
|
/** A player's game blocked a player in the room (a guest's block, or one just made), or took one back. */
|
|
1429
1434
|
private sessionBlock;
|
|
1435
|
+
/**
|
|
1436
|
+
* A player's game page just sent another player here a friend request, or accepted theirs (docs/DECISIONS.md,
|
|
1437
|
+
* "Friend requests in games"): the room tells that player's game to have their page look now, rather than at its next check-in.
|
|
1438
|
+
* Zaisma's record is the request; this says only "look", never who it's from, so a lying client gains nothing.
|
|
1439
|
+
*/
|
|
1440
|
+
private friendRequestTold;
|
|
1430
1441
|
/** Players in the match who haven't walked away: what room lists and "playing now" count. */
|
|
1431
1442
|
get playingCount(): number;
|
|
1432
1443
|
/**
|
package/src/room.js
CHANGED
|
@@ -1071,6 +1071,20 @@ export class RoomCore {
|
|
|
1071
1071
|
this.tellBlocks();
|
|
1072
1072
|
return null;
|
|
1073
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* A player's game page just sent another player here a friend request, or accepted theirs (docs/DECISIONS.md,
|
|
1076
|
+
* "Friend requests in games"): the room tells that player's game to have their page look now, rather than at its next check-in.
|
|
1077
|
+
* Zaisma's record is the request; this says only "look", never who it's from, so a lying client gains nothing.
|
|
1078
|
+
*/
|
|
1079
|
+
friendRequestTold(playerId, message) {
|
|
1080
|
+
const otherPlayerId = message.playerId;
|
|
1081
|
+
if (typeof otherPlayerId !== "string" || otherPlayerId === playerId || !this.players.has(otherPlayerId))
|
|
1082
|
+
return "bad_message";
|
|
1083
|
+
if (this.hasBlockBetween(playerId, otherPlayerId))
|
|
1084
|
+
return null;
|
|
1085
|
+
this.sendText(otherPlayerId, JSON.stringify({ type: "friend_request" }));
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1074
1088
|
/** Players in the match who haven't walked away: what room lists and "playing now" count. */
|
|
1075
1089
|
get playingCount() {
|
|
1076
1090
|
return this.players.size - this.kept.size;
|
|
@@ -1691,6 +1705,8 @@ export class RoomCore {
|
|
|
1691
1705
|
return this.sessionBlock(playerId, message, true);
|
|
1692
1706
|
case "unblock":
|
|
1693
1707
|
return this.sessionBlock(playerId, message, false);
|
|
1708
|
+
case "friend_request":
|
|
1709
|
+
return this.friendRequestTold(playerId, message);
|
|
1694
1710
|
default:
|
|
1695
1711
|
// A type this room doesn't know, maybe from a newer SDK: counted and limited above, then ignored.
|
|
1696
1712
|
return typeof message.type === "string" ? null : "bad_message";
|