dsh-poker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +2419 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +3051 -0
- package/lib/index.js.map +1 -0
- package/lib/invariant-C3ZiFskw.js +13 -0
- package/lib/invariant-C3ZiFskw.js.map +1 -0
- package/lib/invariant.js +3 -0
- package/lib/join/card-back-landlord.png +0 -0
- package/lib/join/card-back.png +0 -0
- package/lib/join/face-a.png +0 -0
- package/lib/join/face-j.png +0 -0
- package/lib/join/face-k.png +0 -0
- package/lib/join/face-q.png +0 -0
- package/lib/join/index.html +13 -0
- package/lib/join/index.js +8910 -0
- package/lib/join/index.js.map +1 -0
- package/lib/join/join.css +14 -0
- package/lib/join/joker-black.png +0 -0
- package/lib/join/joker-red.png +0 -0
- package/lib/join/role-farmer-b.png +0 -0
- package/lib/join/role-farmer-c.png +0 -0
- package/lib/join/role-farmer.png +0 -0
- package/lib/join/role-landlord-b.png +0 -0
- package/lib/join/role-landlord.png +0 -0
- package/lib/join/role-spectator.png +0 -0
- package/lib/types/client/CardBack.d.ts +7 -0
- package/lib/types/client/CardBack.d.ts.map +1 -0
- package/lib/types/client/CardFace.d.ts +8 -0
- package/lib/types/client/CardFace.d.ts.map +1 -0
- package/lib/types/client/HostApp.d.ts +4 -0
- package/lib/types/client/HostApp.d.ts.map +1 -0
- package/lib/types/client/InviteDialog.d.ts +15 -0
- package/lib/types/client/InviteDialog.d.ts.map +1 -0
- package/lib/types/client/JoinForm.d.ts +13 -0
- package/lib/types/client/JoinForm.d.ts.map +1 -0
- package/lib/types/client/LobbyView.d.ts +34 -0
- package/lib/types/client/LobbyView.d.ts.map +1 -0
- package/lib/types/client/SeatAvatar.d.ts +18 -0
- package/lib/types/client/SeatAvatar.d.ts.map +1 -0
- package/lib/types/client/SeatRow.d.ts +8 -0
- package/lib/types/client/SeatRow.d.ts.map +1 -0
- package/lib/types/client/SettingsCard.d.ts +6 -0
- package/lib/types/client/SettingsCard.d.ts.map +1 -0
- package/lib/types/client/SettlementView.d.ts +6 -0
- package/lib/types/client/SettlementView.d.ts.map +1 -0
- package/lib/types/client/TableView.d.ts +16 -0
- package/lib/types/client/TableView.d.ts.map +1 -0
- package/lib/types/client/card-stack.d.ts +3 -0
- package/lib/types/client/card-stack.d.ts.map +1 -0
- package/lib/types/client/host-api.d.ts +50 -0
- package/lib/types/client/host-api.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +5 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/slots.d.ts +3 -0
- package/lib/types/client/slots.d.ts.map +1 -0
- package/lib/types/client/store.d.ts +15 -0
- package/lib/types/client/store.d.ts.map +1 -0
- package/lib/types/config.d.ts +76 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/crypto.d.ts +9 -0
- package/lib/types/crypto.d.ts.map +1 -0
- package/lib/types/engine/cards.d.ts +22 -0
- package/lib/types/engine/cards.d.ts.map +1 -0
- package/lib/types/engine/deal.d.ts +11 -0
- package/lib/types/engine/deal.d.ts.map +1 -0
- package/lib/types/engine/hands.d.ts +16 -0
- package/lib/types/engine/hands.d.ts.map +1 -0
- package/lib/types/engine/index.d.ts +7 -0
- package/lib/types/engine/index.d.ts.map +1 -0
- package/lib/types/engine/play.d.ts +73 -0
- package/lib/types/engine/play.d.ts.map +1 -0
- package/lib/types/engine/random.d.ts +2 -0
- package/lib/types/engine/random.d.ts.map +1 -0
- package/lib/types/engine/score.d.ts +11 -0
- package/lib/types/engine/score.d.ts.map +1 -0
- package/lib/types/identity/player.d.ts +6 -0
- package/lib/types/identity/player.d.ts.map +1 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/invariant.d.ts +9 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/join/JoinApp.d.ts +2 -0
- package/lib/types/join/JoinApp.d.ts.map +1 -0
- package/lib/types/join/main.d.ts +2 -0
- package/lib/types/join/main.d.ts.map +1 -0
- package/lib/types/net/auth.d.ts +16 -0
- package/lib/types/net/auth.d.ts.map +1 -0
- package/lib/types/net/join-page.d.ts +6 -0
- package/lib/types/net/join-page.d.ts.map +1 -0
- package/lib/types/net/json.d.ts +3 -0
- package/lib/types/net/json.d.ts.map +1 -0
- package/lib/types/net/protocol.d.ts +2 -0
- package/lib/types/net/protocol.d.ts.map +1 -0
- package/lib/types/net/routes.d.ts +4 -0
- package/lib/types/net/routes.d.ts.map +1 -0
- package/lib/types/net/views.d.ts +3 -0
- package/lib/types/net/views.d.ts.map +1 -0
- package/lib/types/net/ws.d.ts +8 -0
- package/lib/types/net/ws.d.ts.map +1 -0
- package/lib/types/persist/domain.d.ts +104 -0
- package/lib/types/persist/domain.d.ts.map +1 -0
- package/lib/types/persist/schemas.d.ts +79 -0
- package/lib/types/persist/schemas.d.ts.map +1 -0
- package/lib/types/room/RoomManager.d.ts +142 -0
- package/lib/types/room/RoomManager.d.ts.map +1 -0
- package/lib/types/settle/audit.d.ts +15 -0
- package/lib/types/settle/audit.d.ts.map +1 -0
- package/lib/types/settle/escrow.d.ts +4 -0
- package/lib/types/settle/escrow.d.ts.map +1 -0
- package/lib/types/settle/grant.d.ts +7 -0
- package/lib/types/settle/grant.d.ts.map +1 -0
- package/lib/types/settle/ledger.d.ts +39 -0
- package/lib/types/settle/ledger.d.ts.map +1 -0
- package/lib/types/settle/math.d.ts +12 -0
- package/lib/types/settle/math.d.ts.map +1 -0
- package/lib/types/types.d.ts +253 -0
- package/lib/types/types.d.ts.map +1 -0
- package/package.json +116 -0
- package/prepare.mjs +16 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2419 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "dsh-poker", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
//#region rolldown:runtime
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
21
|
+
value: mod,
|
|
22
|
+
enumerable: true
|
|
23
|
+
}) : target, mod));
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
let react = require("react");
|
|
27
|
+
react = __toESM(react);
|
|
28
|
+
let react_dom = require("react-dom");
|
|
29
|
+
react_dom = __toESM(react_dom);
|
|
30
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
31
|
+
react_jsx_runtime = __toESM(react_jsx_runtime);
|
|
32
|
+
|
|
33
|
+
//#region src/settle/math.ts
|
|
34
|
+
const STAKE_LADDER_M = [
|
|
35
|
+
1,
|
|
36
|
+
2,
|
|
37
|
+
5,
|
|
38
|
+
10,
|
|
39
|
+
20,
|
|
40
|
+
50,
|
|
41
|
+
100
|
|
42
|
+
];
|
|
43
|
+
const DEFAULT_STAKE_M = 1;
|
|
44
|
+
const DEFAULT_MAX_MULTIPLIER = 8;
|
|
45
|
+
const DEFAULT_WELCOME_ATOMS = 200000000n;
|
|
46
|
+
const ATOMS_PER_M = 1000000n;
|
|
47
|
+
const ATOMS_PER_B = 1000000000n;
|
|
48
|
+
function stakeAtomsFromM(stakeM) {
|
|
49
|
+
return BigInt(stakeM) * ATOMS_PER_M;
|
|
50
|
+
}
|
|
51
|
+
function perFarmerCapAtoms(stakeAtoms, maxMultiplier) {
|
|
52
|
+
return stakeAtoms * 3n * BigInt(maxMultiplier) * 2n;
|
|
53
|
+
}
|
|
54
|
+
function seatCapAtoms(stakeAtoms, maxMultiplier, seatCount = 3) {
|
|
55
|
+
return BigInt(seatCount - 1) * perFarmerCapAtoms(stakeAtoms, maxMultiplier);
|
|
56
|
+
}
|
|
57
|
+
/** Display token atoms as AI-token units: M (million) or B (billion). Never dump raw zeros. */
|
|
58
|
+
function formatM(atoms) {
|
|
59
|
+
const sign = atoms < 0n ? "-" : "";
|
|
60
|
+
const abs = atoms < 0n ? -atoms : atoms;
|
|
61
|
+
if (abs >= ATOMS_PER_B) return `${sign}${formatScaled(abs, ATOMS_PER_B)}B`;
|
|
62
|
+
return `${sign}${formatScaled(abs, ATOMS_PER_M)}M`;
|
|
63
|
+
}
|
|
64
|
+
function formatScaled(abs, unit) {
|
|
65
|
+
const whole = abs / unit;
|
|
66
|
+
const rem = abs % unit;
|
|
67
|
+
if (rem === 0n) return whole.toString();
|
|
68
|
+
const decimals = unit.toString().length - 1;
|
|
69
|
+
const frac = rem.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
70
|
+
return `${whole.toString()}.${frac}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/client/host-api.ts
|
|
75
|
+
const PREFIX = "/doudizhu";
|
|
76
|
+
async function request(path, init) {
|
|
77
|
+
const response = await fetch(`${PREFIX}${path}`, {
|
|
78
|
+
credentials: "same-origin",
|
|
79
|
+
...init,
|
|
80
|
+
headers: {
|
|
81
|
+
"content-type": "application/json",
|
|
82
|
+
"x-requested-with": "doudizhu",
|
|
83
|
+
...init?.headers ?? {}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
const text = await response.text();
|
|
87
|
+
const body = text ? JSON.parse(text) : {};
|
|
88
|
+
if (!response.ok) throw new Error(body.message ?? body.error ?? response.statusText);
|
|
89
|
+
return body;
|
|
90
|
+
}
|
|
91
|
+
function createRoom(input) {
|
|
92
|
+
return request("/api/rooms", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: JSON.stringify(input)
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function joinRoom(input) {
|
|
98
|
+
return request("/api/join", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: JSON.stringify(input)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function sendCommand(roomId, command) {
|
|
104
|
+
return request(`/api/rooms/${roomId}/command`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: JSON.stringify(command)
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function fetchSince(roomId, seq) {
|
|
110
|
+
return request(`/api/rooms/${roomId}/since?seq=${seq}`);
|
|
111
|
+
}
|
|
112
|
+
function connectChannel(ticket, onEvent, fallback) {
|
|
113
|
+
const proto = location.protocol === "https:" ? "wss" : "ws";
|
|
114
|
+
let closed = false;
|
|
115
|
+
let ws = null;
|
|
116
|
+
let poll;
|
|
117
|
+
try {
|
|
118
|
+
ws = new WebSocket(`${proto}://${location.host}${PREFIX}/ws?ticket=${encodeURIComponent(ticket)}`, "doudizhu.v1");
|
|
119
|
+
ws.onmessage = (event) => {
|
|
120
|
+
try {
|
|
121
|
+
onEvent(JSON.parse(String(event.data)));
|
|
122
|
+
} catch {}
|
|
123
|
+
};
|
|
124
|
+
ws.onerror = () => {
|
|
125
|
+
startPoll();
|
|
126
|
+
};
|
|
127
|
+
ws.onclose = () => {
|
|
128
|
+
if (!closed) startPoll();
|
|
129
|
+
};
|
|
130
|
+
} catch {
|
|
131
|
+
startPoll();
|
|
132
|
+
}
|
|
133
|
+
function startPoll() {
|
|
134
|
+
if (poll || closed) return;
|
|
135
|
+
poll = setInterval(() => {
|
|
136
|
+
fetchSince(fallback.roomId, fallback.seq()).then((body) => {
|
|
137
|
+
for (const event of body.events) onEvent(event);
|
|
138
|
+
}).catch(() => {});
|
|
139
|
+
}, 800);
|
|
140
|
+
}
|
|
141
|
+
return () => {
|
|
142
|
+
closed = true;
|
|
143
|
+
ws?.close();
|
|
144
|
+
if (poll) clearInterval(poll);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region \0dsh-css:/Users/gehonglu/remote-code/dsh-doudizhu/src/client/styles.module.css.mjs
|
|
150
|
+
const css = "._5O8MPa_root{z-index:20;background:var(--dsw-alias-bg-base,#fff);min-width:0;color:var(--dsw-alias-label-primary,#1a1a1a);pointer-events:auto;flex-direction:column;font-family:inherit;display:flex;position:fixed;inset:0;overflow:hidden}._5O8MPa_topbar{border-bottom:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);justify-content:space-between;align-items:center;padding:12px 20px;display:flex}._5O8MPa_brand{letter-spacing:.01em;color:var(--dsw-alias-label-primary,#1a1a1a);font-size:15px;font-weight:600}._5O8MPa_muted{color:var(--dsw-alias-label-tertiary,#8a8a8a);font-size:13px}._5O8MPa_close{appearance:none;border:1px solid var(--dsw-alias-border-l2,#e8e8e8);color:var(--dsw-alias-label-secondary,#5c5c5c);cursor:pointer;font:inherit;background:0 0;border-radius:8px;padding:5px 14px;font-size:13px}._5O8MPa_close:hover{color:var(--dsw-alias-label-primary,#1a1a1a);border-color:var(--dsw-alias-label-dimmed,#c8c8c8)}._5O8MPa_lobby,._5O8MPa_settlement{flex:1;padding:24px 20px;overflow:auto}._5O8MPa_card{background:var(--dsw-alias-bg-layer-3,#fff);border:1px solid var(--dsw-alias-border-l2,#e8e8e8);border-radius:14px;max-width:760px;margin:0 auto;padding:28px 28px 24px}._5O8MPa_card h2,._5O8MPa_card h3{margin:0 0 4px;font-size:17px;font-weight:600;line-height:1.4}._5O8MPa_card>._5O8MPa_primary{align-self:center;width:100%;margin-top:4px}._5O8MPa_tabs{border-bottom:1px solid var(--dsw-alias-border-l2,#e8e8e8);gap:0;margin:-4px 0 16px;display:flex}._5O8MPa_tab{appearance:none;font:inherit;color:var(--dsw-alias-label-tertiary,#8a8a8a);cursor:pointer;background:0 0;border:0;border-bottom:2px solid #0000;flex:1;margin-bottom:-1px;padding:8px 12px;font-size:14px;font-weight:500}._5O8MPa_tab:hover{color:var(--dsw-alias-label-primary,#1a1a1a)}._5O8MPa_tabActive{color:var(--dsw-alias-label-primary,#1a1a1a);border-bottom-color:var(--dsw-alias-label-primary,#1a1a1a)}._5O8MPa_row{flex-wrap:wrap;gap:12px;margin:4px 0 8px;display:flex}._5O8MPa_createFields{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:4px 16px;display:grid}._5O8MPa_createFields>._5O8MPa_field{min-width:0}._5O8MPa_createFields>._5O8MPa_field+._5O8MPa_field{border-top:0}._5O8MPa_fieldWide{grid-column:1/-1}._5O8MPa_createFields ._5O8MPa_select,._5O8MPa_createFields ._5O8MPa_input{box-sizing:border-box;width:100%}._5O8MPa_field{min-width:160px;color:var(--dsw-alias-label-primary,#1a1a1a);flex-direction:column;gap:6px;padding:12px 0;font-size:13px;font-weight:500;display:flex}._5O8MPa_field+._5O8MPa_field{border-top:1px solid var(--dsw-alias-border-l2,#e8e8e8)}._5O8MPa_fieldRow{flex-direction:row;align-items:center;gap:8px}._5O8MPa_joinForm{flex-direction:column;display:flex}._5O8MPa_joinFields{flex-direction:column;width:100%;display:flex}._5O8MPa_joinFields ._5O8MPa_primary{width:100%;height:44px;font-weight:600}._5O8MPa_joinInline{gap:32px;padding:14px 0;display:flex}._5O8MPa_joinField{flex:1 1 0;align-items:center;gap:16px;min-width:0;display:flex}._5O8MPa_joinField ._5O8MPa_settingsLabel{white-space:nowrap;color:var(--dsw-alias-label-primary,#1a1a1a);flex:none;font-size:13px;font-weight:500;line-height:1.5}._5O8MPa_joinField ._5O8MPa_input{box-sizing:border-box;flex:auto;width:100%;min-width:0}@media (width<=480px){._5O8MPa_joinInline{flex-direction:column;gap:12px}._5O8MPa_joinFields ._5O8MPa_input{height:44px}}._5O8MPa_select,._5O8MPa_input{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);height:40px;color:var(--dsw-alias-label-primary,#1a1a1a);font:inherit;border-radius:10px;padding:0 14px;font-size:14px}._5O8MPa_select:focus-visible,._5O8MPa_input:focus-visible{border-color:var(--dsw-alias-brand-primary,#4d6bfe);outline:none}._5O8MPa_primary,._5O8MPa_ghost,._5O8MPa_danger{appearance:none;cursor:pointer;font:inherit;border-radius:10px;padding:7px 16px;font-size:14px;line-height:1.5}._5O8MPa_primary{background:var(--dsw-alias-label-primary,#1a1a1a);color:var(--dsw-alias-bg-layer-3,#fff);border:1px solid #0000}._5O8MPa_ghost{color:var(--dsw-alias-label-secondary,#5c5c5c);border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:0 0}._5O8MPa_ghost:hover:not(:disabled){color:var(--dsw-alias-label-primary,#1a1a1a);border-color:var(--dsw-alias-label-dimmed,#c8c8c8)}._5O8MPa_danger{color:var(--dsw-alias-label-error,#d64545);border:1px solid var(--dsw-alias-label-error,#d64545);background:0 0}._5O8MPa_primary:disabled,._5O8MPa_ghost:disabled{opacity:.4;cursor:default}._5O8MPa_hint{color:var(--dsw-alias-label-tertiary,#8a8a8a);margin:0 0 8px;font-size:13px;font-weight:400;line-height:1.5}._5O8MPa_warn{color:var(--dsw-alias-label-secondary,#8a6a1a)}._5O8MPa_error{color:var(--dsw-alias-label-error,#d64545);margin:8px 0 0;font-size:13px}._5O8MPa_table{flex-direction:column;flex:1;gap:8px;min-width:0;min-height:0;padding:12px 20px 8px;display:flex;overflow:hidden auto}._5O8MPa_opponents{flex:none;justify-content:space-between;align-items:flex-start;gap:12px;width:100%;min-width:0;display:flex}._5O8MPa_opponentsThree{justify-content:space-between}._5O8MPa_seat{flex-direction:column;flex:1 1 0;align-items:flex-start;gap:4px;min-width:0;display:flex}._5O8MPa_seatRight{align-items:flex-end}._5O8MPa_seatCenter{align-items:center}._5O8MPa_seatHead{align-items:center;gap:8px;min-width:0;max-width:100%;display:flex}._5O8MPa_seatRight ._5O8MPa_seatHead{flex-direction:row-reverse}._5O8MPa_seatMeta{min-width:0;overflow:hidden}._5O8MPa_seatName{flex-wrap:wrap;align-items:center;gap:4px;font-size:15px;font-weight:600;display:flex}._5O8MPa_selfRow{flex-wrap:nowrap;flex:none;justify-content:center;align-items:center;gap:16px;width:100%;min-width:0;display:flex}._5O8MPa_selfSeat{flex-direction:column;flex:none;justify-content:center;align-items:center;gap:4px;display:flex}._5O8MPa_selfRow ._5O8MPa_hand{flex:auto;justify-content:flex-start;width:auto;min-width:0;padding-left:0}._5O8MPa_roleHero{flex-wrap:wrap;justify-content:center;gap:12px 16px;margin:12px 0 4px;display:flex}._5O8MPa_roleHeroItem{color:var(--dsw-alias-label-secondary,#5c5c5c);flex-direction:column;align-items:center;gap:6px;margin:0;font-size:12px;font-weight:500;display:flex}._5O8MPa_roleHeroImg{object-fit:contain;background:0 0;width:128px;height:128px}._5O8MPa_badge{background:var(--dsw-alias-bg-module-platform,#f3f3f3);color:var(--dsw-alias-label-secondary,#5c5c5c);border-radius:999px;flex:none;padding:3px 10px;font-size:13px;font-weight:600;display:inline-block}._5O8MPa_ready{color:var(--dsw-alias-label-success,#2f8f62);font-size:12px}._5O8MPa_avatar{object-fit:contain;background:0 0;width:96px;height:96px}._5O8MPa_remain{align-items:flex-end;gap:8px;max-width:100%;display:flex}._5O8MPa_remainCompact{flex-direction:column;align-items:flex-start;gap:2px;min-height:0}._5O8MPa_seatRight ._5O8MPa_remainCompact{align-items:flex-end}._5O8MPa_backs{isolation:isolate;align-items:flex-end;min-width:0;display:flex}._5O8MPa_backs>*{margin-left:-40px}._5O8MPa_backs>:first-child{margin-left:0}._5O8MPa_backsCompact{width:108px;min-width:0;max-width:100%;padding:0}._5O8MPa_backsCompact>*{margin-left:-28px}._5O8MPa_backsCompact>:first-child{margin-left:0}._5O8MPa_backsCompact ._5O8MPa_cardBack{width:44px;min-width:44px;height:62px;box-shadow:1px 1px #00000014}._5O8MPa_cardsLeft{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#1a1a1a);min-width:1.25em;font-size:22px;font-weight:700;line-height:1}._5O8MPa_centerPlay{flex:auto;justify-content:center;align-items:center;gap:8px;min-width:0;min-height:0;display:flex;overflow:hidden}._5O8MPa_bottom{flex-wrap:wrap;flex:none;justify-content:center;align-items:flex-end;gap:6px;display:flex}._5O8MPa_bottom ._5O8MPa_cardFace,._5O8MPa_bottom ._5O8MPa_cardBack{width:52px;height:73px}._5O8MPa_bottom ._5O8MPa_remain{gap:4px}._5O8MPa_hand{flex:none;justify-content:center;width:100%;min-width:0;padding:22px 8px 6px;display:flex;overflow-x:auto}._5O8MPa_handInner{align-items:flex-end;width:max-content;margin:0 auto;display:flex}._5O8MPa_handInner button{cursor:pointer;z-index:0;background:0 0;border:0;flex:none;margin-left:-36px;padding:0;position:relative}._5O8MPa_handInner button:first-child{margin-left:0}._5O8MPa_handInner button:hover{z-index:2}._5O8MPa_handInner button:has(._5O8MPa_selected){z-index:3}._5O8MPa_selected{transform:translateY(-18px)}._5O8MPa_actions{flex-wrap:wrap;flex:none;justify-content:center;gap:8px;display:flex}._5O8MPa_cardFace,._5O8MPa_cardBack{border:1px solid var(--dsw-alias-border-l2,#e0ddd6);color:#1a1a1a;background:#fff;border-radius:10px;flex-direction:column;flex:none;width:88px;height:124px;padding:0;display:flex;position:relative;overflow:hidden;box-shadow:0 1px 2px #0000000f}._5O8MPa_cardBack{color:#1a1a1a;background:#fff;justify-content:center;align-items:center;padding:0}._5O8MPa_cardBackLandlord{box-shadow:0 0 0 2px #c62828}._5O8MPa_backsCompact ._5O8MPa_cardBackLandlord{box-shadow:0 0 0 2px #c62828,1px 1px #00000014}._5O8MPa_red{color:#c62828}._5O8MPa_wild{box-shadow:0 0 0 2px var(--dsw-alias-brand-primary,#4d6bfe)}._5O8MPa_rank{z-index:1;text-shadow:0 0 3px #fff,0 0 6px #fff;font-size:15px;font-weight:700;line-height:1;position:absolute;top:4px;left:6px}._5O8MPa_suit{z-index:1;text-shadow:0 0 3px #fff,0 0 6px #fff;font-size:16px;line-height:1;position:absolute;bottom:4px;right:6px}._5O8MPa_plainFace{justify-content:center;align-items:center}._5O8MPa_suitCenter{font-size:36px;line-height:1}._5O8MPa_jokerFace{justify-content:center;align-items:center;padding:0}._5O8MPa_jokerArt,._5O8MPa_faceArt,._5O8MPa_cardBackArt{pointer-events:none;object-fit:contain;background:0 0;width:100%;height:100%}@media (width<=720px){._5O8MPa_table{padding:8px 10px 6px}._5O8MPa_avatar{width:56px;height:56px}._5O8MPa_roleHeroImg{width:96px;height:96px}._5O8MPa_selfRow{gap:8px}._5O8MPa_selfSeat{flex-direction:column;gap:4px}._5O8MPa_seatHead,._5O8MPa_seatRight ._5O8MPa_seatHead{flex-direction:column;align-items:flex-start;gap:2px}._5O8MPa_seatRight ._5O8MPa_seatHead{align-items:flex-end}._5O8MPa_cardFace,._5O8MPa_cardBack{width:72px;height:102px}._5O8MPa_hand{padding-top:20px}._5O8MPa_handInner button,._5O8MPa_backs>*{margin-left:-32px}._5O8MPa_backsCompact{width:78px;padding:0}._5O8MPa_backsCompact ._5O8MPa_cardBack{width:30px;min-width:30px;height:42px}._5O8MPa_backsCompact>*{margin-left:-18px}._5O8MPa_cardsLeft{font-size:16px}._5O8MPa_bottom ._5O8MPa_cardFace,._5O8MPa_bottom ._5O8MPa_cardBack{width:44px;height:62px}}._5O8MPa_sidebarSlot{box-sizing:border-box;width:100%;margin:0 2px 8px}._5O8MPa_sidebarSlotRail{align-self:flex-start;width:36px;margin:0 0 12px}._5O8MPa_sidebarBtn{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-button-elevated-fill,#fff);width:100%;height:38px;color:inherit;cursor:pointer;box-sizing:border-box;font:inherit;border-radius:12px;justify-content:center;align-items:center;gap:6px;padding:8px 16px;font-size:14px;font-weight:500;display:flex}._5O8MPa_sidebarBtn:hover{background:var(--dsw-alias-button-floating-hover,#0000000a)}._5O8MPa_sidebarBtnRail{background:0 0;border-color:#0000;width:36px;height:36px;padding:0}._5O8MPa_inviteBox{flex-direction:column;gap:8px;margin-top:12px;display:flex}._5O8MPa_codeRow,._5O8MPa_codeBar,._5O8MPa_codeEdit{flex-wrap:nowrap;align-items:center;gap:8px;display:flex}._5O8MPa_codeBar{width:100%}._5O8MPa_codeValue{letter-spacing:.12em;background:var(--dsw-alias-bg-module-platform,#f3f3f3);user-select:all;cursor:pointer;border-radius:8px;padding:4px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:20px;font-weight:700}._5O8MPa_codeInput{letter-spacing:.12em;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:20px;font-weight:700}._5O8MPa_codeInput::placeholder{letter-spacing:0;font-size:13px;font-weight:500}._5O8MPa_titleBtn{appearance:none;font:inherit;color:inherit;cursor:pointer;background:0 0;border:0;padding:0;font-weight:600}._5O8MPa_titleBtn:disabled{cursor:default}._5O8MPa_miniHand{max-width:320px;display:flex;overflow:hidden}._5O8MPa_miniHand>*{flex:none;width:52px;height:73px;margin-left:-26px}._5O8MPa_miniHand>:first-child{margin-left:0}._5O8MPa_qr{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:#fff;border-radius:8px;width:132px;height:132px}._5O8MPa_timer{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);max-width:280px;color:var(--dsw-alias-label-primary,#1a1a1a);border-radius:999px;justify-content:center;align-items:baseline;gap:6px;margin:0 auto;padding:10px 16px;font-size:14px;display:flex}._5O8MPa_timer strong{font-variant-numeric:tabular-nums;font-size:28px;font-weight:650;line-height:1}._5O8MPa_timerUrgent{border-color:var(--dsw-alias-label-error,#d64545);color:var(--dsw-alias-label-error,#d64545)}._5O8MPa_banner{background:var(--dsw-alias-bg-module-platform,#f6f6f6);color:var(--dsw-alias-label-secondary,#5c5c5c);border-bottom:1px solid var(--dsw-alias-border-l2,#e8e8e8);padding:8px 16px;font-size:13px}._5O8MPa_chat{max-height:56px;color:var(--dsw-alias-label-tertiary,#8a8a8a);flex:none;font-size:12px;overflow:auto}._5O8MPa_settingsCard{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);border-radius:12px;list-style:none}._5O8MPa_settingsCard:hover{border-color:var(--dsw-alias-label-dimmed,#c8c8c8)}._5O8MPa_settingsOpen{background:var(--dsw-alias-bg-layer-2,#fafafa);border-color:var(--dsw-alias-label-dimmed,#c8c8c8)}._5O8MPa_settingsHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}._5O8MPa_settingsHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-2px}._5O8MPa_settingsHeadText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}._5O8MPa_settingsName{color:var(--dsw-alias-label-primary,#1a1a1a);font-size:15px;font-weight:600;line-height:1.4}._5O8MPa_settingsDesc{color:var(--dsw-alias-label-tertiary,#8a8a8a);font-size:13px;line-height:1.5}._5O8MPa_settingsChevron{color:var(--dsw-alias-label-tertiary,#8a8a8a);flex:none;transition:transform .16s}._5O8MPa_settingsChevronOpen{transform:rotate(180deg)}._5O8MPa_settingsBody{border-top:1px solid var(--dsw-alias-border-l2,#e8e8e8);margin:0 16px;padding-bottom:8px}._5O8MPa_settingsHint{color:var(--dsw-alias-label-tertiary,#8a8a8a);margin:12px 0 0;font-size:12px;line-height:1.5}._5O8MPa_settingsField{flex-direction:column;gap:6px;padding:12px 0;display:flex}._5O8MPa_settingsField+._5O8MPa_settingsField{border-top:1px solid var(--dsw-alias-border-l2,#e8e8e8)}._5O8MPa_settingsLabel{color:var(--dsw-alias-label-primary,#1a1a1a);font-size:13px;font-weight:500;line-height:1.5}._5O8MPa_settingsSwitchRow{justify-content:space-between;align-items:center;gap:12px;display:flex}._5O8MPa_settingsSwitch{appearance:none;cursor:pointer;background:0 0;border:0;flex:none;padding:0}._5O8MPa_settingsSwitch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:2px;border-radius:999px}._5O8MPa_settingsSwitchTrack{background:var(--dsw-alias-border-l2,#d9d9d9);border-radius:999px;width:36px;height:20px;transition:background .16s;display:block;position:relative}._5O8MPa_settingsSwitchTrack[data-on]{background:var(--dsw-alias-label-primary,#1a1a1a)}._5O8MPa_settingsSwitchThumb{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .16s;position:absolute;top:2px;left:2px;box-shadow:0 1px 2px #00000029}._5O8MPa_settingsSwitchTrack[data-on] ._5O8MPa_settingsSwitchThumb{transform:translate(16px)}._5O8MPa_settingsInput{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);height:34px;font:inherit;color:var(--dsw-alias-label-primary,#1a1a1a);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}._5O8MPa_settingsInput:focus-visible{border-color:var(--dsw-alias-brand-primary,#4d6bfe);outline:none}._5O8MPa_settingsFieldHint{color:var(--dsw-alias-label-tertiary,#8a8a8a);margin:0;font-size:12px;line-height:1.5}._5O8MPa_settingsActions{border-top:1px solid var(--dsw-alias-border-l2,#e8e8e8);flex-wrap:wrap;align-items:center;gap:8px 12px;padding:12px 0 16px;display:flex}._5O8MPa_settingsSaveOk{color:var(--dsw-alias-label-success,#2f8f62);margin:0;font-size:12px;line-height:1.5}._5O8MPa_settingsSaveError{color:var(--dsw-alias-label-error,#d64545);margin:0;font-size:12px;line-height:1.5}";
|
|
151
|
+
const tagId = "dsh-poker/styles.module.css";
|
|
152
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
153
|
+
const tag = document.createElement("style");
|
|
154
|
+
tag.dataset.plugin = "dsh-poker";
|
|
155
|
+
tag.dataset.pluginCss = tagId;
|
|
156
|
+
tag.textContent = css;
|
|
157
|
+
document.head.appendChild(tag);
|
|
158
|
+
}
|
|
159
|
+
var styles_module_css_default = {
|
|
160
|
+
"settlement": "_5O8MPa_settlement",
|
|
161
|
+
"field": "_5O8MPa_field",
|
|
162
|
+
"ghost": "_5O8MPa_ghost",
|
|
163
|
+
"warn": "_5O8MPa_warn",
|
|
164
|
+
"tabActive": "_5O8MPa_tabActive",
|
|
165
|
+
"badge": "_5O8MPa_badge",
|
|
166
|
+
"remainCompact": "_5O8MPa_remainCompact",
|
|
167
|
+
"bottom": "_5O8MPa_bottom",
|
|
168
|
+
"handInner": "_5O8MPa_handInner",
|
|
169
|
+
"seatRight": "_5O8MPa_seatRight",
|
|
170
|
+
"cardBackArt": "_5O8MPa_cardBackArt",
|
|
171
|
+
"timerUrgent": "_5O8MPa_timerUrgent",
|
|
172
|
+
"settingsField": "_5O8MPa_settingsField",
|
|
173
|
+
"error": "_5O8MPa_error",
|
|
174
|
+
"seatMeta": "_5O8MPa_seatMeta",
|
|
175
|
+
"sidebarSlot": "_5O8MPa_sidebarSlot",
|
|
176
|
+
"codeValue": "_5O8MPa_codeValue",
|
|
177
|
+
"cardsLeft": "_5O8MPa_cardsLeft",
|
|
178
|
+
"joinInline": "_5O8MPa_joinInline",
|
|
179
|
+
"selfRow": "_5O8MPa_selfRow",
|
|
180
|
+
"rank": "_5O8MPa_rank",
|
|
181
|
+
"select": "_5O8MPa_select",
|
|
182
|
+
"settingsChevronOpen": "_5O8MPa_settingsChevronOpen",
|
|
183
|
+
"suitCenter": "_5O8MPa_suitCenter",
|
|
184
|
+
"joinField": "_5O8MPa_joinField",
|
|
185
|
+
"settingsSwitchThumb": "_5O8MPa_settingsSwitchThumb",
|
|
186
|
+
"titleBtn": "_5O8MPa_titleBtn",
|
|
187
|
+
"settingsFieldHint": "_5O8MPa_settingsFieldHint",
|
|
188
|
+
"timer": "_5O8MPa_timer",
|
|
189
|
+
"joinForm": "_5O8MPa_joinForm",
|
|
190
|
+
"settingsSwitchRow": "_5O8MPa_settingsSwitchRow",
|
|
191
|
+
"table": "_5O8MPa_table",
|
|
192
|
+
"joinFields": "_5O8MPa_joinFields",
|
|
193
|
+
"cardFace": "_5O8MPa_cardFace",
|
|
194
|
+
"codeInput": "_5O8MPa_codeInput",
|
|
195
|
+
"settingsInput": "_5O8MPa_settingsInput",
|
|
196
|
+
"seatName": "_5O8MPa_seatName",
|
|
197
|
+
"settingsHint": "_5O8MPa_settingsHint",
|
|
198
|
+
"cardBack": "_5O8MPa_cardBack",
|
|
199
|
+
"qr": "_5O8MPa_qr",
|
|
200
|
+
"lobby": "_5O8MPa_lobby",
|
|
201
|
+
"avatar": "_5O8MPa_avatar",
|
|
202
|
+
"topbar": "_5O8MPa_topbar",
|
|
203
|
+
"roleHeroItem": "_5O8MPa_roleHeroItem",
|
|
204
|
+
"root": "_5O8MPa_root",
|
|
205
|
+
"tabs": "_5O8MPa_tabs",
|
|
206
|
+
"tab": "_5O8MPa_tab",
|
|
207
|
+
"suit": "_5O8MPa_suit",
|
|
208
|
+
"plainFace": "_5O8MPa_plainFace",
|
|
209
|
+
"backs": "_5O8MPa_backs",
|
|
210
|
+
"row": "_5O8MPa_row",
|
|
211
|
+
"sidebarSlotRail": "_5O8MPa_sidebarSlotRail",
|
|
212
|
+
"hint": "_5O8MPa_hint",
|
|
213
|
+
"createFields": "_5O8MPa_createFields",
|
|
214
|
+
"settingsLabel": "_5O8MPa_settingsLabel",
|
|
215
|
+
"opponentsThree": "_5O8MPa_opponentsThree",
|
|
216
|
+
"jokerArt": "_5O8MPa_jokerArt",
|
|
217
|
+
"faceArt": "_5O8MPa_faceArt",
|
|
218
|
+
"fieldWide": "_5O8MPa_fieldWide",
|
|
219
|
+
"miniHand": "_5O8MPa_miniHand",
|
|
220
|
+
"opponents": "_5O8MPa_opponents",
|
|
221
|
+
"settingsCard": "_5O8MPa_settingsCard",
|
|
222
|
+
"seatCenter": "_5O8MPa_seatCenter",
|
|
223
|
+
"codeRow": "_5O8MPa_codeRow",
|
|
224
|
+
"settingsHeader": "_5O8MPa_settingsHeader",
|
|
225
|
+
"settingsDesc": "_5O8MPa_settingsDesc",
|
|
226
|
+
"settingsChevron": "_5O8MPa_settingsChevron",
|
|
227
|
+
"card": "_5O8MPa_card",
|
|
228
|
+
"selected": "_5O8MPa_selected",
|
|
229
|
+
"cardBackLandlord": "_5O8MPa_cardBackLandlord",
|
|
230
|
+
"inviteBox": "_5O8MPa_inviteBox",
|
|
231
|
+
"centerPlay": "_5O8MPa_centerPlay",
|
|
232
|
+
"settingsSwitchTrack": "_5O8MPa_settingsSwitchTrack",
|
|
233
|
+
"red": "_5O8MPa_red",
|
|
234
|
+
"wild": "_5O8MPa_wild",
|
|
235
|
+
"settingsSaveOk": "_5O8MPa_settingsSaveOk",
|
|
236
|
+
"settingsBody": "_5O8MPa_settingsBody",
|
|
237
|
+
"sidebarBtnRail": "_5O8MPa_sidebarBtnRail",
|
|
238
|
+
"settingsOpen": "_5O8MPa_settingsOpen",
|
|
239
|
+
"seat": "_5O8MPa_seat",
|
|
240
|
+
"ready": "_5O8MPa_ready",
|
|
241
|
+
"input": "_5O8MPa_input",
|
|
242
|
+
"roleHeroImg": "_5O8MPa_roleHeroImg",
|
|
243
|
+
"remain": "_5O8MPa_remain",
|
|
244
|
+
"sidebarBtn": "_5O8MPa_sidebarBtn",
|
|
245
|
+
"danger": "_5O8MPa_danger",
|
|
246
|
+
"codeEdit": "_5O8MPa_codeEdit",
|
|
247
|
+
"settingsSaveError": "_5O8MPa_settingsSaveError",
|
|
248
|
+
"chat": "_5O8MPa_chat",
|
|
249
|
+
"settingsActions": "_5O8MPa_settingsActions",
|
|
250
|
+
"backsCompact": "_5O8MPa_backsCompact",
|
|
251
|
+
"fieldRow": "_5O8MPa_fieldRow",
|
|
252
|
+
"banner": "_5O8MPa_banner",
|
|
253
|
+
"seatHead": "_5O8MPa_seatHead",
|
|
254
|
+
"selfSeat": "_5O8MPa_selfSeat",
|
|
255
|
+
"muted": "_5O8MPa_muted",
|
|
256
|
+
"roleHero": "_5O8MPa_roleHero",
|
|
257
|
+
"close": "_5O8MPa_close",
|
|
258
|
+
"jokerFace": "_5O8MPa_jokerFace",
|
|
259
|
+
"codeBar": "_5O8MPa_codeBar",
|
|
260
|
+
"settingsHeadText": "_5O8MPa_settingsHeadText",
|
|
261
|
+
"settingsName": "_5O8MPa_settingsName",
|
|
262
|
+
"brand": "_5O8MPa_brand",
|
|
263
|
+
"settingsSwitch": "_5O8MPa_settingsSwitch",
|
|
264
|
+
"hand": "_5O8MPa_hand",
|
|
265
|
+
"primary": "_5O8MPa_primary",
|
|
266
|
+
"actions": "_5O8MPa_actions"
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/client/InviteDialog.tsx
|
|
271
|
+
function InviteDialog({ roomCode, sitUrl, shareable }) {
|
|
272
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
273
|
+
className: styles_module_css_default.inviteBox,
|
|
274
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
275
|
+
className: styles_module_css_default.codeRow,
|
|
276
|
+
children: [
|
|
277
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "房间号" }),
|
|
278
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
279
|
+
className: styles_module_css_default.codeValue,
|
|
280
|
+
onClick: () => {
|
|
281
|
+
navigator.clipboard.writeText(roomCode);
|
|
282
|
+
},
|
|
283
|
+
children: roomCode
|
|
284
|
+
}),
|
|
285
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
286
|
+
type: "button",
|
|
287
|
+
className: styles_module_css_default.ghost,
|
|
288
|
+
onClick: () => {
|
|
289
|
+
navigator.clipboard.writeText(roomCode);
|
|
290
|
+
},
|
|
291
|
+
children: "复制房号"
|
|
292
|
+
})
|
|
293
|
+
]
|
|
294
|
+
}), shareable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
295
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
296
|
+
className: styles_module_css_default.hint,
|
|
297
|
+
children: "同一条链接。先到的人入座,坐满或开打后来的人观战。"
|
|
298
|
+
}),
|
|
299
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
300
|
+
type: "button",
|
|
301
|
+
className: styles_module_css_default.ghost,
|
|
302
|
+
onClick: () => {
|
|
303
|
+
navigator.clipboard.writeText(sitUrl);
|
|
304
|
+
},
|
|
305
|
+
children: "复制房间链接"
|
|
306
|
+
}),
|
|
307
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
308
|
+
className: styles_module_css_default.qr,
|
|
309
|
+
alt: "room qr",
|
|
310
|
+
src: `https://api.qrserver.com/v1/create-qr-code/?size=132x132&data=${encodeURIComponent(sitUrl)}`
|
|
311
|
+
})
|
|
312
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
313
|
+
className: `${styles_module_css_default.hint} ${styles_module_css_default.warn}`,
|
|
314
|
+
children: [
|
|
315
|
+
"要给别的机器用,先在插件设置里填 publicBaseUrl(named tunnel 或反代到本机端口)。 生产环境的 dsh web 不能绑 0.0.0.0,所以局域网 IP 用不了。本机可以打开 ",
|
|
316
|
+
sitUrl || "loopback 加入页",
|
|
317
|
+
" 自己测。"
|
|
318
|
+
]
|
|
319
|
+
})]
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function RoomCodeBar({ title, roomCode, sitUrl, shareable, canRename, onRename }) {
|
|
323
|
+
const [draft, setDraft] = (0, react.useState)(title);
|
|
324
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
325
|
+
(0, react.useEffect)(() => {
|
|
326
|
+
setDraft(title);
|
|
327
|
+
}, [title]);
|
|
328
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
329
|
+
className: styles_module_css_default.codeBar,
|
|
330
|
+
children: [
|
|
331
|
+
canRename && editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
332
|
+
className: styles_module_css_default.codeEdit,
|
|
333
|
+
onSubmit: (event) => {
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
onRename?.(draft);
|
|
336
|
+
setEditing(false);
|
|
337
|
+
},
|
|
338
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
339
|
+
className: styles_module_css_default.input,
|
|
340
|
+
value: draft,
|
|
341
|
+
maxLength: 24,
|
|
342
|
+
onChange: (event) => {
|
|
343
|
+
setDraft(event.target.value);
|
|
344
|
+
}
|
|
345
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
346
|
+
type: "submit",
|
|
347
|
+
className: styles_module_css_default.ghost,
|
|
348
|
+
children: "保存"
|
|
349
|
+
})]
|
|
350
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
351
|
+
type: "button",
|
|
352
|
+
className: styles_module_css_default.titleBtn,
|
|
353
|
+
disabled: !canRename,
|
|
354
|
+
onClick: () => {
|
|
355
|
+
if (canRename) setEditing(true);
|
|
356
|
+
},
|
|
357
|
+
children: title || "好友局"
|
|
358
|
+
}),
|
|
359
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
360
|
+
className: styles_module_css_default.muted,
|
|
361
|
+
children: "房号"
|
|
362
|
+
}),
|
|
363
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
364
|
+
className: styles_module_css_default.codeValue,
|
|
365
|
+
onClick: () => {
|
|
366
|
+
navigator.clipboard.writeText(roomCode);
|
|
367
|
+
},
|
|
368
|
+
children: roomCode
|
|
369
|
+
}),
|
|
370
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
371
|
+
type: "button",
|
|
372
|
+
className: styles_module_css_default.ghost,
|
|
373
|
+
onClick: () => {
|
|
374
|
+
navigator.clipboard.writeText(roomCode);
|
|
375
|
+
},
|
|
376
|
+
children: "复制房号"
|
|
377
|
+
}),
|
|
378
|
+
shareable && sitUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
379
|
+
type: "button",
|
|
380
|
+
className: styles_module_css_default.ghost,
|
|
381
|
+
onClick: () => {
|
|
382
|
+
navigator.clipboard.writeText(sitUrl);
|
|
383
|
+
},
|
|
384
|
+
children: "复制链接"
|
|
385
|
+
}) : sitUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
386
|
+
className: styles_module_css_default.muted,
|
|
387
|
+
children: "未配置 publicBaseUrl,仅本机可加入"
|
|
388
|
+
}) : null
|
|
389
|
+
]
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/client/SeatAvatar.tsx
|
|
395
|
+
const ASSET$2 = "/doudizhu/assets";
|
|
396
|
+
const ROLE_ICONS = {
|
|
397
|
+
landlord: `${ASSET$2}/role-landlord.png`,
|
|
398
|
+
landlordB: `${ASSET$2}/role-landlord-b.png`,
|
|
399
|
+
farmer: `${ASSET$2}/role-farmer.png`,
|
|
400
|
+
farmerB: `${ASSET$2}/role-farmer-b.png`,
|
|
401
|
+
farmerC: `${ASSET$2}/role-farmer-c.png`,
|
|
402
|
+
spectator: `${ASSET$2}/role-spectator.png`
|
|
403
|
+
};
|
|
404
|
+
const FARMER_ICONS = [
|
|
405
|
+
ROLE_ICONS.farmer,
|
|
406
|
+
ROLE_ICONS.farmerB,
|
|
407
|
+
ROLE_ICONS.farmerC
|
|
408
|
+
];
|
|
409
|
+
function roleIconSrc(role, seat = 0) {
|
|
410
|
+
if (role === "landlord") return ROLE_ICONS.landlord;
|
|
411
|
+
return FARMER_ICONS[Math.abs(seat) % FARMER_ICONS.length] ?? ROLE_ICONS.farmer;
|
|
412
|
+
}
|
|
413
|
+
function SeatAvatar({ avatarUrl, role = "empty", seat = 0, occupied = false, spectator = false }) {
|
|
414
|
+
if (spectator) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
415
|
+
className: styles_module_css_default.avatar,
|
|
416
|
+
src: ROLE_ICONS.spectator,
|
|
417
|
+
alt: "观战",
|
|
418
|
+
onError: (event) => {
|
|
419
|
+
event.currentTarget.remove();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
const sitting = occupied || role === "landlord" || role === "farmer";
|
|
423
|
+
const src = sitting ? roleIconSrc(role === "empty" ? "farmer" : role, seat) : avatarUrl;
|
|
424
|
+
if (!src) return null;
|
|
425
|
+
const landlord = role === "landlord";
|
|
426
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
427
|
+
className: styles_module_css_default.avatar,
|
|
428
|
+
src,
|
|
429
|
+
alt: landlord ? "地主" : sitting ? "农民" : "",
|
|
430
|
+
onError: (event) => {
|
|
431
|
+
event.currentTarget.remove();
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/client/JoinForm.tsx
|
|
438
|
+
function JoinForm({ code, invite, name: name$1, onCode, onInvite, onName, onJoin, joining = false, showInvite = true, error }) {
|
|
439
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
440
|
+
className: styles_module_css_default.joinForm,
|
|
441
|
+
children: [
|
|
442
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
443
|
+
className: styles_module_css_default.roleHero,
|
|
444
|
+
children: [
|
|
445
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
446
|
+
className: styles_module_css_default.roleHeroItem,
|
|
447
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
448
|
+
className: styles_module_css_default.roleHeroImg,
|
|
449
|
+
src: ROLE_ICONS.landlord,
|
|
450
|
+
alt: "地主"
|
|
451
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", { children: "地主" })]
|
|
452
|
+
}),
|
|
453
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
454
|
+
className: styles_module_css_default.roleHeroItem,
|
|
455
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
456
|
+
className: styles_module_css_default.roleHeroImg,
|
|
457
|
+
src: ROLE_ICONS.farmer,
|
|
458
|
+
alt: "农民"
|
|
459
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", { children: "农民" })]
|
|
460
|
+
}),
|
|
461
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
462
|
+
className: styles_module_css_default.roleHeroItem,
|
|
463
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
464
|
+
className: styles_module_css_default.roleHeroImg,
|
|
465
|
+
src: ROLE_ICONS.farmerB,
|
|
466
|
+
alt: "农民"
|
|
467
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", { children: "农民" })]
|
|
468
|
+
}),
|
|
469
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
470
|
+
className: styles_module_css_default.roleHeroItem,
|
|
471
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
472
|
+
className: styles_module_css_default.roleHeroImg,
|
|
473
|
+
src: ROLE_ICONS.farmerC,
|
|
474
|
+
alt: "农民"
|
|
475
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", { children: "农民" })]
|
|
476
|
+
})
|
|
477
|
+
]
|
|
478
|
+
}),
|
|
479
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
480
|
+
className: styles_module_css_default.hint,
|
|
481
|
+
children: "填房号就能进。先到的人入座,坐满或已经开打后来的人观战。出牌每手 120 秒。"
|
|
482
|
+
}),
|
|
483
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
484
|
+
className: styles_module_css_default.joinFields,
|
|
485
|
+
children: [
|
|
486
|
+
showInvite ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
487
|
+
className: styles_module_css_default.field,
|
|
488
|
+
children: ["邀请", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
489
|
+
className: styles_module_css_default.input,
|
|
490
|
+
value: invite,
|
|
491
|
+
autoComplete: "off",
|
|
492
|
+
spellCheck: false,
|
|
493
|
+
onChange: (event) => {
|
|
494
|
+
onInvite(event.target.value);
|
|
495
|
+
}
|
|
496
|
+
})]
|
|
497
|
+
}) : null,
|
|
498
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
499
|
+
className: styles_module_css_default.joinInline,
|
|
500
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
501
|
+
className: styles_module_css_default.joinField,
|
|
502
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
503
|
+
className: styles_module_css_default.settingsLabel,
|
|
504
|
+
children: "房号"
|
|
505
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
506
|
+
className: `${styles_module_css_default.input} ${styles_module_css_default.codeInput}`,
|
|
507
|
+
value: code,
|
|
508
|
+
inputMode: "numeric",
|
|
509
|
+
autoComplete: "off",
|
|
510
|
+
spellCheck: false,
|
|
511
|
+
placeholder: "6 位房号",
|
|
512
|
+
onChange: (event) => {
|
|
513
|
+
onCode(event.target.value);
|
|
514
|
+
}
|
|
515
|
+
})]
|
|
516
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
517
|
+
className: styles_module_css_default.joinField,
|
|
518
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
519
|
+
className: styles_module_css_default.settingsLabel,
|
|
520
|
+
children: "昵称"
|
|
521
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
522
|
+
className: styles_module_css_default.input,
|
|
523
|
+
value: name$1,
|
|
524
|
+
maxLength: 16,
|
|
525
|
+
onChange: (event) => {
|
|
526
|
+
onName(event.target.value);
|
|
527
|
+
}
|
|
528
|
+
})]
|
|
529
|
+
})]
|
|
530
|
+
}),
|
|
531
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
532
|
+
type: "button",
|
|
533
|
+
className: styles_module_css_default.primary,
|
|
534
|
+
disabled: joining || !code.trim(),
|
|
535
|
+
onClick: onJoin,
|
|
536
|
+
children: joining ? "加入中…" : "加入"
|
|
537
|
+
}),
|
|
538
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
539
|
+
className: styles_module_css_default.error,
|
|
540
|
+
children: error
|
|
541
|
+
}) : null
|
|
542
|
+
]
|
|
543
|
+
})
|
|
544
|
+
]
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/client/LobbyView.tsx
|
|
550
|
+
const MAX_MULTIPLIER_OPTIONS = [
|
|
551
|
+
8,
|
|
552
|
+
16,
|
|
553
|
+
32,
|
|
554
|
+
64
|
|
555
|
+
];
|
|
556
|
+
function LobbyView({ tab, onTab, title, stakeM, maxMultiplier, seatCount, laiZi, onTitle, onStake, onCap, onSeats, onLaiZi, onCreate, creating, error, roomCode, sitUrl, watchUrl, shareable, welcomeAtoms, join }) {
|
|
557
|
+
const cap = seatCapAtoms(stakeAtomsFromM(stakeM), maxMultiplier, seatCount);
|
|
558
|
+
const blocked = welcomeAtoms < cap;
|
|
559
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
560
|
+
className: styles_module_css_default.lobby,
|
|
561
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
562
|
+
className: styles_module_css_default.card,
|
|
563
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
564
|
+
className: styles_module_css_default.tabs,
|
|
565
|
+
role: "tablist",
|
|
566
|
+
"aria-label": "开房或加入",
|
|
567
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
568
|
+
type: "button",
|
|
569
|
+
role: "tab",
|
|
570
|
+
"aria-selected": tab === "create",
|
|
571
|
+
className: `${styles_module_css_default.tab} ${tab === "create" ? styles_module_css_default.tabActive : ""}`,
|
|
572
|
+
onClick: () => {
|
|
573
|
+
onTab("create");
|
|
574
|
+
},
|
|
575
|
+
children: "创建"
|
|
576
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
577
|
+
type: "button",
|
|
578
|
+
role: "tab",
|
|
579
|
+
"aria-selected": tab === "join",
|
|
580
|
+
className: `${styles_module_css_default.tab} ${tab === "join" ? styles_module_css_default.tabActive : ""}`,
|
|
581
|
+
onClick: () => {
|
|
582
|
+
onTab("join");
|
|
583
|
+
},
|
|
584
|
+
children: "加入"
|
|
585
|
+
})]
|
|
586
|
+
}), tab === "join" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
587
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "加入房间" }),
|
|
588
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
589
|
+
className: styles_module_css_default.hint,
|
|
590
|
+
children: "别人开的房填房号就能进。积分记在开房那台机器上,不是 DeepSeek 平台余额。"
|
|
591
|
+
}),
|
|
592
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(JoinForm, {
|
|
593
|
+
code: join.code,
|
|
594
|
+
invite: join.invite,
|
|
595
|
+
name: join.name,
|
|
596
|
+
onCode: join.onCode,
|
|
597
|
+
onInvite: join.onInvite,
|
|
598
|
+
onName: join.onName,
|
|
599
|
+
onJoin: join.onJoin,
|
|
600
|
+
joining: join.joining,
|
|
601
|
+
showInvite: false,
|
|
602
|
+
error
|
|
603
|
+
})
|
|
604
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
605
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "创建房间" }),
|
|
606
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
607
|
+
className: styles_module_css_default.hint,
|
|
608
|
+
children: "好友局信房主这台机器上的账本。积分不是 DeepSeek 平台余额。"
|
|
609
|
+
}),
|
|
610
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
611
|
+
className: styles_module_css_default.createFields,
|
|
612
|
+
children: [
|
|
613
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
614
|
+
className: `${styles_module_css_default.field} ${styles_module_css_default.fieldWide}`,
|
|
615
|
+
children: ["房间名", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
616
|
+
className: styles_module_css_default.input,
|
|
617
|
+
value: title,
|
|
618
|
+
maxLength: 24,
|
|
619
|
+
placeholder: "好友局",
|
|
620
|
+
onChange: (event) => {
|
|
621
|
+
onTitle(event.target.value);
|
|
622
|
+
}
|
|
623
|
+
})]
|
|
624
|
+
}),
|
|
625
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
626
|
+
className: styles_module_css_default.field,
|
|
627
|
+
children: ["人数", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
628
|
+
className: styles_module_css_default.select,
|
|
629
|
+
value: seatCount,
|
|
630
|
+
onChange: (event) => {
|
|
631
|
+
onSeats(Number(event.target.value) === 4 ? 4 : 3);
|
|
632
|
+
},
|
|
633
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
634
|
+
value: 3,
|
|
635
|
+
children: "3 人 · 一副牌"
|
|
636
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
637
|
+
value: 4,
|
|
638
|
+
children: "4 人 · 两副牌"
|
|
639
|
+
})]
|
|
640
|
+
})]
|
|
641
|
+
}),
|
|
642
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
643
|
+
className: styles_module_css_default.field,
|
|
644
|
+
children: ["玩法", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
645
|
+
className: styles_module_css_default.select,
|
|
646
|
+
value: laiZi ? "laizi" : "classic",
|
|
647
|
+
onChange: (event) => {
|
|
648
|
+
onLaiZi(event.target.value === "laizi");
|
|
649
|
+
},
|
|
650
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
651
|
+
value: "classic",
|
|
652
|
+
children: "经典"
|
|
653
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
654
|
+
value: "laizi",
|
|
655
|
+
children: "癞子"
|
|
656
|
+
})]
|
|
657
|
+
})]
|
|
658
|
+
}),
|
|
659
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
660
|
+
className: styles_module_css_default.field,
|
|
661
|
+
children: ["底注", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
662
|
+
className: styles_module_css_default.select,
|
|
663
|
+
value: stakeM,
|
|
664
|
+
onChange: (event) => {
|
|
665
|
+
onStake(Number(event.target.value));
|
|
666
|
+
},
|
|
667
|
+
children: STAKE_LADDER_M.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
668
|
+
value,
|
|
669
|
+
children: [value, "M"]
|
|
670
|
+
}, value))
|
|
671
|
+
})]
|
|
672
|
+
}),
|
|
673
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
674
|
+
className: styles_module_css_default.field,
|
|
675
|
+
children: ["倍数封顶", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
676
|
+
className: styles_module_css_default.select,
|
|
677
|
+
value: maxMultiplier,
|
|
678
|
+
onChange: (event) => {
|
|
679
|
+
onCap(Number(event.target.value));
|
|
680
|
+
},
|
|
681
|
+
children: MAX_MULTIPLIER_OPTIONS.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
682
|
+
value,
|
|
683
|
+
children: value
|
|
684
|
+
}, value))
|
|
685
|
+
})]
|
|
686
|
+
})
|
|
687
|
+
]
|
|
688
|
+
}),
|
|
689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
690
|
+
className: styles_module_css_default.hint,
|
|
691
|
+
children: [
|
|
692
|
+
seatCount,
|
|
693
|
+
" 人",
|
|
694
|
+
laiZi ? "癞子" : "经典",
|
|
695
|
+
"。人齐自动开打,后来的人观战。出牌 120 秒。入座冻结 ",
|
|
696
|
+
formatM(cap),
|
|
697
|
+
",欢迎 ",
|
|
698
|
+
formatM(welcomeAtoms),
|
|
699
|
+
"。"
|
|
700
|
+
]
|
|
701
|
+
}),
|
|
702
|
+
blocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
703
|
+
className: styles_module_css_default.error,
|
|
704
|
+
children: "欢迎积分不够入座冻结,把底注或封顶调低,或者去设置里加欢迎积分。"
|
|
705
|
+
}) : null,
|
|
706
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
707
|
+
type: "button",
|
|
708
|
+
className: styles_module_css_default.primary,
|
|
709
|
+
disabled: blocked || creating,
|
|
710
|
+
onClick: onCreate,
|
|
711
|
+
children: creating ? "创建中…" : "创建房间"
|
|
712
|
+
}),
|
|
713
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
714
|
+
className: styles_module_css_default.error,
|
|
715
|
+
children: error
|
|
716
|
+
}) : null,
|
|
717
|
+
roomCode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InviteDialog, {
|
|
718
|
+
roomCode,
|
|
719
|
+
sitUrl,
|
|
720
|
+
watchUrl,
|
|
721
|
+
shareable
|
|
722
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
723
|
+
className: styles_module_css_default.hint,
|
|
724
|
+
children: [
|
|
725
|
+
"默认底注 ",
|
|
726
|
+
DEFAULT_STAKE_M,
|
|
727
|
+
"M / 封顶 ",
|
|
728
|
+
DEFAULT_MAX_MULTIPLIER,
|
|
729
|
+
"。"
|
|
730
|
+
]
|
|
731
|
+
})
|
|
732
|
+
] })]
|
|
733
|
+
})
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
//#endregion
|
|
738
|
+
//#region src/types.ts
|
|
739
|
+
function parseAtoms(value) {
|
|
740
|
+
if (!/^-?\d+$/.test(value)) throw new Error(`invalid atom string: ${value}`);
|
|
741
|
+
return BigInt(value);
|
|
742
|
+
}
|
|
743
|
+
function asCardId(value) {
|
|
744
|
+
return value;
|
|
745
|
+
}
|
|
746
|
+
function decksFor(count) {
|
|
747
|
+
return count === 3 ? 1 : 2;
|
|
748
|
+
}
|
|
749
|
+
function holeCount(count) {
|
|
750
|
+
return count === 3 ? 3 : 8;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/client/SettlementView.tsx
|
|
755
|
+
function SettlementView({ settlement, onRematch }) {
|
|
756
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
757
|
+
className: styles_module_css_default.settlement,
|
|
758
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
759
|
+
className: styles_module_css_default.card,
|
|
760
|
+
children: [
|
|
761
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
762
|
+
className: styles_module_css_default.roleHero,
|
|
763
|
+
children: settlement.winner === "landlord" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
764
|
+
className: styles_module_css_default.roleHeroImg,
|
|
765
|
+
src: ROLE_ICONS.landlord,
|
|
766
|
+
alt: "地主"
|
|
767
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
768
|
+
className: styles_module_css_default.roleHeroImg,
|
|
769
|
+
src: ROLE_ICONS.farmer,
|
|
770
|
+
alt: "农民"
|
|
771
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
772
|
+
className: styles_module_css_default.roleHeroImg,
|
|
773
|
+
src: ROLE_ICONS.farmerB,
|
|
774
|
+
alt: "农民"
|
|
775
|
+
})] })
|
|
776
|
+
}),
|
|
777
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: settlement.winner === "landlord" ? "地主胜" : "农民胜" }),
|
|
778
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
779
|
+
className: styles_module_css_default.hint,
|
|
780
|
+
children: settlement.formula
|
|
781
|
+
}),
|
|
782
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: settlement.deltas.map((delta) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
783
|
+
"座 ",
|
|
784
|
+
delta.seat,
|
|
785
|
+
": ",
|
|
786
|
+
formatM(parseAtoms(delta.atoms))
|
|
787
|
+
] }, delta.seat)) }),
|
|
788
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
789
|
+
className: `${styles_module_css_default.hint} ${styles_module_css_default.warn}`,
|
|
790
|
+
children: settlement.trustNote
|
|
791
|
+
}),
|
|
792
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
793
|
+
className: styles_module_css_default.hint,
|
|
794
|
+
children: "积分 ≠ 平台余额,也不是 API key。"
|
|
795
|
+
}),
|
|
796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
797
|
+
type: "button",
|
|
798
|
+
className: styles_module_css_default.primary,
|
|
799
|
+
onClick: onRematch,
|
|
800
|
+
children: "再来一局"
|
|
801
|
+
})
|
|
802
|
+
]
|
|
803
|
+
})
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
//#endregion
|
|
808
|
+
//#region src/engine/cards.ts
|
|
809
|
+
const RANK_ORDER = [
|
|
810
|
+
"3",
|
|
811
|
+
"4",
|
|
812
|
+
"5",
|
|
813
|
+
"6",
|
|
814
|
+
"7",
|
|
815
|
+
"8",
|
|
816
|
+
"9",
|
|
817
|
+
"10",
|
|
818
|
+
"J",
|
|
819
|
+
"Q",
|
|
820
|
+
"K",
|
|
821
|
+
"A",
|
|
822
|
+
"2",
|
|
823
|
+
"BJ",
|
|
824
|
+
"RJ"
|
|
825
|
+
];
|
|
826
|
+
const RANK_VALUE = Object.fromEntries(RANK_ORDER.map((rank, index) => [rank, index]));
|
|
827
|
+
function cardBase$1(card) {
|
|
828
|
+
return String(card).split("~")[0] ?? String(card);
|
|
829
|
+
}
|
|
830
|
+
function rankOf$1(card) {
|
|
831
|
+
const raw = cardBase$1(card);
|
|
832
|
+
if (raw === "BJ" || raw === "RJ") return raw;
|
|
833
|
+
if (raw.startsWith("S") || raw.startsWith("H") || raw.startsWith("C") || raw.startsWith("D")) return raw.slice(1);
|
|
834
|
+
throw new Error(`invalid card: ${raw}`);
|
|
835
|
+
}
|
|
836
|
+
function rankValue(rank) {
|
|
837
|
+
const value = RANK_VALUE[rank];
|
|
838
|
+
if (value === void 0) throw new Error(`invalid rank: ${rank}`);
|
|
839
|
+
return value;
|
|
840
|
+
}
|
|
841
|
+
function cardValue(card) {
|
|
842
|
+
return rankValue(rankOf$1(card));
|
|
843
|
+
}
|
|
844
|
+
function compareCards(a, b) {
|
|
845
|
+
const byRank = cardValue(a) - cardValue(b);
|
|
846
|
+
if (byRank !== 0) return byRank;
|
|
847
|
+
return a.localeCompare(b);
|
|
848
|
+
}
|
|
849
|
+
function sortCards(cards) {
|
|
850
|
+
return [...cards].sort(compareCards);
|
|
851
|
+
}
|
|
852
|
+
const WILDABLE_RANKS = RANK_ORDER.filter((rank) => rank !== "BJ" && rank !== "RJ");
|
|
853
|
+
function isJokerRank(rank) {
|
|
854
|
+
return rank === "BJ" || rank === "RJ";
|
|
855
|
+
}
|
|
856
|
+
function isWildCard$1(card, wildRanks) {
|
|
857
|
+
if (wildRanks.length === 0) return false;
|
|
858
|
+
const rank = rankOf$1(card);
|
|
859
|
+
return !isJokerRank(rank) && wildRanks.includes(rank);
|
|
860
|
+
}
|
|
861
|
+
function countRanks(cards) {
|
|
862
|
+
const groups = /* @__PURE__ */ new Map();
|
|
863
|
+
for (const card of cards) {
|
|
864
|
+
const rank = rankOf$1(card);
|
|
865
|
+
const bucket = groups.get(rank);
|
|
866
|
+
if (bucket) bucket.push(card);
|
|
867
|
+
else groups.set(rank, [card]);
|
|
868
|
+
}
|
|
869
|
+
return groups;
|
|
870
|
+
}
|
|
871
|
+
function isNaturalSeqRank(rank) {
|
|
872
|
+
return rank !== "2" && rank !== "BJ" && rank !== "RJ";
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/engine/hands.ts
|
|
877
|
+
function classify(cards, laiZiRanks = []) {
|
|
878
|
+
if (laiZiRanks.length === 0) return classifyNatural(cards);
|
|
879
|
+
const wilds = [];
|
|
880
|
+
const naturals = [];
|
|
881
|
+
for (const card of cards) if (isWildCard$1(card, laiZiRanks)) wilds.push(card);
|
|
882
|
+
else naturals.push(card);
|
|
883
|
+
if (wilds.length === 0) return classifyNatural(cards);
|
|
884
|
+
return classifyWithWilds(cards, naturals, wilds.length);
|
|
885
|
+
}
|
|
886
|
+
function classifyNatural(cards) {
|
|
887
|
+
const sorted = sortCards(cards);
|
|
888
|
+
if (sorted.length === 0) return null;
|
|
889
|
+
const ranks = countRanks(sorted);
|
|
890
|
+
const entries = [...ranks.entries()].sort((a, b) => rankValue(a[0]) - rankValue(b[0]));
|
|
891
|
+
if (jokerCount(ranks) === sorted.length && sorted.length >= 2) return {
|
|
892
|
+
type: "rocket",
|
|
893
|
+
cards: sorted,
|
|
894
|
+
key: rankValue("RJ"),
|
|
895
|
+
length: sorted.length,
|
|
896
|
+
wingLength: 0
|
|
897
|
+
};
|
|
898
|
+
if (sorted.length === 1) return {
|
|
899
|
+
type: "solo",
|
|
900
|
+
cards: sorted,
|
|
901
|
+
key: cardValue(sorted[0]),
|
|
902
|
+
length: 1,
|
|
903
|
+
wingLength: 0
|
|
904
|
+
};
|
|
905
|
+
if (sorted.length === 2 && entries.length === 1) return {
|
|
906
|
+
type: "pair",
|
|
907
|
+
cards: sorted,
|
|
908
|
+
key: rankValue(entries[0][0]),
|
|
909
|
+
length: 2,
|
|
910
|
+
wingLength: 0
|
|
911
|
+
};
|
|
912
|
+
if (sorted.length === 3 && entries.length === 1) return {
|
|
913
|
+
type: "trio",
|
|
914
|
+
cards: sorted,
|
|
915
|
+
key: rankValue(entries[0][0]),
|
|
916
|
+
length: 3,
|
|
917
|
+
wingLength: 0
|
|
918
|
+
};
|
|
919
|
+
if (sorted.length >= 4 && entries.length === 1) return {
|
|
920
|
+
type: "bomb",
|
|
921
|
+
cards: sorted,
|
|
922
|
+
key: rankValue(entries[0][0]),
|
|
923
|
+
length: sorted.length,
|
|
924
|
+
wingLength: 0
|
|
925
|
+
};
|
|
926
|
+
const trioSolo = classifyTrioSolo(sorted, entries);
|
|
927
|
+
if (trioSolo) return trioSolo;
|
|
928
|
+
const trioPair = classifyTrioPair(sorted, entries);
|
|
929
|
+
if (trioPair) return trioPair;
|
|
930
|
+
const seq = classifySeq(sorted, entries);
|
|
931
|
+
if (seq) return seq;
|
|
932
|
+
const seqPair = classifySeqPair(sorted, entries);
|
|
933
|
+
if (seqPair) return seqPair;
|
|
934
|
+
const plane = classifyBarePlane(sorted, entries);
|
|
935
|
+
if (plane) return plane;
|
|
936
|
+
const planePair = classifyPlanePair(sorted, entries);
|
|
937
|
+
if (planePair) return planePair;
|
|
938
|
+
const planeSolo = classifyPlaneSolo(sorted, entries);
|
|
939
|
+
if (planeSolo) return planeSolo;
|
|
940
|
+
const fourDualPair = classifyFourDualPair(sorted, entries);
|
|
941
|
+
if (fourDualPair) return fourDualPair;
|
|
942
|
+
const fourDualSolo = classifyFourDualSolo(sorted, entries);
|
|
943
|
+
if (fourDualSolo) return fourDualSolo;
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
function beats(prev, next, decks = 1) {
|
|
947
|
+
const nextBomb = next.type === "bomb" || next.type === "rocket";
|
|
948
|
+
const prevBomb = prev.type === "bomb" || prev.type === "rocket";
|
|
949
|
+
if (next.type === "rocket" && decks === 1) return prev.type !== "rocket";
|
|
950
|
+
if (prev.type === "rocket" && decks === 1) return false;
|
|
951
|
+
if (nextBomb && prevBomb) {
|
|
952
|
+
if (next.length !== prev.length) return next.length > prev.length;
|
|
953
|
+
if (!!next.wild !== !!prev.wild) return !next.wild && !!prev.wild;
|
|
954
|
+
if (next.type !== prev.type) return next.type === "rocket";
|
|
955
|
+
return next.key > prev.key;
|
|
956
|
+
}
|
|
957
|
+
if (nextBomb) return true;
|
|
958
|
+
if (prevBomb) return false;
|
|
959
|
+
if (next.type !== prev.type || next.length !== prev.length || next.wingLength !== prev.wingLength) return false;
|
|
960
|
+
return next.key > prev.key;
|
|
961
|
+
}
|
|
962
|
+
function classifyTrioSolo(cards, entries) {
|
|
963
|
+
if (cards.length !== 4) return null;
|
|
964
|
+
const trio = entries.find(([, group]) => group.length === 3);
|
|
965
|
+
const kicker = entries.find(([, group]) => group.length === 1);
|
|
966
|
+
if (!trio || !kicker) return null;
|
|
967
|
+
if (kicker[0] === "BJ" && entries.some(([rank]) => rank === "RJ")) return null;
|
|
968
|
+
if (kicker[0] === "RJ" && entries.some(([rank]) => rank === "BJ")) return null;
|
|
969
|
+
return {
|
|
970
|
+
type: "trioSolo",
|
|
971
|
+
cards,
|
|
972
|
+
key: rankValue(trio[0]),
|
|
973
|
+
length: 4,
|
|
974
|
+
wingLength: 1
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function classifyTrioPair(cards, entries) {
|
|
978
|
+
if (cards.length !== 5) return null;
|
|
979
|
+
const trio = entries.find(([, group]) => group.length === 3);
|
|
980
|
+
const pair = entries.find(([, group]) => group.length === 2);
|
|
981
|
+
if (!trio || !pair || pair[0] === trio[0]) return null;
|
|
982
|
+
return {
|
|
983
|
+
type: "trioPair",
|
|
984
|
+
cards,
|
|
985
|
+
key: rankValue(trio[0]),
|
|
986
|
+
length: 5,
|
|
987
|
+
wingLength: 2
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function classifySeq(cards, entries) {
|
|
991
|
+
if (cards.length < 5) return null;
|
|
992
|
+
if (!entries.every(([, group]) => group.length === 1)) return null;
|
|
993
|
+
if (!isConsecutive(entries.map(([rank]) => rank), 1)) return null;
|
|
994
|
+
return {
|
|
995
|
+
type: "seq",
|
|
996
|
+
cards,
|
|
997
|
+
key: rankValue(entries[entries.length - 1][0]),
|
|
998
|
+
length: cards.length,
|
|
999
|
+
wingLength: 0
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function classifySeqPair(cards, entries) {
|
|
1003
|
+
if (cards.length < 6 || cards.length % 2 !== 0) return null;
|
|
1004
|
+
if (!entries.every(([, group]) => group.length === 2)) return null;
|
|
1005
|
+
if (!isConsecutive(entries.map(([rank]) => rank), 1)) return null;
|
|
1006
|
+
if (entries.length < 3) return null;
|
|
1007
|
+
return {
|
|
1008
|
+
type: "seqPair",
|
|
1009
|
+
cards,
|
|
1010
|
+
key: rankValue(entries[entries.length - 1][0]),
|
|
1011
|
+
length: cards.length,
|
|
1012
|
+
wingLength: 0
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function classifyBarePlane(cards, entries) {
|
|
1016
|
+
const trios = entries.filter(([, group]) => group.length === 3).map(([rank]) => rank);
|
|
1017
|
+
if (trios.length < 2) return null;
|
|
1018
|
+
if (entries.some(([, group]) => group.length !== 3)) return null;
|
|
1019
|
+
if (!isConsecutive(trios, 1)) return null;
|
|
1020
|
+
return {
|
|
1021
|
+
type: "plane",
|
|
1022
|
+
cards,
|
|
1023
|
+
key: rankValue(trios[trios.length - 1]),
|
|
1024
|
+
length: cards.length,
|
|
1025
|
+
wingLength: 0
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function classifyPlaneSolo(cards, entries = [...countRanks(cards).entries()].sort((a, b) => rankValue(a[0]) - rankValue(b[0]))) {
|
|
1029
|
+
const available = entries.filter(([rank, group]) => group.length >= 3 && isNaturalSeqRank(rank) && rank !== "2").map(([rank]) => rank).sort((a, b) => rankValue(a) - rankValue(b));
|
|
1030
|
+
const maxK = Math.min(available.length, Math.floor(cards.length / 4));
|
|
1031
|
+
for (let k = maxK; k >= 2; k -= 1) {
|
|
1032
|
+
if (cards.length !== k * 4) continue;
|
|
1033
|
+
const found = [];
|
|
1034
|
+
for (const body of consecutiveRuns(available, k)) {
|
|
1035
|
+
const leftover = leftoverAfterBody(cards, body, 3);
|
|
1036
|
+
if (leftover.length !== k) continue;
|
|
1037
|
+
if (containsConsecutiveTrios(leftover)) continue;
|
|
1038
|
+
found.push({
|
|
1039
|
+
type: "planeSolo",
|
|
1040
|
+
cards,
|
|
1041
|
+
key: rankValue(body[body.length - 1]),
|
|
1042
|
+
length: cards.length,
|
|
1043
|
+
wingLength: k
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
if (found.length > 0) {
|
|
1047
|
+
found.sort((a, b) => b.key - a.key);
|
|
1048
|
+
return found[0] ?? null;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
function classifyPlanePair(cards, entries) {
|
|
1054
|
+
entries.filter(([, group]) => group.length >= 3 && isNaturalSeqRank(group[0] ? rankOf$1(group[0]) : "3") && rankOf$1(group[0]) !== "2");
|
|
1055
|
+
const trioRanks = entries.filter(([rank, group]) => group.length >= 3 && isNaturalSeqRank(rank) && rank !== "2").map(([rank]) => rank).sort((a, b) => rankValue(a) - rankValue(b));
|
|
1056
|
+
for (let k = Math.min(trioRanks.length, Math.floor(cards.length / 5)); k >= 2; k -= 1) {
|
|
1057
|
+
if (cards.length !== k * 5) continue;
|
|
1058
|
+
for (const body of consecutiveRuns(trioRanks, k)) if (pairWings(cards, body) === k) return {
|
|
1059
|
+
type: "planePair",
|
|
1060
|
+
cards,
|
|
1061
|
+
key: rankValue(body[body.length - 1]),
|
|
1062
|
+
length: cards.length,
|
|
1063
|
+
wingLength: k
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
function classifyFourDualSolo(cards, entries) {
|
|
1069
|
+
if (cards.length !== 6) return null;
|
|
1070
|
+
const quad = entries.find(([, group]) => group.length === 4);
|
|
1071
|
+
if (!quad) return null;
|
|
1072
|
+
const leftovers = cards.filter((card) => rankOf$1(card) !== quad[0]);
|
|
1073
|
+
if (leftovers.length !== 2) return null;
|
|
1074
|
+
if (hasRocket(countRanks(leftovers))) return null;
|
|
1075
|
+
return {
|
|
1076
|
+
type: "fourDualSolo",
|
|
1077
|
+
cards,
|
|
1078
|
+
key: rankValue(quad[0]),
|
|
1079
|
+
length: 6,
|
|
1080
|
+
wingLength: 2
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function classifyFourDualPair(cards, entries) {
|
|
1084
|
+
if (cards.length !== 8) return null;
|
|
1085
|
+
const quad = entries.find(([, group]) => group.length === 4);
|
|
1086
|
+
if (!quad) return null;
|
|
1087
|
+
const leftoverGroups = [...countRanks(leftoverAfterBody(cards, [quad[0]], 4)).entries()];
|
|
1088
|
+
if (leftoverGroups.some(([rank, group]) => rank === quad[0] || group.length !== 2)) return null;
|
|
1089
|
+
if (leftoverGroups.length !== 2) return null;
|
|
1090
|
+
return {
|
|
1091
|
+
type: "fourDualPair",
|
|
1092
|
+
cards,
|
|
1093
|
+
key: rankValue(quad[0]),
|
|
1094
|
+
length: 8,
|
|
1095
|
+
wingLength: 4
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function hasRocket(ranks) {
|
|
1099
|
+
return (ranks.get("BJ")?.length ?? 0) >= 1 && (ranks.get("RJ")?.length ?? 0) >= 1;
|
|
1100
|
+
}
|
|
1101
|
+
function jokerCount(ranks) {
|
|
1102
|
+
return (ranks.get("BJ")?.length ?? 0) + (ranks.get("RJ")?.length ?? 0);
|
|
1103
|
+
}
|
|
1104
|
+
function classifyWithWilds(original, naturals, wildCount) {
|
|
1105
|
+
if (wildCount <= 0) return classifyNatural(original);
|
|
1106
|
+
const ranksToTry = wildAssignmentRanks(naturals, wildCount);
|
|
1107
|
+
let best = null;
|
|
1108
|
+
const choice = new Array(wildCount).fill(0);
|
|
1109
|
+
const walk = () => {
|
|
1110
|
+
const virtuals = choice.map((index) => asCardId(`S${ranksToTry[index]}`));
|
|
1111
|
+
const result = classifyNatural([...naturals, ...virtuals]);
|
|
1112
|
+
if (!result) return;
|
|
1113
|
+
const hand = {
|
|
1114
|
+
...result,
|
|
1115
|
+
cards: sortCards(original),
|
|
1116
|
+
wild: true
|
|
1117
|
+
};
|
|
1118
|
+
if (!best || wildPreference(hand, best) > 0) best = hand;
|
|
1119
|
+
};
|
|
1120
|
+
const rec = (depth) => {
|
|
1121
|
+
if (depth === wildCount) {
|
|
1122
|
+
walk();
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
for (let i = 0; i < ranksToTry.length; i += 1) {
|
|
1126
|
+
choice[depth] = i;
|
|
1127
|
+
rec(depth + 1);
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
rec(0);
|
|
1131
|
+
return best;
|
|
1132
|
+
}
|
|
1133
|
+
function wildAssignmentRanks(naturals, wildCount) {
|
|
1134
|
+
if (naturals.length === 0) return wildCount >= 4 ? ["2"] : [...WILDABLE_RANKS];
|
|
1135
|
+
const present = new Set(naturals.map((card) => rankOf$1(card)));
|
|
1136
|
+
if (wildCount >= 5) return WILDABLE_RANKS.filter((rank) => present.has(rank) || neighborsOf(present).has(rank));
|
|
1137
|
+
return [...WILDABLE_RANKS];
|
|
1138
|
+
}
|
|
1139
|
+
function neighborsOf(present) {
|
|
1140
|
+
const out = /* @__PURE__ */ new Set();
|
|
1141
|
+
for (const rank of present) {
|
|
1142
|
+
const value = rankValue(rank);
|
|
1143
|
+
for (const candidate of WILDABLE_RANKS) if (Math.abs(rankValue(candidate) - value) === 1) out.add(candidate);
|
|
1144
|
+
}
|
|
1145
|
+
return out;
|
|
1146
|
+
}
|
|
1147
|
+
function wildPreference(next, prev) {
|
|
1148
|
+
const bombScore = (hand) => {
|
|
1149
|
+
if (hand.type === "rocket") return 200 + hand.length;
|
|
1150
|
+
if (hand.type === "bomb") return 100 + hand.length;
|
|
1151
|
+
return 0;
|
|
1152
|
+
};
|
|
1153
|
+
const byBomb = bombScore(next) - bombScore(prev);
|
|
1154
|
+
if (byBomb !== 0) return byBomb;
|
|
1155
|
+
if (next.length !== prev.length) return next.length - prev.length;
|
|
1156
|
+
return next.key - prev.key;
|
|
1157
|
+
}
|
|
1158
|
+
function isConsecutive(ranks, step) {
|
|
1159
|
+
if (ranks.some((rank) => !isNaturalSeqRank(rank))) return false;
|
|
1160
|
+
for (let i = 1; i < ranks.length; i += 1) if (rankValue(ranks[i]) - rankValue(ranks[i - 1]) !== step) return false;
|
|
1161
|
+
return ranks.length > 0;
|
|
1162
|
+
}
|
|
1163
|
+
function consecutiveRuns(ranks, length) {
|
|
1164
|
+
const runs = [];
|
|
1165
|
+
for (let i = 0; i + length <= ranks.length; i += 1) {
|
|
1166
|
+
const slice = ranks.slice(i, i + length);
|
|
1167
|
+
if (isConsecutive(slice, 1)) runs.push(slice);
|
|
1168
|
+
}
|
|
1169
|
+
return runs;
|
|
1170
|
+
}
|
|
1171
|
+
function containsConsecutiveTrios(cards) {
|
|
1172
|
+
return consecutiveRuns([...countRanks(cards).entries()].filter(([, group]) => group.length >= 3).map(([rank]) => rank).filter((rank) => isNaturalSeqRank(rank) && rank !== "2").sort((a, b) => rankValue(a) - rankValue(b)), 2).length > 0;
|
|
1173
|
+
}
|
|
1174
|
+
function pairWings(cards, body) {
|
|
1175
|
+
const groups = countRanks(leftoverAfterBody(cards, body, 3));
|
|
1176
|
+
if ([...groups.values()].some((group) => group.length !== 2 && group.length !== 4)) return null;
|
|
1177
|
+
let pairs = 0;
|
|
1178
|
+
for (const [rank, group] of groups) {
|
|
1179
|
+
if (body.includes(rank)) return null;
|
|
1180
|
+
if (group.length === 2) pairs += 1;
|
|
1181
|
+
else if (group.length === 4) pairs += 2;
|
|
1182
|
+
else return null;
|
|
1183
|
+
}
|
|
1184
|
+
return pairs;
|
|
1185
|
+
}
|
|
1186
|
+
function leftoverAfterBody(cards, body, take) {
|
|
1187
|
+
const used = /* @__PURE__ */ new Map();
|
|
1188
|
+
for (const rank of body) used.set(rank, take);
|
|
1189
|
+
const leftover = [];
|
|
1190
|
+
for (const card of cards) {
|
|
1191
|
+
const rank = rankOf$1(card);
|
|
1192
|
+
const remaining = used.get(rank) ?? 0;
|
|
1193
|
+
if (remaining > 0) used.set(rank, remaining - 1);
|
|
1194
|
+
else leftover.push(card);
|
|
1195
|
+
}
|
|
1196
|
+
return leftover;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
//#endregion
|
|
1200
|
+
//#region src/client/store.ts
|
|
1201
|
+
function emptyState() {
|
|
1202
|
+
return {
|
|
1203
|
+
view: null,
|
|
1204
|
+
settlement: null,
|
|
1205
|
+
selected: [],
|
|
1206
|
+
error: null,
|
|
1207
|
+
shareable: false,
|
|
1208
|
+
sitUrl: "",
|
|
1209
|
+
watchUrl: "",
|
|
1210
|
+
roomCode: ""
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
function toggleCard(selected, card) {
|
|
1214
|
+
return selected.includes(card) ? selected.filter((item) => item !== card) : [...selected, card];
|
|
1215
|
+
}
|
|
1216
|
+
function selectionLegal(view, selected) {
|
|
1217
|
+
if (!view || selected.length === 0) return false;
|
|
1218
|
+
const key = [...selected].sort().join(",");
|
|
1219
|
+
if (view.legal.combos.some((combo) => [...combo.cards].sort().join(",") === key)) return true;
|
|
1220
|
+
const ranks = view.laiZiRanks ?? [];
|
|
1221
|
+
const classified = classify(selected, ranks);
|
|
1222
|
+
if (!classified) return false;
|
|
1223
|
+
const last = [...view.lastPlays].reverse().find((play) => play.type !== "pass");
|
|
1224
|
+
if (!last || view.leadSeat === view.you.seat) return true;
|
|
1225
|
+
const lead = classify(last.cards, ranks);
|
|
1226
|
+
if (!lead) return true;
|
|
1227
|
+
return beats(lead, classified, decksFor(view.room.seatCount ?? (view.room.seats.length >= 4 ? 4 : 3)));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
//#endregion
|
|
1231
|
+
//#region src/client/card-stack.ts
|
|
1232
|
+
const COMPACT_STACK_MAX = 5;
|
|
1233
|
+
function visibleBackCount(remaining, maxVisible = COMPACT_STACK_MAX) {
|
|
1234
|
+
if (remaining <= 0) return 0;
|
|
1235
|
+
return Math.min(remaining, maxVisible);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
//#endregion
|
|
1239
|
+
//#region src/client/CardBack.tsx
|
|
1240
|
+
const ASSET$1 = "/doudizhu/assets";
|
|
1241
|
+
function CardBack({ count = 1, compact = false, landlord = false }) {
|
|
1242
|
+
const remaining = Math.max(0, count);
|
|
1243
|
+
const n = compact ? visibleBackCount(remaining) : Math.min(remaining, 20);
|
|
1244
|
+
const src = landlord ? `${ASSET$1}/card-back-landlord.png` : `${ASSET$1}/card-back.png`;
|
|
1245
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1246
|
+
className: `${styles_module_css_default.remain} ${compact ? styles_module_css_default.remainCompact : ""}`,
|
|
1247
|
+
children: [compact && remaining > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1248
|
+
className: styles_module_css_default.cardsLeft,
|
|
1249
|
+
"aria-label": `剩余 ${remaining} 张`,
|
|
1250
|
+
children: remaining
|
|
1251
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1252
|
+
className: `${styles_module_css_default.backs} ${compact ? styles_module_css_default.backsCompact : ""}`,
|
|
1253
|
+
"aria-hidden": "true",
|
|
1254
|
+
children: Array.from({ length: n }, (_, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1255
|
+
className: `${styles_module_css_default.cardBack} ${landlord ? styles_module_css_default.cardBackLandlord : ""}`,
|
|
1256
|
+
style: { zIndex: index },
|
|
1257
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1258
|
+
className: styles_module_css_default.cardBackArt,
|
|
1259
|
+
src,
|
|
1260
|
+
alt: ""
|
|
1261
|
+
})
|
|
1262
|
+
}, index))
|
|
1263
|
+
})]
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
//#endregion
|
|
1268
|
+
//#region src/client/CardFace.tsx
|
|
1269
|
+
const SUIT_GLYPH = {
|
|
1270
|
+
S: "♠",
|
|
1271
|
+
H: "♥",
|
|
1272
|
+
C: "♣",
|
|
1273
|
+
D: "♦"
|
|
1274
|
+
};
|
|
1275
|
+
const ASSET = "/doudizhu/assets";
|
|
1276
|
+
const FACE_ART_RANKS = [
|
|
1277
|
+
"J",
|
|
1278
|
+
"Q",
|
|
1279
|
+
"K",
|
|
1280
|
+
"A"
|
|
1281
|
+
];
|
|
1282
|
+
const FACE_ART = Object.fromEntries(FACE_ART_RANKS.map((rank) => [rank, `${ASSET}/face-${rank.toLowerCase()}.png`]));
|
|
1283
|
+
function cardBase(card) {
|
|
1284
|
+
return String(card).split("~")[0] ?? String(card);
|
|
1285
|
+
}
|
|
1286
|
+
function rankOf(card) {
|
|
1287
|
+
const raw = cardBase(card);
|
|
1288
|
+
if (raw === "BJ" || raw === "RJ") return raw;
|
|
1289
|
+
return raw.slice(1);
|
|
1290
|
+
}
|
|
1291
|
+
function isWildCard(card, wildRanks) {
|
|
1292
|
+
const rank = rankOf(card);
|
|
1293
|
+
return wildRanks.length > 0 && rank !== "BJ" && rank !== "RJ" && wildRanks.includes(rank);
|
|
1294
|
+
}
|
|
1295
|
+
function CardFace({ card, selected = false, laiZiRanks = [] }) {
|
|
1296
|
+
const raw = cardBase(card);
|
|
1297
|
+
const wild = isWildCard(card, laiZiRanks);
|
|
1298
|
+
if (raw === "BJ" || raw === "RJ" || rankOf(card) === "BJ" || rankOf(card) === "RJ") {
|
|
1299
|
+
const red$1 = rankOf(card) === "RJ";
|
|
1300
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1301
|
+
className: `${styles_module_css_default.cardFace} ${styles_module_css_default.jokerFace} ${red$1 ? styles_module_css_default.red : ""} ${selected ? styles_module_css_default.selected : ""}`,
|
|
1302
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1303
|
+
className: styles_module_css_default.jokerArt,
|
|
1304
|
+
src: red$1 ? `${ASSET}/joker-red.png` : `${ASSET}/joker-black.png`,
|
|
1305
|
+
alt: ""
|
|
1306
|
+
})
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
const suit = raw[0] ?? "S";
|
|
1310
|
+
const rank = raw.slice(1);
|
|
1311
|
+
const red = suit === "H" || suit === "D";
|
|
1312
|
+
const art = FACE_ART[rank];
|
|
1313
|
+
const glyph = SUIT_GLYPH[suit] ?? suit;
|
|
1314
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1315
|
+
className: `${styles_module_css_default.cardFace} ${red ? styles_module_css_default.red : ""} ${selected ? styles_module_css_default.selected : ""} ${wild ? styles_module_css_default.wild : ""} ${art ? "" : styles_module_css_default.plainFace}`,
|
|
1316
|
+
children: [
|
|
1317
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1318
|
+
className: styles_module_css_default.rank,
|
|
1319
|
+
children: [rank, wild ? "*" : ""]
|
|
1320
|
+
}),
|
|
1321
|
+
art ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1322
|
+
className: styles_module_css_default.faceArt,
|
|
1323
|
+
src: art,
|
|
1324
|
+
alt: ""
|
|
1325
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1326
|
+
className: styles_module_css_default.suitCenter,
|
|
1327
|
+
children: glyph
|
|
1328
|
+
}),
|
|
1329
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1330
|
+
className: styles_module_css_default.suit,
|
|
1331
|
+
children: glyph
|
|
1332
|
+
})
|
|
1333
|
+
]
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
//#endregion
|
|
1338
|
+
//#region src/client/SeatRow.tsx
|
|
1339
|
+
function SeatRow({ seat, align = "left", revealed, laiZiRanks = [] }) {
|
|
1340
|
+
const alignClass = align === "right" ? styles_module_css_default.seatRight : align === "center" ? styles_module_css_default.seatCenter : "";
|
|
1341
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1342
|
+
className: `${styles_module_css_default.seat} ${alignClass}`,
|
|
1343
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1344
|
+
className: styles_module_css_default.seatHead,
|
|
1345
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SeatAvatar, {
|
|
1346
|
+
avatarUrl: seat.avatarUrl,
|
|
1347
|
+
role: seat.role,
|
|
1348
|
+
seat: seat.seat,
|
|
1349
|
+
occupied: Boolean(seat.playerId)
|
|
1350
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1351
|
+
className: styles_module_css_default.seatMeta,
|
|
1352
|
+
children: [
|
|
1353
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1354
|
+
className: styles_module_css_default.seatName,
|
|
1355
|
+
children: [
|
|
1356
|
+
seat.displayName ?? "空座",
|
|
1357
|
+
seat.role === "landlord" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1358
|
+
className: styles_module_css_default.badge,
|
|
1359
|
+
children: "地主"
|
|
1360
|
+
}) : null,
|
|
1361
|
+
seat.role === "farmer" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1362
|
+
className: styles_module_css_default.badge,
|
|
1363
|
+
children: "农民"
|
|
1364
|
+
}) : null,
|
|
1365
|
+
revealed && revealed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1366
|
+
className: styles_module_css_default.badge,
|
|
1367
|
+
children: "明牌"
|
|
1368
|
+
}) : null
|
|
1369
|
+
]
|
|
1370
|
+
}),
|
|
1371
|
+
seat.ready && seat.role === "empty" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1372
|
+
className: styles_module_css_default.ready,
|
|
1373
|
+
children: "准备"
|
|
1374
|
+
}) : null,
|
|
1375
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1376
|
+
className: styles_module_css_default.muted,
|
|
1377
|
+
children: seat.connected ? "在线" : seat.playerId ? "离线" : ""
|
|
1378
|
+
})
|
|
1379
|
+
]
|
|
1380
|
+
})]
|
|
1381
|
+
}), revealed && revealed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1382
|
+
className: styles_module_css_default.miniHand,
|
|
1383
|
+
children: revealed.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFace, {
|
|
1384
|
+
card,
|
|
1385
|
+
laiZiRanks
|
|
1386
|
+
}, card))
|
|
1387
|
+
}) : seat.playerId && seat.cardsLeft > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBack, {
|
|
1388
|
+
count: seat.cardsLeft,
|
|
1389
|
+
compact: true,
|
|
1390
|
+
landlord: seat.role === "landlord"
|
|
1391
|
+
}) : null]
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
//#endregion
|
|
1396
|
+
//#region src/client/TableView.tsx
|
|
1397
|
+
function TableView({ view, selected, onToggle, onBid, onDouble, onMingPai, onPlay, onPass, onChat }) {
|
|
1398
|
+
const seats = view.room.seats;
|
|
1399
|
+
const count = view.room.seatCount ?? (seats.length >= 4 ? 4 : 3);
|
|
1400
|
+
const self = view.you.seat ?? 0;
|
|
1401
|
+
const opponents = opponentSeats(self, count).map((seat) => seatAt(seats, seat));
|
|
1402
|
+
const mine = seatAt(seats, self);
|
|
1403
|
+
const legal = selectionLegal(view, selected);
|
|
1404
|
+
const phase = view.room.phase;
|
|
1405
|
+
const laiZi = view.laiZiRanks ?? [];
|
|
1406
|
+
const seconds = useDeadline(phase === "playing" ? view.deadlineAt : null);
|
|
1407
|
+
const seated = seats.filter((seat) => seat.playerId).length;
|
|
1408
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1409
|
+
className: styles_module_css_default.table,
|
|
1410
|
+
children: [
|
|
1411
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1412
|
+
className: styles_module_css_default.topbar,
|
|
1413
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
1414
|
+
view.room.title || "好友局",
|
|
1415
|
+
" · ",
|
|
1416
|
+
count,
|
|
1417
|
+
"人",
|
|
1418
|
+
view.room.laiZi ? "癞子" : "经典",
|
|
1419
|
+
" · 底注 ",
|
|
1420
|
+
formatM(parseAtoms(view.room.stakeAtoms)),
|
|
1421
|
+
" · ",
|
|
1422
|
+
view.bid ? `${view.bid}倍` : "未叫",
|
|
1423
|
+
" · 阶段 ",
|
|
1424
|
+
phaseLabel(phase)
|
|
1425
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1426
|
+
className: styles_module_css_default.muted,
|
|
1427
|
+
children: [
|
|
1428
|
+
"余额 ",
|
|
1429
|
+
formatM(parseAtoms(view.yourAvailableAtoms)),
|
|
1430
|
+
" · 冻结 ",
|
|
1431
|
+
formatM(parseAtoms(view.yourEscrowAtoms))
|
|
1432
|
+
]
|
|
1433
|
+
})]
|
|
1434
|
+
}),
|
|
1435
|
+
seconds !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1436
|
+
className: `${styles_module_css_default.timer} ${seconds <= 10 ? styles_module_css_default.timerUrgent : ""}`,
|
|
1437
|
+
children: [
|
|
1438
|
+
"出牌剩余 ",
|
|
1439
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: seconds }),
|
|
1440
|
+
" / 120 秒"
|
|
1441
|
+
]
|
|
1442
|
+
}) : phase === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1443
|
+
className: styles_module_css_default.hint,
|
|
1444
|
+
children: [
|
|
1445
|
+
seated,
|
|
1446
|
+
"/",
|
|
1447
|
+
count,
|
|
1448
|
+
" 人入座,人齐开打。后来的人观战。"
|
|
1449
|
+
]
|
|
1450
|
+
}) : null,
|
|
1451
|
+
laiZi.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1452
|
+
className: styles_module_css_default.muted,
|
|
1453
|
+
children: ["癞子:", laiZi.join("、")]
|
|
1454
|
+
}) : null,
|
|
1455
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1456
|
+
className: `${styles_module_css_default.opponents} ${opponents.length > 2 ? styles_module_css_default.opponentsThree : ""}`,
|
|
1457
|
+
children: opponents.map((seat, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SeatRow, {
|
|
1458
|
+
seat,
|
|
1459
|
+
align: index === opponents.length - 1 ? "right" : index === 0 ? "left" : "center",
|
|
1460
|
+
...view.revealedBySeat?.[seat.seat] ? { revealed: view.revealedBySeat[seat.seat] } : {},
|
|
1461
|
+
laiZiRanks: laiZi
|
|
1462
|
+
}, seat.seat))
|
|
1463
|
+
}),
|
|
1464
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1465
|
+
className: styles_module_css_default.centerPlay,
|
|
1466
|
+
children: view.lastPlays.slice(-1).map((play, index) => play.type === "pass" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1467
|
+
className: styles_module_css_default.muted,
|
|
1468
|
+
children: "过"
|
|
1469
|
+
}, index) : play.cards.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFace, {
|
|
1470
|
+
card,
|
|
1471
|
+
laiZiRanks: laiZi
|
|
1472
|
+
}, card)))
|
|
1473
|
+
}),
|
|
1474
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1475
|
+
className: styles_module_css_default.bottom,
|
|
1476
|
+
children: view.bottom ? view.bottom.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFace, {
|
|
1477
|
+
card,
|
|
1478
|
+
laiZiRanks: laiZi
|
|
1479
|
+
}, card)) : phase !== "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBack, { count: holeCount(count) }) : null
|
|
1480
|
+
}),
|
|
1481
|
+
view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1482
|
+
className: styles_module_css_default.selfRow,
|
|
1483
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1484
|
+
className: styles_module_css_default.selfSeat,
|
|
1485
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SeatAvatar, { spectator: true }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1486
|
+
className: styles_module_css_default.seatName,
|
|
1487
|
+
children: ["观战", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1488
|
+
className: styles_module_css_default.badge,
|
|
1489
|
+
children: "观战"
|
|
1490
|
+
})]
|
|
1491
|
+
})]
|
|
1492
|
+
})
|
|
1493
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1494
|
+
className: styles_module_css_default.hint,
|
|
1495
|
+
children: "观战中。只能看见已打出的牌与公开区。"
|
|
1496
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1497
|
+
className: styles_module_css_default.selfRow,
|
|
1498
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1499
|
+
className: styles_module_css_default.selfSeat,
|
|
1500
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SeatAvatar, {
|
|
1501
|
+
avatarUrl: mine?.avatarUrl ?? null,
|
|
1502
|
+
role: mine?.role ?? "empty",
|
|
1503
|
+
seat: self,
|
|
1504
|
+
occupied: Boolean(mine?.playerId)
|
|
1505
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1506
|
+
className: styles_module_css_default.seatName,
|
|
1507
|
+
children: [
|
|
1508
|
+
mine?.displayName,
|
|
1509
|
+
mine?.role === "landlord" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1510
|
+
className: styles_module_css_default.badge,
|
|
1511
|
+
children: "地主"
|
|
1512
|
+
}) : null,
|
|
1513
|
+
mine?.role === "farmer" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1514
|
+
className: styles_module_css_default.badge,
|
|
1515
|
+
children: "农民"
|
|
1516
|
+
}) : null,
|
|
1517
|
+
view.mingPaiBySeat?.[self] ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1518
|
+
className: styles_module_css_default.badge,
|
|
1519
|
+
children: "明牌"
|
|
1520
|
+
}) : null
|
|
1521
|
+
]
|
|
1522
|
+
})]
|
|
1523
|
+
}), phase === "waiting" && !view.bottom ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBack, {
|
|
1524
|
+
count: holeCount(count),
|
|
1525
|
+
compact: true
|
|
1526
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1527
|
+
className: styles_module_css_default.hand,
|
|
1528
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1529
|
+
className: styles_module_css_default.handInner,
|
|
1530
|
+
children: view.you.cards.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1531
|
+
type: "button",
|
|
1532
|
+
onClick: () => {
|
|
1533
|
+
onToggle(card);
|
|
1534
|
+
},
|
|
1535
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFace, {
|
|
1536
|
+
card,
|
|
1537
|
+
selected: selected.includes(card),
|
|
1538
|
+
laiZiRanks: laiZi
|
|
1539
|
+
})
|
|
1540
|
+
}, card))
|
|
1541
|
+
})
|
|
1542
|
+
})]
|
|
1543
|
+
}),
|
|
1544
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1545
|
+
className: styles_module_css_default.actions,
|
|
1546
|
+
children: [
|
|
1547
|
+
phase === "waiting" && !view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1548
|
+
className: styles_module_css_default.hint,
|
|
1549
|
+
children: [
|
|
1550
|
+
"等满 ",
|
|
1551
|
+
count,
|
|
1552
|
+
" 人自动开打。"
|
|
1553
|
+
]
|
|
1554
|
+
}) : null,
|
|
1555
|
+
phase === "bidding" && isTurn(view) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: view.auction?.kind === "rob" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1556
|
+
type: "button",
|
|
1557
|
+
className: styles_module_css_default.ghost,
|
|
1558
|
+
onClick: () => {
|
|
1559
|
+
onBid("pass");
|
|
1560
|
+
},
|
|
1561
|
+
children: "不抢"
|
|
1562
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1563
|
+
type: "button",
|
|
1564
|
+
className: styles_module_css_default.primary,
|
|
1565
|
+
onClick: () => {
|
|
1566
|
+
onBid("rob");
|
|
1567
|
+
},
|
|
1568
|
+
children: "抢地主"
|
|
1569
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1570
|
+
type: "button",
|
|
1571
|
+
className: styles_module_css_default.ghost,
|
|
1572
|
+
onClick: () => {
|
|
1573
|
+
onBid("pass");
|
|
1574
|
+
},
|
|
1575
|
+
children: "不叫"
|
|
1576
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1577
|
+
type: "button",
|
|
1578
|
+
className: styles_module_css_default.primary,
|
|
1579
|
+
onClick: () => {
|
|
1580
|
+
onBid("call");
|
|
1581
|
+
},
|
|
1582
|
+
children: "叫地主"
|
|
1583
|
+
})] }) }) : null,
|
|
1584
|
+
canMingPai(view) && onMingPai ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1585
|
+
type: "button",
|
|
1586
|
+
className: styles_module_css_default.ghost,
|
|
1587
|
+
onClick: onMingPai,
|
|
1588
|
+
children: "明牌"
|
|
1589
|
+
}) : null,
|
|
1590
|
+
phase === "doubling" && !view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1591
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1592
|
+
type: "button",
|
|
1593
|
+
className: styles_module_css_default.ghost,
|
|
1594
|
+
onClick: () => {
|
|
1595
|
+
onDouble("pass");
|
|
1596
|
+
},
|
|
1597
|
+
children: "不加倍"
|
|
1598
|
+
}),
|
|
1599
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1600
|
+
type: "button",
|
|
1601
|
+
className: styles_module_css_default.primary,
|
|
1602
|
+
onClick: () => {
|
|
1603
|
+
onDouble("double");
|
|
1604
|
+
},
|
|
1605
|
+
children: "加倍 ×2"
|
|
1606
|
+
}),
|
|
1607
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1608
|
+
type: "button",
|
|
1609
|
+
className: styles_module_css_default.primary,
|
|
1610
|
+
onClick: () => {
|
|
1611
|
+
onDouble("reDouble");
|
|
1612
|
+
},
|
|
1613
|
+
children: "超级加倍 ×4"
|
|
1614
|
+
})
|
|
1615
|
+
] }) : null,
|
|
1616
|
+
phase === "playing" && isTurn(view) && !view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1617
|
+
type: "button",
|
|
1618
|
+
className: styles_module_css_default.primary,
|
|
1619
|
+
disabled: !legal,
|
|
1620
|
+
onClick: onPlay,
|
|
1621
|
+
children: "出牌"
|
|
1622
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1623
|
+
type: "button",
|
|
1624
|
+
className: styles_module_css_default.ghost,
|
|
1625
|
+
disabled: !view.legal.canPass,
|
|
1626
|
+
onClick: onPass,
|
|
1627
|
+
children: "过"
|
|
1628
|
+
})] }) : null
|
|
1629
|
+
]
|
|
1630
|
+
}),
|
|
1631
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1632
|
+
className: styles_module_css_default.chat,
|
|
1633
|
+
children: [view.chat.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
1634
|
+
line.displayName,
|
|
1635
|
+
": ",
|
|
1636
|
+
line.text
|
|
1637
|
+
] }, line.ts)), !view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("form", {
|
|
1638
|
+
onSubmit: (event) => {
|
|
1639
|
+
event.preventDefault();
|
|
1640
|
+
const data = new FormData(event.currentTarget);
|
|
1641
|
+
const text = String(data.get("chat") ?? "");
|
|
1642
|
+
if (text) onChat(text);
|
|
1643
|
+
event.currentTarget.reset();
|
|
1644
|
+
},
|
|
1645
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1646
|
+
className: styles_module_css_default.input,
|
|
1647
|
+
name: "chat",
|
|
1648
|
+
maxLength: 80,
|
|
1649
|
+
placeholder: "聊天"
|
|
1650
|
+
})
|
|
1651
|
+
}) : null]
|
|
1652
|
+
})
|
|
1653
|
+
]
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
function opponentSeats(self, count) {
|
|
1657
|
+
const out = [];
|
|
1658
|
+
for (let step = 1; step < count; step += 1) out.push((self + step) % count);
|
|
1659
|
+
return out;
|
|
1660
|
+
}
|
|
1661
|
+
function seatAt(seats, index) {
|
|
1662
|
+
return seats[index] ?? {
|
|
1663
|
+
seat: index,
|
|
1664
|
+
playerId: null,
|
|
1665
|
+
displayName: null,
|
|
1666
|
+
avatarUrl: null,
|
|
1667
|
+
ready: false,
|
|
1668
|
+
connected: false,
|
|
1669
|
+
role: "empty",
|
|
1670
|
+
grantId: null,
|
|
1671
|
+
cardsLeft: 0
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
function isTurn(view) {
|
|
1675
|
+
return view.you.seat !== null && view.turnSeat === view.you.seat;
|
|
1676
|
+
}
|
|
1677
|
+
function useDeadline(deadlineAt) {
|
|
1678
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
1679
|
+
(0, react.useEffect)(() => {
|
|
1680
|
+
if (!deadlineAt) return void 0;
|
|
1681
|
+
const tick = setInterval(() => {
|
|
1682
|
+
setNow(Date.now());
|
|
1683
|
+
}, 250);
|
|
1684
|
+
return () => {
|
|
1685
|
+
clearInterval(tick);
|
|
1686
|
+
};
|
|
1687
|
+
}, [deadlineAt]);
|
|
1688
|
+
if (!deadlineAt) return null;
|
|
1689
|
+
return Math.max(0, Math.ceil((Date.parse(deadlineAt) - now) / 1e3));
|
|
1690
|
+
}
|
|
1691
|
+
function canMingPai(view) {
|
|
1692
|
+
if (view.you.spectator || view.you.seat === null) return false;
|
|
1693
|
+
if (view.mingPaiBySeat?.[view.you.seat]) return false;
|
|
1694
|
+
return view.room.phase === "bidding" || view.room.phase === "doubling" || view.room.phase === "playing";
|
|
1695
|
+
}
|
|
1696
|
+
function phaseLabel(phase) {
|
|
1697
|
+
if (phase === "waiting") return "等人";
|
|
1698
|
+
if (phase === "bidding") return "叫抢";
|
|
1699
|
+
if (phase === "doubling") return "加倍";
|
|
1700
|
+
if (phase === "playing") return "出牌";
|
|
1701
|
+
if (phase === "settling") return "结算";
|
|
1702
|
+
return phase;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
//#endregion
|
|
1706
|
+
//#region src/client/HostApp.tsx
|
|
1707
|
+
function HostApp({ onClose }) {
|
|
1708
|
+
const [stakeM, setStakeM] = (0, react.useState)(DEFAULT_STAKE_M);
|
|
1709
|
+
const [maxMultiplier, setMaxMultiplier] = (0, react.useState)(DEFAULT_MAX_MULTIPLIER);
|
|
1710
|
+
const [seatCount, setSeatCount] = (0, react.useState)(3);
|
|
1711
|
+
const [laiZi, setLaiZi] = (0, react.useState)(false);
|
|
1712
|
+
const [title, setTitle] = (0, react.useState)("好友局");
|
|
1713
|
+
const [tab, setTab] = (0, react.useState)("create");
|
|
1714
|
+
const [joinCode, setJoinCode] = (0, react.useState)("");
|
|
1715
|
+
const [joinInvite, setJoinInvite] = (0, react.useState)("");
|
|
1716
|
+
const [joinName, setJoinName] = (0, react.useState)("好友");
|
|
1717
|
+
const [state, setState] = (0, react.useState)(emptyState);
|
|
1718
|
+
const [creating, setCreating] = (0, react.useState)(false);
|
|
1719
|
+
const [joining, setJoining] = (0, react.useState)(false);
|
|
1720
|
+
const [roomId, setRoomId] = (0, react.useState)(null);
|
|
1721
|
+
const seq = (0, react.useMemo)(() => ({ value: 0 }), []);
|
|
1722
|
+
(0, react.useEffect)(() => {
|
|
1723
|
+
const onHash = () => {
|
|
1724
|
+
const match = /#\/doudizhu\/room\/([^/]+)/.exec(location.hash);
|
|
1725
|
+
if (match) setRoomId(match[1] ?? null);
|
|
1726
|
+
};
|
|
1727
|
+
onHash();
|
|
1728
|
+
window.addEventListener("hashchange", onHash);
|
|
1729
|
+
return () => {
|
|
1730
|
+
window.removeEventListener("hashchange", onHash);
|
|
1731
|
+
};
|
|
1732
|
+
}, []);
|
|
1733
|
+
(0, react.useEffect)(() => {
|
|
1734
|
+
if (!roomId || !state.view) return;
|
|
1735
|
+
}, [roomId, state.view]);
|
|
1736
|
+
const applyView = (view) => {
|
|
1737
|
+
setState((prev) => ({
|
|
1738
|
+
...prev,
|
|
1739
|
+
view,
|
|
1740
|
+
selected: prev.selected.filter((card) => view.you.cards.includes(card))
|
|
1741
|
+
}));
|
|
1742
|
+
};
|
|
1743
|
+
const startRoom = async () => {
|
|
1744
|
+
setCreating(true);
|
|
1745
|
+
setState((prev) => ({
|
|
1746
|
+
...prev,
|
|
1747
|
+
error: null
|
|
1748
|
+
}));
|
|
1749
|
+
try {
|
|
1750
|
+
const created = await createRoom({
|
|
1751
|
+
stakeM,
|
|
1752
|
+
maxMultiplier,
|
|
1753
|
+
seatCount,
|
|
1754
|
+
laiZi,
|
|
1755
|
+
hostDisplayName: "房主",
|
|
1756
|
+
title
|
|
1757
|
+
});
|
|
1758
|
+
setRoomId(created.roomId);
|
|
1759
|
+
setState((prev) => ({
|
|
1760
|
+
...prev,
|
|
1761
|
+
view: created.view,
|
|
1762
|
+
shareable: created.shareable,
|
|
1763
|
+
sitUrl: created.sitUrl,
|
|
1764
|
+
watchUrl: created.watchUrl,
|
|
1765
|
+
roomCode: created.roomCode
|
|
1766
|
+
}));
|
|
1767
|
+
location.hash = `#/doudizhu/room/${created.roomId}`;
|
|
1768
|
+
listenRoom(created.roomId, created.wsTicket);
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
setState((prev) => ({
|
|
1771
|
+
...prev,
|
|
1772
|
+
error: error instanceof Error ? error.message : "create failed"
|
|
1773
|
+
}));
|
|
1774
|
+
} finally {
|
|
1775
|
+
setCreating(false);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
const enterRoom = async () => {
|
|
1779
|
+
setJoining(true);
|
|
1780
|
+
setState((prev) => ({
|
|
1781
|
+
...prev,
|
|
1782
|
+
error: null
|
|
1783
|
+
}));
|
|
1784
|
+
try {
|
|
1785
|
+
const result = await joinRoom({
|
|
1786
|
+
roomCode: joinCode.trim(),
|
|
1787
|
+
invite: joinInvite.trim(),
|
|
1788
|
+
displayName: joinName,
|
|
1789
|
+
role: "sit"
|
|
1790
|
+
});
|
|
1791
|
+
setRoomId(result.roomId);
|
|
1792
|
+
applyView(result.view);
|
|
1793
|
+
setState((prev) => ({
|
|
1794
|
+
...prev,
|
|
1795
|
+
roomCode: result.view.room.roomCode
|
|
1796
|
+
}));
|
|
1797
|
+
location.hash = `#/doudizhu/room/${result.roomId}`;
|
|
1798
|
+
listenRoom(result.roomId, result.wsTicket);
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
setState((prev) => ({
|
|
1801
|
+
...prev,
|
|
1802
|
+
error: error instanceof Error ? error.message : "join failed"
|
|
1803
|
+
}));
|
|
1804
|
+
} finally {
|
|
1805
|
+
setJoining(false);
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
const listenRoom = (nextRoomId, ticket) => {
|
|
1809
|
+
connectChannel(ticket, (event) => {
|
|
1810
|
+
if (event.type === "snapshot") {
|
|
1811
|
+
seq.value = event.seq;
|
|
1812
|
+
applyView(event.view);
|
|
1813
|
+
}
|
|
1814
|
+
if (event.type === "settled") {
|
|
1815
|
+
seq.value = event.seq;
|
|
1816
|
+
setState((prev) => ({
|
|
1817
|
+
...prev,
|
|
1818
|
+
settlement: event.settlement
|
|
1819
|
+
}));
|
|
1820
|
+
}
|
|
1821
|
+
if (event.type === "reject") setState((prev) => ({
|
|
1822
|
+
...prev,
|
|
1823
|
+
error: event.reason
|
|
1824
|
+
}));
|
|
1825
|
+
}, {
|
|
1826
|
+
roomId: nextRoomId,
|
|
1827
|
+
seq: () => seq.value
|
|
1828
|
+
});
|
|
1829
|
+
};
|
|
1830
|
+
const command = (cmd) => {
|
|
1831
|
+
if (!roomId) return;
|
|
1832
|
+
sendCommand(roomId, cmd).catch((error) => {
|
|
1833
|
+
setState((prev) => ({
|
|
1834
|
+
...prev,
|
|
1835
|
+
error: error instanceof Error ? error.message : "command failed"
|
|
1836
|
+
}));
|
|
1837
|
+
});
|
|
1838
|
+
};
|
|
1839
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1840
|
+
className: styles_module_css_default.root,
|
|
1841
|
+
children: [
|
|
1842
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1843
|
+
className: styles_module_css_default.topbar,
|
|
1844
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1845
|
+
className: styles_module_css_default.brand,
|
|
1846
|
+
children: "斗地主"
|
|
1847
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1848
|
+
type: "button",
|
|
1849
|
+
className: styles_module_css_default.close,
|
|
1850
|
+
onClick: onClose,
|
|
1851
|
+
children: "关闭"
|
|
1852
|
+
})]
|
|
1853
|
+
}),
|
|
1854
|
+
state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1855
|
+
className: styles_module_css_default.banner,
|
|
1856
|
+
children: state.error
|
|
1857
|
+
}) : null,
|
|
1858
|
+
!state.view ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LobbyView, {
|
|
1859
|
+
tab,
|
|
1860
|
+
onTab: setTab,
|
|
1861
|
+
title,
|
|
1862
|
+
stakeM,
|
|
1863
|
+
maxMultiplier,
|
|
1864
|
+
seatCount,
|
|
1865
|
+
laiZi,
|
|
1866
|
+
onTitle: setTitle,
|
|
1867
|
+
onStake: setStakeM,
|
|
1868
|
+
onCap: setMaxMultiplier,
|
|
1869
|
+
onSeats: setSeatCount,
|
|
1870
|
+
onLaiZi: setLaiZi,
|
|
1871
|
+
onCreate: () => {
|
|
1872
|
+
startRoom();
|
|
1873
|
+
},
|
|
1874
|
+
creating,
|
|
1875
|
+
error: state.error,
|
|
1876
|
+
roomCode: state.roomCode,
|
|
1877
|
+
sitUrl: state.sitUrl,
|
|
1878
|
+
watchUrl: state.watchUrl,
|
|
1879
|
+
shareable: state.shareable,
|
|
1880
|
+
welcomeAtoms: DEFAULT_WELCOME_ATOMS,
|
|
1881
|
+
join: {
|
|
1882
|
+
code: joinCode,
|
|
1883
|
+
invite: joinInvite,
|
|
1884
|
+
name: joinName,
|
|
1885
|
+
joining,
|
|
1886
|
+
onCode: setJoinCode,
|
|
1887
|
+
onInvite: setJoinInvite,
|
|
1888
|
+
onName: setJoinName,
|
|
1889
|
+
onJoin: () => {
|
|
1890
|
+
enterRoom();
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}) : state.settlement && state.view.room.phase === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettlementView, {
|
|
1894
|
+
settlement: state.settlement,
|
|
1895
|
+
onRematch: () => {
|
|
1896
|
+
setState((prev) => ({
|
|
1897
|
+
...prev,
|
|
1898
|
+
settlement: null
|
|
1899
|
+
}));
|
|
1900
|
+
command({ type: "rematch" });
|
|
1901
|
+
}
|
|
1902
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.roomCode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1903
|
+
className: styles_module_css_default.banner,
|
|
1904
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoomCodeBar, {
|
|
1905
|
+
title: state.view.room.title,
|
|
1906
|
+
roomCode: state.roomCode,
|
|
1907
|
+
sitUrl: state.sitUrl,
|
|
1908
|
+
shareable: state.shareable,
|
|
1909
|
+
canRename: true,
|
|
1910
|
+
onRename: (next) => {
|
|
1911
|
+
command({
|
|
1912
|
+
type: "rename",
|
|
1913
|
+
title: next
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
})
|
|
1917
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TableView, {
|
|
1918
|
+
view: state.view,
|
|
1919
|
+
selected: state.selected,
|
|
1920
|
+
onToggle: (card) => {
|
|
1921
|
+
setState((prev) => ({
|
|
1922
|
+
...prev,
|
|
1923
|
+
selected: toggleCard(prev.selected, card)
|
|
1924
|
+
}));
|
|
1925
|
+
},
|
|
1926
|
+
onBid: (action) => {
|
|
1927
|
+
command({
|
|
1928
|
+
type: "bid",
|
|
1929
|
+
action
|
|
1930
|
+
});
|
|
1931
|
+
},
|
|
1932
|
+
onDouble: (action) => {
|
|
1933
|
+
command({
|
|
1934
|
+
type: "double",
|
|
1935
|
+
action
|
|
1936
|
+
});
|
|
1937
|
+
},
|
|
1938
|
+
onMingPai: () => {
|
|
1939
|
+
command({ type: "mingPai" });
|
|
1940
|
+
},
|
|
1941
|
+
onPlay: () => {
|
|
1942
|
+
command({
|
|
1943
|
+
type: "play",
|
|
1944
|
+
cards: state.selected,
|
|
1945
|
+
nonce: crypto.randomUUID()
|
|
1946
|
+
});
|
|
1947
|
+
},
|
|
1948
|
+
onPass: () => {
|
|
1949
|
+
command({
|
|
1950
|
+
type: "pass",
|
|
1951
|
+
nonce: crypto.randomUUID()
|
|
1952
|
+
});
|
|
1953
|
+
},
|
|
1954
|
+
onReady: (ready) => {
|
|
1955
|
+
command({
|
|
1956
|
+
type: "ready",
|
|
1957
|
+
ready
|
|
1958
|
+
});
|
|
1959
|
+
},
|
|
1960
|
+
onChat: (text) => {
|
|
1961
|
+
command({
|
|
1962
|
+
type: "chat",
|
|
1963
|
+
text
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
})] })
|
|
1967
|
+
]
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
//#endregion
|
|
1972
|
+
//#region src/client/SettingsCard.tsx
|
|
1973
|
+
function draftFrom(value) {
|
|
1974
|
+
return {
|
|
1975
|
+
publicBaseUrl: value.publicBaseUrl ?? "",
|
|
1976
|
+
welcomeAtoms: value.welcomeAtoms ?? "200000000",
|
|
1977
|
+
defaultSeatCount: value.defaultSeatCount === 4 ? 4 : 3,
|
|
1978
|
+
defaultMaxMultiplier: value.defaultMaxMultiplier ?? 8,
|
|
1979
|
+
defaultLaiZi: value.defaultLaiZi === true,
|
|
1980
|
+
spectatorCardCounter: value.spectatorCardCounter === true
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
function draftsEqual(left, right) {
|
|
1984
|
+
return left.publicBaseUrl === right.publicBaseUrl && left.welcomeAtoms === right.welcomeAtoms && left.defaultSeatCount === right.defaultSeatCount && left.defaultMaxMultiplier === right.defaultMaxMultiplier && left.defaultLaiZi === right.defaultLaiZi && left.spectatorCardCounter === right.spectatorCardCounter;
|
|
1985
|
+
}
|
|
1986
|
+
function validateDraft(draft) {
|
|
1987
|
+
const publicBaseUrl = draft.publicBaseUrl.trim().replace(/\/+$/, "");
|
|
1988
|
+
if (publicBaseUrl !== "") {
|
|
1989
|
+
let parsed;
|
|
1990
|
+
try {
|
|
1991
|
+
parsed = new URL(publicBaseUrl);
|
|
1992
|
+
} catch {
|
|
1993
|
+
return "publicBaseUrl 必须是绝对 http(s) origin,不要路径或尾斜杠";
|
|
1994
|
+
}
|
|
1995
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return "publicBaseUrl 必须是 http 或 https";
|
|
1996
|
+
if (parsed.origin !== publicBaseUrl) return "publicBaseUrl 必须是 origin,不要路径、查询或尾斜杠";
|
|
1997
|
+
}
|
|
1998
|
+
if (!/^\d+$/.test(draft.welcomeAtoms)) return "欢迎积分必须是非负整数";
|
|
1999
|
+
if (![
|
|
2000
|
+
8,
|
|
2001
|
+
16,
|
|
2002
|
+
32,
|
|
2003
|
+
64
|
|
2004
|
+
].includes(draft.defaultMaxMultiplier)) return "倍数封顶只能是 8、16、32 或 64";
|
|
2005
|
+
return null;
|
|
2006
|
+
}
|
|
2007
|
+
function SettingsCard({ scope }) {
|
|
2008
|
+
const [snap, setSnap] = (0, react.useState)(() => scope.getSnapshot());
|
|
2009
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2010
|
+
const [draft, setDraft] = (0, react.useState)(() => snap.value ? draftFrom(snap.value) : null);
|
|
2011
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
2012
|
+
const [saved, setSaved] = (0, react.useState)(false);
|
|
2013
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2014
|
+
(0, react.useEffect)(() => scope.subscribe(() => {
|
|
2015
|
+
const next = scope.getSnapshot();
|
|
2016
|
+
setSnap((prevSnap) => {
|
|
2017
|
+
if (next.value) {
|
|
2018
|
+
const incoming = draftFrom(next.value);
|
|
2019
|
+
const previousStored = prevSnap.value ? draftFrom(prevSnap.value) : incoming;
|
|
2020
|
+
setDraft((prev) => {
|
|
2021
|
+
if (!prev || draftsEqual(prev, previousStored)) return incoming;
|
|
2022
|
+
return prev;
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
return next;
|
|
2026
|
+
});
|
|
2027
|
+
}), [scope]);
|
|
2028
|
+
(0, react.useEffect)(() => {
|
|
2029
|
+
if (draft || !snap.value) return;
|
|
2030
|
+
setDraft(draftFrom(snap.value));
|
|
2031
|
+
}, [draft, snap.value]);
|
|
2032
|
+
const value = snap.value;
|
|
2033
|
+
if (!value || !draft) return null;
|
|
2034
|
+
const stored = draftFrom(value);
|
|
2035
|
+
const dirty = !draftsEqual(draft, stored);
|
|
2036
|
+
const patch = (field, next) => {
|
|
2037
|
+
setSaved(false);
|
|
2038
|
+
setError(null);
|
|
2039
|
+
setDraft((prev) => prev ? {
|
|
2040
|
+
...prev,
|
|
2041
|
+
[field]: next
|
|
2042
|
+
} : prev);
|
|
2043
|
+
};
|
|
2044
|
+
const save = async () => {
|
|
2045
|
+
const invalid = validateDraft(draft);
|
|
2046
|
+
if (invalid) {
|
|
2047
|
+
setError(invalid);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
const next = {
|
|
2051
|
+
...draft,
|
|
2052
|
+
publicBaseUrl: draft.publicBaseUrl.trim().replace(/\/+$/, "")
|
|
2053
|
+
};
|
|
2054
|
+
setDraft(next);
|
|
2055
|
+
setSaving(true);
|
|
2056
|
+
setError(null);
|
|
2057
|
+
try {
|
|
2058
|
+
const writes = [
|
|
2059
|
+
["publicBaseUrl", next.publicBaseUrl],
|
|
2060
|
+
["welcomeAtoms", next.welcomeAtoms],
|
|
2061
|
+
["defaultSeatCount", next.defaultSeatCount],
|
|
2062
|
+
["defaultMaxMultiplier", next.defaultMaxMultiplier],
|
|
2063
|
+
["defaultLaiZi", next.defaultLaiZi],
|
|
2064
|
+
["spectatorCardCounter", next.spectatorCardCounter]
|
|
2065
|
+
];
|
|
2066
|
+
for (const [field, fieldValue] of writes) {
|
|
2067
|
+
if (stored[field] === fieldValue) continue;
|
|
2068
|
+
await scope.set(String(field), fieldValue);
|
|
2069
|
+
}
|
|
2070
|
+
setSaved(true);
|
|
2071
|
+
} catch (cause) {
|
|
2072
|
+
setError(cause instanceof Error ? cause.message : "保存失败");
|
|
2073
|
+
} finally {
|
|
2074
|
+
setSaving(false);
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2078
|
+
className: `${styles_module_css_default.settingsCard} ${open ? styles_module_css_default.settingsOpen : ""}`,
|
|
2079
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2080
|
+
type: "button",
|
|
2081
|
+
className: styles_module_css_default.settingsHeader,
|
|
2082
|
+
"aria-expanded": open,
|
|
2083
|
+
onClick: () => {
|
|
2084
|
+
setOpen(!open);
|
|
2085
|
+
},
|
|
2086
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2087
|
+
className: styles_module_css_default.settingsHeadText,
|
|
2088
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2089
|
+
className: styles_module_css_default.settingsName,
|
|
2090
|
+
children: "dsh-poker"
|
|
2091
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2092
|
+
className: styles_module_css_default.settingsDesc,
|
|
2093
|
+
children: "本机房间默认:人数、癞子、邀请地址和欢迎积分。同一房间先到入座,后来观战。"
|
|
2094
|
+
})]
|
|
2095
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2096
|
+
className: `${styles_module_css_default.settingsChevron} ${open ? styles_module_css_default.settingsChevronOpen : ""}`,
|
|
2097
|
+
width: "14",
|
|
2098
|
+
height: "14",
|
|
2099
|
+
viewBox: "0 0 14 14",
|
|
2100
|
+
"aria-hidden": "true",
|
|
2101
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
2102
|
+
d: "M3.2 5.1 7 8.9l3.8-3.8",
|
|
2103
|
+
fill: "none",
|
|
2104
|
+
stroke: "currentColor",
|
|
2105
|
+
strokeWidth: "1.4",
|
|
2106
|
+
strokeLinecap: "round",
|
|
2107
|
+
strokeLinejoin: "round"
|
|
2108
|
+
})
|
|
2109
|
+
})]
|
|
2110
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2111
|
+
className: styles_module_css_default.settingsBody,
|
|
2112
|
+
children: [
|
|
2113
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2114
|
+
className: styles_module_css_default.settingsHint,
|
|
2115
|
+
children: "开房在侧栏 New Session 下面的「斗地主」,这里只改下次开房的默认值。点保存后生效,已经开着的房间不动。"
|
|
2116
|
+
}),
|
|
2117
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2118
|
+
className: styles_module_css_default.settingsField,
|
|
2119
|
+
children: [
|
|
2120
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2121
|
+
className: styles_module_css_default.settingsLabel,
|
|
2122
|
+
children: "对外地址 publicBaseUrl"
|
|
2123
|
+
}),
|
|
2124
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2125
|
+
className: styles_module_css_default.settingsInput,
|
|
2126
|
+
value: draft.publicBaseUrl,
|
|
2127
|
+
placeholder: "https://your-named-tunnel.example",
|
|
2128
|
+
onChange: (event) => {
|
|
2129
|
+
patch("publicBaseUrl", event.target.value);
|
|
2130
|
+
}
|
|
2131
|
+
}),
|
|
2132
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2133
|
+
className: styles_module_css_default.settingsFieldHint,
|
|
2134
|
+
children: "填能打到本机的 https origin,好友才能用邀请链接进桌。空着只能本机标签页互打。"
|
|
2135
|
+
})
|
|
2136
|
+
]
|
|
2137
|
+
}),
|
|
2138
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2139
|
+
className: styles_module_css_default.settingsField,
|
|
2140
|
+
children: [
|
|
2141
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2142
|
+
className: styles_module_css_default.settingsLabel,
|
|
2143
|
+
children: "欢迎积分"
|
|
2144
|
+
}),
|
|
2145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2146
|
+
className: styles_module_css_default.settingsInput,
|
|
2147
|
+
inputMode: "numeric",
|
|
2148
|
+
value: draft.welcomeAtoms,
|
|
2149
|
+
onChange: (event) => {
|
|
2150
|
+
patch("welcomeAtoms", event.target.value);
|
|
2151
|
+
}
|
|
2152
|
+
}),
|
|
2153
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2154
|
+
className: styles_module_css_default.settingsFieldHint,
|
|
2155
|
+
children: "进房时账本里的起始积分,不是 DeepSeek 平台余额。默认 200M。"
|
|
2156
|
+
})
|
|
2157
|
+
]
|
|
2158
|
+
}),
|
|
2159
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2160
|
+
className: styles_module_css_default.settingsField,
|
|
2161
|
+
children: [
|
|
2162
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2163
|
+
className: styles_module_css_default.settingsLabel,
|
|
2164
|
+
children: "默认人数"
|
|
2165
|
+
}),
|
|
2166
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2167
|
+
className: styles_module_css_default.settingsInput,
|
|
2168
|
+
value: draft.defaultSeatCount,
|
|
2169
|
+
onChange: (event) => {
|
|
2170
|
+
patch("defaultSeatCount", Number(event.target.value) === 4 ? 4 : 3);
|
|
2171
|
+
},
|
|
2172
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2173
|
+
value: 3,
|
|
2174
|
+
children: "3 人"
|
|
2175
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2176
|
+
value: 4,
|
|
2177
|
+
children: "4 人"
|
|
2178
|
+
})]
|
|
2179
|
+
}),
|
|
2180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2181
|
+
className: styles_module_css_default.settingsFieldHint,
|
|
2182
|
+
children: "3 人一副牌 3 张底牌;4 人两副牌 8 张底牌。"
|
|
2183
|
+
})
|
|
2184
|
+
]
|
|
2185
|
+
}),
|
|
2186
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2187
|
+
className: styles_module_css_default.settingsField,
|
|
2188
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2189
|
+
className: styles_module_css_default.settingsLabel,
|
|
2190
|
+
children: "默认倍数封顶"
|
|
2191
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2192
|
+
className: styles_module_css_default.settingsInput,
|
|
2193
|
+
inputMode: "numeric",
|
|
2194
|
+
value: String(draft.defaultMaxMultiplier),
|
|
2195
|
+
onChange: (event) => {
|
|
2196
|
+
patch("defaultMaxMultiplier", Number(event.target.value));
|
|
2197
|
+
}
|
|
2198
|
+
})]
|
|
2199
|
+
}),
|
|
2200
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2201
|
+
className: styles_module_css_default.settingsField,
|
|
2202
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2203
|
+
className: styles_module_css_default.settingsSwitchRow,
|
|
2204
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2205
|
+
className: styles_module_css_default.settingsLabel,
|
|
2206
|
+
children: "默认开癞子"
|
|
2207
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2208
|
+
type: "button",
|
|
2209
|
+
className: styles_module_css_default.settingsSwitch,
|
|
2210
|
+
role: "switch",
|
|
2211
|
+
"aria-checked": draft.defaultLaiZi,
|
|
2212
|
+
onClick: () => {
|
|
2213
|
+
patch("defaultLaiZi", !draft.defaultLaiZi);
|
|
2214
|
+
},
|
|
2215
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2216
|
+
className: styles_module_css_default.settingsSwitchTrack,
|
|
2217
|
+
"data-on": draft.defaultLaiZi ? "" : void 0,
|
|
2218
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: styles_module_css_default.settingsSwitchThumb })
|
|
2219
|
+
})
|
|
2220
|
+
})]
|
|
2221
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2222
|
+
className: styles_module_css_default.settingsFieldHint,
|
|
2223
|
+
children: "下次开房默认癞子。翻两张万能牌,大小王不当癞子。"
|
|
2224
|
+
})]
|
|
2225
|
+
}),
|
|
2226
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2227
|
+
className: styles_module_css_default.settingsField,
|
|
2228
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2229
|
+
className: styles_module_css_default.settingsSwitchRow,
|
|
2230
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2231
|
+
className: styles_module_css_default.settingsLabel,
|
|
2232
|
+
children: "旁观记牌器"
|
|
2233
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2234
|
+
type: "button",
|
|
2235
|
+
className: styles_module_css_default.settingsSwitch,
|
|
2236
|
+
role: "switch",
|
|
2237
|
+
"aria-checked": draft.spectatorCardCounter,
|
|
2238
|
+
onClick: () => {
|
|
2239
|
+
patch("spectatorCardCounter", !draft.spectatorCardCounter);
|
|
2240
|
+
},
|
|
2241
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2242
|
+
className: styles_module_css_default.settingsSwitchTrack,
|
|
2243
|
+
"data-on": draft.spectatorCardCounter ? "" : void 0,
|
|
2244
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: styles_module_css_default.settingsSwitchThumb })
|
|
2245
|
+
})
|
|
2246
|
+
})]
|
|
2247
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2248
|
+
className: styles_module_css_default.settingsFieldHint,
|
|
2249
|
+
children: "观战可见剩余牌张。默认关。"
|
|
2250
|
+
})]
|
|
2251
|
+
}),
|
|
2252
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2253
|
+
className: styles_module_css_default.settingsActions,
|
|
2254
|
+
children: [
|
|
2255
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2256
|
+
type: "button",
|
|
2257
|
+
className: styles_module_css_default.primary,
|
|
2258
|
+
disabled: saving || !dirty || snap.writable === false,
|
|
2259
|
+
onClick: () => {
|
|
2260
|
+
save();
|
|
2261
|
+
},
|
|
2262
|
+
children: saving ? "保存中…" : "Save"
|
|
2263
|
+
}),
|
|
2264
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2265
|
+
type: "button",
|
|
2266
|
+
className: styles_module_css_default.ghost,
|
|
2267
|
+
disabled: saving || !dirty,
|
|
2268
|
+
onClick: () => {
|
|
2269
|
+
setDraft(stored);
|
|
2270
|
+
setSaved(false);
|
|
2271
|
+
setError(null);
|
|
2272
|
+
},
|
|
2273
|
+
children: "Discard"
|
|
2274
|
+
}),
|
|
2275
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2276
|
+
className: styles_module_css_default.settingsSaveError,
|
|
2277
|
+
children: error
|
|
2278
|
+
}) : saved && !dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2279
|
+
className: styles_module_css_default.settingsSaveOk,
|
|
2280
|
+
children: "已保存,下次开房生效"
|
|
2281
|
+
}) : dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2282
|
+
className: styles_module_css_default.settingsFieldHint,
|
|
2283
|
+
children: "有未保存的更改"
|
|
2284
|
+
}) : null
|
|
2285
|
+
]
|
|
2286
|
+
}),
|
|
2287
|
+
snap.status === "unavailable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2288
|
+
className: styles_module_css_default.settingsHint,
|
|
2289
|
+
children: "没挂 storageDomain 时房间只在内存里,重启就没了。"
|
|
2290
|
+
}) : null
|
|
2291
|
+
]
|
|
2292
|
+
}) : null]
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
//#endregion
|
|
2297
|
+
//#region src/client/slots.tsx
|
|
2298
|
+
function registerClientSlots(ctx) {
|
|
2299
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
2300
|
+
name: "sidebar.footer.action",
|
|
2301
|
+
id: "doudizhu"
|
|
2302
|
+
}, DouDizhuSidebarButton));
|
|
2303
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
2304
|
+
name: "shell.overlay",
|
|
2305
|
+
id: "doudizhu"
|
|
2306
|
+
}, DouDizhuOverlay));
|
|
2307
|
+
ctx.slots.inject("settings.plugin.item", () => {
|
|
2308
|
+
const scope = ctx.settingsScope.bind({ namespace: "doudizhu" });
|
|
2309
|
+
return ctx.slots.register({
|
|
2310
|
+
name: "settings.plugin.item",
|
|
2311
|
+
key: "doudizhu"
|
|
2312
|
+
}, function DouDizhuSettingsCard() {
|
|
2313
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsCard, { scope });
|
|
2314
|
+
});
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
function DouDizhuSidebarButton({ wide }) {
|
|
2318
|
+
const [mount, setMount] = (0, react.useState)(null);
|
|
2319
|
+
(0, react.useEffect)(() => {
|
|
2320
|
+
let host = null;
|
|
2321
|
+
const place = () => {
|
|
2322
|
+
const after = findNewSessionAnchor();
|
|
2323
|
+
if (!after) return;
|
|
2324
|
+
if (!host) {
|
|
2325
|
+
host = document.createElement("div");
|
|
2326
|
+
host.dataset.doudizhuEntry = "";
|
|
2327
|
+
}
|
|
2328
|
+
if (host.previousElementSibling !== after || !host.isConnected) after.insertAdjacentElement("afterend", host);
|
|
2329
|
+
setMount((prev) => prev === host ? prev : host);
|
|
2330
|
+
};
|
|
2331
|
+
place();
|
|
2332
|
+
const observer = new MutationObserver(place);
|
|
2333
|
+
observer.observe(document.body, {
|
|
2334
|
+
childList: true,
|
|
2335
|
+
subtree: true
|
|
2336
|
+
});
|
|
2337
|
+
return () => {
|
|
2338
|
+
observer.disconnect();
|
|
2339
|
+
host?.remove();
|
|
2340
|
+
};
|
|
2341
|
+
}, []);
|
|
2342
|
+
(0, react.useEffect)(() => {
|
|
2343
|
+
if (!mount) return;
|
|
2344
|
+
const slot = styles_module_css_default.sidebarSlot ?? "";
|
|
2345
|
+
const rail = styles_module_css_default.sidebarSlotRail ?? "";
|
|
2346
|
+
mount.className = wide ? slot : `${slot} ${rail}`;
|
|
2347
|
+
}, [mount, wide]);
|
|
2348
|
+
if (!mount) return null;
|
|
2349
|
+
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2350
|
+
type: "button",
|
|
2351
|
+
className: `${styles_module_css_default.sidebarBtn} ${wide ? "" : styles_module_css_default.sidebarBtnRail}`,
|
|
2352
|
+
onClick: () => {
|
|
2353
|
+
location.hash = "#/doudizhu";
|
|
2354
|
+
},
|
|
2355
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: wide ? "斗地主" : "斗" }), wide ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2356
|
+
className: styles_module_css_default.muted,
|
|
2357
|
+
children: "dsh-poker"
|
|
2358
|
+
}) : null]
|
|
2359
|
+
}), mount);
|
|
2360
|
+
}
|
|
2361
|
+
function findNewSessionAnchor() {
|
|
2362
|
+
const labeled = document.querySelectorAll("button[aria-label=\"New session\"], button[aria-label=\"新建会话\"]");
|
|
2363
|
+
const button = labeled[labeled.length - 1] ?? [...document.querySelectorAll("button")].find((el) => {
|
|
2364
|
+
const text = el.textContent?.replace(/\s+/g, " ").trim();
|
|
2365
|
+
return text === "New Session" || text === "新会话";
|
|
2366
|
+
}) ?? null;
|
|
2367
|
+
if (!button) return null;
|
|
2368
|
+
let node = button;
|
|
2369
|
+
let parent = node.parentElement;
|
|
2370
|
+
while (parent) {
|
|
2371
|
+
const style = getComputedStyle(parent);
|
|
2372
|
+
if (style.display === "flex" && style.flexDirection === "column") return node;
|
|
2373
|
+
node = parent;
|
|
2374
|
+
parent = node.parentElement;
|
|
2375
|
+
}
|
|
2376
|
+
return button;
|
|
2377
|
+
}
|
|
2378
|
+
function DouDizhuOverlay() {
|
|
2379
|
+
const [open, setOpen] = (0, react.useState)(() => location.hash.startsWith("#/doudizhu"));
|
|
2380
|
+
useHashOpen(setOpen);
|
|
2381
|
+
if (!open) return null;
|
|
2382
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostApp, { onClose: () => {
|
|
2383
|
+
location.hash = "#/";
|
|
2384
|
+
} });
|
|
2385
|
+
}
|
|
2386
|
+
function useHashOpen(setOpen) {
|
|
2387
|
+
(0, react.useEffect)(() => {
|
|
2388
|
+
const sync = () => {
|
|
2389
|
+
setOpen(location.hash.startsWith("#/doudizhu"));
|
|
2390
|
+
};
|
|
2391
|
+
window.addEventListener("hashchange", sync);
|
|
2392
|
+
return () => {
|
|
2393
|
+
window.removeEventListener("hashchange", sync);
|
|
2394
|
+
};
|
|
2395
|
+
}, [setOpen]);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
//#endregion
|
|
2399
|
+
//#region src/client/index.tsx
|
|
2400
|
+
const name = "doudizhu-client";
|
|
2401
|
+
const inject = [
|
|
2402
|
+
"slots",
|
|
2403
|
+
"connection",
|
|
2404
|
+
"remote",
|
|
2405
|
+
"settingsScope"
|
|
2406
|
+
];
|
|
2407
|
+
function apply(ctx) {
|
|
2408
|
+
ctx.effect(() => {
|
|
2409
|
+
registerClientSlots(ctx);
|
|
2410
|
+
return () => {};
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
//#endregion
|
|
2415
|
+
exports.apply = apply;
|
|
2416
|
+
exports.inject = inject;
|
|
2417
|
+
exports.name = name;
|
|
2418
|
+
return module.exports; } });
|
|
2419
|
+
//# sourceMappingURL=client.js.map
|