@lizardon386/game-sdk 0.0.0-stage → 0.2.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -2
  3. package/dist/contract/client.d.ts +2 -0
  4. package/dist/contract/client.d.ts.map +1 -0
  5. package/dist/contract/client.js +2 -0
  6. package/dist/contract/client.js.map +1 -0
  7. package/dist/contract/module-api.d.ts +104 -0
  8. package/dist/contract/module-api.d.ts.map +1 -0
  9. package/dist/contract/module-api.js +22 -0
  10. package/dist/contract/module-api.js.map +1 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/kit/client/chat.d.ts +46 -0
  16. package/dist/kit/client/chat.d.ts.map +1 -0
  17. package/dist/kit/client/chat.js +183 -0
  18. package/dist/kit/client/chat.js.map +1 -0
  19. package/dist/kit/client/feed.d.ts +39 -0
  20. package/dist/kit/client/feed.d.ts.map +1 -0
  21. package/dist/kit/client/feed.js +157 -0
  22. package/dist/kit/client/feed.js.map +1 -0
  23. package/dist/kit/client/index.d.ts +6 -0
  24. package/dist/kit/client/index.d.ts.map +1 -0
  25. package/dist/kit/client/index.js +6 -0
  26. package/dist/kit/client/index.js.map +1 -0
  27. package/dist/kit/client/scoreboard.d.ts +68 -0
  28. package/dist/kit/client/scoreboard.d.ts.map +1 -0
  29. package/dist/kit/client/scoreboard.js +282 -0
  30. package/dist/kit/client/scoreboard.js.map +1 -0
  31. package/dist/kit/palette.d.ts +9 -0
  32. package/dist/kit/palette.d.ts.map +1 -0
  33. package/dist/kit/palette.js +38 -0
  34. package/dist/kit/palette.js.map +1 -0
  35. package/dist/kit/server/feed.d.ts +41 -0
  36. package/dist/kit/server/feed.d.ts.map +1 -0
  37. package/dist/kit/server/feed.js +210 -0
  38. package/dist/kit/server/feed.js.map +1 -0
  39. package/dist/kit/server/index.d.ts +3 -0
  40. package/dist/kit/server/index.d.ts.map +1 -0
  41. package/dist/kit/server/index.js +3 -0
  42. package/dist/kit/server/index.js.map +1 -0
  43. package/dist/kit/shared.d.ts +26 -0
  44. package/dist/kit/shared.d.ts.map +1 -0
  45. package/dist/kit/shared.js +8 -0
  46. package/dist/kit/shared.js.map +1 -0
  47. package/dist/node/pack.d.ts +2 -0
  48. package/dist/node/pack.d.ts.map +1 -0
  49. package/dist/node/pack.js +2 -0
  50. package/dist/node/pack.js.map +1 -0
  51. package/package.json +60 -4
  52. package/src/contract/client.ts +1 -0
  53. package/src/index.ts +30 -0
  54. package/src/kit/client/chat.ts +199 -0
  55. package/src/kit/client/feed.ts +170 -0
  56. package/src/kit/client/index.ts +33 -0
  57. package/src/kit/client/scoreboard.ts +333 -0
  58. package/src/kit/palette.ts +44 -0
  59. package/src/kit/server/feed.ts +221 -0
  60. package/src/kit/server/index.ts +2 -0
  61. package/src/kit/shared.ts +28 -0
  62. package/src/node/pack.ts +1 -0
