@zhin.js/game-shared 1.0.0
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/README.md +163 -0
- package/lib/board-sender.d.ts +14 -0
- package/lib/board-sender.d.ts.map +1 -0
- package/lib/board-sender.js +7 -0
- package/lib/board-sender.js.map +1 -0
- package/lib/choice-keyboard.d.ts +37 -0
- package/lib/choice-keyboard.d.ts.map +1 -0
- package/lib/choice-keyboard.js +60 -0
- package/lib/choice-keyboard.js.map +1 -0
- package/lib/game-action-alias.d.ts +8 -0
- package/lib/game-action-alias.d.ts.map +1 -0
- package/lib/game-action-alias.js +117 -0
- package/lib/game-action-alias.js.map +1 -0
- package/lib/game-hub-feature.d.ts +58 -0
- package/lib/game-hub-feature.d.ts.map +1 -0
- package/lib/game-hub-feature.js +89 -0
- package/lib/game-hub-feature.js.map +1 -0
- package/lib/game-hub-flow.d.ts +9 -0
- package/lib/game-hub-flow.d.ts.map +1 -0
- package/lib/game-hub-flow.js +90 -0
- package/lib/game-hub-flow.js.map +1 -0
- package/lib/game-hub-menu-context.d.ts +25 -0
- package/lib/game-hub-menu-context.d.ts.map +1 -0
- package/lib/game-hub-menu-context.js +61 -0
- package/lib/game-hub-menu-context.js.map +1 -0
- package/lib/game-hub-menu.d.ts +24 -0
- package/lib/game-hub-menu.d.ts.map +1 -0
- package/lib/game-hub-menu.js +114 -0
- package/lib/game-hub-menu.js.map +1 -0
- package/lib/game-hub-mount.d.ts +9 -0
- package/lib/game-hub-mount.d.ts.map +1 -0
- package/lib/game-hub-mount.js +72 -0
- package/lib/game-hub-mount.js.map +1 -0
- package/lib/game-middleware.d.ts +6 -0
- package/lib/game-middleware.d.ts.map +1 -0
- package/lib/game-middleware.js +9 -0
- package/lib/game-middleware.js.map +1 -0
- package/lib/game-registry.d.ts +3 -0
- package/lib/game-registry.d.ts.map +1 -0
- package/lib/game-registry.js +2 -0
- package/lib/game-registry.js.map +1 -0
- package/lib/game-session.d.ts +62 -0
- package/lib/game-session.d.ts.map +1 -0
- package/lib/game-session.js +34 -0
- package/lib/game-session.js.map +1 -0
- package/lib/grid-keyboard.d.ts +60 -0
- package/lib/grid-keyboard.d.ts.map +1 -0
- package/lib/grid-keyboard.js +82 -0
- package/lib/grid-keyboard.js.map +1 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +17 -0
- package/lib/index.js.map +1 -0
- package/package.json +32 -0
- package/src/board-sender.ts +16 -0
- package/src/choice-keyboard.ts +103 -0
- package/src/game-action-alias.ts +129 -0
- package/src/game-hub-feature.ts +147 -0
- package/src/game-hub-flow.ts +114 -0
- package/src/game-hub-menu-context.ts +91 -0
- package/src/game-hub-menu.ts +134 -0
- package/src/game-hub-mount.ts +108 -0
- package/src/game-middleware.ts +14 -0
- package/src/game-session.ts +88 -0
- package/src/grid-keyboard.ts +142 -0
- package/src/index.ts +17 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用网格按钮键盘构建器
|
|
3
|
+
* 支持井字棋(3×3)、五子棋(15×15)、四子棋(7×6)等
|
|
4
|
+
*/
|
|
5
|
+
import { type SendContent } from 'zhin.js';
|
|
6
|
+
export interface GridCell<T = unknown> {
|
|
7
|
+
/** 单元格状态(游戏自定义,如 0=空, 1=X, 2=O) */
|
|
8
|
+
state: T;
|
|
9
|
+
/** 按钮显示文本 */
|
|
10
|
+
label: string;
|
|
11
|
+
/** 是否禁用(已落子或终局) */
|
|
12
|
+
disabled: boolean;
|
|
13
|
+
/** 高亮(胜利连线等) */
|
|
14
|
+
highlight?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface GridKeyboardOptions<T = unknown> {
|
|
17
|
+
/** 游戏前缀(如 'ttt', 'gomoku', 'c4') */
|
|
18
|
+
gamePrefix: string;
|
|
19
|
+
/** 会话 ID */
|
|
20
|
+
sessionId: string;
|
|
21
|
+
/** 行数 */
|
|
22
|
+
rows: number;
|
|
23
|
+
/** 列数 */
|
|
24
|
+
cols: number;
|
|
25
|
+
/** 单元格数据,按行优先 [row * cols + col] */
|
|
26
|
+
cells: GridCell<T>[];
|
|
27
|
+
/** 状态行文本 */
|
|
28
|
+
statusLine: string;
|
|
29
|
+
/** 是否终局(全部按钮禁用) */
|
|
30
|
+
terminal?: boolean;
|
|
31
|
+
/** 省略 ASCII 文本棋盘(QQ 等平台) */
|
|
32
|
+
omitAsciiBoard?: boolean;
|
|
33
|
+
/** ASCII 棋盘渲染器(可选,省略则不渲染 ASCII) */
|
|
34
|
+
renderAscii?: (cells: GridCell<T>[], rows: number, cols: number, highlight?: number[]) => string;
|
|
35
|
+
/** 高亮的单元格索引(胜利连线等) */
|
|
36
|
+
highlight?: number[];
|
|
37
|
+
/** fallback 提示文本 */
|
|
38
|
+
fallbackHint?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 构建 fallback 数字映射(仅可落子的格子)
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildGridFallbackMap(gamePrefix: string, sessionId: string, cells: GridCell[]): Record<string, string>;
|
|
44
|
+
/**
|
|
45
|
+
* 构建网格按钮键盘消息内容
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildGridKeyboard<T>(options: GridKeyboardOptions<T>): SendContent;
|
|
48
|
+
/**
|
|
49
|
+
* 解析网格 payload:`{prefix}:{sessionId}:{cellIndex}`
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseGridPayload(payload: string, expectedPrefix?: string): {
|
|
52
|
+
prefix: string;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
cell: number;
|
|
55
|
+
} | null;
|
|
56
|
+
/**
|
|
57
|
+
* 解析按钮 ID 格式:`c{cellIndex}`(QQ 等平台 action 可能只回传 button_id)
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseCellButtonId(value: string): number | null;
|
|
60
|
+
//# sourceMappingURL=grid-keyboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid-keyboard.d.ts","sourceRoot":"","sources":["../src/grid-keyboard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,WAAW,QAAQ,CAAC,CAAC,GAAG,OAAO;IACnC,mCAAmC;IACnC,KAAK,EAAE,CAAC,CAAC;IACT,aAAa;IACb,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,OAAO;IAC9C,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS;IACT,IAAI,EAAE,MAAM,CAAC;IACb,SAAS;IACT,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACrB,YAAY;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4BAA4B;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mCAAmC;IACnC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC;IACjG,sBAAsB;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,oBAAoB;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,QAAQ,EAAE,GAChB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,WAAW,CAkDjF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,cAAc,CAAC,EAAE,MAAM,GACtB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQ5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM9D"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用网格按钮键盘构建器
|
|
3
|
+
* 支持井字棋(3×3)、五子棋(15×15)、四子棋(7×6)等
|
|
4
|
+
*/
|
|
5
|
+
import { segment } from 'zhin.js';
|
|
6
|
+
/**
|
|
7
|
+
* 构建 fallback 数字映射(仅可落子的格子)
|
|
8
|
+
*/
|
|
9
|
+
export function buildGridFallbackMap(gamePrefix, sessionId, cells) {
|
|
10
|
+
const map = {};
|
|
11
|
+
let n = 1;
|
|
12
|
+
for (let i = 0; i < cells.length; i++) {
|
|
13
|
+
if (!cells[i].disabled) {
|
|
14
|
+
map[String(n)] = `${gamePrefix}:${sessionId}:${i}`;
|
|
15
|
+
n++;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return map;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 构建网格按钮键盘消息内容
|
|
22
|
+
*/
|
|
23
|
+
export function buildGridKeyboard(options) {
|
|
24
|
+
const { gamePrefix, sessionId, rows, cols, cells, statusLine, terminal, omitAsciiBoard, renderAscii, highlight, fallbackHint, } = options;
|
|
25
|
+
const buttonRows = [];
|
|
26
|
+
for (let r = 0; r < rows; r++) {
|
|
27
|
+
const row = [];
|
|
28
|
+
for (let c = 0; c < cols; c++) {
|
|
29
|
+
const i = r * cols + c;
|
|
30
|
+
const cell = cells[i];
|
|
31
|
+
row.push(segment.button({
|
|
32
|
+
id: `c${i}`,
|
|
33
|
+
label: cell.label,
|
|
34
|
+
payload: `${gamePrefix}:${sessionId}:${i}`,
|
|
35
|
+
disabled: terminal || cell.disabled,
|
|
36
|
+
style: cell.highlight ? 'primary' : 'secondary',
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
buttonRows.push(row);
|
|
40
|
+
}
|
|
41
|
+
const fallback = buildGridFallbackMap(gamePrefix, sessionId, cells);
|
|
42
|
+
const textLines = [statusLine];
|
|
43
|
+
if (!omitAsciiBoard && renderAscii) {
|
|
44
|
+
textLines.push('', renderAscii(cells, rows, cols, highlight));
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
segment.text(textLines.join('\n')),
|
|
48
|
+
segment.keyboard(buttonRows, {
|
|
49
|
+
fallback: fallbackHint
|
|
50
|
+
? { hint: fallbackHint, map: fallback }
|
|
51
|
+
: undefined,
|
|
52
|
+
}).toElement(),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 解析网格 payload:`{prefix}:{sessionId}:{cellIndex}`
|
|
57
|
+
*/
|
|
58
|
+
export function parseGridPayload(payload, expectedPrefix) {
|
|
59
|
+
const m = /^([a-z0-9_]+):([^:]+):(\d+)$/i.exec(payload);
|
|
60
|
+
if (!m)
|
|
61
|
+
return null;
|
|
62
|
+
const prefix = m[1];
|
|
63
|
+
if (expectedPrefix && prefix !== expectedPrefix)
|
|
64
|
+
return null;
|
|
65
|
+
const cell = Number(m[3]);
|
|
66
|
+
if (!Number.isInteger(cell) || cell < 0)
|
|
67
|
+
return null;
|
|
68
|
+
return { prefix, sessionId: m[2], cell };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 解析按钮 ID 格式:`c{cellIndex}`(QQ 等平台 action 可能只回传 button_id)
|
|
72
|
+
*/
|
|
73
|
+
export function parseCellButtonId(value) {
|
|
74
|
+
const m = /^c(\d+)$/.exec(value);
|
|
75
|
+
if (!m)
|
|
76
|
+
return null;
|
|
77
|
+
const cell = Number(m[1]);
|
|
78
|
+
if (!Number.isInteger(cell) || cell < 0)
|
|
79
|
+
return null;
|
|
80
|
+
return cell;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=grid-keyboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid-keyboard.js","sourceRoot":"","sources":["../src/grid-keyboard.ts"],"names":[],"mappings":"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"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zhin.js/game-shared - 通用游戏工具包
|
|
3
|
+
*
|
|
4
|
+
* 提供网格棋盘、会话管理、消息发送等通用抽象
|
|
5
|
+
*/
|
|
6
|
+
export * from './grid-keyboard.js';
|
|
7
|
+
export * from './choice-keyboard.js';
|
|
8
|
+
export * from './board-sender.js';
|
|
9
|
+
export * from './game-session.js';
|
|
10
|
+
export * from './game-hub-feature.js';
|
|
11
|
+
export * from './game-hub-menu.js';
|
|
12
|
+
export * from './game-hub-menu-context.js';
|
|
13
|
+
export * from './game-hub-flow.js';
|
|
14
|
+
export * from './game-hub-mount.js';
|
|
15
|
+
export * from './game-middleware.js';
|
|
16
|
+
export * from './game-action-alias.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zhin.js/game-shared - 通用游戏工具包
|
|
3
|
+
*
|
|
4
|
+
* 提供网格棋盘、会话管理、消息发送等通用抽象
|
|
5
|
+
*/
|
|
6
|
+
export * from './grid-keyboard.js';
|
|
7
|
+
export * from './choice-keyboard.js';
|
|
8
|
+
export * from './board-sender.js';
|
|
9
|
+
export * from './game-session.js';
|
|
10
|
+
export * from './game-hub-feature.js';
|
|
11
|
+
export * from './game-hub-menu.js';
|
|
12
|
+
export * from './game-hub-menu-context.js';
|
|
13
|
+
export * from './game-hub-flow.js';
|
|
14
|
+
export * from './game-hub-mount.js';
|
|
15
|
+
export * from './game-middleware.js';
|
|
16
|
+
export * from './game-action-alias.js';
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhin.js/game-shared",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zhin.js 游戏插件通用工具包",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"development": "./src/index.ts",
|
|
11
|
+
"import": "./lib/index.js",
|
|
12
|
+
"types": "./lib/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"zhin.js": "^4.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^6.0.3",
|
|
20
|
+
"zhin.js": "4.1.0"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 游戏棋盘消息辅助工具
|
|
3
|
+
* 提供会话键生成和消息 ID 更新回调封装
|
|
4
|
+
*
|
|
5
|
+
* 注意:实际的发送/编辑逻辑由 Adapter.editMessage 统一处理
|
|
6
|
+
* - 支持编辑的平台:调用平台 API 编辑
|
|
7
|
+
* - 不支持编辑的平台:自动 fallback 到发送新消息
|
|
8
|
+
*/
|
|
9
|
+
import type { Message } from 'zhin.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 构建频道唯一键(用于会话查找)
|
|
13
|
+
*/
|
|
14
|
+
export function channelKey(message: Message<any>): string {
|
|
15
|
+
return `${message.$adapter}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 选项式交互键盘(文字冒险、问答、剧情分支等)
|
|
3
|
+
*/
|
|
4
|
+
import { segment, type SendContent } from 'zhin.js';
|
|
5
|
+
|
|
6
|
+
export interface ChoiceOption {
|
|
7
|
+
/** 选项 ID,写入 payload */
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
style?: 'primary' | 'danger' | 'secondary';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChoiceKeyboardOptions {
|
|
15
|
+
gamePrefix: string;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
/** 剧情/题干正文 */
|
|
18
|
+
narrative: string;
|
|
19
|
+
choices: ChoiceOption[];
|
|
20
|
+
terminal?: boolean;
|
|
21
|
+
/** 文本 fallback 提示 */
|
|
22
|
+
fallbackHint?: string;
|
|
23
|
+
/** 每行最多几个按钮(默认全部一行) */
|
|
24
|
+
buttonsPerRow?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildChoiceFallbackMap(
|
|
28
|
+
gamePrefix: string,
|
|
29
|
+
sessionId: string,
|
|
30
|
+
choices: ChoiceOption[],
|
|
31
|
+
): Record<string, string> {
|
|
32
|
+
const map: Record<string, string> = {};
|
|
33
|
+
let n = 1;
|
|
34
|
+
for (const choice of choices) {
|
|
35
|
+
if (!choice.disabled) {
|
|
36
|
+
map[String(n)] = `${gamePrefix}:${sessionId}:${choice.id}`;
|
|
37
|
+
n++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return map;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function chunkChoices<T>(items: T[], size: number): T[][] {
|
|
44
|
+
if (size <= 0 || items.length <= size) return [items];
|
|
45
|
+
const rows: T[][] = [];
|
|
46
|
+
for (let i = 0; i < items.length; i += size) {
|
|
47
|
+
rows.push(items.slice(i, i + size));
|
|
48
|
+
}
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 构建选项键盘消息:正文 + 一行或多行按钮
|
|
54
|
+
*/
|
|
55
|
+
export function buildChoiceKeyboard(options: ChoiceKeyboardOptions): SendContent {
|
|
56
|
+
const {
|
|
57
|
+
gamePrefix,
|
|
58
|
+
sessionId,
|
|
59
|
+
narrative,
|
|
60
|
+
choices,
|
|
61
|
+
terminal,
|
|
62
|
+
fallbackHint,
|
|
63
|
+
buttonsPerRow,
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
const enabled = choices.filter((c) => !c.disabled);
|
|
67
|
+
const buttonRows = chunkChoices(enabled, buttonsPerRow ?? enabled.length).map((row) =>
|
|
68
|
+
row.map((choice) =>
|
|
69
|
+
segment.button({
|
|
70
|
+
id: choice.id,
|
|
71
|
+
label: choice.label,
|
|
72
|
+
payload: `${gamePrefix}:${sessionId}:${choice.id}`,
|
|
73
|
+
disabled: terminal || choice.disabled,
|
|
74
|
+
style: choice.style ?? 'secondary',
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const fallback = buildChoiceFallbackMap(gamePrefix, sessionId, choices);
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
segment.text(narrative),
|
|
83
|
+
segment.keyboard(buttonRows, {
|
|
84
|
+
fallback: fallbackHint && Object.keys(fallback).length > 0
|
|
85
|
+
? { hint: fallbackHint, map: fallback }
|
|
86
|
+
: undefined,
|
|
87
|
+
}).toElement(),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 解析选项 payload:`{prefix}:{sessionId}:{choiceId}`
|
|
93
|
+
*/
|
|
94
|
+
export function parseChoicePayload(
|
|
95
|
+
payload: string,
|
|
96
|
+
expectedPrefix?: string,
|
|
97
|
+
): { prefix: string; sessionId: string; choiceId: string } | null {
|
|
98
|
+
const m = /^([a-z0-9_]+):([^:]+):([a-z0-9_-]+)$/i.exec(payload);
|
|
99
|
+
if (!m) return null;
|
|
100
|
+
const prefix = m[1]!;
|
|
101
|
+
if (expectedPrefix && prefix !== expectedPrefix) return null;
|
|
102
|
+
return { prefix, sessionId: m[2]!, choiceId: m[3]! };
|
|
103
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const TTT_ACTIONS: Record<string, string> = {
|
|
2
|
+
bot: 'bot',
|
|
3
|
+
join: 'join',
|
|
4
|
+
leave: 'leave',
|
|
5
|
+
quit: 'quit',
|
|
6
|
+
spectate: 'spectate',
|
|
7
|
+
help: 'help',
|
|
8
|
+
人机: 'bot',
|
|
9
|
+
排队: 'join',
|
|
10
|
+
加入: 'join',
|
|
11
|
+
离开: 'leave',
|
|
12
|
+
认输: 'quit',
|
|
13
|
+
观战: 'spectate',
|
|
14
|
+
帮助: 'help',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ADV_ACTIONS: Record<string, string> = {
|
|
18
|
+
start: 'start',
|
|
19
|
+
continue: 'continue',
|
|
20
|
+
map: 'map',
|
|
21
|
+
achievements: 'achievements',
|
|
22
|
+
achievement: 'achievements',
|
|
23
|
+
quit: 'quit',
|
|
24
|
+
help: 'help',
|
|
25
|
+
开始: 'start',
|
|
26
|
+
继续: 'continue',
|
|
27
|
+
地图: 'map',
|
|
28
|
+
成就: 'achievements',
|
|
29
|
+
放弃: 'quit',
|
|
30
|
+
退出: 'quit',
|
|
31
|
+
帮助: 'help',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const RPS_ACTIONS: Record<string, string> = {
|
|
35
|
+
start: 'start',
|
|
36
|
+
continue: 'continue',
|
|
37
|
+
quit: 'quit',
|
|
38
|
+
help: 'help',
|
|
39
|
+
开始: 'start',
|
|
40
|
+
继续: 'continue',
|
|
41
|
+
放弃: 'quit',
|
|
42
|
+
帮助: 'help',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const GUESS_ACTIONS: Record<string, string> = {
|
|
46
|
+
start: 'start',
|
|
47
|
+
quit: 'quit',
|
|
48
|
+
help: 'help',
|
|
49
|
+
开始: 'start',
|
|
50
|
+
放弃: 'quit',
|
|
51
|
+
帮助: 'help',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const DICE_ACTIONS: Record<string, string> = {
|
|
55
|
+
start: 'start',
|
|
56
|
+
continue: 'continue',
|
|
57
|
+
quit: 'quit',
|
|
58
|
+
help: 'help',
|
|
59
|
+
开始: 'start',
|
|
60
|
+
继续: 'continue',
|
|
61
|
+
放弃: 'quit',
|
|
62
|
+
帮助: 'help',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function lookup(map: Record<string, string>, raw: string): string {
|
|
66
|
+
const t = raw.trim();
|
|
67
|
+
if (!t) return 'help';
|
|
68
|
+
return map[t] ?? map[t.toLowerCase()] ?? t.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function normalizeTttAction(raw: string): string {
|
|
72
|
+
return lookup(TTT_ACTIONS, raw);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function normalizeAdvAction(raw: string): string {
|
|
76
|
+
return lookup(ADV_ACTIONS, raw);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function normalizeRpsAction(raw: string): string {
|
|
80
|
+
return lookup(RPS_ACTIONS, raw);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeGuessAction(raw: string): string {
|
|
84
|
+
return lookup(GUESS_ACTIONS, raw);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function normalizeDiceAction(raw: string): string {
|
|
88
|
+
return lookup(DICE_ACTIONS, raw);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const CHAIN_ACTIONS: Record<string, string> = {
|
|
92
|
+
start: 'start_pinyin',
|
|
93
|
+
start_pinyin: 'start_pinyin',
|
|
94
|
+
start_char: 'start_char',
|
|
95
|
+
continue: 'continue',
|
|
96
|
+
quit: 'quit',
|
|
97
|
+
help: 'help',
|
|
98
|
+
pinyin: 'start_pinyin',
|
|
99
|
+
char: 'start_char',
|
|
100
|
+
开始: 'start_pinyin',
|
|
101
|
+
同音: 'start_pinyin',
|
|
102
|
+
同字: 'start_char',
|
|
103
|
+
继续: 'continue',
|
|
104
|
+
放弃: 'quit',
|
|
105
|
+
帮助: 'help',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const RIDDLE_ACTIONS: Record<string, string> = {
|
|
109
|
+
start: 'start',
|
|
110
|
+
continue: 'continue',
|
|
111
|
+
quit: 'quit',
|
|
112
|
+
help: 'help',
|
|
113
|
+
char: 'char',
|
|
114
|
+
idiom: 'idiom',
|
|
115
|
+
开始: 'start',
|
|
116
|
+
继续: 'continue',
|
|
117
|
+
放弃: 'quit',
|
|
118
|
+
帮助: 'help',
|
|
119
|
+
字谜: 'char',
|
|
120
|
+
成语: 'idiom',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export function normalizeChainAction(raw: string): string {
|
|
124
|
+
return lookup(CHAIN_ACTIONS, raw);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function normalizeRiddleAction(raw: string): string {
|
|
128
|
+
return lookup(RIDDLE_ACTIONS, raw);
|
|
129
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Feature,
|
|
3
|
+
getPlugin,
|
|
4
|
+
type FeatureJSON,
|
|
5
|
+
type Plugin,
|
|
6
|
+
type PluginLike,
|
|
7
|
+
} from 'zhin.js';
|
|
8
|
+
import { mountGameHubUi, markGameHubUiMounted } from './game-hub-mount.js';
|
|
9
|
+
|
|
10
|
+
export interface GameHubContext {
|
|
11
|
+
plugin: Plugin;
|
|
12
|
+
message: import('zhin.js').Message<any>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GameMenuAction {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
style?: 'primary' | 'danger' | 'secondary';
|
|
19
|
+
groupOnly?: boolean;
|
|
20
|
+
privateOnly?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RegisteredGame {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
icon: string;
|
|
27
|
+
description: string;
|
|
28
|
+
commandPrefix: string;
|
|
29
|
+
quickStart?: string;
|
|
30
|
+
aliases?: string[];
|
|
31
|
+
menus: GameMenuAction[];
|
|
32
|
+
runAction: (actionId: string, ctx: GameHubContext) => Promise<string | undefined | void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface GameHubContextExtensions {
|
|
36
|
+
registerGame(game: RegisteredGame): () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare module 'zhin.js' {
|
|
40
|
+
namespace Plugin {
|
|
41
|
+
interface Extensions extends GameHubContextExtensions {}
|
|
42
|
+
interface Contexts {
|
|
43
|
+
game: GameHubFeature;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let activeFeature: GameHubFeature | null = null;
|
|
49
|
+
|
|
50
|
+
export class GameHubFeature extends Feature<RegisteredGame> {
|
|
51
|
+
readonly name = 'game' as const;
|
|
52
|
+
readonly icon = 'Gamepad2';
|
|
53
|
+
readonly desc = '游戏大厅';
|
|
54
|
+
|
|
55
|
+
private readonly byId = new Map<string, RegisteredGame>();
|
|
56
|
+
private hubDisposers: (() => void)[] = [];
|
|
57
|
+
|
|
58
|
+
register(game: RegisteredGame, pluginName: string): () => void {
|
|
59
|
+
const prev = this.byId.get(game.id);
|
|
60
|
+
if (prev) {
|
|
61
|
+
this.remove(prev);
|
|
62
|
+
}
|
|
63
|
+
this.byId.set(game.id, game);
|
|
64
|
+
return this.add(game, pluginName);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getGame(id: string): RegisteredGame | undefined {
|
|
68
|
+
return this.byId.get(id);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override remove(item: RegisteredGame, pluginName?: string): boolean {
|
|
72
|
+
this.byId.delete(item.id);
|
|
73
|
+
return super.remove(item, pluginName);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
toJSON(pluginName?: string): FeatureJSON {
|
|
77
|
+
const list = pluginName ? this.getByPlugin(pluginName) : [...this.items];
|
|
78
|
+
return {
|
|
79
|
+
name: this.name,
|
|
80
|
+
icon: this.icon,
|
|
81
|
+
desc: this.desc,
|
|
82
|
+
count: list.length,
|
|
83
|
+
items: list.map((g) => ({
|
|
84
|
+
name: g.id,
|
|
85
|
+
desc: g.title,
|
|
86
|
+
usage: `${g.commandPrefix} ${g.quickStart ?? '开始'}`,
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get extensions() {
|
|
92
|
+
const feature = this;
|
|
93
|
+
return {
|
|
94
|
+
registerGame(game: RegisteredGame) {
|
|
95
|
+
const plugin = getPlugin();
|
|
96
|
+
ensureGameHubService(plugin);
|
|
97
|
+
const dispose = feature.register(game, plugin.name);
|
|
98
|
+
plugin.recordFeatureContribution(feature.name, game.id);
|
|
99
|
+
plugin.onDispose(dispose);
|
|
100
|
+
return dispose;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
mounted(plugin: PluginLike): void {
|
|
106
|
+
activeFeature = this;
|
|
107
|
+
const root = plugin as Plugin;
|
|
108
|
+
this.hubDisposers = mountGameHubUi(root.root ?? root);
|
|
109
|
+
markGameHubUiMounted();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
dispose(): void {
|
|
113
|
+
for (const d of this.hubDisposers) d();
|
|
114
|
+
this.hubDisposers = [];
|
|
115
|
+
if (activeFeature === this) {
|
|
116
|
+
activeFeature = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 在 root 上注册 `game` 服务(幂等)。游戏插件初始化时调用一次即可。
|
|
123
|
+
*/
|
|
124
|
+
export function ensureGameHubService(plugin: Plugin): GameHubFeature {
|
|
125
|
+
const root = plugin.root;
|
|
126
|
+
if (root.contextIsReady('game')) {
|
|
127
|
+
return root.inject('game') as GameHubFeature;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const feature = new GameHubFeature();
|
|
131
|
+
root.provide(feature);
|
|
132
|
+
activeFeature = feature;
|
|
133
|
+
|
|
134
|
+
if (root.started && feature.mounted) {
|
|
135
|
+
feature.mounted(root);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return root.inject('game') as GameHubFeature;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function getRegisteredGames(): readonly RegisteredGame[] {
|
|
142
|
+
return activeFeature?.items ?? [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getRegisteredGame(id: string): RegisteredGame | undefined {
|
|
146
|
+
return activeFeature?.getGame(id);
|
|
147
|
+
}
|