@thegeem/protocol 0.1.35 → 0.1.37
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/dist/index.cjs +12 -5
- package/dist/index.d.cts +89 -11
- package/dist/index.d.ts +89 -11
- package/dist/index.js +10 -4
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,7 +33,8 @@ __export(index_exports, {
|
|
|
33
33
|
ROOM_ALPHABET: () => ROOM_ALPHABET,
|
|
34
34
|
gameIdForPv: () => gameIdForPv,
|
|
35
35
|
gamesForPv: () => gamesForPv,
|
|
36
|
-
modesForGame: () => modesForGame
|
|
36
|
+
modesForGame: () => modesForGame,
|
|
37
|
+
normalizeWesternDigits: () => normalizeWesternDigits
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(index_exports);
|
|
39
40
|
var import_zod = require("zod");
|
|
@@ -75,7 +76,8 @@ var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
|
|
|
75
76
|
var CHARACTER_IDS = ["m", "f"];
|
|
76
77
|
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
77
78
|
var UNSAFE_CHARS = /[\p{Cc}\u202A-\u202E\u2066-\u2069<>]/gu;
|
|
78
|
-
var
|
|
79
|
+
var normalizeWesternDigits = (s) => s.replace(/[٠-٩۰-۹]/g, (d) => String("\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) >= 0 ? "\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) : "\u06F0\u06F1\u06F2\u06F3\u06F4\u06F5\u06F6\u06F7\u06F8\u06F9".indexOf(d)));
|
|
80
|
+
var DisplayName = import_zod.z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform((s) => normalizeWesternDigits(s.replace(UNSAFE_CHARS, "")).trim()).refine((s) => s.length >= 1, "name is empty after sanitizing");
|
|
79
81
|
var RoomCode = import_zod.z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
|
|
80
82
|
var PlayerId = import_zod.z.string().max(40);
|
|
81
83
|
var HelpType = import_zod.z.enum([
|
|
@@ -89,7 +91,7 @@ var HelpType = import_zod.z.enum([
|
|
|
89
91
|
]);
|
|
90
92
|
var Pv = import_zod.z.number().int().min(1).max(1e6).optional();
|
|
91
93
|
var IdToken = import_zod.z.string().max(4096).optional();
|
|
92
|
-
var Platform = import_zod.z.enum(["web", "ios", "android"]).optional();
|
|
94
|
+
var Platform = import_zod.z.enum(["web", "ios", "android", "tv"]).optional();
|
|
93
95
|
var AppVersion = import_zod.z.string().trim().max(24).regex(/^[0-9]+(\.[0-9]+){0,3}([-+][0-9A-Za-z.-]+)?$/, "bad app version").optional();
|
|
94
96
|
var Avatar = import_zod.z.object({
|
|
95
97
|
emoji: import_zod.z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
|
|
@@ -216,7 +218,11 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
216
218
|
// trivia is 4-option (0–3)
|
|
217
219
|
import_zod.z.object({ t: import_zod.z.literal("useHelp"), helpType: HelpType }),
|
|
218
220
|
// sabotage a rival (active team only): spend a sabotage help to debuff targetTeam's next question
|
|
219
|
-
|
|
221
|
+
// `player` (gs#80): WHICH member of the target team sits out — required by استريح/restPlayer and
|
|
222
|
+
// ignored by عرجوب/tripLevel, which targets a whole team. Optional on the wire so pre-gs#80 clients
|
|
223
|
+
// are unchanged; the engine refuses to spend a restPlayer card without a valid one, so an old client
|
|
224
|
+
// cannot burn a lifeline by omitting it.
|
|
225
|
+
import_zod.z.object({ t: import_zod.z.literal("sabotage"), helpType: HelpType, targetTeam: import_zod.z.number().int().min(0).max(LIMITS.MAX_PLAYERS - 1), player: import_zod.z.string().max(64).optional() }),
|
|
220
226
|
// 0-based team index
|
|
221
227
|
// moderation
|
|
222
228
|
// Report another player in the room (App Store 1.2 UGC — display names). Available in lobby or
|
|
@@ -275,5 +281,6 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
275
281
|
ROOM_ALPHABET,
|
|
276
282
|
gameIdForPv,
|
|
277
283
|
gamesForPv,
|
|
278
|
-
modesForGame
|
|
284
|
+
modesForGame,
|
|
285
|
+
normalizeWesternDigits
|
|
279
286
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -103,6 +103,15 @@ declare const CHARACTER_IDS: readonly ["m", "f"];
|
|
|
103
103
|
type CharacterId = (typeof CHARACTER_IDS)[number];
|
|
104
104
|
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
105
105
|
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
106
|
+
/**
|
|
107
|
+
* Arabic-Indic (٠-٩) and Extended/Persian (۰-۹) digits → Western, D-043 applied to NAMES at the
|
|
108
|
+
* gate (ruling 2026-08-13, the «سالم ١٧» leaderboard sighting). A digit is a GLYPH of a number,
|
|
109
|
+
* not identity — «سالم ١٧» and «سالم 17» are the same name — so normalising at write-time is the
|
|
110
|
+
* same class of edit as the bidi strip above, and every surface on every client agrees on what to
|
|
111
|
+
* call the person. Normalising at RENDER instead would have web and iOS disagreeing about a name.
|
|
112
|
+
* Letters are never touched; this maps exactly twenty codepoints.
|
|
113
|
+
*/
|
|
114
|
+
declare const normalizeWesternDigits: (s: string) => string;
|
|
106
115
|
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
107
116
|
declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
108
117
|
t: z.ZodLiteral<"createRoom">;
|
|
@@ -121,7 +130,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
121
130
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
122
131
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
123
132
|
idToken: z.ZodOptional<z.ZodString>;
|
|
124
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
133
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
125
134
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
126
135
|
}, "strip", z.ZodTypeAny, {
|
|
127
136
|
t: "createRoom";
|
|
@@ -134,7 +143,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
134
143
|
character?: "m" | "f" | undefined;
|
|
135
144
|
pv?: number | undefined;
|
|
136
145
|
idToken?: string | undefined;
|
|
137
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
146
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
138
147
|
appVersion?: string | undefined;
|
|
139
148
|
}, {
|
|
140
149
|
t: "createRoom";
|
|
@@ -147,7 +156,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
147
156
|
character?: "m" | "f" | undefined;
|
|
148
157
|
pv?: number | undefined;
|
|
149
158
|
idToken?: string | undefined;
|
|
150
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
159
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
151
160
|
appVersion?: string | undefined;
|
|
152
161
|
}>, z.ZodObject<{
|
|
153
162
|
t: z.ZodLiteral<"renameHost">;
|
|
@@ -205,7 +214,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
205
214
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
206
215
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
207
216
|
idToken: z.ZodOptional<z.ZodString>;
|
|
208
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
217
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
209
218
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
210
219
|
}, "strip", z.ZodTypeAny, {
|
|
211
220
|
code: string;
|
|
@@ -218,7 +227,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
218
227
|
character?: "m" | "f" | undefined;
|
|
219
228
|
pv?: number | undefined;
|
|
220
229
|
idToken?: string | undefined;
|
|
221
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
230
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
222
231
|
appVersion?: string | undefined;
|
|
223
232
|
}, {
|
|
224
233
|
code: string;
|
|
@@ -231,7 +240,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
231
240
|
character?: "m" | "f" | undefined;
|
|
232
241
|
pv?: number | undefined;
|
|
233
242
|
idToken?: string | undefined;
|
|
234
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
243
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
235
244
|
appVersion?: string | undefined;
|
|
236
245
|
}>, z.ZodObject<{
|
|
237
246
|
t: z.ZodLiteral<"resume">;
|
|
@@ -250,7 +259,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
250
259
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
251
260
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
252
261
|
idToken: z.ZodOptional<z.ZodString>;
|
|
253
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
262
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
254
263
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
255
264
|
}, "strip", z.ZodTypeAny, {
|
|
256
265
|
code: string;
|
|
@@ -263,7 +272,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
263
272
|
character?: "m" | "f" | undefined;
|
|
264
273
|
pv?: number | undefined;
|
|
265
274
|
idToken?: string | undefined;
|
|
266
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
275
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
267
276
|
appVersion?: string | undefined;
|
|
268
277
|
}, {
|
|
269
278
|
code: string;
|
|
@@ -276,7 +285,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
276
285
|
character?: "m" | "f" | undefined;
|
|
277
286
|
pv?: number | undefined;
|
|
278
287
|
idToken?: string | undefined;
|
|
279
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
288
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
280
289
|
appVersion?: string | undefined;
|
|
281
290
|
}>, z.ZodObject<{
|
|
282
291
|
t: z.ZodLiteral<"setPlaying">;
|
|
@@ -505,14 +514,17 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
505
514
|
t: z.ZodLiteral<"sabotage">;
|
|
506
515
|
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
507
516
|
targetTeam: z.ZodNumber;
|
|
517
|
+
player: z.ZodOptional<z.ZodString>;
|
|
508
518
|
}, "strip", z.ZodTypeAny, {
|
|
509
519
|
t: "sabotage";
|
|
510
520
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
511
521
|
targetTeam: number;
|
|
522
|
+
player?: string | undefined;
|
|
512
523
|
}, {
|
|
513
524
|
t: "sabotage";
|
|
514
525
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
515
526
|
targetTeam: number;
|
|
527
|
+
player?: string | undefined;
|
|
516
528
|
}>, z.ZodObject<{
|
|
517
529
|
t: z.ZodLiteral<"report">;
|
|
518
530
|
playerId: z.ZodString;
|
|
@@ -691,6 +703,32 @@ interface LobbyPlayer {
|
|
|
691
703
|
avatar?: AvatarView;
|
|
692
704
|
/** Team index once the game has started; null/absent in the lobby. */
|
|
693
705
|
teamIndex?: number | null;
|
|
706
|
+
/**
|
|
707
|
+
* Equipped avatar-frame slug (e.g. "frame-sadu") — resolves to art via /assets/store/catalog.json
|
|
708
|
+
* (the catalog is the ONLY resolver; nothing about the art is derived from the slug). Read from the
|
|
709
|
+
* account once at room entry, so an equip change shows on the player's next join/resume. Absent =
|
|
710
|
+
* frameless. Earnable-class cosmetics only ever ride the room wire (D-059) — a paid exclusive can
|
|
711
|
+
* never appear here, so there is no receipt check in the broadcast path and no have/have-not row
|
|
712
|
+
* between children on one sofa. (gs#84 contract, D-060.)
|
|
713
|
+
*/
|
|
714
|
+
frame?: string;
|
|
715
|
+
/**
|
|
716
|
+
* This room kicked this exact person before — a HINT for the host, never a gate (the ruling is
|
|
717
|
+
* "label the re-knock, no ban list"). **Present ONLY in the HOST's copy of the frame**: telling a
|
|
718
|
+
* whole room who was once removed is a social leak, not a moderation aid.
|
|
719
|
+
*
|
|
720
|
+
* The same bit also rides `joinRequest` for the immediate ping. This one is the DURABLE carrier —
|
|
721
|
+
* it is RE-READ from the answer stored on the seat (decided once, at join; never recomputed by
|
|
722
|
+
* re-matching, which goes stale) and re-sent on every snapshot, so it survives a host reconnect,
|
|
723
|
+
* where a cached one-shot event would not. A client should prefer this and treat the event as a
|
|
724
|
+
* nudge. MID-GAME there are no lobby frames: there a host who resumes is re-sent the pending
|
|
725
|
+
* `joinRequest`s instead (labels included), so the durable promise holds in both halves.
|
|
726
|
+
*
|
|
727
|
+
* Absent means "no reason to think so", which includes the deliberate miss: an anonymous player who
|
|
728
|
+
* force-quits and rejoins reads as a stranger. Matched only on exact socket/sign-in identity — never
|
|
729
|
+
* a display name or IP, because a wrong label is worse than none.
|
|
730
|
+
*/
|
|
731
|
+
wasKicked?: boolean;
|
|
694
732
|
}
|
|
695
733
|
/**
|
|
696
734
|
* Who is in the room, carried ON the game-state push (gs#71).
|
|
@@ -719,6 +757,9 @@ interface RosterEntry {
|
|
|
719
757
|
isHost?: boolean;
|
|
720
758
|
/** Is this person currently connected? Mirrors `LobbyPlayer.connected`. */
|
|
721
759
|
connected?: boolean;
|
|
760
|
+
/** Equipped avatar-frame slug — mirrors `LobbyPlayer.frame`, because the roster is the MID-GAME
|
|
761
|
+
* carrier (gs#71): a cosmetic with no mid-game carrier silently vanishes for watchers and the TV. */
|
|
762
|
+
frame?: string;
|
|
722
763
|
}
|
|
723
764
|
/** Per-recipient context attached to lobby/state pushes. */
|
|
724
765
|
interface YouContext {
|
|
@@ -738,6 +779,25 @@ interface CategoryView {
|
|
|
738
779
|
/** Category's brand color (hex, e.g. '#E8473F') for board coloring; absent if the category has none. */
|
|
739
780
|
color?: string;
|
|
740
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* A content pack — the grouping the category strip renders as a row (geem-server#76).
|
|
784
|
+
*
|
|
785
|
+
* **The ARRAY ORDER is the pack order.** That is the point of this type: pack order previously had no
|
|
786
|
+
* home on the wire, so all four clients derived it by first-seen from `categories[]` (Android's
|
|
787
|
+
* `onlineGroupedPacks`, iOS's `order: [String]`, and web/TV their own). Four derivations of one fact,
|
|
788
|
+
* and when the server's own category order turned out to be non-deterministic the strip reshuffled
|
|
789
|
+
* between visits (gs#74) with nothing on the wire to pin it. One ordered array replaces all four.
|
|
790
|
+
*
|
|
791
|
+
* Deliberately NO `sortOrder` field: an ordered array already carries the order, and a positional index
|
|
792
|
+
* beside it would be the same fact stored twice — which is exactly the drift argument that made
|
|
793
|
+
* `has_image` a boolean rather than a filename. Position cannot disagree with itself.
|
|
794
|
+
*/
|
|
795
|
+
interface PackView {
|
|
796
|
+
id: string;
|
|
797
|
+
name: string;
|
|
798
|
+
/** Does this pack have cover art at `/assets/cat/pack-<id>.png`? Absent until the admin field ships. */
|
|
799
|
+
hasImage?: boolean;
|
|
800
|
+
}
|
|
741
801
|
type GameStatus = 'setup' | 'categorySelection' | 'inProgress' | 'completed';
|
|
742
802
|
type GameplayFlow = 'classic' | 'openJudged' | 'noHost' | 'buzzer';
|
|
743
803
|
type BuzzerGameMode = 'off' | 'solo' | 'race' | 'controller';
|
|
@@ -767,6 +827,8 @@ interface TeamView {
|
|
|
767
827
|
/** The player on THIS team whose turn it is to answer (rotates each turn); null if the team has
|
|
768
828
|
* no seated players. In controller mode only this player may `answer` on the team's turn. */
|
|
769
829
|
currentPlayerId: string | null;
|
|
830
|
+
/** استريح (gs#80): this team's player who sits out their next question, if a rival named one. */
|
|
831
|
+
restedPlayerId?: string | null;
|
|
770
832
|
/** The seated player's privacy-safe avatar (relay-enriched; engine view omits it). */
|
|
771
833
|
avatar?: AvatarView;
|
|
772
834
|
}
|
|
@@ -778,6 +840,10 @@ interface QuestionView {
|
|
|
778
840
|
selectedAnswerIndex: number | null;
|
|
779
841
|
/** Only present once revealed (anti-cheat). Null while the question is live. */
|
|
780
842
|
correctAnswerIndex: number | null;
|
|
843
|
+
/** «معلومة» — optional explanation shown AFTER the reveal. Null while the question is live (it
|
|
844
|
+
* usually names the answer — same anti-cheat gate as correctAnswerIndex) and null when the
|
|
845
|
+
* question simply has none, which is most of the bank: render the card only when non-null. */
|
|
846
|
+
explain?: string | null;
|
|
781
847
|
imageName?: string;
|
|
782
848
|
videoName?: string;
|
|
783
849
|
/** For video questions: seconds into the clip to stop playback (so the answer isn't shown). Absent = play through. */
|
|
@@ -939,6 +1005,7 @@ type ServerMsg = {
|
|
|
939
1005
|
t: 'joinRequest';
|
|
940
1006
|
playerId: string;
|
|
941
1007
|
name: string;
|
|
1008
|
+
wasKicked?: boolean;
|
|
942
1009
|
} | {
|
|
943
1010
|
t: 'joined';
|
|
944
1011
|
youId: string;
|
|
@@ -964,16 +1031,21 @@ type ServerMsg = {
|
|
|
964
1031
|
hostId: string;
|
|
965
1032
|
hostName: string;
|
|
966
1033
|
hostAvatar?: AvatarView;
|
|
1034
|
+
hostFrame?: string;
|
|
967
1035
|
hostPlaying?: boolean;
|
|
968
1036
|
hostPinned?: boolean;
|
|
969
1037
|
players: LobbyPlayer[];
|
|
970
1038
|
categories: CategoryView[];
|
|
1039
|
+
packs?: PackView[];
|
|
971
1040
|
selectedCategoryIds: string[];
|
|
972
1041
|
teams: TeamSlotView[];
|
|
973
1042
|
hostActivity: 'configuring' | 'idle';
|
|
974
1043
|
tally?: {
|
|
975
1044
|
[playerId: string]: number;
|
|
976
1045
|
};
|
|
1046
|
+
tallyNames?: {
|
|
1047
|
+
[playerId: string]: string;
|
|
1048
|
+
};
|
|
977
1049
|
modeId: ModeId | null;
|
|
978
1050
|
you: YouContext;
|
|
979
1051
|
} | {
|
|
@@ -1020,10 +1092,16 @@ type ServerMsg = {
|
|
|
1020
1092
|
message: string;
|
|
1021
1093
|
level?: 'info' | 'warn' | 'maintenance';
|
|
1022
1094
|
restartInSec?: number;
|
|
1023
|
-
}
|
|
1095
|
+
}
|
|
1096
|
+
/** `reason` (gs#83, additive): today only `'ended'`, and only on `no_room` — the code WAS a room
|
|
1097
|
+
* until recently (stale WhatsApp link, knock after the host closed the night). Absent = "no such
|
|
1098
|
+
* room as far as we know". Lets a client say «انتهى اللعب» instead of «ما في غرفة بهذا الكود»
|
|
1099
|
+
* without guessing. Old clients ignore it. */
|
|
1100
|
+
| {
|
|
1024
1101
|
t: 'error';
|
|
1025
1102
|
code: string;
|
|
1026
1103
|
message: string;
|
|
1104
|
+
reason?: string;
|
|
1027
1105
|
};
|
|
1028
1106
|
|
|
1029
|
-
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
|
|
1107
|
+
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type PackView, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame, normalizeWesternDigits };
|
package/dist/index.d.ts
CHANGED
|
@@ -103,6 +103,15 @@ declare const CHARACTER_IDS: readonly ["m", "f"];
|
|
|
103
103
|
type CharacterId = (typeof CHARACTER_IDS)[number];
|
|
104
104
|
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
105
105
|
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
106
|
+
/**
|
|
107
|
+
* Arabic-Indic (٠-٩) and Extended/Persian (۰-۹) digits → Western, D-043 applied to NAMES at the
|
|
108
|
+
* gate (ruling 2026-08-13, the «سالم ١٧» leaderboard sighting). A digit is a GLYPH of a number,
|
|
109
|
+
* not identity — «سالم ١٧» and «سالم 17» are the same name — so normalising at write-time is the
|
|
110
|
+
* same class of edit as the bidi strip above, and every surface on every client agrees on what to
|
|
111
|
+
* call the person. Normalising at RENDER instead would have web and iOS disagreeing about a name.
|
|
112
|
+
* Letters are never touched; this maps exactly twenty codepoints.
|
|
113
|
+
*/
|
|
114
|
+
declare const normalizeWesternDigits: (s: string) => string;
|
|
106
115
|
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
107
116
|
declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
108
117
|
t: z.ZodLiteral<"createRoom">;
|
|
@@ -121,7 +130,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
121
130
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
122
131
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
123
132
|
idToken: z.ZodOptional<z.ZodString>;
|
|
124
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
133
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
125
134
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
126
135
|
}, "strip", z.ZodTypeAny, {
|
|
127
136
|
t: "createRoom";
|
|
@@ -134,7 +143,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
134
143
|
character?: "m" | "f" | undefined;
|
|
135
144
|
pv?: number | undefined;
|
|
136
145
|
idToken?: string | undefined;
|
|
137
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
146
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
138
147
|
appVersion?: string | undefined;
|
|
139
148
|
}, {
|
|
140
149
|
t: "createRoom";
|
|
@@ -147,7 +156,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
147
156
|
character?: "m" | "f" | undefined;
|
|
148
157
|
pv?: number | undefined;
|
|
149
158
|
idToken?: string | undefined;
|
|
150
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
159
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
151
160
|
appVersion?: string | undefined;
|
|
152
161
|
}>, z.ZodObject<{
|
|
153
162
|
t: z.ZodLiteral<"renameHost">;
|
|
@@ -205,7 +214,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
205
214
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
206
215
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
207
216
|
idToken: z.ZodOptional<z.ZodString>;
|
|
208
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
217
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
209
218
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
210
219
|
}, "strip", z.ZodTypeAny, {
|
|
211
220
|
code: string;
|
|
@@ -218,7 +227,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
218
227
|
character?: "m" | "f" | undefined;
|
|
219
228
|
pv?: number | undefined;
|
|
220
229
|
idToken?: string | undefined;
|
|
221
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
230
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
222
231
|
appVersion?: string | undefined;
|
|
223
232
|
}, {
|
|
224
233
|
code: string;
|
|
@@ -231,7 +240,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
231
240
|
character?: "m" | "f" | undefined;
|
|
232
241
|
pv?: number | undefined;
|
|
233
242
|
idToken?: string | undefined;
|
|
234
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
243
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
235
244
|
appVersion?: string | undefined;
|
|
236
245
|
}>, z.ZodObject<{
|
|
237
246
|
t: z.ZodLiteral<"resume">;
|
|
@@ -250,7 +259,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
250
259
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
251
260
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
252
261
|
idToken: z.ZodOptional<z.ZodString>;
|
|
253
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
262
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
254
263
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
255
264
|
}, "strip", z.ZodTypeAny, {
|
|
256
265
|
code: string;
|
|
@@ -263,7 +272,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
263
272
|
character?: "m" | "f" | undefined;
|
|
264
273
|
pv?: number | undefined;
|
|
265
274
|
idToken?: string | undefined;
|
|
266
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
275
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
267
276
|
appVersion?: string | undefined;
|
|
268
277
|
}, {
|
|
269
278
|
code: string;
|
|
@@ -276,7 +285,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
276
285
|
character?: "m" | "f" | undefined;
|
|
277
286
|
pv?: number | undefined;
|
|
278
287
|
idToken?: string | undefined;
|
|
279
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
288
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
280
289
|
appVersion?: string | undefined;
|
|
281
290
|
}>, z.ZodObject<{
|
|
282
291
|
t: z.ZodLiteral<"setPlaying">;
|
|
@@ -505,14 +514,17 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
505
514
|
t: z.ZodLiteral<"sabotage">;
|
|
506
515
|
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
507
516
|
targetTeam: z.ZodNumber;
|
|
517
|
+
player: z.ZodOptional<z.ZodString>;
|
|
508
518
|
}, "strip", z.ZodTypeAny, {
|
|
509
519
|
t: "sabotage";
|
|
510
520
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
511
521
|
targetTeam: number;
|
|
522
|
+
player?: string | undefined;
|
|
512
523
|
}, {
|
|
513
524
|
t: "sabotage";
|
|
514
525
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
515
526
|
targetTeam: number;
|
|
527
|
+
player?: string | undefined;
|
|
516
528
|
}>, z.ZodObject<{
|
|
517
529
|
t: z.ZodLiteral<"report">;
|
|
518
530
|
playerId: z.ZodString;
|
|
@@ -691,6 +703,32 @@ interface LobbyPlayer {
|
|
|
691
703
|
avatar?: AvatarView;
|
|
692
704
|
/** Team index once the game has started; null/absent in the lobby. */
|
|
693
705
|
teamIndex?: number | null;
|
|
706
|
+
/**
|
|
707
|
+
* Equipped avatar-frame slug (e.g. "frame-sadu") — resolves to art via /assets/store/catalog.json
|
|
708
|
+
* (the catalog is the ONLY resolver; nothing about the art is derived from the slug). Read from the
|
|
709
|
+
* account once at room entry, so an equip change shows on the player's next join/resume. Absent =
|
|
710
|
+
* frameless. Earnable-class cosmetics only ever ride the room wire (D-059) — a paid exclusive can
|
|
711
|
+
* never appear here, so there is no receipt check in the broadcast path and no have/have-not row
|
|
712
|
+
* between children on one sofa. (gs#84 contract, D-060.)
|
|
713
|
+
*/
|
|
714
|
+
frame?: string;
|
|
715
|
+
/**
|
|
716
|
+
* This room kicked this exact person before — a HINT for the host, never a gate (the ruling is
|
|
717
|
+
* "label the re-knock, no ban list"). **Present ONLY in the HOST's copy of the frame**: telling a
|
|
718
|
+
* whole room who was once removed is a social leak, not a moderation aid.
|
|
719
|
+
*
|
|
720
|
+
* The same bit also rides `joinRequest` for the immediate ping. This one is the DURABLE carrier —
|
|
721
|
+
* it is RE-READ from the answer stored on the seat (decided once, at join; never recomputed by
|
|
722
|
+
* re-matching, which goes stale) and re-sent on every snapshot, so it survives a host reconnect,
|
|
723
|
+
* where a cached one-shot event would not. A client should prefer this and treat the event as a
|
|
724
|
+
* nudge. MID-GAME there are no lobby frames: there a host who resumes is re-sent the pending
|
|
725
|
+
* `joinRequest`s instead (labels included), so the durable promise holds in both halves.
|
|
726
|
+
*
|
|
727
|
+
* Absent means "no reason to think so", which includes the deliberate miss: an anonymous player who
|
|
728
|
+
* force-quits and rejoins reads as a stranger. Matched only on exact socket/sign-in identity — never
|
|
729
|
+
* a display name or IP, because a wrong label is worse than none.
|
|
730
|
+
*/
|
|
731
|
+
wasKicked?: boolean;
|
|
694
732
|
}
|
|
695
733
|
/**
|
|
696
734
|
* Who is in the room, carried ON the game-state push (gs#71).
|
|
@@ -719,6 +757,9 @@ interface RosterEntry {
|
|
|
719
757
|
isHost?: boolean;
|
|
720
758
|
/** Is this person currently connected? Mirrors `LobbyPlayer.connected`. */
|
|
721
759
|
connected?: boolean;
|
|
760
|
+
/** Equipped avatar-frame slug — mirrors `LobbyPlayer.frame`, because the roster is the MID-GAME
|
|
761
|
+
* carrier (gs#71): a cosmetic with no mid-game carrier silently vanishes for watchers and the TV. */
|
|
762
|
+
frame?: string;
|
|
722
763
|
}
|
|
723
764
|
/** Per-recipient context attached to lobby/state pushes. */
|
|
724
765
|
interface YouContext {
|
|
@@ -738,6 +779,25 @@ interface CategoryView {
|
|
|
738
779
|
/** Category's brand color (hex, e.g. '#E8473F') for board coloring; absent if the category has none. */
|
|
739
780
|
color?: string;
|
|
740
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* A content pack — the grouping the category strip renders as a row (geem-server#76).
|
|
784
|
+
*
|
|
785
|
+
* **The ARRAY ORDER is the pack order.** That is the point of this type: pack order previously had no
|
|
786
|
+
* home on the wire, so all four clients derived it by first-seen from `categories[]` (Android's
|
|
787
|
+
* `onlineGroupedPacks`, iOS's `order: [String]`, and web/TV their own). Four derivations of one fact,
|
|
788
|
+
* and when the server's own category order turned out to be non-deterministic the strip reshuffled
|
|
789
|
+
* between visits (gs#74) with nothing on the wire to pin it. One ordered array replaces all four.
|
|
790
|
+
*
|
|
791
|
+
* Deliberately NO `sortOrder` field: an ordered array already carries the order, and a positional index
|
|
792
|
+
* beside it would be the same fact stored twice — which is exactly the drift argument that made
|
|
793
|
+
* `has_image` a boolean rather than a filename. Position cannot disagree with itself.
|
|
794
|
+
*/
|
|
795
|
+
interface PackView {
|
|
796
|
+
id: string;
|
|
797
|
+
name: string;
|
|
798
|
+
/** Does this pack have cover art at `/assets/cat/pack-<id>.png`? Absent until the admin field ships. */
|
|
799
|
+
hasImage?: boolean;
|
|
800
|
+
}
|
|
741
801
|
type GameStatus = 'setup' | 'categorySelection' | 'inProgress' | 'completed';
|
|
742
802
|
type GameplayFlow = 'classic' | 'openJudged' | 'noHost' | 'buzzer';
|
|
743
803
|
type BuzzerGameMode = 'off' | 'solo' | 'race' | 'controller';
|
|
@@ -767,6 +827,8 @@ interface TeamView {
|
|
|
767
827
|
/** The player on THIS team whose turn it is to answer (rotates each turn); null if the team has
|
|
768
828
|
* no seated players. In controller mode only this player may `answer` on the team's turn. */
|
|
769
829
|
currentPlayerId: string | null;
|
|
830
|
+
/** استريح (gs#80): this team's player who sits out their next question, if a rival named one. */
|
|
831
|
+
restedPlayerId?: string | null;
|
|
770
832
|
/** The seated player's privacy-safe avatar (relay-enriched; engine view omits it). */
|
|
771
833
|
avatar?: AvatarView;
|
|
772
834
|
}
|
|
@@ -778,6 +840,10 @@ interface QuestionView {
|
|
|
778
840
|
selectedAnswerIndex: number | null;
|
|
779
841
|
/** Only present once revealed (anti-cheat). Null while the question is live. */
|
|
780
842
|
correctAnswerIndex: number | null;
|
|
843
|
+
/** «معلومة» — optional explanation shown AFTER the reveal. Null while the question is live (it
|
|
844
|
+
* usually names the answer — same anti-cheat gate as correctAnswerIndex) and null when the
|
|
845
|
+
* question simply has none, which is most of the bank: render the card only when non-null. */
|
|
846
|
+
explain?: string | null;
|
|
781
847
|
imageName?: string;
|
|
782
848
|
videoName?: string;
|
|
783
849
|
/** For video questions: seconds into the clip to stop playback (so the answer isn't shown). Absent = play through. */
|
|
@@ -939,6 +1005,7 @@ type ServerMsg = {
|
|
|
939
1005
|
t: 'joinRequest';
|
|
940
1006
|
playerId: string;
|
|
941
1007
|
name: string;
|
|
1008
|
+
wasKicked?: boolean;
|
|
942
1009
|
} | {
|
|
943
1010
|
t: 'joined';
|
|
944
1011
|
youId: string;
|
|
@@ -964,16 +1031,21 @@ type ServerMsg = {
|
|
|
964
1031
|
hostId: string;
|
|
965
1032
|
hostName: string;
|
|
966
1033
|
hostAvatar?: AvatarView;
|
|
1034
|
+
hostFrame?: string;
|
|
967
1035
|
hostPlaying?: boolean;
|
|
968
1036
|
hostPinned?: boolean;
|
|
969
1037
|
players: LobbyPlayer[];
|
|
970
1038
|
categories: CategoryView[];
|
|
1039
|
+
packs?: PackView[];
|
|
971
1040
|
selectedCategoryIds: string[];
|
|
972
1041
|
teams: TeamSlotView[];
|
|
973
1042
|
hostActivity: 'configuring' | 'idle';
|
|
974
1043
|
tally?: {
|
|
975
1044
|
[playerId: string]: number;
|
|
976
1045
|
};
|
|
1046
|
+
tallyNames?: {
|
|
1047
|
+
[playerId: string]: string;
|
|
1048
|
+
};
|
|
977
1049
|
modeId: ModeId | null;
|
|
978
1050
|
you: YouContext;
|
|
979
1051
|
} | {
|
|
@@ -1020,10 +1092,16 @@ type ServerMsg = {
|
|
|
1020
1092
|
message: string;
|
|
1021
1093
|
level?: 'info' | 'warn' | 'maintenance';
|
|
1022
1094
|
restartInSec?: number;
|
|
1023
|
-
}
|
|
1095
|
+
}
|
|
1096
|
+
/** `reason` (gs#83, additive): today only `'ended'`, and only on `no_room` — the code WAS a room
|
|
1097
|
+
* until recently (stale WhatsApp link, knock after the host closed the night). Absent = "no such
|
|
1098
|
+
* room as far as we know". Lets a client say «انتهى اللعب» instead of «ما في غرفة بهذا الكود»
|
|
1099
|
+
* without guessing. Old clients ignore it. */
|
|
1100
|
+
| {
|
|
1024
1101
|
t: 'error';
|
|
1025
1102
|
code: string;
|
|
1026
1103
|
message: string;
|
|
1104
|
+
reason?: string;
|
|
1027
1105
|
};
|
|
1028
1106
|
|
|
1029
|
-
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
|
|
1107
|
+
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type PackView, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame, normalizeWesternDigits };
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,8 @@ var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
|
|
|
38
38
|
var CHARACTER_IDS = ["m", "f"];
|
|
39
39
|
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
40
40
|
var UNSAFE_CHARS = /[\p{Cc}\u202A-\u202E\u2066-\u2069<>]/gu;
|
|
41
|
-
var
|
|
41
|
+
var normalizeWesternDigits = (s) => s.replace(/[٠-٩۰-۹]/g, (d) => String("\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) >= 0 ? "\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) : "\u06F0\u06F1\u06F2\u06F3\u06F4\u06F5\u06F6\u06F7\u06F8\u06F9".indexOf(d)));
|
|
42
|
+
var DisplayName = z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform((s) => normalizeWesternDigits(s.replace(UNSAFE_CHARS, "")).trim()).refine((s) => s.length >= 1, "name is empty after sanitizing");
|
|
42
43
|
var RoomCode = z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
|
|
43
44
|
var PlayerId = z.string().max(40);
|
|
44
45
|
var HelpType = z.enum([
|
|
@@ -52,7 +53,7 @@ var HelpType = z.enum([
|
|
|
52
53
|
]);
|
|
53
54
|
var Pv = z.number().int().min(1).max(1e6).optional();
|
|
54
55
|
var IdToken = z.string().max(4096).optional();
|
|
55
|
-
var Platform = z.enum(["web", "ios", "android"]).optional();
|
|
56
|
+
var Platform = z.enum(["web", "ios", "android", "tv"]).optional();
|
|
56
57
|
var AppVersion = z.string().trim().max(24).regex(/^[0-9]+(\.[0-9]+){0,3}([-+][0-9A-Za-z.-]+)?$/, "bad app version").optional();
|
|
57
58
|
var Avatar = z.object({
|
|
58
59
|
emoji: z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
|
|
@@ -179,7 +180,11 @@ var ClientMsg = z.discriminatedUnion("t", [
|
|
|
179
180
|
// trivia is 4-option (0–3)
|
|
180
181
|
z.object({ t: z.literal("useHelp"), helpType: HelpType }),
|
|
181
182
|
// sabotage a rival (active team only): spend a sabotage help to debuff targetTeam's next question
|
|
182
|
-
|
|
183
|
+
// `player` (gs#80): WHICH member of the target team sits out — required by استريح/restPlayer and
|
|
184
|
+
// ignored by عرجوب/tripLevel, which targets a whole team. Optional on the wire so pre-gs#80 clients
|
|
185
|
+
// are unchanged; the engine refuses to spend a restPlayer card without a valid one, so an old client
|
|
186
|
+
// cannot burn a lifeline by omitting it.
|
|
187
|
+
z.object({ t: z.literal("sabotage"), helpType: HelpType, targetTeam: z.number().int().min(0).max(LIMITS.MAX_PLAYERS - 1), player: z.string().max(64).optional() }),
|
|
183
188
|
// 0-based team index
|
|
184
189
|
// moderation
|
|
185
190
|
// Report another player in the room (App Store 1.2 UGC — display names). Available in lobby or
|
|
@@ -237,5 +242,6 @@ export {
|
|
|
237
242
|
ROOM_ALPHABET,
|
|
238
243
|
gameIdForPv,
|
|
239
244
|
gamesForPv,
|
|
240
|
-
modesForGame
|
|
245
|
+
modesForGame,
|
|
246
|
+
normalizeWesternDigits
|
|
241
247
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegeem/protocol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "Geem's wire protocol — shared zod schemas + TypeScript types for talking to the Geem game server over Socket.IO. The source of truth all clients implement against.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://geem.tv",
|