@zhin.js/plugin-roulette 0.1.0 → 0.1.2
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 +27 -0
- package/README.md +3 -3
- package/lib/deadline-job.js +4 -6
- package/lib/game-flow.d.ts +1 -1
- package/lib/game-flow.js +30 -12
- package/lib/roulette-actions.js +1 -1
- package/package.json +10 -10
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @zhin.js/plugin-roulette
|
|
2
|
+
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [db06de8]
|
|
8
|
+
- @zhin.js/core@1.1.48
|
|
9
|
+
- @zhin.js/game-kit@1.1.13
|
|
10
|
+
|
|
11
|
+
## 0.1.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- e40899f: Adapters now project group owners and admins onto the canonical `sender.roles` names `owner` / `admin` (Telegram creator/administrator, KOOK guild owner and admin-permission roles, Lark chat owner/managers, QQ official guild owner/admin identities). KOOK numeric role ids moved from `roles` to `permissions` (`role:<id>`). `@zhin.js/command` exports `canManageConversation` ("private chat or owner/admin/master"), now shared by the Bilibili and earthquake subscription commands; Roulette lets owners and admins toggle the mute penalty and cancel a game.
|
|
16
|
+
- 84520a3: New game: Russian roulette (`轮盘` / `roulette`) for game-hub. Players join a lobby, take turns pulling the trigger on a cylinder with one bullet, and whoever is shot loses; turn and lobby timeouts are enforced by a schedule job, and results feed `战绩` / `排行 轮盘`. Groups can opt in to muting the loser; support is read from the Endpoint's declared `muteGroupMember` capability, so platforms that cannot mute only keep score.
|
|
17
|
+
- 37f79a1: 轮盘超时扫描:先读取禁言惩罚设置再提交状态流转,单个对局失败只记录日志并留待下次重试,结果在产生时即刻公布,不再丢失其他对局的通知或输家的惩罚。
|
|
18
|
+
- Updated dependencies [5be09ae]
|
|
19
|
+
- Updated dependencies [a3ab153]
|
|
20
|
+
- Updated dependencies [e8f7309]
|
|
21
|
+
- Updated dependencies [5afc18f]
|
|
22
|
+
- Updated dependencies [d3563d9]
|
|
23
|
+
- Updated dependencies [53d5bd7]
|
|
24
|
+
- Updated dependencies [c618eb7]
|
|
25
|
+
- Updated dependencies [fbfefb7]
|
|
26
|
+
- @zhin.js/core@1.1.47
|
|
27
|
+
- @zhin.js/game-kit@1.1.12
|
package/README.md
CHANGED
|
@@ -25,10 +25,10 @@ database:
|
|
|
25
25
|
| `轮盘 开始` | 房主开局,至少 2 人 |
|
|
26
26
|
| `开枪`(或 `轮盘 开枪`) | 轮到你时扣动扳机;没轮到你时「开枪」只是普通聊天 |
|
|
27
27
|
| `轮盘 退出` | 等待时离开;对局中离开算认输 |
|
|
28
|
-
| `轮盘 取消` |
|
|
28
|
+
| `轮盘 取消` | 房主、群主、群管理员或机器人管理员取消本局,不计分 |
|
|
29
29
|
| `轮盘 状态` | 查看当前对局 |
|
|
30
30
|
| `轮盘 禁言` | 查看本群禁言惩罚和平台是否支持禁言 |
|
|
31
|
-
| `轮盘 开启禁言` / `轮盘 关闭禁言` |
|
|
31
|
+
| `轮盘 开启禁言` / `轮盘 关闭禁言` | 群主(`owner`)、群管理员(`admin`)、机器人主人(`master`)或受信任用户(`trusted`)切换本群禁言惩罚 |
|
|
32
32
|
|
|
33
33
|
`/轮盘赌`、`roulette` 是同一组命令的别名;也可以从 `/游戏` 大厅进入。
|
|
34
34
|
|
|
@@ -43,7 +43,7 @@ database:
|
|
|
43
43
|
|
|
44
44
|
## 禁言惩罚
|
|
45
45
|
|
|
46
|
-
-
|
|
46
|
+
- 按群开关,默认关闭。只有群主、群管理员、机器人主人或受信任用户可以切换;判断的是适配器投影到消息上的规范角色(`owner` / `admin` / `master` / `trusted`),不额外请求平台。
|
|
47
47
|
- 插件不判断平台名称。每次需要时,用消息自带的 `conversation.endpoint`(适配器插件和 Endpoint 能力 id)走 `project(adapterFeatureId).connection(adapter, endpointId)`,再看 `resolveEndpointManagement(endpoint)?.muteGroupMember` 是否存在。存在就能禁言,不存在(或运行时没有 Adapter Feature)就只计分。
|
|
48
48
|
- 超时判负由 `schedules/deadlines` 每 5 秒检查一次,它拿到的也是同样的能力上下文,所以超时的输家同样会按上面的规则处理。
|
|
49
49
|
- 平台不支持禁言时无法开启;开启后平台失去这项能力,或禁言被拒绝(例如机器人不是群管理员),这一局照常结算,只是不禁言。
|
package/lib/deadline-job.js
CHANGED
|
@@ -10,13 +10,11 @@ export const DEADLINE_CRON = '*/5 * * * * *';
|
|
|
10
10
|
*/
|
|
11
11
|
export async function runDeadlineSweep(context, now) {
|
|
12
12
|
const flow = resolveRouletteFlow(context);
|
|
13
|
-
const notices = await expireOverdueSessions(flow, now);
|
|
14
13
|
const outbound = readOutbound(context);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
14
|
+
await expireOverdueSessions(flow, now, (notice) => {
|
|
15
|
+
if (outbound)
|
|
16
|
+
return announce(outbound, notice);
|
|
17
|
+
});
|
|
20
18
|
}
|
|
21
19
|
function readOutbound(context) {
|
|
22
20
|
try {
|
package/lib/game-flow.d.ts
CHANGED
|
@@ -22,5 +22,5 @@ export interface ExpiredSessionNotice {
|
|
|
22
22
|
}
|
|
23
23
|
export declare function runRouletteAction(flow: RouletteFlow, actor: RouletteActor, action: RouletteCommandAction): Promise<string>;
|
|
24
24
|
/** Called by the deadline schedule; each returned notice still has to be delivered. */
|
|
25
|
-
export declare function expireOverdueSessions(flow: RouletteFlow, now: number): Promise<readonly ExpiredSessionNotice[]>;
|
|
25
|
+
export declare function expireOverdueSessions(flow: RouletteFlow, now: number, onNotice?: (notice: ExpiredSessionNotice) => Promise<void> | void): Promise<readonly ExpiredSessionNotice[]>;
|
|
26
26
|
export declare function conversationOfSession(session: RouletteSessionRow): PenaltyConversation;
|
package/lib/game-flow.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ROULETTE_HELP } from './roulette-actions.js';
|
|
|
4
4
|
import { playerFromMessage, stateOf, } from './session-service.js';
|
|
5
5
|
import { describeAbort, describeClick, describeEnding, describeLobby, describeRoundStart, describeStatus, } from './view.js';
|
|
6
6
|
const DEADLINE_ACTOR = 'deadline';
|
|
7
|
-
const MANAGER_ROLES = ['master', 'trusted'];
|
|
7
|
+
const MANAGER_ROLES = ['master', 'trusted', 'owner', 'admin'];
|
|
8
8
|
const NO_GAME_TEXT = '本群没有进行中的轮盘,发送「轮盘 开局」创建一局。';
|
|
9
9
|
export async function runRouletteAction(flow, actor, action) {
|
|
10
10
|
const message = actor.message;
|
|
@@ -27,13 +27,25 @@ export async function runRouletteAction(flow, actor, action) {
|
|
|
27
27
|
return [...notices, reply].join('\n\n');
|
|
28
28
|
}
|
|
29
29
|
/** Called by the deadline schedule; each returned notice still has to be delivered. */
|
|
30
|
-
export async function expireOverdueSessions(flow, now) {
|
|
30
|
+
export async function expireOverdueSessions(flow, now, onNotice) {
|
|
31
31
|
const overdue = await flow.services.sessions.listOverdue(now);
|
|
32
32
|
const notices = [];
|
|
33
33
|
for (const session of overdue) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
try {
|
|
35
|
+
const text = await expireSession(flow, session);
|
|
36
|
+
if (!text)
|
|
37
|
+
continue;
|
|
38
|
+
const notice = { session, text };
|
|
39
|
+
notices.push(notice);
|
|
40
|
+
// Announce as soon as the result is stored, so a later failure cannot drop it.
|
|
41
|
+
if (onNotice)
|
|
42
|
+
await onNotice(notice);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// The session stays active when anything before the commit failed; the next sweep retries it.
|
|
46
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
47
|
+
console.warn(`[roulette] 处理超时对局 ${session.id} 失败,下次扫描重试:${reason}`);
|
|
48
|
+
}
|
|
37
49
|
}
|
|
38
50
|
return notices;
|
|
39
51
|
}
|
|
@@ -46,7 +58,7 @@ async function handleGameAction(flow, actor, action, session) {
|
|
|
46
58
|
if (action === 'status')
|
|
47
59
|
return describeStatus(stateOf(session), flow.config);
|
|
48
60
|
if (action === 'cancel' && !mayCancel(session, message, actor.roles)) {
|
|
49
|
-
return '
|
|
61
|
+
return '只有开局的人、群主、群管理员或机器人管理员可以取消本局。';
|
|
50
62
|
}
|
|
51
63
|
const gameAction = toGameAction(action, message);
|
|
52
64
|
if (!gameAction)
|
|
@@ -98,12 +110,17 @@ async function describeResult(flow, action, result, message) {
|
|
|
98
110
|
return describeLobby(state, flow.config);
|
|
99
111
|
}
|
|
100
112
|
async function expireSession(flow, session) {
|
|
113
|
+
// Read everything that can fail before the state transition is committed: once the
|
|
114
|
+
// session is stored as finished it is no longer overdue and would never be retried.
|
|
115
|
+
const penaltyEnabled = stateOf(session).phase === 'playing'
|
|
116
|
+
? await flow.services.settings.isMutePenaltyEnabled(session.channel_key)
|
|
117
|
+
: false;
|
|
101
118
|
try {
|
|
102
119
|
const result = await flow.services.sessions.applyAction(session, DEADLINE_ACTOR, { type: 'expire' }, flow.config);
|
|
103
120
|
if (!result.applied)
|
|
104
121
|
return '';
|
|
105
122
|
if (result.state.phase === 'finished')
|
|
106
|
-
return finishText(flow, result.session, result.state);
|
|
123
|
+
return finishText(flow, result.session, result.state, penaltyEnabled);
|
|
107
124
|
return describeAbort(result.state);
|
|
108
125
|
}
|
|
109
126
|
catch (error) {
|
|
@@ -113,16 +130,17 @@ async function expireSession(flow, session) {
|
|
|
113
130
|
throw error;
|
|
114
131
|
}
|
|
115
132
|
}
|
|
116
|
-
async function finishText(flow, session, state) {
|
|
117
|
-
const penalty = await applyLoserPenalty(flow, session, state);
|
|
133
|
+
async function finishText(flow, session, state, penaltyEnabled) {
|
|
134
|
+
const penalty = await applyLoserPenalty(flow, session, state, penaltyEnabled);
|
|
118
135
|
return [describeEnding(state), penalty].filter(Boolean).join('\n');
|
|
119
136
|
}
|
|
120
137
|
/** Mute the loser only when this group opted in and the Endpoint declares muting. */
|
|
121
|
-
async function applyLoserPenalty(flow, session, state) {
|
|
138
|
+
async function applyLoserPenalty(flow, session, state, penaltyEnabled) {
|
|
122
139
|
const loser = loserOf(state);
|
|
123
140
|
if (!loser)
|
|
124
141
|
return '';
|
|
125
|
-
const enabled =
|
|
142
|
+
const enabled = penaltyEnabled
|
|
143
|
+
?? await flow.services.settings.isMutePenaltyEnabled(session.channel_key);
|
|
126
144
|
if (!enabled)
|
|
127
145
|
return '';
|
|
128
146
|
const conversation = conversationOfSession(session);
|
|
@@ -149,7 +167,7 @@ async function handlePenaltySetting(flow, actor, action) {
|
|
|
149
167
|
return describePenaltyStatus(enabled, support === 'supported', flow.config);
|
|
150
168
|
}
|
|
151
169
|
if (!isManager(actor.roles))
|
|
152
|
-
return '
|
|
170
|
+
return '只有群主、群管理员、机器人主人或受信任用户可以修改禁言惩罚。';
|
|
153
171
|
if (action === 'penalty-off') {
|
|
154
172
|
await settings.setMutePenalty(key, false, message.$sender.id);
|
|
155
173
|
return '已关闭禁言惩罚,本群轮盘只计分。';
|
package/lib/roulette-actions.js
CHANGED
|
@@ -42,7 +42,7 @@ export const ROULETTE_HELP = [
|
|
|
42
42
|
'轮盘 开始 — 房主开局(弹巢随机装 1 发子弹)',
|
|
43
43
|
'开枪 / 轮盘 开枪 — 轮到你时扣动扳机',
|
|
44
44
|
'轮盘 退出 — 等待时离开;对局中离开算认输',
|
|
45
|
-
'轮盘 取消 —
|
|
45
|
+
'轮盘 取消 — 房主、群主、群管理员或机器人管理员取消本局(不计分)',
|
|
46
46
|
'轮盘 状态 — 查看当前对局',
|
|
47
47
|
'轮盘 禁言 / 开启禁言 / 关闭禁言 — 查看或切换本群的禁言惩罚(默认关闭)',
|
|
48
48
|
'',
|
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.2",
|
|
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.48",
|
|
34
|
+
"@zhin.js/game-kit": "1.1.13"
|
|
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": "1.
|
|
41
|
-
"
|
|
39
|
+
"@zhin.js/schedule-feature": "1.1.7",
|
|
40
|
+
"@zhin.js/testing": "1.0.5",
|
|
41
|
+
"zhin.js": "1.1.13"
|
|
42
42
|
},
|
|
43
43
|
"repository": {
|
|
44
44
|
"type": "git",
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"plugins": []
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"@zhin.js/
|
|
83
|
-
"zhin.js": "^1.1.
|
|
84
|
-
"
|
|
85
|
-
"@zhin.js/schedule-feature": "^1.1.
|
|
82
|
+
"@zhin.js/command": "^1.1.9",
|
|
83
|
+
"@zhin.js/middleware": "^1.1.8",
|
|
84
|
+
"zhin.js": "^1.1.13",
|
|
85
|
+
"@zhin.js/schedule-feature": "^1.1.7"
|
|
86
86
|
},
|
|
87
87
|
"peerDependenciesMeta": {
|
|
88
88
|
"@zhin.js/command": {
|