sa2kit 1.6.33 → 1.6.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calendar/index.js +11 -8
- package/dist/calendar/index.js.map +1 -1
- package/dist/calendar/index.mjs +4 -1
- package/dist/calendar/index.mjs.map +1 -1
- package/dist/chunk-EGJPS7OL.mjs +98 -0
- package/dist/chunk-EGJPS7OL.mjs.map +1 -0
- package/dist/{chunk-EI27JKND.mjs → chunk-GMIUSZXC.mjs} +2 -2
- package/dist/{chunk-EI27JKND.mjs.map → chunk-GMIUSZXC.mjs.map} +1 -1
- package/dist/chunk-HHVDOIPV.js +105 -0
- package/dist/chunk-HHVDOIPV.js.map +1 -0
- package/dist/chunk-PMTY2AI4.js +1110 -0
- package/dist/chunk-PMTY2AI4.js.map +1 -0
- package/dist/{chunk-XGBE4SUV.js → chunk-SCDDMIF6.js} +2 -2
- package/dist/{chunk-XGBE4SUV.js.map → chunk-SCDDMIF6.js.map} +1 -1
- package/dist/chunk-SR4JFEHW.mjs +1071 -0
- package/dist/chunk-SR4JFEHW.mjs.map +1 -0
- package/dist/index.d.mts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +179 -109
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/mikuFireworks3D/index.d.mts +174 -0
- package/dist/mikuFireworks3D/index.d.ts +174 -0
- package/dist/mikuFireworks3D/index.js +69 -0
- package/dist/mikuFireworks3D/index.js.map +1 -0
- package/dist/mikuFireworks3D/index.mjs +4 -0
- package/dist/mikuFireworks3D/index.mjs.map +1 -0
- package/dist/mikuFireworks3D/server/index.d.mts +146 -0
- package/dist/mikuFireworks3D/server/index.d.ts +146 -0
- package/dist/mikuFireworks3D/server/index.js +338 -0
- package/dist/mikuFireworks3D/server/index.js.map +1 -0
- package/dist/mikuFireworks3D/server/index.mjs +335 -0
- package/dist/mikuFireworks3D/server/index.mjs.map +1 -0
- package/dist/mikuFusionGame/index.d.mts +7 -2
- package/dist/mikuFusionGame/index.d.ts +7 -2
- package/dist/mikuFusionGame/index.js +141 -10
- package/dist/mikuFusionGame/index.js.map +1 -1
- package/dist/mikuFusionGame/index.mjs +141 -10
- package/dist/mikuFusionGame/index.mjs.map +1 -1
- package/dist/mmd/index.js +1 -1
- package/dist/mmd/index.mjs +2 -2
- package/dist/types-Cgk9zWhO.d.mts +70 -0
- package/dist/types-Cgk9zWhO.d.ts +70 -0
- package/dist/universalFile/server/index.js +5 -5
- package/dist/universalFile/server/index.mjs +1 -1
- package/package.json +11 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import '../../chunk-BJTO5JO5.mjs';
|
|
2
|
+
|
|
3
|
+
// src/mikuFireworks3D/server/FireworksRoomHub.ts
|
|
4
|
+
var DEFAULT_MAX_USERS_PER_ROOM = 200;
|
|
5
|
+
var DEFAULT_HISTORY_LIMIT = 50;
|
|
6
|
+
var DEFAULT_MAX_DANMAKU_LENGTH = 64;
|
|
7
|
+
var FireworksRoomHub = class {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.rooms = /* @__PURE__ */ new Map();
|
|
10
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
11
|
+
this.maxUsersPerRoom = options?.maxUsersPerRoom ?? DEFAULT_MAX_USERS_PER_ROOM;
|
|
12
|
+
this.historyLimit = options?.historyLimit ?? DEFAULT_HISTORY_LIMIT;
|
|
13
|
+
this.maxDanmakuLength = options?.maxDanmakuLength ?? DEFAULT_MAX_DANMAKU_LENGTH;
|
|
14
|
+
this.logger = options?.logger;
|
|
15
|
+
}
|
|
16
|
+
connect(transport, id) {
|
|
17
|
+
const connectionId = id ?? createId("conn");
|
|
18
|
+
const session = {
|
|
19
|
+
id: connectionId,
|
|
20
|
+
transport
|
|
21
|
+
};
|
|
22
|
+
this.sessions.set(connectionId, session);
|
|
23
|
+
return {
|
|
24
|
+
id: connectionId,
|
|
25
|
+
handleMessage: (raw) => this.handleRawMessage(connectionId, raw),
|
|
26
|
+
disconnect: (reason) => this.disconnect(connectionId, reason),
|
|
27
|
+
send: (message) => this.sendToConnection(connectionId, message)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
disconnect(connectionId, reason) {
|
|
31
|
+
const session = this.sessions.get(connectionId);
|
|
32
|
+
if (!session) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.leaveRoom(session, false);
|
|
36
|
+
this.sessions.delete(connectionId);
|
|
37
|
+
if (reason) {
|
|
38
|
+
this.logger?.info?.(`[FireworksRoomHub] disconnected ${connectionId}: ${reason}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
getStats() {
|
|
42
|
+
return {
|
|
43
|
+
rooms: this.rooms.size,
|
|
44
|
+
connections: this.sessions.size
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
getRoomOnlineCount(roomId) {
|
|
48
|
+
return this.rooms.get(roomId)?.connections.size ?? 0;
|
|
49
|
+
}
|
|
50
|
+
handleRawMessage(connectionId, raw) {
|
|
51
|
+
const message = parseClientMessage(raw);
|
|
52
|
+
if (!message.ok) {
|
|
53
|
+
this.sendError(connectionId, "BAD_MESSAGE", message.error);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const session = this.sessions.get(connectionId);
|
|
57
|
+
if (!session) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const clientMessage = message.value;
|
|
61
|
+
switch (clientMessage.type) {
|
|
62
|
+
case "join": {
|
|
63
|
+
this.joinRoom(session, clientMessage.roomId, clientMessage.user);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
case "leave": {
|
|
67
|
+
this.leaveRoom(session, true);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
case "danmaku.send": {
|
|
71
|
+
this.handleDanmaku(session, clientMessage.payload);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
case "firework.launch": {
|
|
75
|
+
this.handleFirework(session, clientMessage.payload);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
case "ping": {
|
|
79
|
+
this.sendToConnection(connectionId, {
|
|
80
|
+
type: "pong",
|
|
81
|
+
ts: clientMessage.ts ?? Date.now()
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
default: {
|
|
86
|
+
this.sendError(connectionId, "UNSUPPORTED_MESSAGE", "Unsupported message type.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
joinRoom(session, roomId, user) {
|
|
91
|
+
const normalizedRoomId = roomId.trim();
|
|
92
|
+
const normalizedUserId = user.userId.trim();
|
|
93
|
+
if (!normalizedRoomId) {
|
|
94
|
+
this.sendError(session.id, "ROOM_ID_REQUIRED", "roomId is required.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!normalizedUserId) {
|
|
98
|
+
this.sendError(session.id, "USER_ID_REQUIRED", "user.userId is required.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (session.roomId && session.roomId !== normalizedRoomId) {
|
|
102
|
+
this.leaveRoom(session, true);
|
|
103
|
+
}
|
|
104
|
+
const room = this.rooms.get(normalizedRoomId) ?? this.createRoom(normalizedRoomId);
|
|
105
|
+
if (room.connections.size >= this.maxUsersPerRoom && !room.connections.has(session.id)) {
|
|
106
|
+
this.sendError(session.id, "ROOM_FULL", "Room is full.");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const identity = {
|
|
110
|
+
userId: normalizedUserId,
|
|
111
|
+
nickname: user.nickname,
|
|
112
|
+
avatarUrl: user.avatarUrl
|
|
113
|
+
};
|
|
114
|
+
const wasMember = room.connections.has(session.id);
|
|
115
|
+
room.connections.add(session.id);
|
|
116
|
+
room.usersByConnection.set(session.id, identity);
|
|
117
|
+
session.roomId = normalizedRoomId;
|
|
118
|
+
session.user = identity;
|
|
119
|
+
this.sendToConnection(session.id, {
|
|
120
|
+
type: "joined",
|
|
121
|
+
roomId: normalizedRoomId,
|
|
122
|
+
self: identity,
|
|
123
|
+
onlineCount: room.connections.size
|
|
124
|
+
});
|
|
125
|
+
this.sendToConnection(session.id, {
|
|
126
|
+
type: "room.snapshot",
|
|
127
|
+
roomId: normalizedRoomId,
|
|
128
|
+
users: Array.from(room.usersByConnection.values()),
|
|
129
|
+
danmakuHistory: [...room.danmakuHistory],
|
|
130
|
+
fireworkHistory: [...room.fireworkHistory]
|
|
131
|
+
});
|
|
132
|
+
if (!wasMember) {
|
|
133
|
+
this.broadcastToRoom(normalizedRoomId, {
|
|
134
|
+
type: "room.user_joined",
|
|
135
|
+
roomId: normalizedRoomId,
|
|
136
|
+
user: identity,
|
|
137
|
+
onlineCount: room.connections.size
|
|
138
|
+
}, session.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
leaveRoom(session, notify) {
|
|
142
|
+
const roomId = session.roomId;
|
|
143
|
+
if (!roomId) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const room = this.rooms.get(roomId);
|
|
147
|
+
if (!room) {
|
|
148
|
+
session.roomId = void 0;
|
|
149
|
+
session.user = void 0;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const leavingUserId = room.usersByConnection.get(session.id)?.userId;
|
|
153
|
+
room.connections.delete(session.id);
|
|
154
|
+
room.usersByConnection.delete(session.id);
|
|
155
|
+
session.roomId = void 0;
|
|
156
|
+
session.user = void 0;
|
|
157
|
+
if (notify && leavingUserId) {
|
|
158
|
+
this.broadcastToRoom(roomId, {
|
|
159
|
+
type: "room.user_left",
|
|
160
|
+
roomId,
|
|
161
|
+
userId: leavingUserId,
|
|
162
|
+
onlineCount: room.connections.size
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (room.connections.size === 0) {
|
|
166
|
+
this.rooms.delete(roomId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
handleDanmaku(session, payload) {
|
|
170
|
+
if (!session.roomId || !session.user) {
|
|
171
|
+
this.sendError(session.id, "NOT_JOINED", "Join a room before sending danmaku.");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const room = this.rooms.get(session.roomId);
|
|
175
|
+
if (!room) {
|
|
176
|
+
this.sendError(session.id, "ROOM_NOT_FOUND", "Room does not exist.");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const text = payload.text.trim().slice(0, this.maxDanmakuLength);
|
|
180
|
+
if (!text) {
|
|
181
|
+
this.sendError(session.id, "EMPTY_DANMAKU", "Danmaku text cannot be empty.");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const event = {
|
|
185
|
+
id: `${session.roomId}-d-${room.seq++}`,
|
|
186
|
+
roomId: session.roomId,
|
|
187
|
+
text,
|
|
188
|
+
color: payload.color,
|
|
189
|
+
kind: payload.kind,
|
|
190
|
+
user: session.user,
|
|
191
|
+
timestamp: Date.now()
|
|
192
|
+
};
|
|
193
|
+
room.danmakuHistory.push(event);
|
|
194
|
+
trimHistory(room.danmakuHistory, this.historyLimit);
|
|
195
|
+
this.broadcastToRoom(session.roomId, {
|
|
196
|
+
type: "danmaku.broadcast",
|
|
197
|
+
roomId: session.roomId,
|
|
198
|
+
event
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
handleFirework(session, payload) {
|
|
202
|
+
if (!session.roomId || !session.user) {
|
|
203
|
+
this.sendError(session.id, "NOT_JOINED", "Join a room before launching fireworks.");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const room = this.rooms.get(session.roomId);
|
|
207
|
+
if (!room) {
|
|
208
|
+
this.sendError(session.id, "ROOM_NOT_FOUND", "Room does not exist.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const event = {
|
|
212
|
+
id: `${session.roomId}-f-${room.seq++}`,
|
|
213
|
+
roomId: session.roomId,
|
|
214
|
+
payload,
|
|
215
|
+
user: session.user,
|
|
216
|
+
timestamp: Date.now()
|
|
217
|
+
};
|
|
218
|
+
room.fireworkHistory.push(event);
|
|
219
|
+
trimHistory(room.fireworkHistory, this.historyLimit);
|
|
220
|
+
this.broadcastToRoom(session.roomId, {
|
|
221
|
+
type: "firework.broadcast",
|
|
222
|
+
roomId: session.roomId,
|
|
223
|
+
event
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
createRoom(roomId) {
|
|
227
|
+
const room = {
|
|
228
|
+
id: roomId,
|
|
229
|
+
connections: /* @__PURE__ */ new Set(),
|
|
230
|
+
usersByConnection: /* @__PURE__ */ new Map(),
|
|
231
|
+
danmakuHistory: [],
|
|
232
|
+
fireworkHistory: [],
|
|
233
|
+
seq: 1
|
|
234
|
+
};
|
|
235
|
+
this.rooms.set(roomId, room);
|
|
236
|
+
return room;
|
|
237
|
+
}
|
|
238
|
+
sendToConnection(connectionId, message) {
|
|
239
|
+
const session = this.sessions.get(connectionId);
|
|
240
|
+
if (!session) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
session.transport.send(JSON.stringify(message));
|
|
245
|
+
} catch (error) {
|
|
246
|
+
this.logger?.warn?.("[FireworksRoomHub] failed to send message", error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
broadcastToRoom(roomId, message, excludeConnectionId) {
|
|
250
|
+
const room = this.rooms.get(roomId);
|
|
251
|
+
if (!room) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
for (const connectionId of room.connections) {
|
|
255
|
+
if (excludeConnectionId && connectionId === excludeConnectionId) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
this.sendToConnection(connectionId, message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
sendError(connectionId, code, message) {
|
|
262
|
+
this.sendToConnection(connectionId, {
|
|
263
|
+
type: "error",
|
|
264
|
+
code,
|
|
265
|
+
message
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
function createId(prefix) {
|
|
270
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
271
|
+
}
|
|
272
|
+
function trimHistory(history, limit) {
|
|
273
|
+
if (history.length <= limit) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
history.splice(0, history.length - limit);
|
|
277
|
+
}
|
|
278
|
+
function parseClientMessage(raw) {
|
|
279
|
+
if (typeof raw === "string") {
|
|
280
|
+
try {
|
|
281
|
+
const parsed = JSON.parse(raw);
|
|
282
|
+
return { ok: true, value: parsed };
|
|
283
|
+
} catch {
|
|
284
|
+
return { ok: false, error: "Invalid JSON payload." };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (isObject(raw)) {
|
|
288
|
+
return { ok: true, value: raw };
|
|
289
|
+
}
|
|
290
|
+
return { ok: false, error: "Unsupported message payload." };
|
|
291
|
+
}
|
|
292
|
+
function isObject(value) {
|
|
293
|
+
return typeof value === "object" && value !== null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/mikuFireworks3D/server/wsAdapter.ts
|
|
297
|
+
function bindWsSocket(hub, socket, options) {
|
|
298
|
+
const connection = hub.connect(
|
|
299
|
+
{
|
|
300
|
+
send: (encodedMessage) => {
|
|
301
|
+
socket.send(encodedMessage);
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
options?.connectionId
|
|
305
|
+
);
|
|
306
|
+
options?.onConnected?.(connection);
|
|
307
|
+
socket.on("message", (raw) => {
|
|
308
|
+
connection.handleMessage(normalizeWsRawData(raw));
|
|
309
|
+
});
|
|
310
|
+
socket.on("close", () => {
|
|
311
|
+
connection.disconnect("socket_closed");
|
|
312
|
+
options?.onDisconnected?.(connection.id);
|
|
313
|
+
});
|
|
314
|
+
socket.on("error", () => {
|
|
315
|
+
connection.disconnect("socket_error");
|
|
316
|
+
options?.onDisconnected?.(connection.id);
|
|
317
|
+
});
|
|
318
|
+
return connection;
|
|
319
|
+
}
|
|
320
|
+
function normalizeWsRawData(raw) {
|
|
321
|
+
if (typeof raw === "string") {
|
|
322
|
+
return raw;
|
|
323
|
+
}
|
|
324
|
+
if (raw instanceof ArrayBuffer) {
|
|
325
|
+
return new TextDecoder().decode(new Uint8Array(raw));
|
|
326
|
+
}
|
|
327
|
+
if (ArrayBuffer.isView(raw)) {
|
|
328
|
+
return new TextDecoder().decode(raw);
|
|
329
|
+
}
|
|
330
|
+
return "";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export { FireworksRoomHub, bindWsSocket };
|
|
334
|
+
//# sourceMappingURL=index.mjs.map
|
|
335
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/mikuFireworks3D/server/FireworksRoomHub.ts","../../../src/mikuFireworks3D/server/wsAdapter.ts"],"names":[],"mappings":";;;AA2BA,IAAM,0BAAA,GAA6B,GAAA;AACnC,IAAM,qBAAA,GAAwB,EAAA;AAC9B,IAAM,0BAAA,GAA6B,EAAA;AAE5B,IAAM,mBAAN,MAAuB;AAAA,EAQ5B,YAAY,OAAA,EAAmC;AAP/C,IAAA,IAAA,CAAiB,KAAA,uBAAY,GAAA,EAAuB;AACpD,IAAA,IAAA,CAAiB,QAAA,uBAAe,GAAA,EAAqB;AAOnD,IAAA,IAAA,CAAK,eAAA,GAAkB,SAAS,eAAA,IAAmB,0BAAA;AACnD,IAAA,IAAA,CAAK,YAAA,GAAe,SAAS,YAAA,IAAgB,qBAAA;AAC7C,IAAA,IAAA,CAAK,gBAAA,GAAmB,SAAS,gBAAA,IAAoB,0BAAA;AACrD,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,MAAA;AAAA,EACzB;AAAA,EAEA,OAAA,CAAQ,WAAyC,EAAA,EAAkC;AACjF,IAAA,MAAM,YAAA,GAAe,EAAA,IAAM,QAAA,CAAS,MAAM,CAAA;AAC1C,IAAA,MAAM,OAAA,GAAmB;AAAA,MACvB,EAAA,EAAI,YAAA;AAAA,MACJ;AAAA,KACF;AAEA,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,YAAA,EAAc,OAAO,CAAA;AAEvC,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,YAAA;AAAA,MACJ,eAAe,CAAC,GAAA,KAAQ,IAAA,CAAK,gBAAA,CAAiB,cAAc,GAAG,CAAA;AAAA,MAC/D,YAAY,CAAC,MAAA,KAAW,IAAA,CAAK,UAAA,CAAW,cAAc,MAAM,CAAA;AAAA,MAC5D,MAAM,CAAC,OAAA,KAAY,IAAA,CAAK,gBAAA,CAAiB,cAAc,OAAO;AAAA,KAChE;AAAA,EACF;AAAA,EAEA,UAAA,CAAW,cAAsB,MAAA,EAAuB;AACtD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,YAAY,CAAA;AAC9C,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,SAAA,CAAU,SAAS,KAAK,CAAA;AAC7B,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,YAAY,CAAA;AAEjC,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,IAAA,CAAK,QAAQ,IAAA,GAAO,CAAA,gCAAA,EAAmC,YAAY,CAAA,EAAA,EAAK,MAAM,CAAA,CAAE,CAAA;AAAA,IAClF;AAAA,EACF;AAAA,EAEA,QAAA,GAAmD;AACjD,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,KAAK,KAAA,CAAM,IAAA;AAAA,MAClB,WAAA,EAAa,KAAK,QAAA,CAAS;AAAA,KAC7B;AAAA,EACF;AAAA,EAEA,mBAAmB,MAAA,EAAwB;AACzC,IAAA,OAAO,KAAK,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA,EAAG,YAAY,IAAA,IAAQ,CAAA;AAAA,EACrD;AAAA,EAEQ,gBAAA,CAAiB,cAAsB,GAAA,EAAoB;AACjE,IAAA,MAAM,OAAA,GAAU,mBAAmB,GAAG,CAAA;AACtC,IAAA,IAAI,CAAC,QAAQ,EAAA,EAAI;AACf,MAAA,IAAA,CAAK,SAAA,CAAU,YAAA,EAAc,aAAA,EAAe,OAAA,CAAQ,KAAK,CAAA;AACzD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,YAAY,CAAA;AAC9C,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,gBAAgB,OAAA,CAAQ,KAAA;AAE9B,IAAA,QAAQ,cAAc,IAAA;AAAM,MAC1B,KAAK,MAAA,EAAQ;AACX,QAAA,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS,aAAA,CAAc,MAAA,EAAQ,cAAc,IAAI,CAAA;AAC/D,QAAA;AAAA,MACF;AAAA,MACA,KAAK,OAAA,EAAS;AACZ,QAAA,IAAA,CAAK,SAAA,CAAU,SAAS,IAAI,CAAA;AAC5B,QAAA;AAAA,MACF;AAAA,MACA,KAAK,cAAA,EAAgB;AACnB,QAAA,IAAA,CAAK,aAAA,CAAc,OAAA,EAAS,aAAA,CAAc,OAAO,CAAA;AACjD,QAAA;AAAA,MACF;AAAA,MACA,KAAK,iBAAA,EAAmB;AACtB,QAAA,IAAA,CAAK,cAAA,CAAe,OAAA,EAAS,aAAA,CAAc,OAAO,CAAA;AAClD,QAAA;AAAA,MACF;AAAA,MACA,KAAK,MAAA,EAAQ;AACX,QAAA,IAAA,CAAK,iBAAiB,YAAA,EAAc;AAAA,UAClC,IAAA,EAAM,MAAA;AAAA,UACN,EAAA,EAAI,aAAA,CAAc,EAAA,IAAM,IAAA,CAAK,GAAA;AAAI,SAClC,CAAA;AACD,QAAA;AAAA,MACF;AAAA,MACA,SAAS;AACP,QAAA,IAAA,CAAK,SAAA,CAAU,YAAA,EAAc,qBAAA,EAAuB,2BAA2B,CAAA;AAAA,MACjF;AAAA;AACF,EACF;AAAA,EAEQ,QAAA,CAAS,OAAA,EAAkB,MAAA,EAAgB,IAAA,EAAmC;AACpF,IAAA,MAAM,gBAAA,GAAmB,OAAO,IAAA,EAAK;AACrC,IAAA,MAAM,gBAAA,GAAmB,IAAA,CAAK,MAAA,CAAO,IAAA,EAAK;AAE1C,IAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,MAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,EAAA,EAAI,kBAAA,EAAoB,qBAAqB,CAAA;AACpE,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,MAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,EAAA,EAAI,kBAAA,EAAoB,0BAA0B,CAAA;AACzE,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,OAAA,CAAQ,MAAA,IAAU,OAAA,CAAQ,MAAA,KAAW,gBAAA,EAAkB;AACzD,MAAA,IAAA,CAAK,SAAA,CAAU,SAAS,IAAI,CAAA;AAAA,IAC9B;AAEA,IAAA,MAAM,IAAA,GAAO,KAAK,KAAA,CAAM,GAAA,CAAI,gBAAgB,CAAA,IAAK,IAAA,CAAK,WAAW,gBAAgB,CAAA;AACjF,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,IAAA,IAAQ,IAAA,CAAK,eAAA,IAAmB,CAAC,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAA,EAAG;AACtF,MAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,EAAA,EAAI,WAAA,EAAa,eAAe,CAAA;AACvD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,QAAA,GAAkC;AAAA,MACtC,MAAA,EAAQ,gBAAA;AAAA,MACR,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,WAAW,IAAA,CAAK;AAAA,KAClB;AAEA,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,QAAQ,EAAE,CAAA;AACjD,IAAA,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAA;AAC/B,IAAA,IAAA,CAAK,iBAAA,CAAkB,GAAA,CAAI,OAAA,CAAQ,EAAA,EAAI,QAAQ,CAAA;AAE/C,IAAA,OAAA,CAAQ,MAAA,GAAS,gBAAA;AACjB,IAAA,OAAA,CAAQ,IAAA,GAAO,QAAA;AAEf,IAAA,IAAA,CAAK,gBAAA,CAAiB,QAAQ,EAAA,EAAI;AAAA,MAChC,IAAA,EAAM,QAAA;AAAA,MACN,MAAA,EAAQ,gBAAA;AAAA,MACR,IAAA,EAAM,QAAA;AAAA,MACN,WAAA,EAAa,KAAK,WAAA,CAAY;AAAA,KAC/B,CAAA;AAED,IAAA,IAAA,CAAK,gBAAA,CAAiB,QAAQ,EAAA,EAAI;AAAA,MAChC,IAAA,EAAM,eAAA;AAAA,MACN,MAAA,EAAQ,gBAAA;AAAA,MACR,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,iBAAA,CAAkB,QAAQ,CAAA;AAAA,MACjD,cAAA,EAAgB,CAAC,GAAG,IAAA,CAAK,cAAc,CAAA;AAAA,MACvC,eAAA,EAAiB,CAAC,GAAG,IAAA,CAAK,eAAe;AAAA,KAC1C,CAAA;AAED,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,IAAA,CAAK,gBAAgB,gBAAA,EAAkB;AAAA,QACrC,IAAA,EAAM,kBAAA;AAAA,QACN,MAAA,EAAQ,gBAAA;AAAA,QACR,IAAA,EAAM,QAAA;AAAA,QACN,WAAA,EAAa,KAAK,WAAA,CAAY;AAAA,OAChC,EAAG,QAAQ,EAAE,CAAA;AAAA,IACf;AAAA,EACF;AAAA,EAEQ,SAAA,CAAU,SAAkB,MAAA,EAAuB;AACzD,IAAA,MAAM,SAAS,OAAA,CAAQ,MAAA;AACvB,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA;AAClC,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAA,CAAQ,MAAA,GAAS,MAAA;AACjB,MAAA,OAAA,CAAQ,IAAA,GAAO,MAAA;AACf,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,gBAAgB,IAAA,CAAK,iBAAA,CAAkB,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAA,EAAG,MAAA;AAE9D,IAAA,IAAA,CAAK,WAAA,CAAY,MAAA,CAAO,OAAA,CAAQ,EAAE,CAAA;AAClC,IAAA,IAAA,CAAK,iBAAA,CAAkB,MAAA,CAAO,OAAA,CAAQ,EAAE,CAAA;AAExC,IAAA,OAAA,CAAQ,MAAA,GAAS,MAAA;AACjB,IAAA,OAAA,CAAQ,IAAA,GAAO,MAAA;AAEf,IAAA,IAAI,UAAU,aAAA,EAAe;AAC3B,MAAA,IAAA,CAAK,gBAAgB,MAAA,EAAQ;AAAA,QAC3B,IAAA,EAAM,gBAAA;AAAA,QACN,MAAA;AAAA,QACA,MAAA,EAAQ,aAAA;AAAA,QACR,WAAA,EAAa,KAAK,WAAA,CAAY;AAAA,OAC/B,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,IAAA,KAAS,CAAA,EAAG;AAC/B,MAAA,IAAA,CAAK,KAAA,CAAM,OAAO,MAAM,CAAA;AAAA,IAC1B;AAAA,EACF;AAAA,EAEQ,aAAA,CACN,SACA,OAAA,EAKM;AACN,IAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,IAAU,CAAC,QAAQ,IAAA,EAAM;AACpC,MAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,EAAA,EAAI,YAAA,EAAc,qCAAqC,CAAA;AAC9E,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,QAAQ,MAAM,CAAA;AAC1C,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,EAAA,EAAI,gBAAA,EAAkB,sBAAsB,CAAA;AACnE,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,CAAK,IAAA,GAAO,KAAA,CAAM,CAAA,EAAG,KAAK,gBAAgB,CAAA;AAC/D,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,EAAA,EAAI,eAAA,EAAiB,+BAA+B,CAAA;AAC3E,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,KAAA,GAA+B;AAAA,MACnC,IAAI,CAAA,EAAG,OAAA,CAAQ,MAAM,CAAA,GAAA,EAAM,KAAK,GAAA,EAAK,CAAA,CAAA;AAAA,MACrC,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,IAAA;AAAA,MACA,OAAO,OAAA,CAAQ,KAAA;AAAA,MACf,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAA,EAAW,KAAK,GAAA;AAAI,KACtB;AAEA,IAAA,IAAA,CAAK,cAAA,CAAe,KAAK,KAAK,CAAA;AAC9B,IAAA,WAAA,CAAY,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,YAAY,CAAA;AAElD,IAAA,IAAA,CAAK,eAAA,CAAgB,QAAQ,MAAA,EAAQ;AAAA,MACnC,IAAA,EAAM,mBAAA;AAAA,MACN,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB;AAAA,KACD,CAAA;AAAA,EACH;AAAA,EAEQ,cAAA,CAAe,SAAkB,OAAA,EAAkD;AACzF,IAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,IAAU,CAAC,QAAQ,IAAA,EAAM;AACpC,MAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,EAAA,EAAI,YAAA,EAAc,yCAAyC,CAAA;AAClF,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,QAAQ,MAAM,CAAA;AAC1C,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,EAAA,EAAI,gBAAA,EAAkB,sBAAsB,CAAA;AACnE,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,KAAA,GAAgC;AAAA,MACpC,IAAI,CAAA,EAAG,OAAA,CAAQ,MAAM,CAAA,GAAA,EAAM,KAAK,GAAA,EAAK,CAAA,CAAA;AAAA,MACrC,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,OAAA;AAAA,MACA,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAA,EAAW,KAAK,GAAA;AAAI,KACtB;AAEA,IAAA,IAAA,CAAK,eAAA,CAAgB,KAAK,KAAK,CAAA;AAC/B,IAAA,WAAA,CAAY,IAAA,CAAK,eAAA,EAAiB,IAAA,CAAK,YAAY,CAAA;AAEnD,IAAA,IAAA,CAAK,eAAA,CAAgB,QAAQ,MAAA,EAAQ;AAAA,MACnC,IAAA,EAAM,oBAAA;AAAA,MACN,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB;AAAA,KACD,CAAA;AAAA,EACH;AAAA,EAEQ,WAAW,MAAA,EAA2B;AAC5C,IAAA,MAAM,IAAA,GAAkB;AAAA,MACtB,EAAA,EAAI,MAAA;AAAA,MACJ,WAAA,sBAAiB,GAAA,EAAI;AAAA,MACrB,iBAAA,sBAAuB,GAAA,EAAI;AAAA,MAC3B,gBAAgB,EAAC;AAAA,MACjB,iBAAiB,EAAC;AAAA,MAClB,GAAA,EAAK;AAAA,KACP;AAEA,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,MAAA,EAAQ,IAAI,CAAA;AAC3B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEQ,gBAAA,CAAiB,cAAsB,OAAA,EAAuC;AACpF,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,YAAY,CAAA;AAC9C,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,OAAA,CAAQ,SAAA,CAAU,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA,IAChD,SAAS,KAAA,EAAO;AACd,MAAA,IAAA,CAAK,MAAA,EAAQ,IAAA,GAAO,2CAAA,EAA6C,KAAK,CAAA;AAAA,IACxE;AAAA,EACF;AAAA,EAEQ,eAAA,CAAgB,MAAA,EAAgB,OAAA,EAAiC,mBAAA,EAAoC;AAC3G,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA;AAClC,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA;AAAA,IACF;AAEA,IAAA,KAAA,MAAW,YAAA,IAAgB,KAAK,WAAA,EAAa;AAC3C,MAAA,IAAI,mBAAA,IAAuB,iBAAiB,mBAAA,EAAqB;AAC/D,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,gBAAA,CAAiB,cAAc,OAAO,CAAA;AAAA,IAC7C;AAAA,EACF;AAAA,EAEQ,SAAA,CAAU,YAAA,EAAsB,IAAA,EAAc,OAAA,EAAuB;AAC3E,IAAA,IAAA,CAAK,iBAAiB,YAAA,EAAc;AAAA,MAClC,IAAA,EAAM,OAAA;AAAA,MACN,IAAA;AAAA,MACA;AAAA,KACD,CAAA;AAAA,EACH;AACF;AAEA,SAAS,SAAS,MAAA,EAAwB;AACxC,EAAA,OAAO,GAAG,MAAM,CAAA,CAAA,EAAI,IAAA,CAAK,GAAA,EAAK,CAAA,CAAA,EAAI,IAAA,CAAK,MAAA,EAAO,CAAE,SAAS,EAAE,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AAC1E;AAEA,SAAS,WAAA,CAAe,SAAc,KAAA,EAAqB;AACzD,EAAA,IAAI,OAAA,CAAQ,UAAU,KAAA,EAAO;AAC3B,IAAA;AAAA,EACF;AACA,EAAA,OAAA,CAAQ,MAAA,CAAO,CAAA,EAAG,OAAA,CAAQ,MAAA,GAAS,KAAK,CAAA;AAC1C;AAEA,SAAS,mBAAmB,GAAA,EAEK;AAC/B,EAAA,IAAI,OAAO,QAAQ,QAAA,EAAU;AAC3B,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC7B,MAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,MAAA,EAAO;AAAA,IACnC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,uBAAA,EAAwB;AAAA,IACrD;AAAA,EACF;AAEA,EAAA,IAAI,QAAA,CAAS,GAAG,CAAA,EAAG;AACjB,IAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,GAAA,EAA8B;AAAA,EAC1D;AAEA,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,8BAAA,EAA+B;AAC5D;AAEA,SAAS,SAAS,KAAA,EAAkD;AAClE,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,IAAA;AAChD;;;ACjXO,SAAS,YAAA,CACd,GAAA,EACA,MAAA,EACA,OAAA,EACqB;AACrB,EAAA,MAAM,aAAa,GAAA,CAAI,OAAA;AAAA,IACrB;AAAA,MACE,IAAA,EAAM,CAAC,cAAA,KAAmB;AACxB,QAAA,MAAA,CAAO,KAAK,cAAc,CAAA;AAAA,MAC5B;AAAA,KACF;AAAA,IACA,OAAA,EAAS;AAAA,GACX;AAEA,EAAA,OAAA,EAAS,cAAc,UAAU,CAAA;AAEjC,EAAA,MAAA,CAAO,EAAA,CAAG,SAAA,EAAW,CAAC,GAAA,KAAQ;AAC5B,IAAA,UAAA,CAAW,aAAA,CAAc,kBAAA,CAAmB,GAAG,CAAC,CAAA;AAAA,EAClD,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,EAAA,CAAG,SAAS,MAAM;AACvB,IAAA,UAAA,CAAW,WAAW,eAAe,CAAA;AACrC,IAAA,OAAA,EAAS,cAAA,GAAiB,WAAW,EAAE,CAAA;AAAA,EACzC,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,EAAA,CAAG,SAAS,MAAM;AACvB,IAAA,UAAA,CAAW,WAAW,cAAc,CAAA;AACpC,IAAA,OAAA,EAAS,cAAA,GAAiB,WAAW,EAAE,CAAA;AAAA,EACzC,CAAC,CAAA;AAED,EAAA,OAAO,UAAA;AACT;AAEA,SAAS,mBAAmB,GAAA,EAAsB;AAChD,EAAA,IAAI,OAAO,QAAQ,QAAA,EAAU;AAC3B,IAAA,OAAO,GAAA;AAAA,EACT;AAEA,EAAA,IAAI,eAAe,WAAA,EAAa;AAC9B,IAAA,OAAO,IAAI,WAAA,EAAY,CAAE,OAAO,IAAI,UAAA,CAAW,GAAG,CAAC,CAAA;AAAA,EACrD;AAEA,EAAA,IAAI,WAAA,CAAY,MAAA,CAAO,GAAG,CAAA,EAAG;AAC3B,IAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,GAAG,CAAA;AAAA,EACrC;AAEA,EAAA,OAAO,EAAA;AACT","file":"index.mjs","sourcesContent":["import type {\n DanmakuBroadcastEvent,\n FireworkBroadcastEvent,\n FireworksClientMessage,\n FireworksConnection,\n FireworksConnectionTransport,\n FireworksRoomHubOptions,\n FireworksServerMessage,\n FireworksUserIdentity,\n} from './types';\n\ninterface Session {\n id: string;\n transport: FireworksConnectionTransport;\n roomId?: string;\n user?: FireworksUserIdentity;\n}\n\ninterface RoomState {\n id: string;\n connections: Set<string>;\n usersByConnection: Map<string, FireworksUserIdentity>;\n danmakuHistory: DanmakuBroadcastEvent[];\n fireworkHistory: FireworkBroadcastEvent[];\n seq: number;\n}\n\nconst DEFAULT_MAX_USERS_PER_ROOM = 200;\nconst DEFAULT_HISTORY_LIMIT = 50;\nconst DEFAULT_MAX_DANMAKU_LENGTH = 64;\n\nexport class FireworksRoomHub {\n private readonly rooms = new Map<string, RoomState>();\n private readonly sessions = new Map<string, Session>();\n private readonly maxUsersPerRoom: number;\n private readonly historyLimit: number;\n private readonly maxDanmakuLength: number;\n private readonly logger?: Pick<Console, 'info' | 'warn' | 'error'>;\n\n constructor(options?: FireworksRoomHubOptions) {\n this.maxUsersPerRoom = options?.maxUsersPerRoom ?? DEFAULT_MAX_USERS_PER_ROOM;\n this.historyLimit = options?.historyLimit ?? DEFAULT_HISTORY_LIMIT;\n this.maxDanmakuLength = options?.maxDanmakuLength ?? DEFAULT_MAX_DANMAKU_LENGTH;\n this.logger = options?.logger;\n }\n\n connect(transport: FireworksConnectionTransport, id?: string): FireworksConnection {\n const connectionId = id ?? createId('conn');\n const session: Session = {\n id: connectionId,\n transport,\n };\n\n this.sessions.set(connectionId, session);\n\n return {\n id: connectionId,\n handleMessage: (raw) => this.handleRawMessage(connectionId, raw),\n disconnect: (reason) => this.disconnect(connectionId, reason),\n send: (message) => this.sendToConnection(connectionId, message),\n };\n }\n\n disconnect(connectionId: string, reason?: string): void {\n const session = this.sessions.get(connectionId);\n if (!session) {\n return;\n }\n\n this.leaveRoom(session, false);\n this.sessions.delete(connectionId);\n\n if (reason) {\n this.logger?.info?.(`[FireworksRoomHub] disconnected ${connectionId}: ${reason}`);\n }\n }\n\n getStats(): { rooms: number; connections: number } {\n return {\n rooms: this.rooms.size,\n connections: this.sessions.size,\n };\n }\n\n getRoomOnlineCount(roomId: string): number {\n return this.rooms.get(roomId)?.connections.size ?? 0;\n }\n\n private handleRawMessage(connectionId: string, raw: unknown): void {\n const message = parseClientMessage(raw);\n if (!message.ok) {\n this.sendError(connectionId, 'BAD_MESSAGE', message.error);\n return;\n }\n\n const session = this.sessions.get(connectionId);\n if (!session) {\n return;\n }\n\n const clientMessage = message.value;\n\n switch (clientMessage.type) {\n case 'join': {\n this.joinRoom(session, clientMessage.roomId, clientMessage.user);\n return;\n }\n case 'leave': {\n this.leaveRoom(session, true);\n return;\n }\n case 'danmaku.send': {\n this.handleDanmaku(session, clientMessage.payload);\n return;\n }\n case 'firework.launch': {\n this.handleFirework(session, clientMessage.payload);\n return;\n }\n case 'ping': {\n this.sendToConnection(connectionId, {\n type: 'pong',\n ts: clientMessage.ts ?? Date.now(),\n });\n return;\n }\n default: {\n this.sendError(connectionId, 'UNSUPPORTED_MESSAGE', 'Unsupported message type.');\n }\n }\n }\n\n private joinRoom(session: Session, roomId: string, user: FireworksUserIdentity): void {\n const normalizedRoomId = roomId.trim();\n const normalizedUserId = user.userId.trim();\n\n if (!normalizedRoomId) {\n this.sendError(session.id, 'ROOM_ID_REQUIRED', 'roomId is required.');\n return;\n }\n if (!normalizedUserId) {\n this.sendError(session.id, 'USER_ID_REQUIRED', 'user.userId is required.');\n return;\n }\n\n if (session.roomId && session.roomId !== normalizedRoomId) {\n this.leaveRoom(session, true);\n }\n\n const room = this.rooms.get(normalizedRoomId) ?? this.createRoom(normalizedRoomId);\n if (room.connections.size >= this.maxUsersPerRoom && !room.connections.has(session.id)) {\n this.sendError(session.id, 'ROOM_FULL', 'Room is full.');\n return;\n }\n\n const identity: FireworksUserIdentity = {\n userId: normalizedUserId,\n nickname: user.nickname,\n avatarUrl: user.avatarUrl,\n };\n\n const wasMember = room.connections.has(session.id);\n room.connections.add(session.id);\n room.usersByConnection.set(session.id, identity);\n\n session.roomId = normalizedRoomId;\n session.user = identity;\n\n this.sendToConnection(session.id, {\n type: 'joined',\n roomId: normalizedRoomId,\n self: identity,\n onlineCount: room.connections.size,\n });\n\n this.sendToConnection(session.id, {\n type: 'room.snapshot',\n roomId: normalizedRoomId,\n users: Array.from(room.usersByConnection.values()),\n danmakuHistory: [...room.danmakuHistory],\n fireworkHistory: [...room.fireworkHistory],\n });\n\n if (!wasMember) {\n this.broadcastToRoom(normalizedRoomId, {\n type: 'room.user_joined',\n roomId: normalizedRoomId,\n user: identity,\n onlineCount: room.connections.size,\n }, session.id);\n }\n }\n\n private leaveRoom(session: Session, notify: boolean): void {\n const roomId = session.roomId;\n if (!roomId) {\n return;\n }\n\n const room = this.rooms.get(roomId);\n if (!room) {\n session.roomId = undefined;\n session.user = undefined;\n return;\n }\n\n const leavingUserId = room.usersByConnection.get(session.id)?.userId;\n\n room.connections.delete(session.id);\n room.usersByConnection.delete(session.id);\n\n session.roomId = undefined;\n session.user = undefined;\n\n if (notify && leavingUserId) {\n this.broadcastToRoom(roomId, {\n type: 'room.user_left',\n roomId,\n userId: leavingUserId,\n onlineCount: room.connections.size,\n });\n }\n\n if (room.connections.size === 0) {\n this.rooms.delete(roomId);\n }\n }\n\n private handleDanmaku(\n session: Session,\n payload: {\n text: string;\n color?: string;\n kind?: 'normal' | 'miku' | 'avatar';\n }\n ): void {\n if (!session.roomId || !session.user) {\n this.sendError(session.id, 'NOT_JOINED', 'Join a room before sending danmaku.');\n return;\n }\n\n const room = this.rooms.get(session.roomId);\n if (!room) {\n this.sendError(session.id, 'ROOM_NOT_FOUND', 'Room does not exist.');\n return;\n }\n\n const text = payload.text.trim().slice(0, this.maxDanmakuLength);\n if (!text) {\n this.sendError(session.id, 'EMPTY_DANMAKU', 'Danmaku text cannot be empty.');\n return;\n }\n\n const event: DanmakuBroadcastEvent = {\n id: `${session.roomId}-d-${room.seq++}`,\n roomId: session.roomId,\n text,\n color: payload.color,\n kind: payload.kind,\n user: session.user,\n timestamp: Date.now(),\n };\n\n room.danmakuHistory.push(event);\n trimHistory(room.danmakuHistory, this.historyLimit);\n\n this.broadcastToRoom(session.roomId, {\n type: 'danmaku.broadcast',\n roomId: session.roomId,\n event,\n });\n }\n\n private handleFirework(session: Session, payload: FireworkBroadcastEvent['payload']): void {\n if (!session.roomId || !session.user) {\n this.sendError(session.id, 'NOT_JOINED', 'Join a room before launching fireworks.');\n return;\n }\n\n const room = this.rooms.get(session.roomId);\n if (!room) {\n this.sendError(session.id, 'ROOM_NOT_FOUND', 'Room does not exist.');\n return;\n }\n\n const event: FireworkBroadcastEvent = {\n id: `${session.roomId}-f-${room.seq++}`,\n roomId: session.roomId,\n payload,\n user: session.user,\n timestamp: Date.now(),\n };\n\n room.fireworkHistory.push(event);\n trimHistory(room.fireworkHistory, this.historyLimit);\n\n this.broadcastToRoom(session.roomId, {\n type: 'firework.broadcast',\n roomId: session.roomId,\n event,\n });\n }\n\n private createRoom(roomId: string): RoomState {\n const room: RoomState = {\n id: roomId,\n connections: new Set(),\n usersByConnection: new Map(),\n danmakuHistory: [],\n fireworkHistory: [],\n seq: 1,\n };\n\n this.rooms.set(roomId, room);\n return room;\n }\n\n private sendToConnection(connectionId: string, message: FireworksServerMessage): void {\n const session = this.sessions.get(connectionId);\n if (!session) {\n return;\n }\n\n try {\n session.transport.send(JSON.stringify(message));\n } catch (error) {\n this.logger?.warn?.('[FireworksRoomHub] failed to send message', error);\n }\n }\n\n private broadcastToRoom(roomId: string, message: FireworksServerMessage, excludeConnectionId?: string): void {\n const room = this.rooms.get(roomId);\n if (!room) {\n return;\n }\n\n for (const connectionId of room.connections) {\n if (excludeConnectionId && connectionId === excludeConnectionId) {\n continue;\n }\n this.sendToConnection(connectionId, message);\n }\n }\n\n private sendError(connectionId: string, code: string, message: string): void {\n this.sendToConnection(connectionId, {\n type: 'error',\n code,\n message,\n });\n }\n}\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\nfunction trimHistory<T>(history: T[], limit: number): void {\n if (history.length <= limit) {\n return;\n }\n history.splice(0, history.length - limit);\n}\n\nfunction parseClientMessage(raw: unknown):\n | { ok: true; value: FireworksClientMessage }\n | { ok: false; error: string } {\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw) as FireworksClientMessage;\n return { ok: true, value: parsed };\n } catch {\n return { ok: false, error: 'Invalid JSON payload.' };\n }\n }\n\n if (isObject(raw)) {\n return { ok: true, value: raw as FireworksClientMessage };\n }\n\n return { ok: false, error: 'Unsupported message payload.' };\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n","import type { FireworksRoomHub } from './FireworksRoomHub';\nimport type { FireworksConnection, WsLikeSocket } from './types';\n\nexport interface BindWsSocketOptions {\n connectionId?: string;\n onConnected?: (connection: FireworksConnection) => void;\n onDisconnected?: (connectionId: string) => void;\n}\n\n/**\n * Bind a ws-like socket to FireworksRoomHub.\n *\n * Example (ws package):\n * const hub = new FireworksRoomHub();\n * wss.on('connection', (socket) => bindWsSocket(hub, socket));\n */\nexport function bindWsSocket(\n hub: FireworksRoomHub,\n socket: WsLikeSocket,\n options?: BindWsSocketOptions\n): FireworksConnection {\n const connection = hub.connect(\n {\n send: (encodedMessage) => {\n socket.send(encodedMessage);\n },\n },\n options?.connectionId\n );\n\n options?.onConnected?.(connection);\n\n socket.on('message', (raw) => {\n connection.handleMessage(normalizeWsRawData(raw));\n });\n\n socket.on('close', () => {\n connection.disconnect('socket_closed');\n options?.onDisconnected?.(connection.id);\n });\n\n socket.on('error', () => {\n connection.disconnect('socket_error');\n options?.onDisconnected?.(connection.id);\n });\n\n return connection;\n}\n\nfunction normalizeWsRawData(raw: unknown): string {\n if (typeof raw === 'string') {\n return raw;\n }\n\n if (raw instanceof ArrayBuffer) {\n return new TextDecoder().decode(new Uint8Array(raw));\n }\n\n if (ArrayBuffer.isView(raw)) {\n return new TextDecoder().decode(raw);\n }\n\n return '';\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React__default, { RefObject } from 'react';
|
|
2
2
|
|
|
3
3
|
type MikuFusionGameStatus = 'ready' | 'playing' | 'paused' | 'gameOver';
|
|
4
|
+
type OrbImageMapping = Partial<Record<number, string>>;
|
|
4
5
|
interface FusionOrb {
|
|
5
6
|
id: string;
|
|
6
7
|
x: number;
|
|
@@ -46,11 +47,15 @@ interface MergeResult {
|
|
|
46
47
|
interface MikuFusionGameProps extends Partial<MikuFusionGameConfig>, MikuFusionGameCallbacks {
|
|
47
48
|
className?: string;
|
|
48
49
|
storageKey?: string;
|
|
50
|
+
orbImageStorageKey?: string;
|
|
51
|
+
enableImageConfigPanel?: boolean;
|
|
52
|
+
presetOrbImageMapping?: OrbImageMapping;
|
|
49
53
|
}
|
|
50
|
-
declare function MikuFusionGame({ className, storageKey, ...options }: MikuFusionGameProps): React__default.JSX.Element;
|
|
54
|
+
declare function MikuFusionGame({ className, storageKey, orbImageStorageKey, enableImageConfigPanel, presetOrbImageMapping, ...options }: MikuFusionGameProps): React__default.JSX.Element;
|
|
51
55
|
|
|
52
56
|
interface UseMikuFusionGameOptions extends Partial<MikuFusionGameConfig>, MikuFusionGameCallbacks {
|
|
53
57
|
storageKey?: string;
|
|
58
|
+
orbImageMapping?: OrbImageMapping;
|
|
54
59
|
}
|
|
55
60
|
interface UseMikuFusionGameResult {
|
|
56
61
|
canvasRef: RefObject<HTMLCanvasElement>;
|
|
@@ -109,4 +114,4 @@ declare const RADIUS_STEP = 4;
|
|
|
109
114
|
declare const LEVEL_LABEL_PREFIX = "M";
|
|
110
115
|
declare const DEFAULT_MIKU_FUSION_CONFIG: MikuFusionGameConfig;
|
|
111
116
|
|
|
112
|
-
export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, type FusionOrb, LEVEL_LABEL_PREFIX, type MergeResult, type MikuFusionContentRegistry, MikuFusionGame, type MikuFusionGameCallbacks, type MikuFusionGameConfig, type MikuFusionGameProps, type MikuFusionGameStatus, type MikuFusionTheme, type ProducerPack, RADIUS_STEP, type SongPack, type ThemePack, type UseMikuFusionGameOptions, type UseMikuFusionGameResult, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
|
|
117
|
+
export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, type FusionOrb, LEVEL_LABEL_PREFIX, type MergeResult, type MikuFusionContentRegistry, MikuFusionGame, type MikuFusionGameCallbacks, type MikuFusionGameConfig, type MikuFusionGameProps, type MikuFusionGameStatus, type MikuFusionTheme, type OrbImageMapping, type ProducerPack, RADIUS_STEP, type SongPack, type ThemePack, type UseMikuFusionGameOptions, type UseMikuFusionGameResult, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React__default, { RefObject } from 'react';
|
|
2
2
|
|
|
3
3
|
type MikuFusionGameStatus = 'ready' | 'playing' | 'paused' | 'gameOver';
|
|
4
|
+
type OrbImageMapping = Partial<Record<number, string>>;
|
|
4
5
|
interface FusionOrb {
|
|
5
6
|
id: string;
|
|
6
7
|
x: number;
|
|
@@ -46,11 +47,15 @@ interface MergeResult {
|
|
|
46
47
|
interface MikuFusionGameProps extends Partial<MikuFusionGameConfig>, MikuFusionGameCallbacks {
|
|
47
48
|
className?: string;
|
|
48
49
|
storageKey?: string;
|
|
50
|
+
orbImageStorageKey?: string;
|
|
51
|
+
enableImageConfigPanel?: boolean;
|
|
52
|
+
presetOrbImageMapping?: OrbImageMapping;
|
|
49
53
|
}
|
|
50
|
-
declare function MikuFusionGame({ className, storageKey, ...options }: MikuFusionGameProps): React__default.JSX.Element;
|
|
54
|
+
declare function MikuFusionGame({ className, storageKey, orbImageStorageKey, enableImageConfigPanel, presetOrbImageMapping, ...options }: MikuFusionGameProps): React__default.JSX.Element;
|
|
51
55
|
|
|
52
56
|
interface UseMikuFusionGameOptions extends Partial<MikuFusionGameConfig>, MikuFusionGameCallbacks {
|
|
53
57
|
storageKey?: string;
|
|
58
|
+
orbImageMapping?: OrbImageMapping;
|
|
54
59
|
}
|
|
55
60
|
interface UseMikuFusionGameResult {
|
|
56
61
|
canvasRef: RefObject<HTMLCanvasElement>;
|
|
@@ -109,4 +114,4 @@ declare const RADIUS_STEP = 4;
|
|
|
109
114
|
declare const LEVEL_LABEL_PREFIX = "M";
|
|
110
115
|
declare const DEFAULT_MIKU_FUSION_CONFIG: MikuFusionGameConfig;
|
|
111
116
|
|
|
112
|
-
export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, type FusionOrb, LEVEL_LABEL_PREFIX, type MergeResult, type MikuFusionContentRegistry, MikuFusionGame, type MikuFusionGameCallbacks, type MikuFusionGameConfig, type MikuFusionGameProps, type MikuFusionGameStatus, type MikuFusionTheme, type ProducerPack, RADIUS_STEP, type SongPack, type ThemePack, type UseMikuFusionGameOptions, type UseMikuFusionGameResult, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
|
|
117
|
+
export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, type FusionOrb, LEVEL_LABEL_PREFIX, type MergeResult, type MikuFusionContentRegistry, MikuFusionGame, type MikuFusionGameCallbacks, type MikuFusionGameConfig, type MikuFusionGameProps, type MikuFusionGameStatus, type MikuFusionTheme, type OrbImageMapping, type ProducerPack, RADIUS_STEP, type SongPack, type ThemePack, type UseMikuFusionGameOptions, type UseMikuFusionGameResult, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var chunkHHVDOIPV_js = require('../chunk-HHVDOIPV.js');
|
|
4
|
+
require('../chunk-ZWQJSZEY.js');
|
|
3
5
|
var chunkKO73EBUT_js = require('../chunk-KO73EBUT.js');
|
|
4
6
|
require('../chunk-XJ7ZAGC5.js');
|
|
7
|
+
require('../chunk-25OFOKNF.js');
|
|
5
8
|
require('../chunk-DGUM43GV.js');
|
|
6
9
|
var React3 = require('react');
|
|
7
10
|
|
|
@@ -223,7 +226,7 @@ function getOrbColor(level, config) {
|
|
|
223
226
|
const index = Math.max(0, Math.min(config.theme.orbColors.length - 1, level - 1));
|
|
224
227
|
return config.theme.orbColors[index] || "#22d3ee";
|
|
225
228
|
}
|
|
226
|
-
function drawScene(context, width, height, config, orbs, aimX, nextLevel, status) {
|
|
229
|
+
function drawScene(context, width, height, config, orbs, aimX, nextLevel, status, imageCache) {
|
|
227
230
|
const gradient = context.createLinearGradient(0, 0, 0, height);
|
|
228
231
|
gradient.addColorStop(0, config.theme.backgroundTop);
|
|
229
232
|
gradient.addColorStop(1, config.theme.backgroundBottom);
|
|
@@ -244,15 +247,51 @@ function drawScene(context, width, height, config, orbs, aimX, nextLevel, status
|
|
|
244
247
|
context.lineTo(aimX, config.spawnY);
|
|
245
248
|
context.stroke();
|
|
246
249
|
const previewRadius = getRadiusByLevel(nextLevel);
|
|
247
|
-
|
|
250
|
+
const previewY = config.spawnY - previewRadius - 4;
|
|
251
|
+
const previewImage = imageCache[nextLevel];
|
|
248
252
|
context.beginPath();
|
|
249
|
-
context.arc(aimX,
|
|
250
|
-
context.
|
|
253
|
+
context.arc(aimX, previewY, previewRadius, 0, Math.PI * 2);
|
|
254
|
+
context.closePath();
|
|
255
|
+
if (previewImage && previewImage.complete && previewImage.naturalWidth > 0) {
|
|
256
|
+
context.save();
|
|
257
|
+
context.clip();
|
|
258
|
+
context.drawImage(
|
|
259
|
+
previewImage,
|
|
260
|
+
aimX - previewRadius,
|
|
261
|
+
previewY - previewRadius,
|
|
262
|
+
previewRadius * 2,
|
|
263
|
+
previewRadius * 2
|
|
264
|
+
);
|
|
265
|
+
context.restore();
|
|
266
|
+
} else {
|
|
267
|
+
context.fillStyle = getOrbColor(nextLevel, config);
|
|
268
|
+
context.fill();
|
|
269
|
+
}
|
|
251
270
|
orbs.forEach((orb) => {
|
|
252
271
|
context.beginPath();
|
|
253
|
-
context.fillStyle = getOrbColor(orb.level, config);
|
|
254
272
|
context.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2);
|
|
255
|
-
context.
|
|
273
|
+
context.closePath();
|
|
274
|
+
const orbImage = imageCache[orb.level];
|
|
275
|
+
if (orbImage && orbImage.complete && orbImage.naturalWidth > 0) {
|
|
276
|
+
context.save();
|
|
277
|
+
context.clip();
|
|
278
|
+
context.drawImage(
|
|
279
|
+
orbImage,
|
|
280
|
+
orb.x - orb.radius,
|
|
281
|
+
orb.y - orb.radius,
|
|
282
|
+
orb.radius * 2,
|
|
283
|
+
orb.radius * 2
|
|
284
|
+
);
|
|
285
|
+
context.restore();
|
|
286
|
+
context.beginPath();
|
|
287
|
+
context.strokeStyle = "rgba(15, 23, 42, 0.15)";
|
|
288
|
+
context.lineWidth = 1;
|
|
289
|
+
context.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2);
|
|
290
|
+
context.stroke();
|
|
291
|
+
} else {
|
|
292
|
+
context.fillStyle = getOrbColor(orb.level, config);
|
|
293
|
+
context.fill();
|
|
294
|
+
}
|
|
256
295
|
context.fillStyle = "#0f172a";
|
|
257
296
|
context.font = "bold 12px system-ui";
|
|
258
297
|
context.textAlign = "center";
|
|
@@ -291,10 +330,26 @@ function useMikuFusionGame(options = {}) {
|
|
|
291
330
|
const config = configRef.current;
|
|
292
331
|
const onScoreChangeRef = React3.useRef(options.onScoreChange);
|
|
293
332
|
const onGameOverRef = React3.useRef(options.onGameOver);
|
|
333
|
+
const orbImageMappingRef = React3.useRef(options.orbImageMapping ?? {});
|
|
334
|
+
const imageCacheRef = React3.useRef({});
|
|
294
335
|
React3.useEffect(() => {
|
|
295
336
|
onScoreChangeRef.current = options.onScoreChange;
|
|
296
337
|
onGameOverRef.current = options.onGameOver;
|
|
297
338
|
}, [options.onGameOver, options.onScoreChange]);
|
|
339
|
+
React3.useEffect(() => {
|
|
340
|
+
orbImageMappingRef.current = options.orbImageMapping ?? {};
|
|
341
|
+
const nextCache = {};
|
|
342
|
+
Object.entries(orbImageMappingRef.current).forEach(([levelText, src]) => {
|
|
343
|
+
const level = Number(levelText);
|
|
344
|
+
if (!src || Number.isNaN(level)) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const image = new window.Image();
|
|
348
|
+
image.src = src;
|
|
349
|
+
nextCache[level] = image;
|
|
350
|
+
});
|
|
351
|
+
imageCacheRef.current = nextCache;
|
|
352
|
+
}, [options.orbImageMapping]);
|
|
298
353
|
const [bestScore, setBestScore] = chunkKO73EBUT_js.useLocalStorage(
|
|
299
354
|
options.storageKey || "sa2kit:mikuFusionGame:bestScore",
|
|
300
355
|
0
|
|
@@ -327,7 +382,8 @@ function useMikuFusionGame(options = {}) {
|
|
|
327
382
|
orbsRef.current,
|
|
328
383
|
aimXRef.current,
|
|
329
384
|
nextLevel,
|
|
330
|
-
nextStatus
|
|
385
|
+
nextStatus,
|
|
386
|
+
imageCacheRef.current
|
|
331
387
|
);
|
|
332
388
|
},
|
|
333
389
|
[config, nextLevel, status]
|
|
@@ -571,8 +627,24 @@ function GameResultModal({ open, score, bestScore, onRestart }) {
|
|
|
571
627
|
}
|
|
572
628
|
|
|
573
629
|
// src/mikuFusionGame/components/MikuFusionGame.tsx
|
|
574
|
-
function MikuFusionGame({
|
|
630
|
+
function MikuFusionGame({
|
|
631
|
+
className,
|
|
632
|
+
storageKey,
|
|
633
|
+
orbImageStorageKey = "sa2kit:mikuFusionGame:orbImages",
|
|
634
|
+
enableImageConfigPanel = true,
|
|
635
|
+
presetOrbImageMapping = {},
|
|
636
|
+
...options
|
|
637
|
+
}) {
|
|
575
638
|
const containerRef = React3.useRef(null);
|
|
639
|
+
const [orbImageMapping, setOrbImageMapping] = React3.useState({});
|
|
640
|
+
const [isUserDefineMode] = React3.useState(() => {
|
|
641
|
+
if (typeof window === "undefined") {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
const value = new URLSearchParams(window.location.search).get("userDefine");
|
|
645
|
+
return value === "true";
|
|
646
|
+
});
|
|
647
|
+
const activeOrbImageMapping = isUserDefineMode ? orbImageMapping : presetOrbImageMapping;
|
|
576
648
|
const {
|
|
577
649
|
canvasRef,
|
|
578
650
|
status,
|
|
@@ -584,7 +656,46 @@ function MikuFusionGame({ className, storageKey, ...options }) {
|
|
|
584
656
|
handleDrop,
|
|
585
657
|
togglePause,
|
|
586
658
|
restart
|
|
587
|
-
} = useMikuFusionGame({ ...options, storageKey });
|
|
659
|
+
} = useMikuFusionGame({ ...options, storageKey, orbImageMapping: activeOrbImageMapping });
|
|
660
|
+
const panelItems = React3.useMemo(
|
|
661
|
+
() => Array.from({ length: config.maxLevel }, (_, index) => ({
|
|
662
|
+
id: String(index + 1),
|
|
663
|
+
label: `M${index + 1}`
|
|
664
|
+
})),
|
|
665
|
+
[config.maxLevel]
|
|
666
|
+
);
|
|
667
|
+
const panelValue = React3.useMemo(
|
|
668
|
+
() => Object.entries(orbImageMapping).reduce((acc, [key, value]) => {
|
|
669
|
+
if (value) {
|
|
670
|
+
acc[String(key)] = value;
|
|
671
|
+
}
|
|
672
|
+
return acc;
|
|
673
|
+
}, {}),
|
|
674
|
+
[orbImageMapping]
|
|
675
|
+
);
|
|
676
|
+
React3.useEffect(() => {
|
|
677
|
+
if (typeof window === "undefined") {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
const raw = window.localStorage.getItem(orbImageStorageKey);
|
|
682
|
+
if (!raw) {
|
|
683
|
+
setOrbImageMapping({});
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const parsed = JSON.parse(raw);
|
|
687
|
+
const normalized = {};
|
|
688
|
+
Object.entries(parsed).forEach(([key, value]) => {
|
|
689
|
+
const level = Number(key);
|
|
690
|
+
if (!Number.isNaN(level) && typeof value === "string" && value) {
|
|
691
|
+
normalized[level] = value;
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
setOrbImageMapping(normalized);
|
|
695
|
+
} catch {
|
|
696
|
+
setOrbImageMapping({});
|
|
697
|
+
}
|
|
698
|
+
}, [orbImageStorageKey]);
|
|
588
699
|
const { displayWidth, displayHeight } = useResponsiveCanvas(
|
|
589
700
|
config.width,
|
|
590
701
|
config.height,
|
|
@@ -624,6 +735,16 @@ function MikuFusionGame({ className, storageKey, ...options }) {
|
|
|
624
735
|
togglePause();
|
|
625
736
|
}
|
|
626
737
|
};
|
|
738
|
+
const handlePanelValueChange = (next) => {
|
|
739
|
+
const normalized = {};
|
|
740
|
+
Object.entries(next).forEach(([key, value]) => {
|
|
741
|
+
const level = Number(key);
|
|
742
|
+
if (!Number.isNaN(level) && value) {
|
|
743
|
+
normalized[level] = value;
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
setOrbImageMapping(normalized);
|
|
747
|
+
};
|
|
627
748
|
return /* @__PURE__ */ React3__default.default.createElement(
|
|
628
749
|
"div",
|
|
629
750
|
{
|
|
@@ -657,7 +778,17 @@ function MikuFusionGame({ className, storageKey, ...options }) {
|
|
|
657
778
|
onTogglePause: togglePause,
|
|
658
779
|
onRestart: restart
|
|
659
780
|
}
|
|
660
|
-
)
|
|
781
|
+
),
|
|
782
|
+
enableImageConfigPanel && isUserDefineMode ? /* @__PURE__ */ React3__default.default.createElement(
|
|
783
|
+
chunkHHVDOIPV_js.LocalImageMappingPanel,
|
|
784
|
+
{
|
|
785
|
+
storageKey: orbImageStorageKey,
|
|
786
|
+
title: "\u7403\u4F53\u56FE\u7247\u914D\u7F6E\uFF08\u4FDD\u5B58\u5230\u672C\u5730\uFF09",
|
|
787
|
+
items: panelItems,
|
|
788
|
+
defaultValue: panelValue,
|
|
789
|
+
onValueChange: handlePanelValueChange
|
|
790
|
+
}
|
|
791
|
+
) : null
|
|
661
792
|
);
|
|
662
793
|
}
|
|
663
794
|
|