@irtio/lobby 0.7.0 → 0.8.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/dist/index.d.ts +79 -1
- package/dist/index.js +285 -9
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,35 @@ interface AttachableRoom {
|
|
|
40
40
|
connected: boolean;
|
|
41
41
|
}[]) => void): () => void;
|
|
42
42
|
on(event: 'rtt', cb: (rtt: number) => void): () => void;
|
|
43
|
+
/**
|
|
44
|
+
* D75: the room's lobby, when it has one. Absent lobby, absent UI — every consumer that existed
|
|
45
|
+
* before this renders exactly as it did, and the package still imports nothing.
|
|
46
|
+
*
|
|
47
|
+
* Structural, like the rest of this type. `@irtio/client`'s `lobbyOf(room)` answers this shape,
|
|
48
|
+
* and so does a hand-rolled object in a framework app that would rather drive the panel itself.
|
|
49
|
+
*/
|
|
50
|
+
readonly lobby?: AttachableLobby;
|
|
51
|
+
}
|
|
52
|
+
/** D75: the lobby half of an attachable room. See `AttachableRoom.lobby`. */
|
|
53
|
+
interface AttachableLobby {
|
|
54
|
+
readonly phase: 'lobby' | 'started';
|
|
55
|
+
readonly players: readonly {
|
|
56
|
+
ready: boolean;
|
|
57
|
+
connected: boolean;
|
|
58
|
+
}[];
|
|
59
|
+
/** Whether the room is currently findable by strangers. The ROOM's state, never the click. */
|
|
60
|
+
readonly public?: boolean;
|
|
61
|
+
/** True only under a `'when-ready'` start policy; the only thing that draws readiness. */
|
|
62
|
+
readonly readyUi?: boolean;
|
|
63
|
+
/** How many players the room starts at, when it starts on a count. */
|
|
64
|
+
readonly capacity?: number;
|
|
65
|
+
ready(value: boolean): void;
|
|
66
|
+
/**
|
|
67
|
+
* Present only when the app wired one. The platform takes no public toggle from a client — that
|
|
68
|
+
* would be a built-in RPC — so a game that wants this offers its own and passes it through.
|
|
69
|
+
*/
|
|
70
|
+
setPublic?(value: boolean): void;
|
|
71
|
+
on(event: 'change', cb: () => void): () => void;
|
|
43
72
|
}
|
|
44
73
|
/**
|
|
45
74
|
* The custom element class. Registered as `irt-lobby` by `defineLobby()`, which this package
|
|
@@ -70,6 +99,55 @@ declare class IrtLobbyElement extends HTMLElement {
|
|
|
70
99
|
/** The room's capacity, if known. `0` or the attribute absent both mean "unknown". */
|
|
71
100
|
get maxPlayers(): number;
|
|
72
101
|
set maxPlayers(value: number);
|
|
102
|
+
/**
|
|
103
|
+
* D75: the panel is the game's front door. Mirrors the `quick-match` boolean attribute.
|
|
104
|
+
*
|
|
105
|
+
* With it, and before a room is attached, the panel renders the chooser: **Start a private
|
|
106
|
+
* lobby** and **Join a game**. It also unlocks the searching state and the public toggle. Without
|
|
107
|
+
* it the panel is exactly what it has always been.
|
|
108
|
+
*/
|
|
109
|
+
get quickMatch(): boolean;
|
|
110
|
+
set quickMatch(value: boolean);
|
|
111
|
+
/**
|
|
112
|
+
* D75: this game has no private games. Mirrors the `no-private` boolean attribute.
|
|
113
|
+
*
|
|
114
|
+
* Only meaningful alongside `quick-match`: the chooser drops to **Join a game** alone and the
|
|
115
|
+
* code, link and QR never render, because there is nothing to share. On its own it is invalid —
|
|
116
|
+
* the panel renders as it always did and says so once on the console — because a panel with no
|
|
117
|
+
* private games and no quick match is a panel with no way in at all.
|
|
118
|
+
*/
|
|
119
|
+
get noPrivate(): boolean;
|
|
120
|
+
set noPrivate(value: boolean);
|
|
121
|
+
/** D75: a quick match is in flight. Mirrors `searching`; `attachLobby` sets and clears it. */
|
|
122
|
+
get searching(): boolean;
|
|
123
|
+
set searching(value: boolean);
|
|
124
|
+
/**
|
|
125
|
+
* D75: how many players are waiting in the queue, from `GET /match/status`.
|
|
126
|
+
*
|
|
127
|
+
* Rendered under "finding a game…" as `n of m waiting`, and only when both are set. Absent, the
|
|
128
|
+
* searching state is an honest spinner rather than a made-up count.
|
|
129
|
+
*/
|
|
130
|
+
get waiting(): number | undefined;
|
|
131
|
+
set waiting(value: number | undefined);
|
|
132
|
+
/** D75: how many the queue takes. Mirrors `waiting-of`. */
|
|
133
|
+
get waitingOf(): number | undefined;
|
|
134
|
+
set waitingOf(value: number | undefined);
|
|
135
|
+
/**
|
|
136
|
+
* D75: the queue name the **Join a game** event carries. Mirrors the `queue` attribute.
|
|
137
|
+
*
|
|
138
|
+
* Empty means "the project's default queue", which is what a game with one queue wants and what
|
|
139
|
+
* omitting it has always meant everywhere else in the platform.
|
|
140
|
+
*/
|
|
141
|
+
get queue(): string;
|
|
142
|
+
set queue(value: string);
|
|
143
|
+
/**
|
|
144
|
+
* D75: draw the roster but not the ready button, for a game that draws its own.
|
|
145
|
+
*
|
|
146
|
+
* Mirrors `hide-ready`, and only bites under a `'when-ready'` room — the other two policies have
|
|
147
|
+
* no ready button to hide.
|
|
148
|
+
*/
|
|
149
|
+
get hideReady(): boolean;
|
|
150
|
+
set hideReady(value: boolean);
|
|
73
151
|
/** Whether the panel renders inside a fixed full-viewport backdrop. Mirrors `modal`. */
|
|
74
152
|
get modal(): boolean;
|
|
75
153
|
set modal(value: boolean);
|
|
@@ -239,4 +317,4 @@ interface QrMatrix {
|
|
|
239
317
|
*/
|
|
240
318
|
declare function encodeQr(text: string): QrMatrix;
|
|
241
319
|
|
|
242
|
-
export { type AttachableRoom, IrtLobbyElement, IrtProfileElement, IrtStatusElement, LOBBY_TAG, PROFILE_TAG, type ProfilableRoom, type ProfileReading, type QrMatrix, STATUS_TAG, defineLobby, defineProfile, defineStatus, encodeQr };
|
|
320
|
+
export { type AttachableLobby, type AttachableRoom, IrtLobbyElement, IrtProfileElement, IrtStatusElement, LOBBY_TAG, PROFILE_TAG, type ProfilableRoom, type ProfileReading, type QrMatrix, STATUS_TAG, defineLobby, defineProfile, defineStatus, encodeQr };
|
package/dist/index.js
CHANGED
|
@@ -438,6 +438,8 @@ var STATUS_COLORS = {
|
|
|
438
438
|
var UNKNOWN_STATUS_COLOR = "#9aa1ad";
|
|
439
439
|
|
|
440
440
|
// src/element.ts
|
|
441
|
+
var warnedNoPrivate = /* @__PURE__ */ new WeakSet();
|
|
442
|
+
var PUBLIC_SETTLE_MS = 600;
|
|
441
443
|
function countConnected(clients) {
|
|
442
444
|
return clients.filter((c) => c.connected !== false).length;
|
|
443
445
|
}
|
|
@@ -449,7 +451,15 @@ var OBSERVED = [
|
|
|
449
451
|
"players",
|
|
450
452
|
"max-players",
|
|
451
453
|
"modal",
|
|
452
|
-
"open"
|
|
454
|
+
"open",
|
|
455
|
+
// ---- M6 lane H: lobby front door ----
|
|
456
|
+
"quick-match",
|
|
457
|
+
"no-private",
|
|
458
|
+
"searching",
|
|
459
|
+
"waiting",
|
|
460
|
+
"waiting-of",
|
|
461
|
+
"hide-ready",
|
|
462
|
+
"queue"
|
|
453
463
|
];
|
|
454
464
|
var STYLE = `
|
|
455
465
|
:host {
|
|
@@ -520,6 +530,34 @@ var STYLE = `
|
|
|
520
530
|
cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0;
|
|
521
531
|
}
|
|
522
532
|
.close:hover { background: #1e2129; }
|
|
533
|
+
/* ---- M6 lane H: lobby front door ---- */
|
|
534
|
+
.choose { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 2px; }
|
|
535
|
+
.choose button {
|
|
536
|
+
padding: 10px 12px; border-radius: 8px; border: 1px solid #2a2e37; cursor: pointer;
|
|
537
|
+
background: #1e2129; color: inherit; font: inherit; font-size: 14px; font-weight: 600;
|
|
538
|
+
text-align: center;
|
|
539
|
+
}
|
|
540
|
+
.choose button:hover { background: #262a33; }
|
|
541
|
+
.choose button.primary { background: #f2f3f5; color: #16181d; border-color: #f2f3f5; }
|
|
542
|
+
.choose button.primary:hover { background: #ffffff; }
|
|
543
|
+
.searching { text-align: center; font-size: 13px; color: #9aa1ad; padding: 18px 0 6px; }
|
|
544
|
+
.searching .count { display: block; margin-top: 6px; font-size: 12px; color: #6c7280; }
|
|
545
|
+
.roster { list-style: none; margin: 12px 0 0; padding: 0; display: flex;
|
|
546
|
+
flex-direction: column; gap: 4px; }
|
|
547
|
+
.roster li { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #cdd2da; }
|
|
548
|
+
.roster .flag { width: 8px; height: 8px; border-radius: 50%; background: #3a3f4a; flex: none; }
|
|
549
|
+
.roster li[data-ready='true'] .flag { background: #3ac177; }
|
|
550
|
+
.roster li[data-me='true'] { color: #f2f3f5; font-weight: 600; }
|
|
551
|
+
.lobby-note { margin-top: 10px; font-size: 12px; color: #9aa1ad; text-align: center; }
|
|
552
|
+
.ready {
|
|
553
|
+
margin-top: 10px; width: 100%; padding: 9px 12px; border-radius: 8px; border: 0;
|
|
554
|
+
cursor: pointer; background: #3ac177; color: #08130c; font: inherit; font-size: 13px;
|
|
555
|
+
font-weight: 700;
|
|
556
|
+
}
|
|
557
|
+
.ready[aria-pressed='true'] { background: #1e2129; color: #9aa1ad; }
|
|
558
|
+
.toggle { display: flex; align-items: center; gap: 8px; margin-top: 12px; font-size: 12px;
|
|
559
|
+
color: #9aa1ad; }
|
|
560
|
+
.toggle input { margin: 0; }
|
|
523
561
|
`;
|
|
524
562
|
var IrtLobbyElement = class extends HTMLElement {
|
|
525
563
|
static get observedAttributes() {
|
|
@@ -586,6 +624,95 @@ var IrtLobbyElement = class extends HTMLElement {
|
|
|
586
624
|
set maxPlayers(value) {
|
|
587
625
|
this.setAttribute("max-players", String(value));
|
|
588
626
|
}
|
|
627
|
+
// ---- M6 lane H: lobby front door ----
|
|
628
|
+
/**
|
|
629
|
+
* D75: the panel is the game's front door. Mirrors the `quick-match` boolean attribute.
|
|
630
|
+
*
|
|
631
|
+
* With it, and before a room is attached, the panel renders the chooser: **Start a private
|
|
632
|
+
* lobby** and **Join a game**. It also unlocks the searching state and the public toggle. Without
|
|
633
|
+
* it the panel is exactly what it has always been.
|
|
634
|
+
*/
|
|
635
|
+
get quickMatch() {
|
|
636
|
+
return this.hasAttribute("quick-match");
|
|
637
|
+
}
|
|
638
|
+
set quickMatch(value) {
|
|
639
|
+
if (value) this.setAttribute("quick-match", "");
|
|
640
|
+
else this.removeAttribute("quick-match");
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* D75: this game has no private games. Mirrors the `no-private` boolean attribute.
|
|
644
|
+
*
|
|
645
|
+
* Only meaningful alongside `quick-match`: the chooser drops to **Join a game** alone and the
|
|
646
|
+
* code, link and QR never render, because there is nothing to share. On its own it is invalid —
|
|
647
|
+
* the panel renders as it always did and says so once on the console — because a panel with no
|
|
648
|
+
* private games and no quick match is a panel with no way in at all.
|
|
649
|
+
*/
|
|
650
|
+
get noPrivate() {
|
|
651
|
+
return this.hasAttribute("no-private");
|
|
652
|
+
}
|
|
653
|
+
set noPrivate(value) {
|
|
654
|
+
if (value) this.setAttribute("no-private", "");
|
|
655
|
+
else this.removeAttribute("no-private");
|
|
656
|
+
}
|
|
657
|
+
/** D75: a quick match is in flight. Mirrors `searching`; `attachLobby` sets and clears it. */
|
|
658
|
+
get searching() {
|
|
659
|
+
return this.hasAttribute("searching");
|
|
660
|
+
}
|
|
661
|
+
set searching(value) {
|
|
662
|
+
if (value) this.setAttribute("searching", "");
|
|
663
|
+
else this.removeAttribute("searching");
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* D75: how many players are waiting in the queue, from `GET /match/status`.
|
|
667
|
+
*
|
|
668
|
+
* Rendered under "finding a game…" as `n of m waiting`, and only when both are set. Absent, the
|
|
669
|
+
* searching state is an honest spinner rather than a made-up count.
|
|
670
|
+
*/
|
|
671
|
+
get waiting() {
|
|
672
|
+
if (!this.hasAttribute("waiting")) return void 0;
|
|
673
|
+
const value = Number(this.getAttribute("waiting"));
|
|
674
|
+
return Number.isFinite(value) ? value : void 0;
|
|
675
|
+
}
|
|
676
|
+
set waiting(value) {
|
|
677
|
+
if (value === void 0) this.removeAttribute("waiting");
|
|
678
|
+
else this.setAttribute("waiting", String(value));
|
|
679
|
+
}
|
|
680
|
+
/** D75: how many the queue takes. Mirrors `waiting-of`. */
|
|
681
|
+
get waitingOf() {
|
|
682
|
+
if (!this.hasAttribute("waiting-of")) return void 0;
|
|
683
|
+
const value = Number(this.getAttribute("waiting-of"));
|
|
684
|
+
return Number.isFinite(value) ? value : void 0;
|
|
685
|
+
}
|
|
686
|
+
set waitingOf(value) {
|
|
687
|
+
if (value === void 0) this.removeAttribute("waiting-of");
|
|
688
|
+
else this.setAttribute("waiting-of", String(value));
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* D75: the queue name the **Join a game** event carries. Mirrors the `queue` attribute.
|
|
692
|
+
*
|
|
693
|
+
* Empty means "the project's default queue", which is what a game with one queue wants and what
|
|
694
|
+
* omitting it has always meant everywhere else in the platform.
|
|
695
|
+
*/
|
|
696
|
+
get queue() {
|
|
697
|
+
return this.getAttribute("queue") ?? "";
|
|
698
|
+
}
|
|
699
|
+
set queue(value) {
|
|
700
|
+
if (value === "") this.removeAttribute("queue");
|
|
701
|
+
else this.setAttribute("queue", value);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* D75: draw the roster but not the ready button, for a game that draws its own.
|
|
705
|
+
*
|
|
706
|
+
* Mirrors `hide-ready`, and only bites under a `'when-ready'` room — the other two policies have
|
|
707
|
+
* no ready button to hide.
|
|
708
|
+
*/
|
|
709
|
+
get hideReady() {
|
|
710
|
+
return this.hasAttribute("hide-ready");
|
|
711
|
+
}
|
|
712
|
+
set hideReady(value) {
|
|
713
|
+
if (value) this.setAttribute("hide-ready", "");
|
|
714
|
+
else this.removeAttribute("hide-ready");
|
|
715
|
+
}
|
|
589
716
|
/** Whether the panel renders inside a fixed full-viewport backdrop. Mirrors `modal`. */
|
|
590
717
|
get modal() {
|
|
591
718
|
return this.hasAttribute("modal");
|
|
@@ -671,6 +798,8 @@ var IrtLobbyElement = class extends HTMLElement {
|
|
|
671
798
|
this.players = countConnected(clients);
|
|
672
799
|
})
|
|
673
800
|
);
|
|
801
|
+
const lobby = room.lobby;
|
|
802
|
+
if (lobby) offs.push(lobby.on("change", () => this.render()));
|
|
674
803
|
const detach = () => {
|
|
675
804
|
for (const off of offs) off();
|
|
676
805
|
if (this.#detach === detach) {
|
|
@@ -721,6 +850,12 @@ var IrtLobbyElement = class extends HTMLElement {
|
|
|
721
850
|
} else {
|
|
722
851
|
host = this.#root;
|
|
723
852
|
}
|
|
853
|
+
const mode = this.#mode();
|
|
854
|
+
if (mode !== "panel" && this.roomCode === "") {
|
|
855
|
+
host.append(this.searching ? this.#searchingState(doc) : this.#chooser(doc, mode));
|
|
856
|
+
host.append(this.#badge(doc));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
724
859
|
const status = this.status;
|
|
725
860
|
const statusRow = doc.createElement("div");
|
|
726
861
|
statusRow.className = "status";
|
|
@@ -742,6 +877,7 @@ var IrtLobbyElement = class extends HTMLElement {
|
|
|
742
877
|
statusRow.append(playersSpan);
|
|
743
878
|
}
|
|
744
879
|
host.append(statusRow);
|
|
880
|
+
const sharing = mode !== "quick-match-only";
|
|
745
881
|
const code = doc.createElement("button");
|
|
746
882
|
code.type = "button";
|
|
747
883
|
code.className = "code";
|
|
@@ -751,7 +887,7 @@ var IrtLobbyElement = class extends HTMLElement {
|
|
|
751
887
|
code.addEventListener("click", () => {
|
|
752
888
|
void this.#copy(this.roomCode, "room code copied");
|
|
753
889
|
});
|
|
754
|
-
host.append(code);
|
|
890
|
+
if (sharing) host.append(code);
|
|
755
891
|
const link = doc.createElement("button");
|
|
756
892
|
link.type = "button";
|
|
757
893
|
link.className = "link";
|
|
@@ -761,16 +897,156 @@ var IrtLobbyElement = class extends HTMLElement {
|
|
|
761
897
|
link.addEventListener("click", () => {
|
|
762
898
|
void this.#copy(this.link, "link copied");
|
|
763
899
|
});
|
|
764
|
-
host.append(link);
|
|
900
|
+
if (sharing) host.append(link);
|
|
765
901
|
const toast = doc.createElement("div");
|
|
766
902
|
toast.className = "toast";
|
|
767
903
|
host.append(toast);
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
904
|
+
if (sharing) {
|
|
905
|
+
const canvas = doc.createElement("canvas");
|
|
906
|
+
canvas.setAttribute("part", "qr");
|
|
907
|
+
host.append(canvas);
|
|
908
|
+
if (this.link) this.#drawQr(canvas, this.link);
|
|
909
|
+
else canvas.hidden = true;
|
|
910
|
+
}
|
|
773
911
|
if (this.nameEntry) host.append(this.#nameForm(doc));
|
|
912
|
+
this.#renderLobby(doc, host, mode);
|
|
913
|
+
host.append(this.#badge(doc));
|
|
914
|
+
}
|
|
915
|
+
// ---- M6 lane H: lobby front door ----
|
|
916
|
+
/**
|
|
917
|
+
* Which of the design's three attribute states this element is in.
|
|
918
|
+
*
|
|
919
|
+
* `no-private` alone is the invalid one, and the answer is deliberately today's panel plus one
|
|
920
|
+
* console warning rather than a throw or a silent reinterpretation: the element is a share panel
|
|
921
|
+
* a game already has on screen, and breaking it over an attribute pair would take a working
|
|
922
|
+
* feature away to punish a typo.
|
|
923
|
+
*/
|
|
924
|
+
#mode() {
|
|
925
|
+
if (this.quickMatch) return this.noPrivate ? "quick-match-only" : "quick-match";
|
|
926
|
+
if (this.noPrivate && !warnedNoPrivate.has(this.ownerDocument)) {
|
|
927
|
+
warnedNoPrivate.add(this.ownerDocument);
|
|
928
|
+
console.warn(
|
|
929
|
+
"<irt-lobby>: no-private does nothing without quick-match \u2014 a panel with no private games and no quick match has no way into a game at all. Rendering the ordinary panel."
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return "panel";
|
|
933
|
+
}
|
|
934
|
+
/** The pre-room chooser. It emits; it never joins. See the package header for why. */
|
|
935
|
+
#chooser(doc, mode) {
|
|
936
|
+
const wrap = doc.createElement("div");
|
|
937
|
+
wrap.className = "choose";
|
|
938
|
+
wrap.setAttribute("part", "choose");
|
|
939
|
+
const join = doc.createElement("button");
|
|
940
|
+
join.type = "button";
|
|
941
|
+
join.className = "primary";
|
|
942
|
+
join.setAttribute("part", "join");
|
|
943
|
+
join.textContent = "Join a game";
|
|
944
|
+
join.addEventListener("click", () => {
|
|
945
|
+
const queue = this.queue;
|
|
946
|
+
this.dispatchEvent(
|
|
947
|
+
new CustomEvent("quickmatch", {
|
|
948
|
+
detail: queue ? { queue } : {},
|
|
949
|
+
bubbles: true,
|
|
950
|
+
composed: true
|
|
951
|
+
})
|
|
952
|
+
);
|
|
953
|
+
});
|
|
954
|
+
if (mode === "quick-match") {
|
|
955
|
+
const priv = doc.createElement("button");
|
|
956
|
+
priv.type = "button";
|
|
957
|
+
priv.setAttribute("part", "private");
|
|
958
|
+
priv.textContent = "Start a private lobby";
|
|
959
|
+
priv.addEventListener("click", () => {
|
|
960
|
+
this.dispatchEvent(new CustomEvent("private", { bubbles: true, composed: true }));
|
|
961
|
+
});
|
|
962
|
+
wrap.append(priv);
|
|
963
|
+
}
|
|
964
|
+
wrap.append(join);
|
|
965
|
+
return wrap;
|
|
966
|
+
}
|
|
967
|
+
/** The searching state, with the queue's own numbers when the app has them. */
|
|
968
|
+
#searchingState(doc) {
|
|
969
|
+
const wrap = doc.createElement("div");
|
|
970
|
+
wrap.className = "searching";
|
|
971
|
+
wrap.setAttribute("part", "searching");
|
|
972
|
+
wrap.textContent = "finding a game\u2026";
|
|
973
|
+
const waiting = this.waiting;
|
|
974
|
+
const of = this.waitingOf;
|
|
975
|
+
if (waiting !== void 0 && of !== void 0) {
|
|
976
|
+
const count = doc.createElement("span");
|
|
977
|
+
count.className = "count";
|
|
978
|
+
count.setAttribute("part", "waiting");
|
|
979
|
+
count.textContent = `${waiting} of ${of} waiting`;
|
|
980
|
+
wrap.append(count);
|
|
981
|
+
}
|
|
982
|
+
return wrap;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* The roster, the ready button and the public toggle, for a room that exposes a lobby.
|
|
986
|
+
*
|
|
987
|
+
* Three renderings, and which one appears is the ROOM's decision rather than an attribute's.
|
|
988
|
+
* `readyUi` is true only under a `'when-ready'` policy, and only then is readiness drawn at all;
|
|
989
|
+
* a room that starts when it is full says so and shows who has arrived; a `'manual'` room shows
|
|
990
|
+
* the roster alone, because only its own code knows what it is waiting for.
|
|
991
|
+
*/
|
|
992
|
+
#renderLobby(doc, host, mode) {
|
|
993
|
+
const lobby = this.#room?.lobby;
|
|
994
|
+
if (!lobby) return;
|
|
995
|
+
if (lobby.phase === "started") return;
|
|
996
|
+
const players = lobby.players;
|
|
997
|
+
const list = doc.createElement("ul");
|
|
998
|
+
list.className = "roster";
|
|
999
|
+
list.setAttribute("part", "roster");
|
|
1000
|
+
for (const [index, player] of players.entries()) {
|
|
1001
|
+
const row = doc.createElement("li");
|
|
1002
|
+
row.dataset.ready = String(lobby.readyUi === true && player.ready === true);
|
|
1003
|
+
row.dataset.me = String(player.me === true);
|
|
1004
|
+
const flag = doc.createElement("span");
|
|
1005
|
+
flag.className = "flag";
|
|
1006
|
+
const label = doc.createElement("span");
|
|
1007
|
+
label.textContent = `player ${index + 1}${player.connected === false ? " (away)" : ""}`;
|
|
1008
|
+
row.append(flag, label);
|
|
1009
|
+
list.append(row);
|
|
1010
|
+
}
|
|
1011
|
+
host.append(list);
|
|
1012
|
+
const note = doc.createElement("div");
|
|
1013
|
+
note.className = "lobby-note";
|
|
1014
|
+
note.setAttribute("part", "lobby-note");
|
|
1015
|
+
const capacity = lobby.capacity ?? 0;
|
|
1016
|
+
note.textContent = lobby.readyUi === true ? `${players.filter((p) => p.ready).length} of ${players.length} ready` : capacity > 0 ? `${players.length}/${capacity} players, starting when full` : `${players.length} ${players.length === 1 ? "player" : "players"}`;
|
|
1017
|
+
host.append(note);
|
|
1018
|
+
if (lobby.readyUi === true && !this.hideReady) {
|
|
1019
|
+
const mine = players.find((p) => p.me === true);
|
|
1020
|
+
const isReady = mine?.ready === true;
|
|
1021
|
+
const button = doc.createElement("button");
|
|
1022
|
+
button.type = "button";
|
|
1023
|
+
button.className = "ready";
|
|
1024
|
+
button.setAttribute("part", "ready");
|
|
1025
|
+
button.setAttribute("aria-pressed", String(isReady));
|
|
1026
|
+
button.textContent = isReady ? "not ready" : "ready";
|
|
1027
|
+
button.addEventListener("click", () => lobby.ready(!isReady));
|
|
1028
|
+
host.append(button);
|
|
1029
|
+
}
|
|
1030
|
+
if (mode === "quick-match" && lobby.setPublic) {
|
|
1031
|
+
const wrap = doc.createElement("label");
|
|
1032
|
+
wrap.className = "toggle";
|
|
1033
|
+
wrap.setAttribute("part", "public-toggle");
|
|
1034
|
+
const box = doc.createElement("input");
|
|
1035
|
+
box.type = "checkbox";
|
|
1036
|
+
box.checked = lobby.public === true;
|
|
1037
|
+
box.addEventListener("change", () => {
|
|
1038
|
+
box.disabled = true;
|
|
1039
|
+
lobby.setPublic?.(box.checked);
|
|
1040
|
+
setTimeout(() => this.render(), PUBLIC_SETTLE_MS);
|
|
1041
|
+
});
|
|
1042
|
+
const text = doc.createElement("span");
|
|
1043
|
+
text.textContent = "anyone can join";
|
|
1044
|
+
wrap.append(box, text);
|
|
1045
|
+
host.append(wrap);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
/** The badge, unchanged; extracted so the chooser can render it too. */
|
|
1049
|
+
#badge(doc) {
|
|
774
1050
|
const badge = doc.createElement("div");
|
|
775
1051
|
badge.className = "badge";
|
|
776
1052
|
badge.setAttribute("part", "badge");
|
|
@@ -780,7 +1056,7 @@ var IrtLobbyElement = class extends HTMLElement {
|
|
|
780
1056
|
anchor.rel = "noreferrer";
|
|
781
1057
|
anchor.textContent = "multiplayer by irt.io";
|
|
782
1058
|
badge.append(anchor);
|
|
783
|
-
|
|
1059
|
+
return badge;
|
|
784
1060
|
}
|
|
785
1061
|
/** The optional name box. Submitting emits `name` **before** the app joins, which is the point. */
|
|
786
1062
|
#nameForm(doc) {
|