@zhin.js/plugin-roulette 0.1.4 → 0.1.6
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 +35 -0
- package/commands/roulette/[[action]]/index.js +4 -3
- package/lib/deadline-job.js +2 -0
- package/lib/engine.d.ts +53 -1
- package/lib/engine.js +39 -19
- package/lib/game-flow.js +64 -36
- package/lib/models.d.ts +4 -1
- package/lib/models.js +2 -1
- package/lib/roulette-actions.d.ts +2 -0
- package/lib/roulette-actions.js +23 -14
- package/lib/roulette-command.js +3 -3
- package/lib/runtime.d.ts +3 -0
- package/lib/session-service.js +1 -0
- package/lib/view.d.ts +11 -8
- package/lib/view.js +63 -58
- package/package.json +10 -10
- package/plugin.js +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @zhin.js/plugin-roulette
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [f67883e]
|
|
8
|
+
- Updated dependencies [4d728c0]
|
|
9
|
+
- Updated dependencies [24a2b71]
|
|
10
|
+
- Updated dependencies [3771f7a]
|
|
11
|
+
- Updated dependencies [4f96ad5]
|
|
12
|
+
- @zhin.js/core@1.1.52
|
|
13
|
+
- @zhin.js/game-kit@1.1.17
|
|
14
|
+
|
|
15
|
+
## 0.1.5
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 9cade30: 修复国际化调用链遗漏:Agent 审批、管理命令和错误反馈采用可信语言及实际命令前缀;游戏快捷帮助、工具反馈、平台默认卡片和后台通知贯通语言。协议即时回复复用受运行代约束的个人语言查询,业务验证支持不改变审批身份的双语展示元数据。
|
|
20
|
+
|
|
21
|
+
AI 正文、进度与耗尽摘要统一优先遵从用户明确语言要求,再跟随实际输入;个人/项目语言仅作兜底,固定界面文案继续使用配置语言,不增加翻译调用。
|
|
22
|
+
|
|
23
|
+
GitHub 工作区为 Git clone/pull/fetch/ls-remote 显式分隔选项与位置参数,保留仓库和分支校验;顺序写入 Bot 名称和邮箱,避免竞争同一配置文件锁。
|
|
24
|
+
|
|
25
|
+
- 53bc6a0: 官方工具插件与游戏支持中英文反馈、帮助和配置标签;提醒增加与中文对应的有限英文时间语法。聊天后台任务记录创建者当时语言,管理员任务记录项目语言,旧任务保持中文;多轮选择与游戏使用开始时语言,保留第三方内容和已生成报告。
|
|
26
|
+
- Updated dependencies [18296cf]
|
|
27
|
+
- Updated dependencies [08e64de]
|
|
28
|
+
- Updated dependencies [9cade30]
|
|
29
|
+
- Updated dependencies [53bc6a0]
|
|
30
|
+
- Updated dependencies [53bc6a0]
|
|
31
|
+
- Updated dependencies [66b6404]
|
|
32
|
+
- Updated dependencies [adb4af2]
|
|
33
|
+
- Updated dependencies [69f9ef6]
|
|
34
|
+
- Updated dependencies [17d1301]
|
|
35
|
+
- @zhin.js/core@1.1.51
|
|
36
|
+
- @zhin.js/game-kit@1.1.16
|
|
37
|
+
|
|
3
38
|
## 0.1.4
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { gameCommandFeedback } from '@zhin.js/game-kit';
|
|
2
3
|
import { defineCommand } from 'zhin.js/command';
|
|
3
|
-
import {
|
|
4
|
+
import { getRouletteHelp } from "../../../lib/roulette-actions.js";
|
|
4
5
|
import { runRouletteCommand } from "../../../lib/roulette-command.js";
|
|
5
6
|
export default defineCommand({
|
|
6
|
-
description: '俄罗斯轮盘:多人轮流开枪,中弹者输',
|
|
7
|
+
description: '俄罗斯轮盘:多人轮流开枪,中弹者输', descriptionI18n: { en: "Russian roulette: take turns; the player hit loses" },
|
|
7
8
|
params: { action: { type: 'string', default: '' } },
|
|
8
9
|
async execute(context) {
|
|
9
10
|
const action = String(context.params.action ?? '');
|
|
10
11
|
const reply = await runRouletteCommand(context, context.input, action);
|
|
11
|
-
return reply ??
|
|
12
|
+
return reply ?? gameCommandFeedback(context, 'unknown', action) + '\n\n' + getRouletteHelp(context);
|
|
12
13
|
},
|
|
13
14
|
});
|
package/lib/deadline-job.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { outboundHostToken } from 'zhin.js';
|
|
2
|
+
import { resolveLocale } from 'zhin.js/i18n';
|
|
2
3
|
import { expireOverdueSessions } from './game-flow.js';
|
|
3
4
|
import { resolveRouletteFlow } from './runtime.js';
|
|
4
5
|
/** Turn and lobby deadlines are checked every five seconds. */
|
|
@@ -28,6 +29,7 @@ async function announce(outbound, notice) {
|
|
|
28
29
|
const session = notice.session;
|
|
29
30
|
try {
|
|
30
31
|
await outbound.send({
|
|
32
|
+
locale: resolveLocale(session.locale),
|
|
31
33
|
adapter: session.adapter,
|
|
32
34
|
endpointKey: session.endpoint,
|
|
33
35
|
conversation: {
|
package/lib/engine.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Locale } from 'zhin.js/i18n';
|
|
1
2
|
/**
|
|
2
3
|
* Pure Russian roulette rules. No I/O, no clock and no randomness: callers pass
|
|
3
4
|
* the bullet position in, so every transition is deterministic and testable.
|
|
@@ -37,7 +38,10 @@ export interface TriggerResult {
|
|
|
37
38
|
}
|
|
38
39
|
/** A rule violation the player should read, e.g. firing out of turn. */
|
|
39
40
|
export declare class RouletteRuleError extends Error {
|
|
40
|
-
|
|
41
|
+
readonly code: RouletteRuleCode;
|
|
42
|
+
readonly parameters: Readonly<Record<string, string | number>>;
|
|
43
|
+
constructor(code: RouletteRuleCode, parameters?: Readonly<Record<string, string | number>>);
|
|
44
|
+
format(locale?: Locale): string;
|
|
41
45
|
}
|
|
42
46
|
export declare function createLobby(host: RoulettePlayer, rules: RouletteTableRules): RouletteState;
|
|
43
47
|
export declare function joinLobby(state: RouletteState, player: RoulettePlayer, rules: RouletteTableRules): RouletteState;
|
|
@@ -59,3 +63,51 @@ export declare function loserOf(state: RouletteState): RoulettePlayer | undefine
|
|
|
59
63
|
export declare function survivorsOf(state: RouletteState): readonly RoulettePlayer[];
|
|
60
64
|
export declare function remainingChambers(state: RouletteState): number;
|
|
61
65
|
export declare function decodeRouletteState(json: string): RouletteState;
|
|
66
|
+
declare const ruleCatalog: {
|
|
67
|
+
readonly cannotJoin: {
|
|
68
|
+
readonly 'zh-CN': "对局已经开始,不能再加入了。";
|
|
69
|
+
readonly en: "The game has started; you can no longer join.";
|
|
70
|
+
};
|
|
71
|
+
readonly alreadyJoined: {
|
|
72
|
+
readonly 'zh-CN': "你已经在这局轮盘里了。";
|
|
73
|
+
readonly en: "You are already in this game.";
|
|
74
|
+
};
|
|
75
|
+
readonly full: {
|
|
76
|
+
readonly 'zh-CN': "人数已满(最多 {max} 人)。";
|
|
77
|
+
readonly en: "The table is full (maximum {max} players).";
|
|
78
|
+
};
|
|
79
|
+
readonly started: {
|
|
80
|
+
readonly 'zh-CN': "对局已经开始了。";
|
|
81
|
+
readonly en: "The game has already started.";
|
|
82
|
+
};
|
|
83
|
+
readonly hostOnly: {
|
|
84
|
+
readonly 'zh-CN': "只有开局的人可以开始。";
|
|
85
|
+
readonly en: "Only the host may start the game.";
|
|
86
|
+
};
|
|
87
|
+
readonly minimum: {
|
|
88
|
+
readonly 'zh-CN': "至少需要 {min} 名玩家才能开始。";
|
|
89
|
+
readonly en: "At least {min} players are required.";
|
|
90
|
+
};
|
|
91
|
+
readonly notStarted: {
|
|
92
|
+
readonly 'zh-CN': "对局还没开始。";
|
|
93
|
+
readonly en: "The game has not started.";
|
|
94
|
+
};
|
|
95
|
+
readonly noPlayer: {
|
|
96
|
+
readonly 'zh-CN': "对局没有可以开枪的玩家。";
|
|
97
|
+
readonly en: "There is no player who can pull the trigger.";
|
|
98
|
+
};
|
|
99
|
+
readonly notTurn: {
|
|
100
|
+
readonly 'zh-CN': "还没轮到你,现在轮到 {name}。";
|
|
101
|
+
readonly en: "It is not your turn. {name} plays next.";
|
|
102
|
+
};
|
|
103
|
+
readonly notJoined: {
|
|
104
|
+
readonly 'zh-CN': "你不在这局轮盘里。";
|
|
105
|
+
readonly en: "You are not in this game.";
|
|
106
|
+
};
|
|
107
|
+
readonly ended: {
|
|
108
|
+
readonly 'zh-CN': "对局已经结束。";
|
|
109
|
+
readonly en: "The game has ended.";
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
type RouletteRuleCode = keyof typeof ruleCatalog;
|
|
113
|
+
export {};
|
package/lib/engine.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMessages } from 'zhin.js/i18n';
|
|
1
2
|
/**
|
|
2
3
|
* Pure Russian roulette rules. No I/O, no clock and no randomness: callers pass
|
|
3
4
|
* the bullet position in, so every transition is deterministic and testable.
|
|
@@ -6,10 +7,15 @@ export const ROULETTE_STATE_VERSION = 1;
|
|
|
6
7
|
export const MIN_ROULETTE_PLAYERS = 2;
|
|
7
8
|
/** A rule violation the player should read, e.g. firing out of turn. */
|
|
8
9
|
export class RouletteRuleError extends Error {
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
code;
|
|
11
|
+
parameters;
|
|
12
|
+
constructor(code, parameters = {}) {
|
|
13
|
+
super(ruleMessages('zh-CN', code, parameters));
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.parameters = parameters;
|
|
11
16
|
this.name = 'RouletteRuleError';
|
|
12
17
|
}
|
|
18
|
+
format(locale) { return ruleMessages(locale, this.code, this.parameters); }
|
|
13
19
|
}
|
|
14
20
|
export function createLobby(host, rules) {
|
|
15
21
|
return {
|
|
@@ -24,22 +30,22 @@ export function createLobby(host, rules) {
|
|
|
24
30
|
};
|
|
25
31
|
}
|
|
26
32
|
export function joinLobby(state, player, rules) {
|
|
27
|
-
requirePhase(state, 'lobby', '
|
|
33
|
+
requirePhase(state, 'lobby', 'cannotJoin');
|
|
28
34
|
if (findPlayer(state, player.id)) {
|
|
29
|
-
throw new RouletteRuleError('
|
|
35
|
+
throw new RouletteRuleError('alreadyJoined');
|
|
30
36
|
}
|
|
31
37
|
if (state.players.length >= rules.maxPlayers) {
|
|
32
|
-
throw new RouletteRuleError(
|
|
38
|
+
throw new RouletteRuleError('full', { max: rules.maxPlayers });
|
|
33
39
|
}
|
|
34
40
|
return { ...state, players: [...state.players, player] };
|
|
35
41
|
}
|
|
36
42
|
export function startRound(state, actorId, bulletChamber) {
|
|
37
|
-
requirePhase(state, 'lobby', '
|
|
43
|
+
requirePhase(state, 'lobby', 'started');
|
|
38
44
|
if (actorId !== state.hostId) {
|
|
39
|
-
throw new RouletteRuleError('
|
|
45
|
+
throw new RouletteRuleError('hostOnly');
|
|
40
46
|
}
|
|
41
47
|
if (state.players.length < MIN_ROULETTE_PLAYERS) {
|
|
42
|
-
throw new RouletteRuleError(
|
|
48
|
+
throw new RouletteRuleError('minimum', { min: MIN_ROULETTE_PLAYERS });
|
|
43
49
|
}
|
|
44
50
|
if (!Number.isInteger(bulletChamber) || bulletChamber < 0 || bulletChamber >= state.chamberCount) {
|
|
45
51
|
throw new RangeError(`bulletChamber must be within 0..${state.chamberCount - 1}`);
|
|
@@ -53,12 +59,12 @@ export function startRound(state, actorId, bulletChamber) {
|
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
export function pullTrigger(state, actorId) {
|
|
56
|
-
requirePhase(state, 'playing', '
|
|
62
|
+
requirePhase(state, 'playing', 'notStarted');
|
|
57
63
|
const shooter = currentPlayer(state);
|
|
58
64
|
if (!shooter)
|
|
59
|
-
throw new RouletteRuleError('
|
|
65
|
+
throw new RouletteRuleError('noPlayer');
|
|
60
66
|
if (shooter.id !== actorId) {
|
|
61
|
-
throw new RouletteRuleError(
|
|
67
|
+
throw new RouletteRuleError('notTurn', { name: shooter.name });
|
|
62
68
|
}
|
|
63
69
|
const firedChamber = state.pullsMade;
|
|
64
70
|
const pullsMade = state.pullsMade + 1;
|
|
@@ -76,11 +82,11 @@ export function pullTrigger(state, actorId) {
|
|
|
76
82
|
export function leaveGame(state, playerId) {
|
|
77
83
|
const leaver = findPlayer(state, playerId);
|
|
78
84
|
if (!leaver)
|
|
79
|
-
throw new RouletteRuleError('
|
|
85
|
+
throw new RouletteRuleError('notJoined');
|
|
80
86
|
if (state.phase === 'playing') {
|
|
81
87
|
return finishWithLoser(state, playerId, 'forfeit');
|
|
82
88
|
}
|
|
83
|
-
requirePhase(state, 'lobby', '
|
|
89
|
+
requirePhase(state, 'lobby', 'ended');
|
|
84
90
|
const players = state.players.filter((player) => player.id !== playerId);
|
|
85
91
|
const nextHost = players[0];
|
|
86
92
|
if (!nextHost) {
|
|
@@ -91,19 +97,19 @@ export function leaveGame(state, playerId) {
|
|
|
91
97
|
}
|
|
92
98
|
/** The player on turn ran out of time: treated like a shot, they lose. */
|
|
93
99
|
export function expireTurn(state) {
|
|
94
|
-
requirePhase(state, 'playing', '
|
|
100
|
+
requirePhase(state, 'playing', 'notStarted');
|
|
95
101
|
const late = currentPlayer(state);
|
|
96
102
|
if (!late)
|
|
97
|
-
throw new RouletteRuleError('
|
|
103
|
+
throw new RouletteRuleError('noPlayer');
|
|
98
104
|
return finishWithLoser(state, late.id, 'turn-timeout');
|
|
99
105
|
}
|
|
100
106
|
export function expireLobby(state) {
|
|
101
|
-
requirePhase(state, 'lobby', '
|
|
107
|
+
requirePhase(state, 'lobby', 'started');
|
|
102
108
|
return { ...state, phase: 'aborted', endReason: 'lobby-expired' };
|
|
103
109
|
}
|
|
104
110
|
export function cancelGame(state) {
|
|
105
111
|
if (!isOpen(state))
|
|
106
|
-
throw new RouletteRuleError('
|
|
112
|
+
throw new RouletteRuleError('ended');
|
|
107
113
|
return { ...state, phase: 'aborted', endReason: 'cancelled' };
|
|
108
114
|
}
|
|
109
115
|
export function isOpen(state) {
|
|
@@ -138,9 +144,9 @@ export function decodeRouletteState(json) {
|
|
|
138
144
|
function finishWithLoser(state, loserId, endReason) {
|
|
139
145
|
return { ...state, phase: 'finished', loserId, endReason };
|
|
140
146
|
}
|
|
141
|
-
function requirePhase(state, phase,
|
|
147
|
+
function requirePhase(state, phase, code) {
|
|
142
148
|
if (state.phase !== phase)
|
|
143
|
-
throw new RouletteRuleError(
|
|
149
|
+
throw new RouletteRuleError(code);
|
|
144
150
|
}
|
|
145
151
|
function isRouletteState(value) {
|
|
146
152
|
if (!value || typeof value !== 'object')
|
|
@@ -155,3 +161,17 @@ function isRouletteState(value) {
|
|
|
155
161
|
&& typeof state.pullsMade === 'number'
|
|
156
162
|
&& typeof state.turnIndex === 'number';
|
|
157
163
|
}
|
|
164
|
+
const ruleCatalog = {
|
|
165
|
+
cannotJoin: { 'zh-CN': "对局已经开始,不能再加入了。", en: "The game has started; you can no longer join." },
|
|
166
|
+
alreadyJoined: { 'zh-CN': "你已经在这局轮盘里了。", en: "You are already in this game." },
|
|
167
|
+
full: { 'zh-CN': "人数已满(最多 {max} 人)。", en: "The table is full (maximum {max} players)." },
|
|
168
|
+
started: { 'zh-CN': "对局已经开始了。", en: "The game has already started." },
|
|
169
|
+
hostOnly: { 'zh-CN': "只有开局的人可以开始。", en: "Only the host may start the game." },
|
|
170
|
+
minimum: { 'zh-CN': "至少需要 {min} 名玩家才能开始。", en: "At least {min} players are required." },
|
|
171
|
+
notStarted: { 'zh-CN': "对局还没开始。", en: "The game has not started." },
|
|
172
|
+
noPlayer: { 'zh-CN': "对局没有可以开枪的玩家。", en: "There is no player who can pull the trigger." },
|
|
173
|
+
notTurn: { 'zh-CN': "还没轮到你,现在轮到 {name}。", en: "It is not your turn. {name} plays next." },
|
|
174
|
+
notJoined: { 'zh-CN': "你不在这局轮盘里。", en: "You are not in this game." },
|
|
175
|
+
ended: { 'zh-CN': "对局已经结束。", en: "The game has ended." },
|
|
176
|
+
};
|
|
177
|
+
const ruleMessages = createMessages(ruleCatalog);
|
package/lib/game-flow.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createMessages, resolveLocale } from 'zhin.js/i18n';
|
|
2
|
+
import { gameCommandHint, GameSessionConflictError, SessionRevisionConflictError, channelKey } from '@zhin.js/game-kit';
|
|
2
3
|
import { RouletteRuleError, loserOf } from './engine.js';
|
|
3
|
-
import {
|
|
4
|
+
import { getRouletteHelp } from './roulette-actions.js';
|
|
4
5
|
import { playerFromMessage, stateOf, } from './session-service.js';
|
|
5
6
|
import { describeAbort, describeClick, describeEnding, describeLobby, describeRoundStart, describeStatus, } from './view.js';
|
|
6
7
|
const DEADLINE_ACTOR = 'deadline';
|
|
7
8
|
const MANAGER_ROLES = ['master', 'trusted', 'owner', 'admin'];
|
|
8
|
-
|
|
9
|
+
function noGame(message) { return messages(message.locale, 'noGame') + gameCommandHint(message, 'roulette', 'open'); }
|
|
9
10
|
export async function runRouletteAction(flow, actor, action) {
|
|
10
11
|
const message = actor.message;
|
|
12
|
+
const locale = resolveLocale(message.locale);
|
|
11
13
|
if (action === 'help')
|
|
12
|
-
return
|
|
14
|
+
return getRouletteHelp(message);
|
|
13
15
|
if (message.$channel.type === 'private')
|
|
14
|
-
return '
|
|
16
|
+
return messages(locale, 'text1');
|
|
15
17
|
if (action === 'penalty-on' || action === 'penalty-off' || action === 'penalty-status') {
|
|
16
18
|
return handlePenaltySetting(flow, actor, action);
|
|
17
19
|
}
|
|
@@ -51,63 +53,67 @@ export async function expireOverdueSessions(flow, now, onNotice) {
|
|
|
51
53
|
}
|
|
52
54
|
async function handleGameAction(flow, actor, action, session) {
|
|
53
55
|
const message = actor.message;
|
|
56
|
+
const locale = resolveLocale(session ? session.locale : message.locale);
|
|
54
57
|
if (action === 'open')
|
|
55
58
|
return openLobby(flow, actor, session);
|
|
56
59
|
if (!session)
|
|
57
|
-
return
|
|
60
|
+
return noGame(message);
|
|
58
61
|
if (action === 'status')
|
|
59
|
-
return describeStatus(stateOf(session), flow.config);
|
|
62
|
+
return describeStatus(stateOf(session), flow.config, { ...message, locale });
|
|
60
63
|
if (action === 'cancel' && !mayCancel(session, message, actor.roles)) {
|
|
61
|
-
return '
|
|
64
|
+
return messages(locale, 'text2');
|
|
62
65
|
}
|
|
63
66
|
const gameAction = toGameAction(action, message);
|
|
64
67
|
if (!gameAction)
|
|
65
|
-
return
|
|
68
|
+
return noGame(message);
|
|
66
69
|
try {
|
|
67
70
|
const result = await flow.services.sessions.applyAction(session, message.$sender.id, gameAction, flow.config);
|
|
68
71
|
return describeResult(flow, gameAction, result, message);
|
|
69
72
|
}
|
|
70
73
|
catch (error) {
|
|
71
74
|
if (error instanceof RouletteRuleError)
|
|
72
|
-
return error.
|
|
75
|
+
return error.format(locale);
|
|
73
76
|
if (error instanceof GameSessionConflictError)
|
|
74
|
-
return error.
|
|
77
|
+
return error.format(locale);
|
|
75
78
|
if (error instanceof SessionRevisionConflictError)
|
|
76
|
-
return '
|
|
79
|
+
return messages(locale, 'text3');
|
|
77
80
|
throw error;
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
async function openLobby(flow, actor, session) {
|
|
84
|
+
const locale = resolveLocale(session ? session.locale : actor.message.locale);
|
|
81
85
|
if (session) {
|
|
82
|
-
return
|
|
86
|
+
return messages(locale, 'text4', { p0: describeStatus(stateOf(session), flow.config, { ...actor.message, locale }) });
|
|
83
87
|
}
|
|
84
88
|
try {
|
|
85
89
|
const created = await flow.services.sessions.createLobby(actor.message, actor.endpoint, flow.config);
|
|
86
|
-
return describeLobby(stateOf(created), flow.config);
|
|
90
|
+
return describeLobby(stateOf(created), flow.config, { ...actor.message, locale });
|
|
87
91
|
}
|
|
88
92
|
catch (error) {
|
|
89
93
|
if (error instanceof GameSessionConflictError)
|
|
90
|
-
return error.
|
|
94
|
+
return error.format(locale);
|
|
91
95
|
throw error;
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
async function describeResult(flow, action, result, message) {
|
|
95
99
|
const state = result.state;
|
|
100
|
+
const locale = resolveLocale(result.session.locale);
|
|
101
|
+
const presentation = { ...message, locale };
|
|
96
102
|
if (!result.applied)
|
|
97
|
-
return describeStatus(state, flow.config);
|
|
103
|
+
return describeStatus(state, flow.config, presentation);
|
|
98
104
|
if (state.phase === 'finished')
|
|
99
105
|
return finishText(flow, result.session, state);
|
|
100
106
|
if (state.phase === 'aborted')
|
|
101
|
-
return describeAbort(state);
|
|
107
|
+
return describeAbort(state, presentation);
|
|
102
108
|
if (action.type === 'start')
|
|
103
|
-
return describeRoundStart(state, flow.config);
|
|
109
|
+
return describeRoundStart(state, flow.config, presentation);
|
|
104
110
|
if (action.type === 'fire') {
|
|
105
|
-
return describeClick(playerFromMessage(message), state, flow.config);
|
|
111
|
+
return describeClick(playerFromMessage(message), state, flow.config, presentation);
|
|
106
112
|
}
|
|
107
113
|
if (action.type === 'leave') {
|
|
108
|
-
return
|
|
114
|
+
return messages(locale, 'text5', { p0: playerFromMessage(message).name, p1: describeLobby(state, flow.config, presentation) });
|
|
109
115
|
}
|
|
110
|
-
return describeLobby(state, flow.config);
|
|
116
|
+
return describeLobby(state, flow.config, presentation);
|
|
111
117
|
}
|
|
112
118
|
async function expireSession(flow, session) {
|
|
113
119
|
// Read everything that can fail before the state transition is committed: once the
|
|
@@ -121,7 +127,7 @@ async function expireSession(flow, session) {
|
|
|
121
127
|
return '';
|
|
122
128
|
if (result.state.phase === 'finished')
|
|
123
129
|
return finishText(flow, result.session, result.state, penaltyEnabled);
|
|
124
|
-
return describeAbort(result.state);
|
|
130
|
+
return describeAbort(result.state, { locale: resolveLocale(result.session.locale) });
|
|
125
131
|
}
|
|
126
132
|
catch (error) {
|
|
127
133
|
// A player acted on the session in the meantime; the next sweep re-reads it.
|
|
@@ -132,10 +138,11 @@ async function expireSession(flow, session) {
|
|
|
132
138
|
}
|
|
133
139
|
async function finishText(flow, session, state, penaltyEnabled) {
|
|
134
140
|
const penalty = await applyLoserPenalty(flow, session, state, penaltyEnabled);
|
|
135
|
-
return [describeEnding(state), penalty].filter(Boolean).join('\n');
|
|
141
|
+
return [describeEnding(state, { locale: resolveLocale(session.locale) }), penalty].filter(Boolean).join('\n');
|
|
136
142
|
}
|
|
137
143
|
/** Mute the loser only when this group opted in and the Endpoint declares muting. */
|
|
138
144
|
async function applyLoserPenalty(flow, session, state, penaltyEnabled) {
|
|
145
|
+
const locale = resolveLocale(session.locale);
|
|
139
146
|
const loser = loserOf(state);
|
|
140
147
|
if (!loser)
|
|
141
148
|
return '';
|
|
@@ -145,44 +152,45 @@ async function applyLoserPenalty(flow, session, state, penaltyEnabled) {
|
|
|
145
152
|
return '';
|
|
146
153
|
const conversation = conversationOfSession(session);
|
|
147
154
|
if (flow.penalty.support(conversation) !== 'supported') {
|
|
148
|
-
return '
|
|
155
|
+
return messages(locale, 'text6');
|
|
149
156
|
}
|
|
150
157
|
try {
|
|
151
158
|
await flow.penalty.mute(conversation, loser.id, flow.config.muteSeconds);
|
|
152
|
-
return
|
|
159
|
+
return messages(locale, 'text7', { p0: loser.name, p1: flow.config.muteSeconds });
|
|
153
160
|
}
|
|
154
161
|
catch {
|
|
155
|
-
return '
|
|
162
|
+
return messages(locale, 'text8');
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
async function handlePenaltySetting(flow, actor, action) {
|
|
159
166
|
const message = actor.message;
|
|
167
|
+
const locale = resolveLocale(message.locale);
|
|
160
168
|
if (message.$channel.type !== 'group')
|
|
161
|
-
return '
|
|
169
|
+
return messages(locale, 'text9');
|
|
162
170
|
const key = channelKey(message);
|
|
163
171
|
const settings = flow.services.settings;
|
|
164
172
|
const support = flow.penalty.support(conversationOfActor(actor));
|
|
165
173
|
if (action === 'penalty-status') {
|
|
166
174
|
const enabled = await settings.isMutePenaltyEnabled(key);
|
|
167
|
-
return describePenaltyStatus(enabled, support === 'supported', flow.config);
|
|
175
|
+
return describePenaltyStatus(enabled, support === 'supported', flow.config, locale);
|
|
168
176
|
}
|
|
169
177
|
if (!isManager(actor.roles))
|
|
170
|
-
return '
|
|
178
|
+
return messages(locale, 'text10');
|
|
171
179
|
if (action === 'penalty-off') {
|
|
172
180
|
await settings.setMutePenalty(key, false, message.$sender.id);
|
|
173
|
-
return '
|
|
181
|
+
return messages(locale, 'text11');
|
|
174
182
|
}
|
|
175
183
|
if (support !== 'supported') {
|
|
176
|
-
return '
|
|
184
|
+
return messages(locale, 'text12');
|
|
177
185
|
}
|
|
178
186
|
await settings.setMutePenalty(key, true, message.$sender.id);
|
|
179
|
-
return
|
|
187
|
+
return messages(locale, 'text13', { p0: flow.config.muteSeconds });
|
|
180
188
|
}
|
|
181
|
-
function describePenaltyStatus(enabled, supported, config) {
|
|
182
|
-
const platform = supported ? '
|
|
189
|
+
function describePenaltyStatus(enabled, supported, config, locale) {
|
|
190
|
+
const platform = supported ? messages(locale, 'text14') : messages(locale, 'text15');
|
|
183
191
|
if (!enabled)
|
|
184
|
-
return
|
|
185
|
-
return
|
|
192
|
+
return messages(locale, 'text16', { p0: platform });
|
|
193
|
+
return messages(locale, 'text17', { p0: config.muteSeconds, p1: platform });
|
|
186
194
|
}
|
|
187
195
|
function toGameAction(action, message) {
|
|
188
196
|
if (action === 'join')
|
|
@@ -224,3 +232,23 @@ function conversationOfActor(actor) {
|
|
|
224
232
|
id: actor.message.$channel.id,
|
|
225
233
|
};
|
|
226
234
|
}
|
|
235
|
+
const messages = createMessages({
|
|
236
|
+
noGame: { 'zh-CN': '本群没有进行中的轮盘。', en: 'This group has no active roulette game.' },
|
|
237
|
+
text1: { 'zh-CN': "轮盘需要多人参与,请在群聊里玩。", en: "Roulette needs multiple players. Please play in a group chat." },
|
|
238
|
+
text2: { 'zh-CN': "只有开局的人、群主、群管理员或机器人管理员可以取消本局。", en: "Only the host, group owner, group admin or bot administrator may cancel this game." },
|
|
239
|
+
text3: { 'zh-CN': "对局状态刚刚变了,请再发一次。", en: "The game state just changed. Please try again." },
|
|
240
|
+
text4: { 'zh-CN': "本群已经有一局轮盘了。\n{p0}", en: "This group already has a roulette game.\n{p0}" },
|
|
241
|
+
text5: { 'zh-CN': "{p0} 离开了。\n{p1}", en: "{p0} left.\n{p1}" },
|
|
242
|
+
text6: { 'zh-CN': "(本群开启了禁言惩罚,但当前平台无法禁言,本局只计分。)", en: "(Mute penalties are enabled, but this platform cannot mute players. Only scores were recorded.)" },
|
|
243
|
+
text7: { 'zh-CN': "🔇 {p0} 被禁言 {p1} 秒。", en: "🔇 {p0} was muted for {p1} seconds." },
|
|
244
|
+
text8: { 'zh-CN': "(禁言没有成功,可能是机器人没有群管理权限;本局只计分。)", en: "(Muting failed, possibly because the bot lacks group management permission. Only scores were recorded.)" },
|
|
245
|
+
text9: { 'zh-CN': "禁言惩罚只能在群聊里设置。", en: "Mute penalties can only be configured in a group chat." },
|
|
246
|
+
text10: { 'zh-CN': "只有群主、群管理员、机器人主人或受信任用户可以修改禁言惩罚。", en: "Only the group owner, group admin, bot owner or a trusted user may change mute penalties." },
|
|
247
|
+
text11: { 'zh-CN': "已关闭禁言惩罚,本群轮盘只计分。", en: "Mute penalties are disabled. This group now records scores only." },
|
|
248
|
+
text12: { 'zh-CN': "当前平台不支持禁言,禁言惩罚没有开启;本群轮盘只计分。", en: "This platform does not support muting. Mute penalties remain disabled; only scores are recorded." },
|
|
249
|
+
text13: { 'zh-CN': "已开启禁言惩罚:每局输家会被禁言 {p0} 秒(需要机器人有群管理权限)。", en: "Mute penalties enabled: each loser is muted for {p0} seconds (the bot needs group management permission)." },
|
|
250
|
+
text14: { 'zh-CN': "当前平台支持禁言。", en: "This platform supports muting." },
|
|
251
|
+
text15: { 'zh-CN': "当前平台不支持禁言,只能计分。", en: "This platform does not support muting; only scores are recorded." },
|
|
252
|
+
text16: { 'zh-CN': "本群禁言惩罚:关闭。{p0}", en: "Group mute penalty: off. {p0}" },
|
|
253
|
+
text17: { 'zh-CN': "本群禁言惩罚:开启,输家禁言 {p0} 秒。{p1}", en: "Group mute penalty: on, the loser is muted for {p0} seconds. {p1}" },
|
|
254
|
+
});
|
package/lib/models.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ declare module '@zhin.js/core' {
|
|
|
4
4
|
interface Models {
|
|
5
5
|
roulette_sessions: {
|
|
6
6
|
id: string;
|
|
7
|
+
locale?: 'zh-CN' | 'en' | null;
|
|
7
8
|
adapter: string;
|
|
8
9
|
endpoint: string;
|
|
9
10
|
channel_type: string;
|
|
@@ -37,5 +38,7 @@ export type RouletteSessionRow = Models['roulette_sessions'];
|
|
|
37
38
|
export type RouletteGroupSettingsRow = Models['roulette_group_settings'];
|
|
38
39
|
export declare const ROULETTE_TABLES: readonly ["roulette_sessions", "roulette_group_settings"];
|
|
39
40
|
export declare function defineHostTables(database: {
|
|
40
|
-
define(name: string, definition: Record<string, unknown
|
|
41
|
+
define(name: string, definition: Record<string, unknown>, options?: {
|
|
42
|
+
readonly addColumns?: readonly string[];
|
|
43
|
+
}): void;
|
|
41
44
|
}): void;
|
package/lib/models.js
CHANGED
|
@@ -2,6 +2,7 @@ export const ROULETTE_TABLES = ['roulette_sessions', 'roulette_group_settings'];
|
|
|
2
2
|
export function defineHostTables(database) {
|
|
3
3
|
database.define('roulette_sessions', {
|
|
4
4
|
id: { type: 'text', primary: true },
|
|
5
|
+
locale: { type: 'text', nullable: true },
|
|
5
6
|
adapter: { type: 'text', nullable: false },
|
|
6
7
|
endpoint: { type: 'text', nullable: false },
|
|
7
8
|
channel_type: { type: 'text', nullable: false },
|
|
@@ -19,7 +20,7 @@ export function defineHostTables(database) {
|
|
|
19
20
|
status: { type: 'text', default: 'active' },
|
|
20
21
|
updated_at: { type: 'bigint', default: 0 },
|
|
21
22
|
created_at: { type: 'bigint', default: 0 },
|
|
22
|
-
});
|
|
23
|
+
}, { addColumns: ['locale'] });
|
|
23
24
|
database.define('roulette_group_settings', {
|
|
24
25
|
id: { type: 'text', primary: true },
|
|
25
26
|
mute_penalty: { type: 'integer', default: 0 },
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { GameMessageLike } from '@zhin.js/game-kit';
|
|
1
2
|
export type RouletteCommandAction = 'help' | 'open' | 'join' | 'start' | 'fire' | 'leave' | 'cancel' | 'status' | 'penalty-on' | 'penalty-off' | 'penalty-status';
|
|
2
3
|
/** Empty input means help; unknown words return null so normal chat is never hijacked. */
|
|
3
4
|
export declare function normalizeRouletteAction(raw: string): RouletteCommandAction | null;
|
|
5
|
+
export declare function getRouletteHelp(message?: Pick<GameMessageLike, 'locale' | 'formatCommand'>): string;
|
|
4
6
|
export declare const ROULETTE_HELP: string;
|
package/lib/roulette-actions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMessages } from 'zhin.js/i18n';
|
|
1
2
|
const ACTION_WORDS = {
|
|
2
3
|
help: 'help',
|
|
3
4
|
帮助: 'help',
|
|
@@ -35,17 +36,25 @@ export function normalizeRouletteAction(raw) {
|
|
|
35
36
|
return 'help';
|
|
36
37
|
return ACTION_WORDS[word] ?? ACTION_WORDS[word.toLowerCase()] ?? null;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
'🔫 俄罗斯轮盘(2 人起,轮流开枪,中弹者输)',
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'
|
|
44
|
-
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
'',
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
|
|
39
|
+
const messages = createMessages({
|
|
40
|
+
title: { 'zh-CN': '🔫 俄罗斯轮盘(2 人起,轮流开枪,中弹者输)', en: '🔫 Russian Roulette (two or more players, take turns; the player hit loses)' },
|
|
41
|
+
open: { 'zh-CN': '创建一局并上桌', en: 'Open a lobby and join it' },
|
|
42
|
+
join: { 'zh-CN': '加入等待中的对局', en: 'Join a waiting lobby' },
|
|
43
|
+
start: { 'zh-CN': '房主开局(弹巢随机装 1 发子弹)', en: 'Host starts the game (one randomly placed bullet)' },
|
|
44
|
+
fire: { 'zh-CN': '轮到你时扣动扳机', en: 'Pull the trigger on your turn' },
|
|
45
|
+
leave: { 'zh-CN': '等待时离开;对局中离开算认输', en: 'Leave the lobby; leaving during play forfeits the game' },
|
|
46
|
+
cancel: { 'zh-CN': '房主或管理员取消本局(不计分)', en: 'Host or manager cancels the game (no score)' },
|
|
47
|
+
status: { 'zh-CN': '查看当前对局', en: 'View the current game' },
|
|
48
|
+
mute: { 'zh-CN': '查看禁言惩罚', en: 'View the mute penalty setting' },
|
|
49
|
+
'mute-on': { 'zh-CN': '开启本群禁言惩罚', en: 'Enable mute penalties for this group' },
|
|
50
|
+
'mute-off': { 'zh-CN': '关闭本群禁言惩罚', en: 'Disable mute penalties for this group' },
|
|
51
|
+
rules: { 'zh-CN': '规则:按加入顺序轮流开枪;轮到的人超时不开枪判负;幸存者各记 1 胜,输家记 1 负。禁言惩罚默认关闭。', en: 'Players take turns in join order. Missing your turn deadline loses the game. Each survivor gets one win; the loser gets one loss. Mute penalties are off by default.' },
|
|
52
|
+
});
|
|
53
|
+
export function getRouletteHelp(message = {}) {
|
|
54
|
+
const command = message.formatCommand?.('roulette');
|
|
55
|
+
const actions = ['open', 'join', 'start', 'fire', 'leave', 'cancel', 'status', 'mute', 'mute-on', 'mute-off'];
|
|
56
|
+
return [messages(message.locale, 'title'),
|
|
57
|
+
...(command === undefined ? [] : actions.map(action => `${command} ${action} — ${messages(message.locale, action)}`)),
|
|
58
|
+
'', messages(message.locale, 'rules')].join('\n');
|
|
59
|
+
}
|
|
60
|
+
export const ROULETTE_HELP = getRouletteHelp();
|
package/lib/roulette-command.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { messageFromCommandInput } from '@zhin.js/game-kit';
|
|
2
2
|
import { runRouletteAction } from './game-flow.js';
|
|
3
|
-
import { normalizeRouletteAction,
|
|
3
|
+
import { normalizeRouletteAction, getRouletteHelp } from './roulette-actions.js';
|
|
4
4
|
import { endpointOf, resolveRouletteFlow, senderRolesOf } from './runtime.js';
|
|
5
5
|
/**
|
|
6
6
|
* Shared entry for the `roulette` command and the `轮盘` alias middleware.
|
|
@@ -11,10 +11,10 @@ export async function runRouletteCommand(context, input, rawAction) {
|
|
|
11
11
|
if (!action)
|
|
12
12
|
return null;
|
|
13
13
|
if (action === 'help')
|
|
14
|
-
return
|
|
14
|
+
return getRouletteHelp(messageFromCommandInput(input, context));
|
|
15
15
|
const flow = resolveRouletteFlow(context);
|
|
16
16
|
const actor = {
|
|
17
|
-
message: messageFromCommandInput(input),
|
|
17
|
+
message: messageFromCommandInput(input, context),
|
|
18
18
|
endpoint: endpointOf(input),
|
|
19
19
|
roles: senderRolesOf(input),
|
|
20
20
|
};
|
package/lib/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Locale } from 'zhin.js/i18n';
|
|
1
2
|
import { type Token } from 'zhin.js';
|
|
2
3
|
import type { RouletteFlow } from './game-flow.js';
|
|
3
4
|
import { type AdapterProjectionContext } from './mute-penalty.js';
|
|
@@ -6,6 +7,8 @@ import type { EndpointIdentity, RouletteServices } from './session-service.js';
|
|
|
6
7
|
export declare const rouletteServicesToken: Token<RouletteServices>;
|
|
7
8
|
/** A command, middleware or schedule capability context. */
|
|
8
9
|
export interface RouletteCapabilityContext extends AdapterProjectionContext {
|
|
10
|
+
readonly locale?: Locale;
|
|
11
|
+
readonly formatCommand?: (path: string) => string | undefined;
|
|
9
12
|
use<T>(token: Token<T>): T;
|
|
10
13
|
readonly config?: unknown;
|
|
11
14
|
}
|
package/lib/session-service.js
CHANGED
|
@@ -17,6 +17,7 @@ export class RouletteSessionService extends VersionedSessionService {
|
|
|
17
17
|
const state = createLobby(host, tableRules(config));
|
|
18
18
|
return this.createRow({
|
|
19
19
|
id: generateCompactId('rlt_'),
|
|
20
|
+
locale: message.locale ?? 'zh-CN',
|
|
20
21
|
adapter: String(message.$adapter),
|
|
21
22
|
endpoint: message.$endpoint,
|
|
22
23
|
channel_type: message.$channel.type,
|
package/lib/view.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import type { GameMessageLike } from '@zhin.js/game-kit';
|
|
1
2
|
import type { RouletteConfig } from './config.js';
|
|
2
3
|
import { type RoulettePlayer, type RouletteState } from './engine.js';
|
|
3
|
-
|
|
4
|
-
export declare
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
4
|
+
type Presentation = Pick<GameMessageLike, 'locale' | 'formatCommand'>;
|
|
5
|
+
export declare const ROULETTE_TITLE: string;
|
|
6
|
+
export declare function describeLobby(state: RouletteState, config: RouletteConfig, presentation?: Presentation): string;
|
|
7
|
+
export declare function describeRoundStart(state: RouletteState, config: RouletteConfig, presentation?: Presentation): string;
|
|
8
|
+
export declare function describeTurn(state: RouletteState, config: RouletteConfig, presentation?: Presentation): string;
|
|
9
|
+
export declare function describeClick(shooter: RoulettePlayer, state: RouletteState, config: RouletteConfig, presentation?: Presentation): string;
|
|
10
|
+
export declare function describeEnding(state: RouletteState, presentation?: Presentation): string;
|
|
11
|
+
export declare function describeAbort(state: RouletteState, presentation?: Presentation): string;
|
|
12
|
+
export declare function describeStatus(state: RouletteState, config: RouletteConfig, presentation?: Presentation): string;
|
|
13
|
+
export {};
|
package/lib/view.js
CHANGED
|
@@ -1,73 +1,78 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { createMessages } from 'zhin.js/i18n';
|
|
2
|
+
import { MIN_ROULETTE_PLAYERS, currentPlayer, loserOf, remainingChambers, survivorsOf } from './engine.js';
|
|
3
|
+
const messages = createMessages({
|
|
4
|
+
title: { 'zh-CN': '🔫 俄罗斯轮盘', en: '🔫 Russian Roulette' },
|
|
5
|
+
lobby: { 'zh-CN': '{title} · 等待玩家({count}/{max})', en: '{title} · Waiting for players ({count}/{max})' },
|
|
6
|
+
players: { 'zh-CN': '玩家:{players}', en: 'Players: {players}' },
|
|
7
|
+
host: { 'zh-CN': '房主', en: 'Host' },
|
|
8
|
+
joinStart: { 'zh-CN': '发送「{join}」上桌;{host} 发送「{start}」开局(至少 {min} 人)。', en: 'Send “{join}” to join. {host}: send “{start}” to start (at least {min} players).' },
|
|
9
|
+
lobbyExpiry: { 'zh-CN': '{seconds} 秒内没有开始会自动解散。', en: 'The lobby closes if the game does not start within {seconds} seconds.' },
|
|
10
|
+
started: { 'zh-CN': '{title} · 开局!{chambers} 个弹巢里装了 1 发子弹,已转好弹巢。', en: '{title} · Started! One bullet in {chambers} chambers. The cylinder has been spun.' },
|
|
11
|
+
order: { 'zh-CN': '开枪顺序:', en: 'Turn order:' },
|
|
12
|
+
turn: { 'zh-CN': '👉 轮到 {name}:{instruction}{seconds} 秒内不开枪判负。(剩余 {chambers} 个弹巢)', en: '👉 {name} to play: {instruction}pull the trigger within {seconds} seconds or lose. ({chambers} chambers remaining)' },
|
|
13
|
+
fire: { 'zh-CN': '发送「{command}」,', en: 'send “{command}”; ' },
|
|
14
|
+
click: { 'zh-CN': '咔哒……{name} 的这一枪是空膛。', en: 'Click… {name} found an empty chamber.' },
|
|
15
|
+
survived: { 'zh-CN': '🏆 幸存:{names}(各 +1 胜)', en: '🏆 Survivors: {names} (+1 win each)' },
|
|
16
|
+
leaderboard: { 'zh-CN': '可在游戏大厅查看本群排行。', en: 'The group leaderboard is available in the game lobby.' },
|
|
17
|
+
expired: { 'zh-CN': '{title} 长时间没有开始,已自动解散。', en: '{title}: the lobby expired before the game started.' },
|
|
18
|
+
empty: { 'zh-CN': '{title} 所有人都离开了,已解散。', en: '{title}: everyone left; the lobby is closed.' },
|
|
19
|
+
aborted: { 'zh-CN': '{title} 已取消,本局不计分。', en: '{title}: cancelled. No scores were recorded.' },
|
|
20
|
+
status: { 'zh-CN': '{title} · 进行中,已开 {pulls} 枪', en: '{title} · In progress, {pulls} trigger pulls' },
|
|
21
|
+
timeout: { 'zh-CN': '⌛ {name} 超时没有开枪,判负!', en: '⌛ {name} ran out of time and loses!' },
|
|
22
|
+
forfeit: { 'zh-CN': '🏳️ {name} 中途离场,判负!', en: '🏳️ {name} left the game and forfeits!' },
|
|
23
|
+
shot: { 'zh-CN': '💥 砰!{name} 中弹出局(第 {pulls} 枪)。', en: '💥 Bang! {name} is out (trigger pull {pulls}).' },
|
|
24
|
+
none: { 'zh-CN': '无', en: 'None' },
|
|
25
|
+
});
|
|
26
|
+
export const ROULETTE_TITLE = messages('zh-CN', 'title');
|
|
27
|
+
export function describeLobby(state, config, presentation = {}) {
|
|
28
|
+
const locale = presentation.locale;
|
|
29
|
+
const host = state.players.find(player => player.id === state.hostId);
|
|
30
|
+
const command = presentation.formatCommand?.('roulette');
|
|
31
|
+
return [messages(locale, 'lobby', { title: messages(locale, 'title'), count: state.players.length, max: config.maxPlayers }),
|
|
32
|
+
messages(locale, 'players', { players: listNames(state.players, presentation) }),
|
|
33
|
+
...(command === undefined ? [] : [messages(locale, 'joinStart', { join: `${command} join`, start: `${command} start`, host: host?.name ?? messages(locale, 'host'), min: MIN_ROULETTE_PLAYERS })]),
|
|
34
|
+
messages(locale, 'lobbyExpiry', { seconds: config.lobbyTimeoutSeconds }),
|
|
35
|
+
].join('\n');
|
|
12
36
|
}
|
|
13
|
-
export function describeRoundStart(state, config) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
...order,
|
|
19
|
-
describeTurn(state, config),
|
|
20
|
-
];
|
|
21
|
-
return lines.join('\n');
|
|
37
|
+
export function describeRoundStart(state, config, presentation = {}) {
|
|
38
|
+
return [messages(presentation.locale, 'started', { title: messages(presentation.locale, 'title'), chambers: state.chamberCount }),
|
|
39
|
+
messages(presentation.locale, 'order'), ...state.players.map((player, index) => `${index + 1}. ${player.name}`),
|
|
40
|
+
describeTurn(state, config, presentation),
|
|
41
|
+
].join('\n');
|
|
22
42
|
}
|
|
23
|
-
export function describeTurn(state, config) {
|
|
43
|
+
export function describeTurn(state, config, presentation = {}) {
|
|
24
44
|
const shooter = currentPlayer(state);
|
|
25
45
|
if (!shooter)
|
|
26
46
|
return '';
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
const command = presentation.formatCommand?.('roulette');
|
|
48
|
+
return messages(presentation.locale, 'turn', { name: shooter.name, seconds: config.turnTimeoutSeconds,
|
|
49
|
+
chambers: remainingChambers(state), instruction: command === undefined ? ''
|
|
50
|
+
: messages(presentation.locale, 'fire', { command: `${command} fire` }) });
|
|
29
51
|
}
|
|
30
|
-
export function describeClick(shooter, state, config) {
|
|
31
|
-
return [
|
|
52
|
+
export function describeClick(shooter, state, config, presentation = {}) {
|
|
53
|
+
return [messages(presentation.locale, 'click', { name: shooter.name }), describeTurn(state, config, presentation)].join('\n');
|
|
32
54
|
}
|
|
33
|
-
export function describeEnding(state) {
|
|
55
|
+
export function describeEnding(state, presentation = {}) {
|
|
34
56
|
const loser = loserOf(state);
|
|
35
57
|
if (!loser)
|
|
36
|
-
return describeAbort(state);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`🏆 幸存:${listNames(survivors)}(各 +1 胜)`,
|
|
42
|
-
'发送「排行 轮盘」查看本群排行。',
|
|
58
|
+
return describeAbort(state, presentation);
|
|
59
|
+
const key = state.endReason === 'turn-timeout' ? 'timeout' : state.endReason === 'forfeit' ? 'forfeit' : 'shot';
|
|
60
|
+
return [messages(presentation.locale, key, { name: loser.name, pulls: state.pullsMade }),
|
|
61
|
+
messages(presentation.locale, 'survived', { names: listNames(survivorsOf(state), presentation) }),
|
|
62
|
+
messages(presentation.locale, 'leaderboard'),
|
|
43
63
|
].join('\n');
|
|
44
64
|
}
|
|
45
|
-
export function describeAbort(state) {
|
|
46
|
-
|
|
47
|
-
return `${ROULETTE_TITLE} 长时间没有开始,已自动解散。`;
|
|
48
|
-
if (state.endReason === 'lobby-empty')
|
|
49
|
-
return `${ROULETTE_TITLE} 所有人都离开了,已解散。`;
|
|
50
|
-
return `${ROULETTE_TITLE} 已取消,本局不计分。`;
|
|
65
|
+
export function describeAbort(state, presentation = {}) {
|
|
66
|
+
return messages(presentation.locale, state.endReason === 'lobby-expired' ? 'expired' : state.endReason === 'lobby-empty' ? 'empty' : 'aborted', { title: messages(presentation.locale, 'title') });
|
|
51
67
|
}
|
|
52
|
-
export function describeStatus(state, config) {
|
|
68
|
+
export function describeStatus(state, config, presentation = {}) {
|
|
53
69
|
if (state.phase === 'lobby')
|
|
54
|
-
return describeLobby(state, config);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
];
|
|
60
|
-
return lines.join('\n');
|
|
61
|
-
}
|
|
62
|
-
function endingHeadline(state, loser) {
|
|
63
|
-
if (state.endReason === 'turn-timeout')
|
|
64
|
-
return `⌛ ${loser.name} 超时没有开枪,判负!`;
|
|
65
|
-
if (state.endReason === 'forfeit')
|
|
66
|
-
return `🏳️ ${loser.name} 中途离场,判负!`;
|
|
67
|
-
return `💥 砰!${loser.name} 中弹出局(第 ${state.pullsMade} 枪)。`;
|
|
70
|
+
return describeLobby(state, config, presentation);
|
|
71
|
+
return [messages(presentation.locale, 'status', { title: messages(presentation.locale, 'title'), pulls: state.pullsMade }),
|
|
72
|
+
messages(presentation.locale, 'players', { players: listNames(state.players, presentation) }),
|
|
73
|
+
describeTurn(state, config, presentation),
|
|
74
|
+
].join('\n');
|
|
68
75
|
}
|
|
69
|
-
function listNames(players) {
|
|
70
|
-
|
|
71
|
-
return '无';
|
|
72
|
-
return players.map((player) => player.name).join('、');
|
|
76
|
+
function listNames(players, presentation) {
|
|
77
|
+
return players.length ? players.map(player => player.name).join(presentation.locale === 'en' ? ', ' : '、') : messages(presentation.locale, 'none');
|
|
73
78
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-roulette",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Zhin.js 俄罗斯轮盘 — 多人轮流开枪,可按群开启禁言惩罚 (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@zhin.js/core": "1.1.
|
|
34
|
-
"@zhin.js/game-kit": "1.1.
|
|
33
|
+
"@zhin.js/core": "1.1.52",
|
|
34
|
+
"@zhin.js/game-kit": "1.1.17"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"typescript": "^6.0.3",
|
|
38
38
|
"vitest": "^4.1.10",
|
|
39
|
-
"@zhin.js/schedule-feature": "1.1.
|
|
40
|
-
"@zhin.js/testing": "1.0.
|
|
41
|
-
"zhin.js": "1.1.
|
|
39
|
+
"@zhin.js/schedule-feature": "1.1.9",
|
|
40
|
+
"@zhin.js/testing": "1.0.6",
|
|
41
|
+
"zhin.js": "1.1.17"
|
|
42
42
|
},
|
|
43
43
|
"repository": {
|
|
44
44
|
"type": "git",
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"plugins": []
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"@zhin.js/command": "^1.1.
|
|
83
|
-
"@zhin.js/middleware": "^1.1.
|
|
84
|
-
"zhin.js": "^1.1.
|
|
85
|
-
"@zhin.js/schedule-feature": "^1.1.
|
|
82
|
+
"@zhin.js/command": "^1.1.11",
|
|
83
|
+
"@zhin.js/middleware": "^1.1.10",
|
|
84
|
+
"zhin.js": "^1.1.17",
|
|
85
|
+
"@zhin.js/schedule-feature": "^1.1.9"
|
|
86
86
|
},
|
|
87
87
|
"peerDependenciesMeta": {
|
|
88
88
|
"@zhin.js/command": {
|
package/plugin.js
CHANGED
|
@@ -17,6 +17,10 @@ export default defineGamePlugin({
|
|
|
17
17
|
displayName: 'Russian Roulette',
|
|
18
18
|
},
|
|
19
19
|
game: {
|
|
20
|
+
commandPath: 'roulette',
|
|
21
|
+
titleI18n: { en: 'Roulette' },
|
|
22
|
+
descriptionI18n: { en: 'Take turns in a multiplayer roulette game' },
|
|
23
|
+
quickStartI18n: { en: 'open' },
|
|
20
24
|
id: 'roulette',
|
|
21
25
|
title: '俄罗斯轮盘',
|
|
22
26
|
icon: '🔫',
|