@@ -0,0 +1,41 @@
1
+ import type { Effect, ViewerId } from '@lizardon386/core-abi';
2
+ import { type FeedEntry, type FeedVisibility } from '../shared.js';
3
+ export interface FeedSeat {
4
+ seatId: string;
5
+ team: string | number | null;
6
+ }
7
+ export interface FeedLogOptions {
8
+ max?: number;
9
+ maxText?: number;
10
+ now?: () => number;
11
+ /** team 可见性用;返回当前所有座位及其队伍。 */
12
+ seatTeams?: () => ReadonlyArray<FeedSeat>;
13
+ }
14
+ export interface FeedEmitInput {
15
+ type: string;
16
+ text: string;
17
+ actor?: string;
18
+ target?: string;
19
+ data?: Record<string, unknown>;
20
+ visibility?: FeedVisibility;
21
+ at?: number;
22
+ }
23
+ /** 服务端事件流水:生成序号/时间、按可见性发 Effect、保留最近 N 条。 */
24
+ export declare class FeedLog {
25
+ private readonly max;
26
+ private readonly maxText;
27
+ private readonly now;
28
+ private readonly seatTeams;
29
+ private entries;
30
+ private nextSeq;
31
+ private pending;
32
+ constructor(options?: FeedLogOptions);
33
+ emit(input: FeedEmitInput): FeedEntry;
34
+ /** 按视角裁剪历史;viewer 为 null 时只返回全员事件。 */
35
+ viewFor(viewer: ViewerId): FeedEntry[];
36
+ drainEffects(): Effect[];
37
+ clear(): void;
38
+ snapshot(): unknown;
39
+ restore(value: unknown): void;
40
+ }
41
+ //# sourceMappingURL=feed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feed.d.ts","sourceRoot":"","sources":["../../../src/kit/server/feed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAyB,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAE1F,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AA8ED,6CAA6C;AAC7C,qBAAa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IACnE,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,OAAO,CAAgB;gBAEnB,OAAO,GAAE,cAAmB;IAOxC,IAAI,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS;IAiDrC,sCAAsC;IACtC,OAAO,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,EAAE;IActC,YAAY,IAAI,MAAM,EAAE;IAMxB,KAAK,IAAI,IAAI;IAMb,QAAQ,IAAI,OAAO;IAQnB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;CAiB9B"}
@@ -0,0 +1,210 @@
1
+ import { FEED_EVENT, FeedError } from '../shared.js';
2
+ const MAX_TYPE_LENGTH = 64;
3
+ const MAX_DATA_JSON = 2_000;
4
+ function clamp(value, min, max, fallback) {
5
+ if (value === undefined || !Number.isFinite(value))
6
+ return fallback;
7
+ return Math.min(max, Math.max(min, Math.trunc(value)));
8
+ }
9
+ function validateVisibility(value) {
10
+ if (value === undefined || value === 'all')
11
+ return 'all';
12
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
13
+ throw new FeedError('事件可见性必须是 all、{ seatId } 或 { team }');
14
+ }
15
+ const keys = Object.keys(value);
16
+ if (keys.length !== 1)
17
+ throw new FeedError('事件可见性只能包含 seatId 或 team');
18
+ if (keys[0] === 'seatId') {
19
+ const seatId = value.seatId;
20
+ if (typeof seatId !== 'string' || seatId.length === 0)
21
+ throw new FeedError('事件 seatId 不合法');
22
+ return { seatId };
23
+ }
24
+ if (keys[0] === 'team') {
25
+ const team = value.team;
26
+ if (typeof team !== 'string' && (typeof team !== 'number' || !Number.isFinite(team))) {
27
+ throw new FeedError('事件 team 不合法');
28
+ }
29
+ return { team };
30
+ }
31
+ throw new FeedError('事件可见性只能包含 seatId 或 team');
32
+ }
33
+ function publicEntry(entry) {
34
+ const copy = {
35
+ seq: entry.seq,
36
+ at: entry.at,
37
+ type: entry.type,
38
+ text: entry.text,
39
+ };
40
+ if (entry.actor !== undefined)
41
+ copy.actor = entry.actor;
42
+ if (entry.target !== undefined)
43
+ copy.target = entry.target;
44
+ if (entry.data !== undefined)
45
+ copy.data = entry.data;
46
+ return copy;
47
+ }
48
+ function validateEntry(value) {
49
+ if (!value || typeof value !== 'object' || Array.isArray(value))
50
+ return null;
51
+ const raw = value;
52
+ if (!Number.isSafeInteger(raw.seq) || raw.seq < 1)
53
+ return null;
54
+ if (typeof raw.at !== 'number' || !Number.isFinite(raw.at))
55
+ return null;
56
+ if (typeof raw.type !== 'string' || raw.type.trim().length === 0 || raw.type.length > MAX_TYPE_LENGTH)
57
+ return null;
58
+ if (typeof raw.text !== 'string' || raw.text.length === 0)
59
+ return null;
60
+ if (raw.actor !== undefined && typeof raw.actor !== 'string')
61
+ return null;
62
+ if (raw.target !== undefined && typeof raw.target !== 'string')
63
+ return null;
64
+ if (raw.data !== undefined && (typeof raw.data !== 'object' || raw.data === null || Array.isArray(raw.data)))
65
+ return null;
66
+ let visibility;
67
+ try {
68
+ visibility = validateVisibility(raw.visibility);
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ const entry = {
74
+ seq: raw.seq,
75
+ at: raw.at,
76
+ type: raw.type.trim(),
77
+ text: raw.text,
78
+ visibility,
79
+ };
80
+ if (raw.actor !== undefined)
81
+ entry.actor = raw.actor;
82
+ if (raw.target !== undefined)
83
+ entry.target = raw.target;
84
+ if (raw.data !== undefined)
85
+ entry.data = raw.data;
86
+ return entry;
87
+ }
88
+ /** 服务端事件流水:生成序号/时间、按可见性发 Effect、保留最近 N 条。 */
89
+ export class FeedLog {
90
+ max;
91
+ maxText;
92
+ now;
93
+ seatTeams;
94
+ entries = [];
95
+ nextSeq = 1;
96
+ pending = [];
97
+ constructor(options = {}) {
98
+ this.max = clamp(options.max, 1, 500, 100);
99
+ this.maxText = clamp(options.maxText, 1, 2_000, 200);
100
+ this.now = options.now ?? (() => Date.now());
101
+ this.seatTeams = options.seatTeams ?? null;
102
+ }
103
+ emit(input) {
104
+ const type = typeof input.type === 'string' ? input.type.trim() : '';
105
+ const text = typeof input.text === 'string' ? input.text.trim() : '';
106
+ if (!type || type.length > MAX_TYPE_LENGTH)
107
+ throw new FeedError('事件 type 为空或过长');
108
+ if (!text)
109
+ throw new FeedError('事件 text 不能为空');
110
+ const visibility = validateVisibility(input.visibility);
111
+ let recipients = [];
112
+ if (typeof visibility === 'object' && 'team' in visibility) {
113
+ if (!this.seatTeams)
114
+ throw new FeedError('team 可见性需要 seatTeams 选项');
115
+ recipients = [...new Set(this.seatTeams()
116
+ .filter((seat) => seat.team === visibility.team)
117
+ .map((seat) => seat.seatId))];
118
+ }
119
+ const at = typeof input.at === 'number' && Number.isFinite(input.at) ? input.at : this.now();
120
+ const entry = {
121
+ seq: this.nextSeq,
122
+ at,
123
+ type,
124
+ text: text.slice(0, this.maxText),
125
+ visibility,
126
+ };
127
+ if (input.actor)
128
+ entry.actor = input.actor;
129
+ if (input.target)
130
+ entry.target = input.target;
131
+ if (input.data) {
132
+ const json = JSON.stringify(input.data);
133
+ if (json.length > MAX_DATA_JSON)
134
+ throw new FeedError('事件 data 过大');
135
+ entry.data = input.data;
136
+ }
137
+ this.nextSeq += 1;
138
+ this.entries.push(entry);
139
+ if (this.entries.length > this.max)
140
+ this.entries.splice(0, this.entries.length - this.max);
141
+ const payload = publicEntry(entry);
142
+ if (visibility === 'all') {
143
+ this.pending.push({ kind: 'broadcast', name: FEED_EVENT, payload });
144
+ }
145
+ else if ('seatId' in visibility) {
146
+ this.pending.push({ kind: 'toSeat', seatId: visibility.seatId, name: FEED_EVENT, payload });
147
+ }
148
+ else {
149
+ for (const seatId of recipients) {
150
+ this.pending.push({ kind: 'toSeat', seatId, name: FEED_EVENT, payload });
151
+ }
152
+ }
153
+ return payload;
154
+ }
155
+ /** 按视角裁剪历史;viewer 为 null 时只返回全员事件。 */
156
+ viewFor(viewer) {
157
+ const viewerTeam = viewer !== null && this.seatTeams
158
+ ? this.seatTeams().find((seat) => seat.seatId === viewer)?.team ?? null
159
+ : null;
160
+ return this.entries
161
+ .filter((entry) => {
162
+ if (entry.visibility === 'all')
163
+ return true;
164
+ if (viewer === null)
165
+ return false;
166
+ if ('seatId' in entry.visibility)
167
+ return entry.visibility.seatId === viewer;
168
+ return viewerTeam !== null && entry.visibility.team === viewerTeam;
169
+ })
170
+ .map(publicEntry);
171
+ }
172
+ drainEffects() {
173
+ const effects = this.pending;
174
+ this.pending = [];
175
+ return effects;
176
+ }
177
+ clear() {
178
+ this.entries = [];
179
+ this.pending = [];
180
+ this.nextSeq = 1;
181
+ }
182
+ snapshot() {
183
+ return {
184
+ version: 1,
185
+ nextSeq: this.nextSeq,
186
+ entries: this.entries.map((entry) => ({ ...entry, data: entry.data ? { ...entry.data } : undefined })),
187
+ };
188
+ }
189
+ restore(value) {
190
+ if (!value || typeof value !== 'object' || Array.isArray(value))
191
+ throw new FeedError('事件流快照不合法');
192
+ const raw = value;
193
+ if (raw.version !== 1 || !Array.isArray(raw.entries))
194
+ throw new FeedError('事件流快照版本不支持');
195
+ const entries = [];
196
+ for (const item of raw.entries) {
197
+ const entry = validateEntry(item);
198
+ if (!entry)
199
+ throw new FeedError('事件流快照含坏记录');
200
+ entries.push(entry);
201
+ }
202
+ entries.sort((a, b) => a.seq - b.seq);
203
+ this.entries = entries.slice(-this.max);
204
+ const maxSeq = this.entries.reduce((max, entry) => Math.max(max, entry.seq), 0);
205
+ const savedNext = typeof raw.nextSeq === 'number' && Number.isSafeInteger(raw.nextSeq) ? raw.nextSeq : 1;
206
+ this.nextSeq = Math.max(savedNext, maxSeq + 1, 1);
207
+ this.pending = [];
208
+ }
209
+ }
210
+ //# sourceMappingURL=feed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feed.js","sourceRoot":"","sources":["../../../src/kit/server/feed.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { FeedLog, type FeedEmitInput, type FeedLogOptions, type FeedSeat } from './feed.js';
2
+ export { FEED_EVENT, FeedError, type FeedEntry, type FeedVisibility, type SeatLike } from '../shared.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/kit/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC5F,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { FeedLog } from './feed.js';
2
+ export { FEED_EVENT, FeedError } from '../shared.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/kit/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA0D,MAAM,WAAW,CAAC;AAC5F,OAAO,EAAE,UAAU,EAAE,SAAS,EAAsD,MAAM,cAAc,CAAC"}
@@ -0,0 +1,26 @@
1
+ /** 游戏事件流的一条公开记录:玩法文本由游戏填,平台只搬运。 */
2
+ export interface FeedEntry {
3
+ seq: number;
4
+ at: number;
5
+ type: string;
6
+ text: string;
7
+ actor?: string;
8
+ target?: string;
9
+ data?: Record<string, unknown>;
10
+ }
11
+ /** 事件对谁可见;team 需要 FeedLog 提供 seatTeams。 */
12
+ export type FeedVisibility = 'all' | {
13
+ seatId: string;
14
+ } | {
15
+ team: string | number;
16
+ };
17
+ export declare const FEED_EVENT = "bp.feed";
18
+ export declare class FeedError extends Error {
19
+ constructor(message: string);
20
+ }
21
+ export interface SeatLike {
22
+ seatId: string;
23
+ slot?: number;
24
+ team?: string | number | null;
25
+ }
26
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/kit/shared.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,2CAA2C;AAC3C,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC;AAEpF,eAAO,MAAM,UAAU,YAAY,CAAC;AAEpC,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAC/B"}
@@ -0,0 +1,8 @@
1
+ export const FEED_EVENT = 'bp.feed';
2
+ export class FeedError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'FeedError';
6
+ }
7
+ }
8
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/kit/shared.ts"],"names":[],"mappings":"AAcA,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC;AAEpC,MAAM,OAAO,SAAU,SAAQ,KAAK;IAClC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ export * from '@lizardon386/pack-format';
2
+ //# sourceMappingURL=pack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack.d.ts","sourceRoot":"","sources":["../../src/node/pack.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from '@lizardon386/pack-format';
2
+ //# sourceMappingURL=pack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack.js","sourceRoot":"","sources":["../../src/node/pack.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@lizardon386/game-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "3178 游戏接入 SDK:游戏 ABI、客户端契约、单文件包和 game-kit",
5
+ "license": "MIT",
6
+ "author": "3178-battle-platform contributors",
7
+ "homepage": "https://github.com/Lizardon386/3178-battle-platform#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/Lizardon386/3178-battle-platform/issues"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/Lizardon386/3178-battle-platform.git",
14
+ "directory": "packages/game-sdk"
15
+ },
16
+ "engines": {
17
+ "node": ">=20.19.0"
18
+ },
19
+ "keywords": [
20
+ "3178",
21
+ "game-platform",
22
+ "game-sdk",
23
+ "game-kit"
24
+ ],
25
+ "type": "module",
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./client": {
34
+ "types": "./dist/contract/client.d.ts",
35
+ "default": "./dist/contract/client.js"
36
+ },
37
+ "./node": {
38
+ "types": "./dist/node/pack.d.ts",
39
+ "default": "./dist/node/pack.js"
40
+ },
41
+ "./kit": {
42
+ "types": "./dist/kit/client/index.d.ts",
43
+ "default": "./dist/kit/client/index.js"
44
+ },
45
+ "./kit/server": {
46
+ "types": "./dist/kit/server/index.d.ts",
47
+ "default": "./dist/kit/server/index.js"
48
+ },
49
+ "./package.json": "./package.json"
50
+ },
51
+ "dependencies": {
52
+ "@lizardon386/core-abi": "^0.2.0",
53
+ "@lizardon386/pack-format": "^0.2.0"
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "src"
58
+ ],
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }
@@ -0,0 +1 @@
1
+ export type { GameActions, GameClientContract } from '@lizardon386/core-abi';
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ export type {
2
+ Effect,
3
+ GameAction,
4
+ GameAsset,
5
+ GameContext,
6
+ GameInstance,
7
+ GameManifest,
8
+ GameMode,
9
+ GameModule,
10
+ GameSeatInfo,
11
+ GameUiOptions,
12
+ NoticeCode,
13
+ Outcome,
14
+ PlatformCode,
15
+ Reject,
16
+ RoomPhase,
17
+ SeatRole,
18
+ SeatView,
19
+ ViewerId,
20
+ } from '@lizardon386/core-abi';
21
+ export {
22
+ API_VERSION,
23
+ DEFAULT_MODE,
24
+ GAME_MODES,
25
+ SUPPORTED_API_VERSIONS,
26
+ SUPPORTED_MODES,
27
+ isReject,
28
+ reject,
29
+ stableDigest,
30
+ } from '@lizardon386/core-abi';
@@ -0,0 +1,199 @@
1
+ export interface ChatLine {
2
+ seatId: string;
3
+ name: string;
4
+ text: string;
5
+ at: number;
6
+ kind: 'say' | 'system';
7
+ }
8
+
9
+ export interface ChatPanelOptions {
10
+ mount?: HTMLElement;
11
+ max?: number;
12
+ maxText?: number;
13
+ showSystem?: boolean;
14
+ placeholder?: string;
15
+ sendLabel?: string;
16
+ /** 覆盖默认发送方式;不填时在 iframe 里 parent.postMessage。 */
17
+ onSend?: (text: string) => void;
18
+ /** 新到的消息回调;初始历史不会触发。 */
19
+ onLine?: (line: ChatLine) => void;
20
+ /** 是否监听 window 的 bp:chat-history / bp:chat;有 mount 时默认开。 */
21
+ listen?: boolean;
22
+ }
23
+
24
+ const MAX_CHAT_TEXT = 300;
25
+
26
+ function clamp(value: number | undefined, min: number, max: number, fallback: number): number {
27
+ if (value === undefined || !Number.isFinite(value)) return fallback;
28
+ return Math.min(max, Math.max(min, Math.trunc(value)));
29
+ }
30
+
31
+ export function chatText(value: string): string | null {
32
+ const text = value.trim();
33
+ if (text.length < 1 || text.length > MAX_CHAT_TEXT) return null;
34
+ return text;
35
+ }
36
+
37
+ export function chatLineKey(line: ChatLine): string {
38
+ return `${line.at}|${line.seatId}|${line.kind}|${line.text}`;
39
+ }
40
+
41
+ export function normalizeChatLine(value: unknown): ChatLine | null {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
43
+ const raw = value as Record<string, unknown>;
44
+ if (typeof raw.seatId !== 'string' || typeof raw.name !== 'string') return null;
45
+ if (typeof raw.text !== 'string' || typeof raw.at !== 'number' || !Number.isFinite(raw.at)) return null;
46
+ if (raw.kind !== 'say' && raw.kind !== 'system') return null;
47
+ const text = raw.text.trim();
48
+ if (!text || text.length > MAX_CHAT_TEXT) return null;
49
+ return { seatId: raw.seatId, name: raw.name, text, at: raw.at, kind: raw.kind };
50
+ }
51
+
52
+ export function mergeChatLines(current: readonly ChatLine[], incoming: readonly ChatLine[], max: number): ChatLine[] {
53
+ const byKey = new Map<string, ChatLine>();
54
+ for (const line of current) byKey.set(chatLineKey(line), line);
55
+ for (const line of incoming) byKey.set(chatLineKey(line), line);
56
+ return [...byKey.values()].sort((a, b) => a.at - b.at).slice(-Math.max(1, max));
57
+ }
58
+
59
+ /** 房间聊天面板:历史以平台推送的 bp:chat-history 为准,发送只发纯文本。 */
60
+ export class ChatPanel {
61
+ private readonly options: ChatPanelOptions;
62
+ private readonly max: number;
63
+ private readonly maxText: number;
64
+ private readonly root: HTMLDivElement | null = null;
65
+ private readonly log: HTMLOListElement | null = null;
66
+ private readonly input: HTMLInputElement | null = null;
67
+ private readonly form: HTMLFormElement | null = null;
68
+ private readonly onChatHistory: ((event: Event) => void) | null = null;
69
+ private readonly onChatLine: ((event: Event) => void) | null = null;
70
+ private lines: ChatLine[] = [];
71
+
72
+ constructor(options: ChatPanelOptions = {}) {
73
+ this.options = options;
74
+ this.max = clamp(options.max, 1, 500, 200);
75
+ this.maxText = clamp(options.maxText, 1, MAX_CHAT_TEXT, MAX_CHAT_TEXT);
76
+ if (options.mount) {
77
+ const doc = options.mount.ownerDocument;
78
+ const root = doc.createElement('div');
79
+ root.className = 'bp-chat';
80
+ root.style.cssText = 'display:flex;flex-direction:column;min-height:0;font:13px/1.5 system-ui, "Microsoft YaHei UI", "Microsoft YaHei", sans-serif';
81
+ const log = doc.createElement('ol');
82
+ log.className = 'bp-chat-log';
83
+ log.setAttribute('role', 'log');
84
+ log.setAttribute('aria-live', 'polite');
85
+ log.style.cssText = 'margin:0;padding:4px 8px;list-style:none;overflow-y:auto;flex:1 1 auto;min-height:120px;max-height:260px';
86
+ const form = doc.createElement('form');
87
+ form.className = 'bp-chat-form';
88
+ form.style.cssText = 'display:flex;gap:6px;padding:6px 0 0';
89
+ const input = doc.createElement('input');
90
+ input.className = 'bp-chat-input';
91
+ input.type = 'text';
92
+ input.maxLength = MAX_CHAT_TEXT;
93
+ input.autocomplete = 'off';
94
+ input.placeholder = options.placeholder ?? '说点什么';
95
+ input.style.cssText = 'flex:1 1 auto;min-width:0;padding:4px 6px';
96
+ const button = doc.createElement('button');
97
+ button.className = 'bp-chat-send';
98
+ button.type = 'submit';
99
+ button.textContent = options.sendLabel ?? '发送';
100
+ button.style.cssText = 'flex:none;padding:4px 10px';
101
+ form.append(input, button);
102
+ root.append(log, form);
103
+ options.mount.replaceChildren(root);
104
+ this.root = root;
105
+ this.log = log;
106
+ this.input = input;
107
+ this.form = form;
108
+ form.addEventListener('submit', (event) => {
109
+ event.preventDefault();
110
+ this.send();
111
+ });
112
+ }
113
+
114
+ if (options.listen ?? Boolean(options.mount)) {
115
+ if (typeof window !== 'undefined') {
116
+ this.onChatHistory = (event: Event) => {
117
+ const lines = (event as CustomEvent).detail;
118
+ if (Array.isArray(lines)) this.setLines(lines as ChatLine[]);
119
+ };
120
+ this.onChatLine = (event: Event) => {
121
+ this.append((event as CustomEvent).detail as ChatLine);
122
+ };
123
+ window.addEventListener('bp:chat-history', this.onChatHistory);
124
+ window.addEventListener('bp:chat', this.onChatLine);
125
+ }
126
+ }
127
+ }
128
+
129
+ setLines(lines: readonly ChatLine[]): void {
130
+ const normalized = lines.map(normalizeChatLine).filter((line): line is ChatLine => line !== null);
131
+ this.lines = mergeChatLines([], normalized, this.max);
132
+ this.render();
133
+ }
134
+
135
+ append(line: ChatLine): void {
136
+ const normalized = normalizeChatLine(line);
137
+ if (!normalized) return;
138
+ if (this.lines.some((item) => chatLineKey(item) === chatLineKey(normalized))) return;
139
+ this.lines = mergeChatLines(this.lines, [normalized], this.max);
140
+ this.render();
141
+ this.options.onLine?.(normalized);
142
+ }
143
+
144
+ send(): void {
145
+ const input = this.input;
146
+ if (!input) return;
147
+ const text = chatText(input.value);
148
+ if (!text) return;
149
+ if (this.options.onSend) {
150
+ this.options.onSend(text);
151
+ } else if (typeof parent !== 'undefined' && parent !== window) {
152
+ parent.postMessage({ type: 'chat', text }, '*');
153
+ }
154
+ input.value = '';
155
+ }
156
+
157
+ clear(): void {
158
+ this.lines = [];
159
+ this.render();
160
+ }
161
+
162
+ destroy(): void {
163
+ if (typeof window !== 'undefined') {
164
+ if (this.onChatHistory) window.removeEventListener('bp:chat-history', this.onChatHistory);
165
+ if (this.onChatLine) window.removeEventListener('bp:chat', this.onChatLine);
166
+ }
167
+ this.options.mount?.replaceChildren();
168
+ this.lines = [];
169
+ }
170
+
171
+ private render(): void {
172
+ const log = this.log;
173
+ if (!log) return;
174
+ const nearBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 8;
175
+ const allowed = this.options.showSystem === false ? this.lines.filter((line) => line.kind !== 'system') : this.lines;
176
+ log.replaceChildren();
177
+ for (const line of allowed) {
178
+ const item = log.ownerDocument.createElement('li');
179
+ item.className = `bp-chat-line bp-chat-${line.kind}`;
180
+ item.dataset.seatId = line.seatId;
181
+ item.style.cssText = 'padding:1px 0;overflow-wrap:anywhere';
182
+ if (line.kind === 'system') {
183
+ item.style.opacity = '0.65';
184
+ item.textContent = line.text.length > this.maxText ? `${line.text.slice(0, this.maxText)}…` : line.text;
185
+ } else {
186
+ const name = log.ownerDocument.createElement('span');
187
+ name.className = 'bp-chat-name';
188
+ name.textContent = `${line.name}:`;
189
+ name.style.cssText = 'font-weight:600;margin-right:2px';
190
+ const text = log.ownerDocument.createElement('span');
191
+ text.className = 'bp-chat-text';
192
+ text.textContent = line.text.length > this.maxText ? `${line.text.slice(0, this.maxText)}…` : line.text;
193
+ item.append(name, text);
194
+ }
195
+ log.append(item);
196
+ }
197
+ if (nearBottom) log.scrollTop = log.scrollHeight;
198
+ }
199
+ }