@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 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 { ROULETTE_HELP } from "../../../lib/roulette-actions.js";
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 ?? `未知操作:${action}\n\n${ROULETTE_HELP}`;
12
+ return reply ?? gameCommandFeedback(context, 'unknown', action) + '\n\n' + getRouletteHelp(context);
12
13
  },
13
14
  });
@@ -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
- constructor(message: string);
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
- constructor(message) {
10
- super(message);
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(`人数已满(最多 ${rules.maxPlayers} 人)。`);
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(`至少需要 ${MIN_ROULETTE_PLAYERS} 名玩家才能开始。`);
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(`还没轮到你,现在轮到 ${shooter.name}。`);
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, message) {
147
+ function requirePhase(state, phase, code) {
142
148
  if (state.phase !== phase)
143
- throw new RouletteRuleError(message);
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 { GameSessionConflictError, SessionRevisionConflictError, channelKey, } from '@zhin.js/game-kit';
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 { ROULETTE_HELP } from './roulette-actions.js';
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
- const NO_GAME_TEXT = '本群没有进行中的轮盘,发送「轮盘 开局」创建一局。';
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 ROULETTE_HELP;
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 NO_GAME_TEXT;
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 NO_GAME_TEXT;
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.message;
75
+ return error.format(locale);
73
76
  if (error instanceof GameSessionConflictError)
74
- return error.message;
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 `本群已经有一局轮盘了。\n${describeStatus(stateOf(session), flow.config)}`;
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.message;
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 `${playerFromMessage(message).name} 离开了。\n${describeLobby(state, flow.config)}`;
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 `🔇 ${loser.name} 被禁言 ${flow.config.muteSeconds} 秒。`;
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 `已开启禁言惩罚:每局输家会被禁言 ${flow.config.muteSeconds} 秒(需要机器人有群管理权限)。`;
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 `本群禁言惩罚:关闭。${platform}`;
185
- return `本群禁言惩罚:开启,输家禁言 ${config.muteSeconds} 秒。${platform}`;
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>): void;
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;
@@ -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
- export const ROULETTE_HELP = [
39
- '🔫 俄罗斯轮盘(2 人起,轮流开枪,中弹者输)',
40
- '轮盘 开局 — 创建一局并上桌',
41
- '轮盘 加入 — 加入等待中的对局',
42
- '轮盘 开始 — 房主开局(弹巢随机装 1 发子弹)',
43
- '开枪 / 轮盘 开枪 — 轮到你时扣动扳机',
44
- '轮盘 退出 — 等待时离开;对局中离开算认输',
45
- '轮盘 取消 — 房主、群主、群管理员或机器人管理员取消本局(不计分)',
46
- '轮盘 状态 — 查看当前对局',
47
- '轮盘 禁言 / 开启禁言 / 关闭禁言 — 查看或切换本群的禁言惩罚(默认关闭)',
48
- '',
49
- '规则:按加入顺序轮流开枪;轮到的人超时不开枪判负;幸存者各记 1 胜,输家记 1 负。',
50
- '战绩与排行:「战绩」「排行 轮盘」。',
51
- ].join('\n');
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();
@@ -1,6 +1,6 @@
1
1
  import { messageFromCommandInput } from '@zhin.js/game-kit';
2
2
  import { runRouletteAction } from './game-flow.js';
3
- import { normalizeRouletteAction, ROULETTE_HELP } from './roulette-actions.js';
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 ROULETTE_HELP;
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
  }
@@ -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
- export declare const ROULETTE_TITLE = "\uD83D\uDD2B \u4FC4\u7F57\u65AF\u8F6E\u76D8";
4
- export declare function describeLobby(state: RouletteState, config: RouletteConfig): string;
5
- export declare function describeRoundStart(state: RouletteState, config: RouletteConfig): string;
6
- export declare function describeTurn(state: RouletteState, config: RouletteConfig): string;
7
- export declare function describeClick(shooter: RoulettePlayer, state: RouletteState, config: RouletteConfig): string;
8
- export declare function describeEnding(state: RouletteState): string;
9
- export declare function describeAbort(state: RouletteState): string;
10
- export declare function describeStatus(state: RouletteState, config: RouletteConfig): string;
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 { MIN_ROULETTE_PLAYERS, currentPlayer, loserOf, remainingChambers, survivorsOf, } from './engine.js';
2
- export const ROULETTE_TITLE = '🔫 俄罗斯轮盘';
3
- export function describeLobby(state, config) {
4
- const host = state.players.find((player) => player.id === state.hostId);
5
- const lines = [
6
- `${ROULETTE_TITLE} · 等待玩家(${state.players.length}/${config.maxPlayers})`,
7
- `玩家:${listNames(state.players)}`,
8
- `发送「轮盘 加入」上桌;${host?.name ?? '房主'} 发送「轮盘 开始」开局(至少 ${MIN_ROULETTE_PLAYERS} 人)。`,
9
- `${config.lobbyTimeoutSeconds} 秒内没有开始会自动解散。`,
10
- ];
11
- return lines.join('\n');
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
- const order = state.players.map((player, index) => `${index + 1}. ${player.name}`);
15
- const lines = [
16
- `${ROULETTE_TITLE} · 开局!${state.chamberCount} 个弹巢里装了 1 发子弹,已转好弹巢。`,
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
- return `👉 轮到 ${shooter.name}:发送「开枪」,${config.turnTimeoutSeconds} 秒内不开枪判负。`
28
- + `(剩余 ${remainingChambers(state)} 个弹巢)`;
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 [`咔哒……${shooter.name} 的这一枪是空膛。`, describeTurn(state, config)].join('\n');
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 headline = endingHeadline(state, loser);
38
- const survivors = survivorsOf(state);
39
- return [
40
- headline,
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
- if (state.endReason === 'lobby-expired')
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
- const lines = [
56
- `${ROULETTE_TITLE} · 进行中,已开 ${state.pullsMade} 枪`,
57
- `玩家:${listNames(state.players)}`,
58
- describeTurn(state, config),
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
- if (players.length === 0)
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.4",
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.50",
34
- "@zhin.js/game-kit": "1.1.15"
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.8",
40
- "@zhin.js/testing": "1.0.5",
41
- "zhin.js": "1.1.15"
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.10",
83
- "@zhin.js/middleware": "^1.1.9",
84
- "zhin.js": "^1.1.15",
85
- "@zhin.js/schedule-feature": "^1.1.8"
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: '🔫',