@zhin.js/plugin-roulette 0.0.0-stage → 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 +67 -2
- package/commands/roulette/[[action]]/index.js +13 -0
- package/lib/config.d.ts +14 -0
- package/lib/config.js +27 -0
- package/lib/deadline-job.d.ts +9 -0
- package/lib/deadline-job.js +43 -0
- package/lib/engine.d.ts +61 -0
- package/lib/engine.js +157 -0
- package/lib/game-flow.d.ts +26 -0
- package/lib/game-flow.js +226 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +8 -0
- package/lib/models.d.ts +41 -0
- package/lib/models.js +29 -0
- package/lib/mute-penalty.d.ts +31 -0
- package/lib/mute-penalty.js +53 -0
- package/lib/roulette-actions.d.ts +4 -0
- package/lib/roulette-actions.js +51 -0
- package/lib/roulette-command.d.ts +6 -0
- package/lib/roulette-command.js +22 -0
- package/lib/runtime.d.ts +16 -0
- package/lib/runtime.js +40 -0
- package/lib/session-service.d.ts +59 -0
- package/lib/session-service.js +167 -0
- package/lib/view.d.ts +10 -0
- package/lib/view.js +73 -0
- package/middlewares/roulette-alias/index.js +10 -0
- package/middlewares/roulette-fire/index.js +30 -0
- package/package.json +100 -3
- package/plugin.js +39 -0
- package/schedules/deadlines/index.js +8 -0
- package/schema.json +42 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { VersionedSessionService, channelKey, generateCompactId, secureRandomInt, } from '@zhin.js/game-kit';
|
|
2
|
+
import { cancelGame, createLobby, currentPlayer, decodeRouletteState, expireLobby, expireTurn, isOpen, joinLobby, leaveGame, pullTrigger, startRound, } from './engine.js';
|
|
3
|
+
export class RouletteSessionService extends VersionedSessionService {
|
|
4
|
+
#chooseBulletChamber;
|
|
5
|
+
constructor(database, options = {}) {
|
|
6
|
+
super(database, {
|
|
7
|
+
gameId: 'roulette',
|
|
8
|
+
table: 'roulette_sessions',
|
|
9
|
+
userFields: ['host_id'],
|
|
10
|
+
projectOutcomes: projectRouletteOutcomes,
|
|
11
|
+
});
|
|
12
|
+
this.#chooseBulletChamber = options.chooseBulletChamber ?? secureRandomInt;
|
|
13
|
+
}
|
|
14
|
+
async createLobby(message, endpoint, config) {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const host = playerFromMessage(message);
|
|
17
|
+
const state = createLobby(host, tableRules(config));
|
|
18
|
+
return this.createRow({
|
|
19
|
+
id: generateCompactId('rlt_'),
|
|
20
|
+
adapter: String(message.$adapter),
|
|
21
|
+
endpoint: message.$endpoint,
|
|
22
|
+
channel_type: message.$channel.type,
|
|
23
|
+
channel_id: message.$channel.id,
|
|
24
|
+
channel_key: channelKey(message),
|
|
25
|
+
endpoint_adapter: endpoint.adapter,
|
|
26
|
+
endpoint_id: endpoint.id,
|
|
27
|
+
host_id: host.id,
|
|
28
|
+
state_json: JSON.stringify(state),
|
|
29
|
+
phase: state.phase,
|
|
30
|
+
turn: '',
|
|
31
|
+
revision: 0,
|
|
32
|
+
processed_actions: '[]',
|
|
33
|
+
deadline_at: now + config.lobbyTimeoutSeconds * 1000,
|
|
34
|
+
status: 'active',
|
|
35
|
+
updated_at: now,
|
|
36
|
+
created_at: now,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Every seated player counts as busy, not just the host. */
|
|
40
|
+
async getActiveForUser(channelKeyValue, userId) {
|
|
41
|
+
const rows = await this.model.findAll({ channel_key: channelKeyValue, status: 'active' });
|
|
42
|
+
const seated = rows.find((row) => decodeRouletteState(row.state_json).players.some((player) => player.id === userId));
|
|
43
|
+
return seated ?? null;
|
|
44
|
+
}
|
|
45
|
+
async applyAction(session, actorId, action, config) {
|
|
46
|
+
if (action.type === 'join') {
|
|
47
|
+
await this.assertPlayersAvailable(session.channel_key, [action.player.id]);
|
|
48
|
+
}
|
|
49
|
+
const result = await this.mutateSession(session.id, {
|
|
50
|
+
actionId: `${session.id}:${session.revision}:${actorId}:${action.type}`,
|
|
51
|
+
expectedRevision: session.revision,
|
|
52
|
+
apply: (current) => {
|
|
53
|
+
const before = decodeRouletteState(current.state_json);
|
|
54
|
+
const after = this.#transition(before, actorId, action, config);
|
|
55
|
+
return rowPatch(after, config, Date.now());
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
session: result.session,
|
|
60
|
+
state: decodeRouletteState(result.session.state_json),
|
|
61
|
+
applied: result.kind === 'applied',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Open sessions whose lobby or turn deadline has passed. */
|
|
65
|
+
async listOverdue(now) {
|
|
66
|
+
const rows = await this.model.findAll({ status: 'active' });
|
|
67
|
+
return rows.filter((row) => row.deadline_at > 0 && row.deadline_at <= now);
|
|
68
|
+
}
|
|
69
|
+
#transition(state, actorId, action, config) {
|
|
70
|
+
switch (action.type) {
|
|
71
|
+
case 'join':
|
|
72
|
+
return joinLobby(state, action.player, tableRules(config));
|
|
73
|
+
case 'start':
|
|
74
|
+
return startRound(state, actorId, this.#chooseBulletChamber(state.chamberCount));
|
|
75
|
+
case 'fire':
|
|
76
|
+
return pullTrigger(state, actorId).state;
|
|
77
|
+
case 'leave':
|
|
78
|
+
return leaveGame(state, actorId);
|
|
79
|
+
case 'cancel':
|
|
80
|
+
return cancelGame(state);
|
|
81
|
+
case 'expire':
|
|
82
|
+
return state.phase === 'lobby' ? expireLobby(state) : expireTurn(state);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Per-group switches. The mute penalty is off until a group turns it on. */
|
|
87
|
+
export class RouletteGroupSettings {
|
|
88
|
+
#model;
|
|
89
|
+
constructor(database) {
|
|
90
|
+
const model = database.models.get('roulette_group_settings');
|
|
91
|
+
if (!model)
|
|
92
|
+
throw new Error('roulette_group_settings not registered');
|
|
93
|
+
this.#model = model;
|
|
94
|
+
}
|
|
95
|
+
async isMutePenaltyEnabled(channelKeyValue) {
|
|
96
|
+
const row = await this.#model.findOne({ id: channelKeyValue });
|
|
97
|
+
return row?.mute_penalty === 1;
|
|
98
|
+
}
|
|
99
|
+
async setMutePenalty(channelKeyValue, enabled, actorId) {
|
|
100
|
+
const patch = {
|
|
101
|
+
mute_penalty: enabled ? 1 : 0,
|
|
102
|
+
updated_by: actorId,
|
|
103
|
+
updated_at: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
const existing = await this.#model.findOne({ id: channelKeyValue });
|
|
106
|
+
if (existing) {
|
|
107
|
+
await this.#model.updateWhere({ id: channelKeyValue }, patch);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await this.#model.create({ id: channelKeyValue, ...patch });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function createServices(database, options = {}) {
|
|
114
|
+
return {
|
|
115
|
+
sessions: new RouletteSessionService(database, options),
|
|
116
|
+
settings: new RouletteGroupSettings(database),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function stateOf(session) {
|
|
120
|
+
return decodeRouletteState(session.state_json);
|
|
121
|
+
}
|
|
122
|
+
export function playerFromMessage(message) {
|
|
123
|
+
const name = message.$sender.name?.trim() || message.$sender.id;
|
|
124
|
+
return { id: message.$sender.id, name };
|
|
125
|
+
}
|
|
126
|
+
function tableRules(config) {
|
|
127
|
+
return { chamberCount: config.chambers, maxPlayers: config.maxPlayers };
|
|
128
|
+
}
|
|
129
|
+
function rowPatch(state, config, now) {
|
|
130
|
+
return {
|
|
131
|
+
state_json: JSON.stringify(state),
|
|
132
|
+
phase: state.phase,
|
|
133
|
+
turn: currentPlayer(state)?.id ?? '',
|
|
134
|
+
host_id: state.hostId,
|
|
135
|
+
deadline_at: deadlineFor(state, config, now),
|
|
136
|
+
status: statusFor(state),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function deadlineFor(state, config, now) {
|
|
140
|
+
if (state.phase === 'playing')
|
|
141
|
+
return now + config.turnTimeoutSeconds * 1000;
|
|
142
|
+
if (state.phase === 'lobby')
|
|
143
|
+
return now + config.lobbyTimeoutSeconds * 1000;
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
function statusFor(state) {
|
|
147
|
+
if (isOpen(state))
|
|
148
|
+
return 'active';
|
|
149
|
+
if (state.phase === 'finished')
|
|
150
|
+
return 'finished';
|
|
151
|
+
return 'aborted';
|
|
152
|
+
}
|
|
153
|
+
/** Survivors win one point each; the loser records a loss. Aborted games record nothing. */
|
|
154
|
+
function projectRouletteOutcomes(session) {
|
|
155
|
+
if (session.status !== 'finished')
|
|
156
|
+
return [];
|
|
157
|
+
const state = decodeRouletteState(session.state_json);
|
|
158
|
+
return state.players.map((player) => {
|
|
159
|
+
const lost = player.id === state.loserId;
|
|
160
|
+
return {
|
|
161
|
+
userId: player.id,
|
|
162
|
+
userName: player.name,
|
|
163
|
+
result: lost ? 'lost' : 'won',
|
|
164
|
+
score: lost ? 0 : 1,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
package/lib/view.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RouletteConfig } from './config.js';
|
|
2
|
+
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;
|
package/lib/view.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
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');
|
|
12
|
+
}
|
|
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');
|
|
22
|
+
}
|
|
23
|
+
export function describeTurn(state, config) {
|
|
24
|
+
const shooter = currentPlayer(state);
|
|
25
|
+
if (!shooter)
|
|
26
|
+
return '';
|
|
27
|
+
return `👉 轮到 ${shooter.name}:发送「开枪」,${config.turnTimeoutSeconds} 秒内不开枪判负。`
|
|
28
|
+
+ `(剩余 ${remainingChambers(state)} 个弹巢)`;
|
|
29
|
+
}
|
|
30
|
+
export function describeClick(shooter, state, config) {
|
|
31
|
+
return [`咔哒……${shooter.name} 的这一枪是空膛。`, describeTurn(state, config)].join('\n');
|
|
32
|
+
}
|
|
33
|
+
export function describeEnding(state) {
|
|
34
|
+
const loser = loserOf(state);
|
|
35
|
+
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
|
+
'发送「排行 轮盘」查看本群排行。',
|
|
43
|
+
].join('\n');
|
|
44
|
+
}
|
|
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} 已取消,本局不计分。`;
|
|
51
|
+
}
|
|
52
|
+
export function describeStatus(state, config) {
|
|
53
|
+
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} 枪)。`;
|
|
68
|
+
}
|
|
69
|
+
function listNames(players) {
|
|
70
|
+
if (players.length === 0)
|
|
71
|
+
return '无';
|
|
72
|
+
return players.map((player) => player.name).join('、');
|
|
73
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineGameCommandAliasMiddleware } from '@zhin.js/game-kit';
|
|
3
|
+
import { runRouletteCommand } from "../../lib/roulette-command.js";
|
|
4
|
+
/** `轮盘 加入` / `/轮盘赌 开枪` / `roulette start` route to the same logic as the command. */
|
|
5
|
+
export default defineGameCommandAliasMiddleware({
|
|
6
|
+
aliases: ['轮盘', '轮盘赌', 'roulette'],
|
|
7
|
+
async run(action, input, context) {
|
|
8
|
+
return runRouletteCommand(context, input, action);
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { channelKey, messageFromCommandInput } from '@zhin.js/game-kit';
|
|
3
|
+
import { defineMiddleware } from 'zhin.js/middleware';
|
|
4
|
+
import { runRouletteCommand } from "../../lib/roulette-command.js";
|
|
5
|
+
import { rouletteServicesToken } from "../../lib/runtime.js";
|
|
6
|
+
const FIRE_WORDS = ['开枪', '扣动扳机'];
|
|
7
|
+
/**
|
|
8
|
+
* A bare 「开枪」 fires only for the player whose turn it is in this group,
|
|
9
|
+
* so the word stays ordinary chat everywhere else.
|
|
10
|
+
*/
|
|
11
|
+
export default defineMiddleware({
|
|
12
|
+
target: 'inbound',
|
|
13
|
+
async handle(context, next) {
|
|
14
|
+
const text = context.input.content?.trim() ?? '';
|
|
15
|
+
if (!FIRE_WORDS.includes(text)) {
|
|
16
|
+
await next();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const message = messageFromCommandInput(context.input);
|
|
20
|
+
const services = context.use(rouletteServicesToken);
|
|
21
|
+
const session = await services.sessions.getActiveByChannel(channelKey(message));
|
|
22
|
+
if (!session || session.turn !== message.$sender.id) {
|
|
23
|
+
await next();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const reply = await runRouletteCommand(context, context.input, 'fire');
|
|
27
|
+
if (reply)
|
|
28
|
+
await context.input.$reply(reply);
|
|
29
|
+
},
|
|
30
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,103 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-roulette",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Zhin.js 俄罗斯轮盘 — 多人轮流开枪,可按群开启禁言惩罚 (Plugin Runtime)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"import": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"plugin.js",
|
|
18
|
+
"schema.json",
|
|
19
|
+
"commands/**/*.js",
|
|
20
|
+
"middlewares/**/*.js",
|
|
21
|
+
"schedules/**/*.js",
|
|
22
|
+
"README.md",
|
|
23
|
+
"CHANGELOG.md"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"zhin",
|
|
27
|
+
"zhin.js",
|
|
28
|
+
"game",
|
|
29
|
+
"roulette"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@zhin.js/core": "1.1.48",
|
|
34
|
+
"@zhin.js/game-kit": "1.1.13"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^6.0.3",
|
|
38
|
+
"vitest": "^4.1.10",
|
|
39
|
+
"@zhin.js/schedule-feature": "1.1.7",
|
|
40
|
+
"@zhin.js/testing": "1.0.5",
|
|
41
|
+
"zhin.js": "1.1.13"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/zhinjs/zhin.git",
|
|
46
|
+
"directory": "plugins/games/roulette"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"registry": "https://registry.npmjs.org"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22.19.0"
|
|
54
|
+
},
|
|
55
|
+
"zhin": {
|
|
56
|
+
"protocol": 1,
|
|
57
|
+
"type": "plugin",
|
|
58
|
+
"entry": "./plugin.js",
|
|
59
|
+
"engine": "^1.0.0",
|
|
60
|
+
"runtime": "trusted",
|
|
61
|
+
"features": [
|
|
62
|
+
{
|
|
63
|
+
"package": "@zhin.js/command",
|
|
64
|
+
"api": "^1.0.0"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"package": "@zhin.js/middleware",
|
|
68
|
+
"api": "^1.0.0"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"package": "@zhin.js/game-kit",
|
|
72
|
+
"api": "^1.0.0"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"package": "@zhin.js/schedule-feature",
|
|
76
|
+
"api": "^1.0.0"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"plugins": []
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
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
|
+
},
|
|
87
|
+
"peerDependenciesMeta": {
|
|
88
|
+
"@zhin.js/command": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"@zhin.js/middleware": {
|
|
92
|
+
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"@zhin.js/schedule-feature": {
|
|
95
|
+
"optional": true
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"scripts": {
|
|
99
|
+
"build": "tsc",
|
|
100
|
+
"clean": "rimraf lib",
|
|
101
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/games/roulette/tests"
|
|
102
|
+
}
|
|
6
103
|
}
|
package/plugin.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineGamePlugin } from '@zhin.js/game-kit';
|
|
3
|
+
import { ROULETTE_TABLES, defineHostTables } from "./lib/models.js";
|
|
4
|
+
import { rouletteServicesToken } from "./lib/runtime.js";
|
|
5
|
+
import { createServices, } from "./lib/session-service.js";
|
|
6
|
+
/**
|
|
7
|
+
* Plugin Runtime wiring:
|
|
8
|
+
* - `commands/roulette` and the `轮盘` alias middleware share one action runner.
|
|
9
|
+
* - `middlewares/roulette-fire` lets the player on turn just say 「开枪」.
|
|
10
|
+
* - Sessions use the host database when installed, otherwise memory.
|
|
11
|
+
* - `schedules/deadlines` enforces turn and lobby timeouts and posts the result.
|
|
12
|
+
* - Results feed game-hub records, so 「战绩」 and 「排行 轮盘」 work.
|
|
13
|
+
*/
|
|
14
|
+
export default defineGamePlugin({
|
|
15
|
+
name: 'roulette',
|
|
16
|
+
metadata: {
|
|
17
|
+
displayName: 'Russian Roulette',
|
|
18
|
+
},
|
|
19
|
+
game: {
|
|
20
|
+
id: 'roulette',
|
|
21
|
+
title: '俄罗斯轮盘',
|
|
22
|
+
icon: '🔫',
|
|
23
|
+
description: '多人轮流开枪,中弹者输;可按群开启禁言惩罚',
|
|
24
|
+
commandPrefix: '/轮盘',
|
|
25
|
+
quickStart: '开局',
|
|
26
|
+
aliases: ['轮盘赌', 'roulette'],
|
|
27
|
+
menus: [
|
|
28
|
+
{ id: 'open', label: '🎮 开局' },
|
|
29
|
+
{ id: 'join', label: '🙋 加入' },
|
|
30
|
+
{ id: 'help', label: '📖 玩法说明' },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
tables: ROULETTE_TABLES,
|
|
34
|
+
servicesToken: rouletteServicesToken,
|
|
35
|
+
defineHostTables,
|
|
36
|
+
createServices: (database) => createServices(database),
|
|
37
|
+
session: (services) => services.sessions,
|
|
38
|
+
stale: false,
|
|
39
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineSchedule } from '@zhin.js/schedule-feature';
|
|
3
|
+
import { DEADLINE_CRON, runDeadlineSweep } from "../../lib/deadline-job.js";
|
|
4
|
+
export default defineSchedule({
|
|
5
|
+
cron: DEADLINE_CRON,
|
|
6
|
+
description: '结束超时未开枪的回合和无人开始的房间',
|
|
7
|
+
execute: (context) => runDeadlineSweep(context, Date.now()),
|
|
8
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"chambers": {
|
|
7
|
+
"type": "integer",
|
|
8
|
+
"minimum": 2,
|
|
9
|
+
"maximum": 12,
|
|
10
|
+
"default": 6,
|
|
11
|
+
"description": "弹巢数量,其中一发是实弹"
|
|
12
|
+
},
|
|
13
|
+
"maxPlayers": {
|
|
14
|
+
"type": "integer",
|
|
15
|
+
"minimum": 2,
|
|
16
|
+
"maximum": 10,
|
|
17
|
+
"default": 6,
|
|
18
|
+
"description": "单局最多玩家数"
|
|
19
|
+
},
|
|
20
|
+
"turnTimeoutSeconds": {
|
|
21
|
+
"type": "integer",
|
|
22
|
+
"minimum": 10,
|
|
23
|
+
"maximum": 600,
|
|
24
|
+
"default": 60,
|
|
25
|
+
"description": "轮到的玩家超过这个秒数不开枪,判负"
|
|
26
|
+
},
|
|
27
|
+
"lobbyTimeoutSeconds": {
|
|
28
|
+
"type": "integer",
|
|
29
|
+
"minimum": 30,
|
|
30
|
+
"maximum": 3600,
|
|
31
|
+
"default": 300,
|
|
32
|
+
"description": "开局后超过这个秒数仍未开始,自动解散"
|
|
33
|
+
},
|
|
34
|
+
"muteSeconds": {
|
|
35
|
+
"type": "integer",
|
|
36
|
+
"minimum": 10,
|
|
37
|
+
"maximum": 3600,
|
|
38
|
+
"default": 60,
|
|
39
|
+
"description": "开启禁言惩罚的群里,输家被禁言的秒数(每个群默认关闭)"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|