@zhin.js/plugin-dungeon-expedition 1.1.14 → 1.1.16
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/commands/dungeon/[[action]]/index.js +4 -4
- package/commands/dungeon/[[action]]/index.ts +4 -4
- package/lib/dungeon-command.d.ts +1 -0
- package/lib/dungeon-command.js +22 -14
- package/lib/dungeon-command.js.map +1 -1
- package/lib/engine.d.ts +3 -2
- package/lib/engine.js +119 -70
- package/lib/engine.js.map +1 -1
- package/lib/game-flow.js +27 -13
- package/lib/game-flow.js.map +1 -1
- package/lib/models.d.ts +4 -1
- package/lib/models.js +2 -1
- package/lib/models.js.map +1 -1
- package/lib/session-service.js +3 -2
- package/lib/session-service.js.map +1 -1
- package/lib/view.d.ts +2 -1
- package/lib/view.js +65 -31
- package/lib/view.js.map +1 -1
- package/package.json +7 -7
- package/plugin.js +9 -1
- package/src/dungeon-command.ts +22 -18
- package/src/engine.ts +120 -59
- package/src/game-flow.ts +27 -14
- package/src/models.ts +4 -2
- package/src/session-service.ts +3 -1
- package/src/view.ts +67 -32
package/src/engine.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMessages, type Locale } from 'zhin.js/i18n';
|
|
1
2
|
import { DeterministicRandom } from '@zhin.js/game-kit';
|
|
2
3
|
|
|
3
4
|
export const DUNGEON_SCHEMA_VERSION = 1;
|
|
@@ -62,6 +63,7 @@ export class DungeonRuleError extends Error {
|
|
|
62
63
|
export function createDungeonState(
|
|
63
64
|
ownerId: string,
|
|
64
65
|
ownerName: string,
|
|
66
|
+
locale?: Locale,
|
|
65
67
|
): DungeonState {
|
|
66
68
|
return {
|
|
67
69
|
schemaVersion: DUNGEON_SCHEMA_VERSION,
|
|
@@ -74,7 +76,7 @@ export function createDungeonState(
|
|
|
74
76
|
turnIndex: 0,
|
|
75
77
|
players: [createPlayer(ownerId, ownerName, true)],
|
|
76
78
|
enemy: null,
|
|
77
|
-
log: [
|
|
79
|
+
log: [messages(locale, 'text1', { p0: ownerName })],
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -83,41 +85,42 @@ export function applyDungeonAction(
|
|
|
83
85
|
actorId: string,
|
|
84
86
|
action: DungeonAction,
|
|
85
87
|
random: DeterministicRandom,
|
|
88
|
+
locale?: Locale,
|
|
86
89
|
): DungeonState {
|
|
87
90
|
if (state.phase === 'completed') {
|
|
88
|
-
throw new DungeonRuleError('
|
|
91
|
+
throw new DungeonRuleError(messages(locale, 'text2'));
|
|
89
92
|
}
|
|
90
|
-
if (action.type === 'join') return joinParty(state, actorId, action.name);
|
|
93
|
+
if (action.type === 'join') return joinParty(state, actorId, action.name, locale);
|
|
91
94
|
const actorIndex = state.players.findIndex((player) => player.id === actorId);
|
|
92
|
-
if (actorIndex < 0) throw new DungeonRuleError('
|
|
95
|
+
if (actorIndex < 0) throw new DungeonRuleError(messages(locale, 'text3'));
|
|
93
96
|
if (action.type === 'abort') {
|
|
94
97
|
if (state.ownerId !== actorId) {
|
|
95
|
-
throw new DungeonRuleError('
|
|
98
|
+
throw new DungeonRuleError(messages(locale, 'text4'));
|
|
96
99
|
}
|
|
97
100
|
return appendLog({
|
|
98
101
|
...state,
|
|
99
102
|
phase: 'completed',
|
|
100
103
|
result: 'aborted',
|
|
101
104
|
enemy: null,
|
|
102
|
-
}, '
|
|
105
|
+
}, messages(locale, 'text5'));
|
|
103
106
|
}
|
|
104
107
|
if (action.type === 'ready') {
|
|
105
108
|
if (state.phase !== 'lobby') {
|
|
106
|
-
throw new DungeonRuleError('
|
|
109
|
+
throw new DungeonRuleError(messages(locale, 'text6'));
|
|
107
110
|
}
|
|
108
111
|
const players = state.players.map((player, index) =>
|
|
109
112
|
index === actorIndex ? { ...player, ready: !player.ready } : player);
|
|
110
113
|
return appendLog(
|
|
111
114
|
{ ...state, players },
|
|
112
|
-
`${players[actorIndex]?.name ?? actorId}${players[actorIndex]?.ready ? '
|
|
115
|
+
`${players[actorIndex]?.name ?? actorId}${players[actorIndex]?.ready ? messages(locale, 'text7') : messages(locale, 'text8')}。`,
|
|
113
116
|
);
|
|
114
117
|
}
|
|
115
|
-
if (action.type === 'start') return startExpedition(state, actorId);
|
|
116
|
-
assertActorTurn(state, actorId);
|
|
117
|
-
if (action.type === 'explore') return explore(state, random);
|
|
118
|
-
if (action.type === 'attack') return attack(state, actorIndex, random);
|
|
119
|
-
if (action.type === 'defend') return defend(state, actorIndex, random);
|
|
120
|
-
return drinkPotion(state, actorIndex, random);
|
|
118
|
+
if (action.type === 'start') return startExpedition(state, actorId, locale);
|
|
119
|
+
assertActorTurn(state, actorId, locale);
|
|
120
|
+
if (action.type === 'explore') return explore(state, random, locale);
|
|
121
|
+
if (action.type === 'attack') return attack(state, actorIndex, random, locale);
|
|
122
|
+
if (action.type === 'defend') return defend(state, actorIndex, random, locale);
|
|
123
|
+
return drinkPotion(state, actorIndex, random, locale);
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
export function activePlayer(state: Readonly<DungeonState>): DungeonPlayer | null {
|
|
@@ -156,73 +159,76 @@ function joinParty(
|
|
|
156
159
|
state: Readonly<DungeonState>,
|
|
157
160
|
actorId: string,
|
|
158
161
|
actorName: string,
|
|
162
|
+
locale?: Locale,
|
|
159
163
|
): DungeonState {
|
|
160
164
|
if (state.phase !== 'lobby') {
|
|
161
|
-
throw new DungeonRuleError('
|
|
165
|
+
throw new DungeonRuleError(messages(locale, 'text9'));
|
|
162
166
|
}
|
|
163
167
|
if (state.players.some((player) => player.id === actorId)) {
|
|
164
|
-
throw new DungeonRuleError('
|
|
168
|
+
throw new DungeonRuleError(messages(locale, 'text10'));
|
|
165
169
|
}
|
|
166
170
|
if (state.players.length >= MAX_PARTY_SIZE) {
|
|
167
|
-
throw new DungeonRuleError(
|
|
171
|
+
throw new DungeonRuleError(messages(locale, 'text11'));
|
|
168
172
|
}
|
|
169
173
|
const player = createPlayer(actorId, actorName, false);
|
|
170
174
|
return appendLog(
|
|
171
175
|
{ ...state, players: [...state.players, player] },
|
|
172
|
-
|
|
176
|
+
messages(locale, 'text12', { p0: player.name }),
|
|
173
177
|
);
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
function startExpedition(
|
|
177
181
|
state: Readonly<DungeonState>,
|
|
178
182
|
actorId: string,
|
|
183
|
+
locale?: Locale,
|
|
179
184
|
): DungeonState {
|
|
180
185
|
if (state.phase !== 'lobby') {
|
|
181
|
-
throw new DungeonRuleError('
|
|
186
|
+
throw new DungeonRuleError(messages(locale, 'text13'));
|
|
182
187
|
}
|
|
183
188
|
if (state.ownerId !== actorId) {
|
|
184
|
-
throw new DungeonRuleError('
|
|
189
|
+
throw new DungeonRuleError(messages(locale, 'text14'));
|
|
185
190
|
}
|
|
186
191
|
const waiting = state.players.filter((player) => !player.ready);
|
|
187
192
|
if (waiting.length > 0) {
|
|
188
193
|
throw new DungeonRuleError(
|
|
189
|
-
|
|
194
|
+
messages(locale, 'text15', { p0: waiting.map((player) => player.name).join('、') }),
|
|
190
195
|
);
|
|
191
196
|
}
|
|
192
|
-
return appendLog({ ...state, phase: 'exploring' }, '
|
|
197
|
+
return appendLog({ ...state, phase: 'exploring' }, messages(locale, 'text16'));
|
|
193
198
|
}
|
|
194
199
|
|
|
195
|
-
function assertActorTurn(state: Readonly<DungeonState>, actorId: string): void {
|
|
200
|
+
function assertActorTurn(state: Readonly<DungeonState>, actorId: string, locale?: Locale): void {
|
|
196
201
|
if (state.phase === 'lobby') {
|
|
197
|
-
throw new DungeonRuleError('
|
|
202
|
+
throw new DungeonRuleError(messages(locale, 'text17'));
|
|
198
203
|
}
|
|
199
204
|
const current = activePlayer(state);
|
|
200
205
|
if (!current || current.id !== actorId) {
|
|
201
|
-
throw new DungeonRuleError(
|
|
206
|
+
throw new DungeonRuleError(messages(locale, 'text18', { p0: current?.name ?? messages(locale, 'otherPlayer') }));
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
function explore(
|
|
206
211
|
state: Readonly<DungeonState>,
|
|
207
212
|
random: DeterministicRandom,
|
|
213
|
+
locale?: Locale,
|
|
208
214
|
): DungeonState {
|
|
209
215
|
if (state.phase !== 'exploring') {
|
|
210
|
-
throw new DungeonRuleError('
|
|
216
|
+
throw new DungeonRuleError(messages(locale, 'text19'));
|
|
211
217
|
}
|
|
212
218
|
const room = state.room + 1;
|
|
213
219
|
if (room >= BOSS_ROOM) {
|
|
214
|
-
const enemy = createEnemy(state.floor, true, random);
|
|
220
|
+
const enemy = createEnemy(state.floor, true, random, locale);
|
|
215
221
|
return appendLog(
|
|
216
222
|
{ ...state, room, phase: 'combat', enemy },
|
|
217
|
-
|
|
223
|
+
messages(locale, 'text20', { p0: enemy.name }),
|
|
218
224
|
);
|
|
219
225
|
}
|
|
220
226
|
const roll = random.int(100);
|
|
221
227
|
if (roll < 58) {
|
|
222
|
-
const enemy = createEnemy(state.floor, false, random);
|
|
228
|
+
const enemy = createEnemy(state.floor, false, random, locale);
|
|
223
229
|
return appendLog(
|
|
224
230
|
{ ...state, room, phase: 'combat', enemy },
|
|
225
|
-
|
|
231
|
+
messages(locale, 'text21', { p0: enemy.name }),
|
|
226
232
|
);
|
|
227
233
|
}
|
|
228
234
|
if (roll < 78) {
|
|
@@ -236,7 +242,7 @@ function explore(
|
|
|
236
242
|
gold: player.gold + gold,
|
|
237
243
|
})),
|
|
238
244
|
},
|
|
239
|
-
|
|
245
|
+
messages(locale, 'text22', { p0: gold }),
|
|
240
246
|
);
|
|
241
247
|
}
|
|
242
248
|
if (roll < 90) {
|
|
@@ -249,7 +255,7 @@ function explore(
|
|
|
249
255
|
hp: Math.min(player.maxHp, player.hp + 18),
|
|
250
256
|
})),
|
|
251
257
|
},
|
|
252
|
-
'
|
|
258
|
+
messages(locale, 'text23'),
|
|
253
259
|
);
|
|
254
260
|
}
|
|
255
261
|
const damage = random.intInclusive(8, 16);
|
|
@@ -261,9 +267,9 @@ function explore(
|
|
|
261
267
|
: player);
|
|
262
268
|
const trapped = advanceAfterRoom(
|
|
263
269
|
{ ...state, room, players },
|
|
264
|
-
|
|
270
|
+
messages(locale, 'text24', { p0: target?.name ?? messages(locale, 'player'), p1: damage }),
|
|
265
271
|
);
|
|
266
|
-
const resolved = finishIfDefeated(trapped);
|
|
272
|
+
const resolved = finishIfDefeated(trapped, locale);
|
|
267
273
|
if (resolved.phase === 'completed' || (players[targetIndex]?.hp ?? 0) > 0) {
|
|
268
274
|
return resolved;
|
|
269
275
|
}
|
|
@@ -274,49 +280,52 @@ function attack(
|
|
|
274
280
|
state: Readonly<DungeonState>,
|
|
275
281
|
actorIndex: number,
|
|
276
282
|
random: DeterministicRandom,
|
|
283
|
+
locale?: Locale,
|
|
277
284
|
): DungeonState {
|
|
278
285
|
if (state.phase !== 'combat' || !state.enemy) {
|
|
279
|
-
throw new DungeonRuleError('
|
|
286
|
+
throw new DungeonRuleError(messages(locale, 'text25'));
|
|
280
287
|
}
|
|
281
288
|
const actor = state.players[actorIndex];
|
|
282
|
-
if (!actor || actor.hp <= 0) throw new DungeonRuleError('
|
|
289
|
+
if (!actor || actor.hp <= 0) throw new DungeonRuleError(messages(locale, 'text26'));
|
|
283
290
|
const damage = random.intInclusive(12, 22);
|
|
284
291
|
const enemy = { ...state.enemy, hp: Math.max(0, state.enemy.hp - damage) };
|
|
285
292
|
let next = appendLog(
|
|
286
293
|
{ ...state, enemy },
|
|
287
|
-
|
|
294
|
+
messages(locale, 'text27', { p0: actor.name, p1: enemy.name, p2: damage }),
|
|
288
295
|
);
|
|
289
|
-
if (enemy.hp <= 0) return defeatEnemy(next, random);
|
|
290
|
-
next = enemyTurn(next, actorIndex, random);
|
|
291
|
-
return advanceTurn(finishIfDefeated(next));
|
|
296
|
+
if (enemy.hp <= 0) return defeatEnemy(next, random, locale);
|
|
297
|
+
next = enemyTurn(next, actorIndex, random, locale);
|
|
298
|
+
return advanceTurn(finishIfDefeated(next, locale));
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
function defend(
|
|
295
302
|
state: Readonly<DungeonState>,
|
|
296
303
|
actorIndex: number,
|
|
297
304
|
random: DeterministicRandom,
|
|
305
|
+
locale?: Locale,
|
|
298
306
|
): DungeonState {
|
|
299
307
|
if (state.phase !== 'combat' || !state.enemy) {
|
|
300
|
-
throw new DungeonRuleError('
|
|
308
|
+
throw new DungeonRuleError(messages(locale, 'text28'));
|
|
301
309
|
}
|
|
302
310
|
const actor = state.players[actorIndex];
|
|
303
|
-
if (!actor || actor.hp <= 0) throw new DungeonRuleError('
|
|
311
|
+
if (!actor || actor.hp <= 0) throw new DungeonRuleError(messages(locale, 'text26'));
|
|
304
312
|
const players = state.players.map((player, index) =>
|
|
305
313
|
index === actorIndex ? { ...player, guard: 8 } : player);
|
|
306
|
-
let next = appendLog({ ...state, players },
|
|
307
|
-
next = enemyTurn(next, actorIndex, random);
|
|
308
|
-
return advanceTurn(finishIfDefeated(next));
|
|
314
|
+
let next = appendLog({ ...state, players }, messages(locale, 'text29', { p0: actor.name }));
|
|
315
|
+
next = enemyTurn(next, actorIndex, random, locale);
|
|
316
|
+
return advanceTurn(finishIfDefeated(next, locale));
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
function drinkPotion(
|
|
312
320
|
state: Readonly<DungeonState>,
|
|
313
321
|
actorIndex: number,
|
|
314
322
|
random: DeterministicRandom,
|
|
323
|
+
locale?: Locale,
|
|
315
324
|
): DungeonState {
|
|
316
325
|
const actor = state.players[actorIndex];
|
|
317
|
-
if (!actor || actor.hp <= 0) throw new DungeonRuleError('
|
|
318
|
-
if (actor.potions <= 0) throw new DungeonRuleError('
|
|
319
|
-
if (actor.hp >= actor.maxHp) throw new DungeonRuleError('
|
|
326
|
+
if (!actor || actor.hp <= 0) throw new DungeonRuleError(messages(locale, 'text26'));
|
|
327
|
+
if (actor.potions <= 0) throw new DungeonRuleError(messages(locale, 'text30'));
|
|
328
|
+
if (actor.hp >= actor.maxHp) throw new DungeonRuleError(messages(locale, 'text31'));
|
|
320
329
|
const players = state.players.map((player, index) =>
|
|
321
330
|
index === actorIndex
|
|
322
331
|
? {
|
|
@@ -325,10 +334,10 @@ function drinkPotion(
|
|
|
325
334
|
potions: player.potions - 1,
|
|
326
335
|
}
|
|
327
336
|
: player);
|
|
328
|
-
let next = appendLog({ ...state, players },
|
|
337
|
+
let next = appendLog({ ...state, players }, messages(locale, 'text32', { p0: actor.name }));
|
|
329
338
|
if (state.phase === 'combat') {
|
|
330
|
-
next = enemyTurn(next, actorIndex, random);
|
|
331
|
-
return advanceTurn(finishIfDefeated(next));
|
|
339
|
+
next = enemyTurn(next, actorIndex, random, locale);
|
|
340
|
+
return advanceTurn(finishIfDefeated(next, locale));
|
|
332
341
|
}
|
|
333
342
|
return next;
|
|
334
343
|
}
|
|
@@ -337,6 +346,7 @@ function enemyTurn(
|
|
|
337
346
|
state: Readonly<DungeonState>,
|
|
338
347
|
targetIndex: number,
|
|
339
348
|
random: DeterministicRandom,
|
|
349
|
+
locale?: Locale,
|
|
340
350
|
): DungeonState {
|
|
341
351
|
const enemy = state.enemy;
|
|
342
352
|
const target = state.players[targetIndex];
|
|
@@ -352,13 +362,14 @@ function enemyTurn(
|
|
|
352
362
|
: player);
|
|
353
363
|
return appendLog(
|
|
354
364
|
{ ...state, players },
|
|
355
|
-
|
|
365
|
+
messages(locale, 'text33', { p0: enemy.name, p1: target.name, p2: damage }),
|
|
356
366
|
);
|
|
357
367
|
}
|
|
358
368
|
|
|
359
369
|
function defeatEnemy(
|
|
360
370
|
state: Readonly<DungeonState>,
|
|
361
371
|
random: DeterministicRandom,
|
|
372
|
+
locale?: Locale,
|
|
362
373
|
): DungeonState {
|
|
363
374
|
const enemy = state.enemy;
|
|
364
375
|
if (!enemy) return state;
|
|
@@ -377,7 +388,7 @@ function defeatEnemy(
|
|
|
377
388
|
phase: 'completed',
|
|
378
389
|
result: 'victory',
|
|
379
390
|
roomsCleared: state.roomsCleared + 1,
|
|
380
|
-
},
|
|
391
|
+
}, messages(locale, 'text34', { p0: gold }));
|
|
381
392
|
}
|
|
382
393
|
if (enemy.boss) {
|
|
383
394
|
return appendLog({
|
|
@@ -386,11 +397,11 @@ function defeatEnemy(
|
|
|
386
397
|
floor: state.floor + 1,
|
|
387
398
|
room: 0,
|
|
388
399
|
roomsCleared: state.roomsCleared + 1,
|
|
389
|
-
},
|
|
400
|
+
}, messages(locale, 'text35', { p0: gold }));
|
|
390
401
|
}
|
|
391
402
|
return advanceAfterRoom(
|
|
392
403
|
{ ...rewarded, phase: 'exploring' },
|
|
393
|
-
|
|
404
|
+
messages(locale, 'text36', { p0: enemy.name, p1: gold }),
|
|
394
405
|
);
|
|
395
406
|
}
|
|
396
407
|
|
|
@@ -398,10 +409,11 @@ function createEnemy(
|
|
|
398
409
|
floor: number,
|
|
399
410
|
boss: boolean,
|
|
400
411
|
random: DeterministicRandom,
|
|
412
|
+
locale?: Locale,
|
|
401
413
|
): DungeonEnemy {
|
|
402
414
|
const names = boss
|
|
403
|
-
? ['
|
|
404
|
-
: ['
|
|
415
|
+
? [messages(locale, 'text37'), messages(locale, 'text38'), messages(locale, 'text39')]
|
|
416
|
+
: [messages(locale, 'text40'), messages(locale, 'text41'), messages(locale, 'text42'), messages(locale, 'text43')];
|
|
405
417
|
const name = boss
|
|
406
418
|
? names[Math.min(names.length - 1, floor - 1)] as string
|
|
407
419
|
: random.item(names);
|
|
@@ -445,14 +457,14 @@ function nextLivingIndex(
|
|
|
445
457
|
return 0;
|
|
446
458
|
}
|
|
447
459
|
|
|
448
|
-
function finishIfDefeated(state: Readonly<DungeonState
|
|
460
|
+
function finishIfDefeated(state: Readonly<DungeonState>, locale?: Locale): DungeonState {
|
|
449
461
|
if (state.players.some((player) => player.hp > 0)) return state as DungeonState;
|
|
450
462
|
return appendLog({
|
|
451
463
|
...state,
|
|
452
464
|
phase: 'completed',
|
|
453
465
|
result: 'defeat',
|
|
454
466
|
enemy: null,
|
|
455
|
-
}, '
|
|
467
|
+
}, messages(locale, 'text44'));
|
|
456
468
|
}
|
|
457
469
|
|
|
458
470
|
function appendLog(
|
|
@@ -464,3 +476,52 @@ function appendLog(
|
|
|
464
476
|
log: [...state.log, message].slice(-8),
|
|
465
477
|
} as DungeonState;
|
|
466
478
|
}
|
|
479
|
+
|
|
480
|
+
const messages = createMessages({
|
|
481
|
+
otherPlayer: { 'zh-CN': '其他队员', en: 'another player' },
|
|
482
|
+
player: { 'zh-CN': '队员', en: 'A player' },
|
|
483
|
+
text1: { 'zh-CN': "{p0} 创建了远征队。", en: "{p0} created the expedition party." },
|
|
484
|
+
text2: { 'zh-CN': "本次远征已经结束。", en: "This expedition has ended." },
|
|
485
|
+
text3: { 'zh-CN': "你还没有加入这支远征队。", en: "You have not joined this expedition." },
|
|
486
|
+
text4: { 'zh-CN': "只有队长可以结束整支远征。", en: "Only the leader may end the expedition." },
|
|
487
|
+
text5: { 'zh-CN': "队长结束了本次远征。", en: "The leader ended the expedition." },
|
|
488
|
+
text6: { 'zh-CN': "远征已经开始,无法修改准备状态。", en: "The expedition has started; readiness can no longer be changed." },
|
|
489
|
+
text7: { 'zh-CN': "已准备", en: " is ready" },
|
|
490
|
+
text8: { 'zh-CN': "取消准备", en: " is no longer ready" },
|
|
491
|
+
text9: { 'zh-CN': "远征已经出发,无法中途加入。", en: "The expedition has started; you cannot join mid-game." },
|
|
492
|
+
text10: { 'zh-CN': "你已经在远征队中。", en: "You are already in the party." },
|
|
493
|
+
text11: { 'zh-CN': "远征队已满,最多 4 人。", en: "The party is full (maximum four players)." },
|
|
494
|
+
text12: { 'zh-CN': "{p0} 加入了远征队。", en: "{p0} joined the party." },
|
|
495
|
+
text13: { 'zh-CN': "远征已经开始。", en: "The expedition has already started." },
|
|
496
|
+
text14: { 'zh-CN': "只有队长可以带队出发。", en: "Only the leader may start the expedition." },
|
|
497
|
+
text15: { 'zh-CN': "仍有队员未准备:{p0}", en: "Players not ready: {p0}" },
|
|
498
|
+
text16: { 'zh-CN': "远征队进入了第一层地牢。", en: "The party entered the first floor." },
|
|
499
|
+
text17: { 'zh-CN': "请先由队长开始远征。", en: "Ask the leader to start the expedition first." },
|
|
500
|
+
text18: { 'zh-CN': "现在轮到 {p0} 行动。", en: "It is {p0}’s turn." },
|
|
501
|
+
text19: { 'zh-CN': "战斗尚未结束,无法继续探索。", en: "Finish the battle before exploring further." },
|
|
502
|
+
text20: { 'zh-CN': "守层者 {p0} 挡住了去路!", en: "Floor guardian {p0} blocks the way!" },
|
|
503
|
+
text21: { 'zh-CN': "遭遇了 {p0}!", en: "Encountered {p0}!" },
|
|
504
|
+
text22: { 'zh-CN': "发现宝箱,每位队员获得 {p0} 金币。", en: "A treasure chest! Each player gains {p0} gold." },
|
|
505
|
+
text23: { 'zh-CN': "发现治愈泉,队伍恢复了生命。", en: "A healing spring restores the party’s health." },
|
|
506
|
+
text24: { 'zh-CN': "{p0} 触发陷阱,受到 {p1} 点伤害。", en: "{p0} triggered a trap and took {p1} damage." },
|
|
507
|
+
text25: { 'zh-CN': "当前没有可以攻击的敌人。", en: "There is no enemy to attack." },
|
|
508
|
+
text26: { 'zh-CN': "倒下的队员无法行动。", en: "Defeated players cannot act." },
|
|
509
|
+
text27: { 'zh-CN': "{p0} 对 {p1} 造成 {p2} 点伤害。", en: "{p0} dealt {p2} damage to {p1}." },
|
|
510
|
+
text28: { 'zh-CN': "当前不需要防御。", en: "There is no need to defend right now." },
|
|
511
|
+
text29: { 'zh-CN': "{p0} 架起了盾牌。", en: "{p0} raised a shield." },
|
|
512
|
+
text30: { 'zh-CN': "你的药水已经用完了。", en: "You have no potions left." },
|
|
513
|
+
text31: { 'zh-CN': "你的生命值已经满了。", en: "Your health is already full." },
|
|
514
|
+
text32: { 'zh-CN': "{p0} 使用药水恢复生命。", en: "{p0} used a potion to restore health." },
|
|
515
|
+
text33: { 'zh-CN': "{p0} 反击 {p1},造成 {p2} 点伤害。", en: "{p0} counterattacked {p1} for {p2} damage." },
|
|
516
|
+
text34: { 'zh-CN': "击败最终守层者,每人获得 {p0} 金币。远征胜利!", en: "The final guardian is defeated! Each player gains {p0} gold. Expedition victory!" },
|
|
517
|
+
text35: { 'zh-CN': "击败守层者,每人获得 {p0} 金币。队伍进入下一层。", en: "The floor guardian is defeated! Each player gains {p0} gold. The party advances to the next floor." },
|
|
518
|
+
text36: { 'zh-CN': "击败 {p0},每人获得 {p1} 金币。", en: "Defeated {p0}! Each player gains {p1} gold." },
|
|
519
|
+
text37: { 'zh-CN': "石像守卫", en: "Stone Guardian" },
|
|
520
|
+
text38: { 'zh-CN': "深渊术士", en: "Abyss Warlock" },
|
|
521
|
+
text39: { 'zh-CN': "远古龙影", en: "Ancient Dragon Shade" },
|
|
522
|
+
text40: { 'zh-CN': "洞穴史莱姆", en: "Cave Slime" },
|
|
523
|
+
text41: { 'zh-CN': "骷髅巡卫", en: "Skeleton Patrol" },
|
|
524
|
+
text42: { 'zh-CN': "暗影猎手", en: "Shadow Hunter" },
|
|
525
|
+
text43: { 'zh-CN': "失控魔像", en: "Rampaging Golem" },
|
|
526
|
+
text44: { 'zh-CN': "远征队全员倒下,本次远征失败。", en: "The entire party fell. The expedition failed." },
|
|
527
|
+
});
|
package/src/game-flow.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMessages, resolveLocale } from 'zhin.js/i18n';
|
|
1
2
|
import {
|
|
2
3
|
GameSessionConflictError,
|
|
3
4
|
SessionRevisionConflictError,
|
|
@@ -29,7 +30,7 @@ export async function startDungeon(
|
|
|
29
30
|
const session = await service.createSession(message);
|
|
30
31
|
return buildDungeonView(session, stateFromSession(session), message.$channel.type);
|
|
31
32
|
} catch (error) {
|
|
32
|
-
if (error instanceof GameSessionConflictError) return error.message;
|
|
33
|
+
if (error instanceof GameSessionConflictError) return error.format(message.locale);
|
|
33
34
|
throw error;
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -38,8 +39,9 @@ export async function continueDungeon(
|
|
|
38
39
|
service: SessionService,
|
|
39
40
|
message: GameMessageLike,
|
|
40
41
|
): Promise<GameReply> {
|
|
42
|
+
const locale = resolveLocale(message.locale);
|
|
41
43
|
const session = await service.getActiveByChannel(channelKey(message));
|
|
42
|
-
if (!session) return
|
|
44
|
+
if (!session) return messages(locale, 'text1');
|
|
43
45
|
return buildDungeonView(session, stateFromSession(session), message.$channel.type);
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -49,19 +51,21 @@ export async function handleDungeonChoice(
|
|
|
49
51
|
sessionToken: string,
|
|
50
52
|
choiceId: string,
|
|
51
53
|
): Promise<GameReply> {
|
|
54
|
+
let locale = resolveLocale(message.locale);
|
|
52
55
|
const token = parseDungeonSessionToken(sessionToken);
|
|
53
|
-
if (!token) return '
|
|
56
|
+
if (!token) return messages(locale, 'text2');
|
|
54
57
|
const session = await service.getById(token.sessionId);
|
|
55
58
|
if (!session || session.channel_key !== channelKey(message)) {
|
|
56
|
-
return '
|
|
59
|
+
return messages(locale, 'text3');
|
|
57
60
|
}
|
|
61
|
+
locale = resolveLocale(session.locale);
|
|
58
62
|
if (choiceId === 'restart') {
|
|
59
|
-
if (session.status === 'active') return '
|
|
60
|
-
if (session.owner_id !== message.$sender.id) return '
|
|
63
|
+
if (session.status === 'active') return messages(locale, 'text4');
|
|
64
|
+
if (session.owner_id !== message.$sender.id) return messages(locale, 'text5');
|
|
61
65
|
return startDungeon(service, message);
|
|
62
66
|
}
|
|
63
67
|
const action = actionFromChoice(choiceId, message);
|
|
64
|
-
if (!action) return '
|
|
68
|
+
if (!action) return messages(locale, 'text6');
|
|
65
69
|
try {
|
|
66
70
|
const result = await service.performAction({
|
|
67
71
|
sessionId: session.id,
|
|
@@ -77,21 +81,19 @@ export async function handleDungeonChoice(
|
|
|
77
81
|
message.$channel.type,
|
|
78
82
|
);
|
|
79
83
|
} catch (error) {
|
|
80
|
-
if (error instanceof DungeonRuleError
|
|
81
|
-
|
|
82
|
-
return error.message;
|
|
83
|
-
}
|
|
84
|
+
if (error instanceof DungeonRuleError) return error.message;
|
|
85
|
+
if (error instanceof GameSessionConflictError) return error.format(locale);
|
|
84
86
|
if (error instanceof SessionRevisionConflictError) {
|
|
85
87
|
const latest = await service.getById(session.id);
|
|
86
|
-
if (!latest) return '
|
|
88
|
+
if (!latest) return messages(locale, 'text7');
|
|
87
89
|
const view = buildDungeonView(
|
|
88
90
|
latest,
|
|
89
91
|
stateFromSession(latest),
|
|
90
92
|
message.$channel.type,
|
|
91
93
|
);
|
|
92
94
|
return Array.isArray(view)
|
|
93
|
-
? ['
|
|
94
|
-
: ['
|
|
95
|
+
? [messages(locale, 'text8'), ...view]
|
|
96
|
+
: [messages(locale, 'text8'), view];
|
|
95
97
|
}
|
|
96
98
|
throw error;
|
|
97
99
|
}
|
|
@@ -124,3 +126,14 @@ function actionFromChoice(
|
|
|
124
126
|
if (choiceId === 'abort') return { type: 'abort' };
|
|
125
127
|
return null;
|
|
126
128
|
}
|
|
129
|
+
|
|
130
|
+
const messages = createMessages({
|
|
131
|
+
text1: { 'zh-CN': "当前频道没有进行中的地牢远征。", en: "There is no active dungeon expedition in this channel." },
|
|
132
|
+
text2: { 'zh-CN': "无效的地牢操作。", en: "Invalid dungeon action." },
|
|
133
|
+
text3: { 'zh-CN': "远征不存在,或不属于当前频道。", en: "The expedition does not exist or belongs to another channel." },
|
|
134
|
+
text4: { 'zh-CN': "当前远征尚未结束。", en: "The current expedition has not ended." },
|
|
135
|
+
text5: { 'zh-CN': "只有原队长可以重新创建远征。", en: "Only the original leader may restart the expedition." },
|
|
136
|
+
text6: { 'zh-CN': "未知的地牢操作。", en: "Unknown dungeon action." },
|
|
137
|
+
text7: { 'zh-CN': "远征状态已变化,请重新打开当前界面。", en: "The expedition state changed. Reopen its current view." },
|
|
138
|
+
text8: { 'zh-CN': "操作来自旧回合,已为你刷新最新状态。", en: "This action was from an earlier turn. The latest state is shown below." },
|
|
139
|
+
});
|
package/src/models.ts
CHANGED
|
@@ -6,6 +6,7 @@ declare module '@zhin.js/core' {
|
|
|
6
6
|
interface Models {
|
|
7
7
|
dungeon_sessions: {
|
|
8
8
|
id: string;
|
|
9
|
+
locale?: 'zh-CN' | 'en' | null;
|
|
9
10
|
adapter: string;
|
|
10
11
|
endpoint: string;
|
|
11
12
|
channel_type: string;
|
|
@@ -32,11 +33,12 @@ export type DungeonSessionRow = Models['dungeon_sessions'];
|
|
|
32
33
|
|
|
33
34
|
export function defineHostTables(
|
|
34
35
|
database: {
|
|
35
|
-
define(name: string, definition: Record<string, unknown
|
|
36
|
+
define(name: string, definition: Record<string, unknown>, options?: { readonly addColumns?: readonly string[] }): void;
|
|
36
37
|
},
|
|
37
38
|
): void {
|
|
38
39
|
database.define('dungeon_sessions', {
|
|
39
40
|
id: { type: 'text', primary: true },
|
|
41
|
+
locale: { type: 'text', nullable: true },
|
|
40
42
|
adapter: { type: 'text', nullable: false },
|
|
41
43
|
endpoint: { type: 'text', nullable: false },
|
|
42
44
|
channel_type: { type: 'text', nullable: false },
|
|
@@ -55,5 +57,5 @@ export function defineHostTables(
|
|
|
55
57
|
status: { type: 'text', default: 'active' },
|
|
56
58
|
updated_at: { type: 'bigint', default: 0 },
|
|
57
59
|
created_at: { type: 'bigint', default: 0 },
|
|
58
|
-
});
|
|
60
|
+
}, { addColumns: ['locale'] });
|
|
59
61
|
}
|
package/src/session-service.ts
CHANGED
|
@@ -47,10 +47,11 @@ export class SessionService extends VersionedSessionService<DungeonSessionRow> {
|
|
|
47
47
|
const now = Date.now();
|
|
48
48
|
const id = generateCompactId('dng_');
|
|
49
49
|
const ownerName = message.$sender.name?.trim() || message.$sender.id;
|
|
50
|
-
const state = createDungeonState(message.$sender.id, ownerName);
|
|
50
|
+
const state = createDungeonState(message.$sender.id, ownerName, message.locale);
|
|
51
51
|
const random = DeterministicRandom.fromSeed(seed ?? id);
|
|
52
52
|
return this.createRow({
|
|
53
53
|
id,
|
|
54
|
+
locale: message.locale ?? 'zh-CN',
|
|
54
55
|
adapter: String(message.$adapter),
|
|
55
56
|
endpoint: message.$endpoint,
|
|
56
57
|
channel_type: message.$channel.type,
|
|
@@ -113,6 +114,7 @@ export class SessionService extends VersionedSessionService<DungeonSessionRow> {
|
|
|
113
114
|
options.actorId,
|
|
114
115
|
options.action,
|
|
115
116
|
random,
|
|
117
|
+
session.locale ?? undefined,
|
|
116
118
|
);
|
|
117
119
|
return {
|
|
118
120
|
state_json: JSON.stringify(next),
|