steamsheep-ts-game-engine 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 +1087 -0
- package/dist/core/index.d.mts +328 -0
- package/dist/core/index.d.ts +328 -0
- package/dist/core/index.js +220 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +195 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1196 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1160 -0
- package/dist/index.mjs.map +1 -0
- package/dist/state/index.d.mts +110 -0
- package/dist/state/index.d.ts +110 -0
- package/dist/state/index.js +479 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/index.mjs +476 -0
- package/dist/state/index.mjs.map +1 -0
- package/dist/store-BP5bpjRr.d.ts +327 -0
- package/dist/store-BeEHel1o.d.mts +327 -0
- package/dist/systems/index.d.mts +318 -0
- package/dist/systems/index.d.ts +318 -0
- package/dist/systems/index.js +347 -0
- package/dist/systems/index.js.map +1 -0
- package/dist/systems/index.mjs +341 -0
- package/dist/systems/index.mjs.map +1 -0
- package/dist/types-CZueoTHl.d.mts +464 -0
- package/dist/types-CZueoTHl.d.ts +464 -0
- package/dist/ui/index.d.mts +56 -0
- package/dist/ui/index.d.ts +56 -0
- package/dist/ui/index.js +412 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +407 -0
- package/dist/ui/index.mjs.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
7
|
+
|
|
8
|
+
// systems/events.ts
|
|
9
|
+
var EventBus = class {
|
|
10
|
+
constructor() {
|
|
11
|
+
/**
|
|
12
|
+
* 事件监听器的内部存储
|
|
13
|
+
* 将事件名映射到监听器回调函数数组
|
|
14
|
+
* 使用 Listener[](默认为 Listener<unknown>[])以允许同一事件名有不同类型的监听器
|
|
15
|
+
* 类型安全在订阅/发射时强制执行
|
|
16
|
+
*/
|
|
17
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 订阅事件
|
|
21
|
+
*
|
|
22
|
+
* @template T - 事件载荷的预期类型
|
|
23
|
+
* @param event - 要订阅的事件名称
|
|
24
|
+
* @param callback - 事件触发时调用的监听器函数
|
|
25
|
+
* @returns 取消订阅的函数
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const handler = (data: { count: number }) => console.log(data.count);
|
|
30
|
+
* const unsubscribe = eventBus.on('update', handler);
|
|
31
|
+
* // 稍后取消订阅
|
|
32
|
+
* unsubscribe();
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
on(event, callback) {
|
|
36
|
+
if (!this.listeners.has(event)) {
|
|
37
|
+
this.listeners.set(event, []);
|
|
38
|
+
}
|
|
39
|
+
this.listeners.get(event).push(callback);
|
|
40
|
+
return () => this.off(event, callback);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 取消订阅事件
|
|
44
|
+
*
|
|
45
|
+
* @template T - 事件载荷的预期类型
|
|
46
|
+
* @param event - 要取消订阅的事件名称
|
|
47
|
+
* @param callback - 要移除的监听器函数
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const handler = (data: number) => console.log(data);
|
|
52
|
+
* eventBus.on('count', handler);
|
|
53
|
+
* eventBus.off('count', handler); // 移除监听器
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
off(event, callback) {
|
|
57
|
+
const callbacks = this.listeners.get(event);
|
|
58
|
+
if (callbacks) {
|
|
59
|
+
this.listeners.set(
|
|
60
|
+
event,
|
|
61
|
+
callbacks.filter((cb) => cb !== callback)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 发射事件并携带可选的类型化载荷数据
|
|
67
|
+
* 该事件的所有已注册监听器都将被调用并传入数据
|
|
68
|
+
* 单个监听器中的错误会被捕获并记录,不会影响其他监听器
|
|
69
|
+
*
|
|
70
|
+
* @template T - 事件载荷的类型
|
|
71
|
+
* @param event - 要发射的事件名称
|
|
72
|
+
* @param data - 传递给监听器的可选载荷数据
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* eventBus.emit<{ userId: string }>('login', { userId: '123' });
|
|
77
|
+
* eventBus.emit('logout'); // 无载荷
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
emit(event, data) {
|
|
81
|
+
const callbacks = this.listeners.get(event);
|
|
82
|
+
if (callbacks) {
|
|
83
|
+
callbacks.forEach((cb) => {
|
|
84
|
+
try {
|
|
85
|
+
cb(data);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`Error in event listener for "${event}":`, error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 清空所有事件的所有监听器
|
|
94
|
+
* 用于清理或重置事件系统
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* eventBus.clear(); // 所有监听器被移除
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
clear() {
|
|
102
|
+
this.listeners.clear();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var gameEvents = new EventBus();
|
|
106
|
+
var EngineEvents = {
|
|
107
|
+
/** 当角色属性变化时触发 */
|
|
108
|
+
STAT_CHANGE: "engine:stat_change",
|
|
109
|
+
/** 当物品添加到背包时触发 */
|
|
110
|
+
ITEM_ADD: "engine:item_add",
|
|
111
|
+
/** 当物品从背包移除时触发 */
|
|
112
|
+
ITEM_REMOVE: "engine:item_remove",
|
|
113
|
+
/** 当游戏标志变化时触发 */
|
|
114
|
+
FLAG_CHANGE: "engine:flag_change",
|
|
115
|
+
/** 当需要显示瞬时通知时触发(Toast/Modal,不持久化) */
|
|
116
|
+
NOTIFICATION: "engine:notification"};
|
|
117
|
+
|
|
118
|
+
// core/constants.ts
|
|
119
|
+
var DEFAULT_CONFIG = {
|
|
120
|
+
/** 最大日志条数 */
|
|
121
|
+
MAX_LOG_ENTRIES: 50,
|
|
122
|
+
/** 最大历史快照数 */
|
|
123
|
+
MAX_HISTORY_SNAPSHOTS: 20};
|
|
124
|
+
|
|
125
|
+
// state/history.ts
|
|
126
|
+
var HistoryManager = class {
|
|
127
|
+
/**
|
|
128
|
+
* 创建历史管理器实例
|
|
129
|
+
* @param maxSnapshots - 最大保存的快照数量,默认使用配置中的值
|
|
130
|
+
*/
|
|
131
|
+
constructor(maxSnapshots = DEFAULT_CONFIG.MAX_HISTORY_SNAPSHOTS) {
|
|
132
|
+
/** 存储的状态快照数组 */
|
|
133
|
+
__publicField(this, "snapshots", []);
|
|
134
|
+
/** 最大快照数量限制 */
|
|
135
|
+
__publicField(this, "maxSnapshots");
|
|
136
|
+
this.maxSnapshots = maxSnapshots;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 保存当前状态快照
|
|
140
|
+
*
|
|
141
|
+
* 通过深拷贝保存状态,防止后续修改影响历史记录
|
|
142
|
+
* 当快照数量超过最大限制时,自动移除最旧的快照
|
|
143
|
+
*
|
|
144
|
+
* @param state - 要保存的游戏状态
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* history.push(gameState);
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
push(state) {
|
|
152
|
+
const snapshot = JSON.parse(JSON.stringify(state));
|
|
153
|
+
this.snapshots.push(snapshot);
|
|
154
|
+
if (this.snapshots.length > this.maxSnapshots) {
|
|
155
|
+
this.snapshots.shift();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 回退到上一个状态(撤销操作)
|
|
160
|
+
*
|
|
161
|
+
* 移除并返回最近的一个快照
|
|
162
|
+
* 如果没有可用的快照,返回 undefined
|
|
163
|
+
*
|
|
164
|
+
* @returns 上一个状态快照,如果历史为空则返回 undefined
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* const previousState = history.pop();
|
|
169
|
+
* if (previousState) {
|
|
170
|
+
* // 恢复到上一个状态
|
|
171
|
+
* restoreState(previousState);
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
pop() {
|
|
176
|
+
return this.snapshots.pop();
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 查看最近的快照但不移除
|
|
180
|
+
*
|
|
181
|
+
* 返回最新的快照但不从历史记录中删除它
|
|
182
|
+
* 用于预览上一个状态而不实际执行撤销操作
|
|
183
|
+
*
|
|
184
|
+
* @returns 最近的状态快照,如果历史为空则返回 undefined
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* const lastState = history.peek();
|
|
189
|
+
* if (lastState) {
|
|
190
|
+
* console.log('上一个状态:', lastState);
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
peek() {
|
|
195
|
+
return this.snapshots[this.snapshots.length - 1];
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 清空所有历史记录
|
|
199
|
+
*
|
|
200
|
+
* 移除所有保存的快照,释放内存
|
|
201
|
+
* 通常在开始新游戏或重置时使用
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* history.clear(); // 清空所有历史
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
clear() {
|
|
209
|
+
this.snapshots = [];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 获取当前历史记录数量
|
|
213
|
+
*
|
|
214
|
+
* @returns 当前保存的快照数量
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* console.log(`历史记录数量: ${history.size}`);
|
|
219
|
+
* if (history.size > 0) {
|
|
220
|
+
* // 可以执行撤销操作
|
|
221
|
+
* }
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
get size() {
|
|
225
|
+
return this.snapshots.length;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// core/messages.ts
|
|
230
|
+
var SystemMessages = {
|
|
231
|
+
/** 时间回溯成功提示 */
|
|
232
|
+
UNDO_SUCCESS: "sys:undo_success"
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// core/utils.ts
|
|
236
|
+
function generateId() {
|
|
237
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// state/store.ts
|
|
241
|
+
var createGameEngineStore = (initialState, persistName = "generic-rpg-save") => {
|
|
242
|
+
const history = new HistoryManager(
|
|
243
|
+
DEFAULT_CONFIG.MAX_HISTORY_SNAPSHOTS
|
|
244
|
+
);
|
|
245
|
+
return create()(
|
|
246
|
+
persist(
|
|
247
|
+
(set, get) => ({
|
|
248
|
+
// 展开初始状态,包含所有游戏数据
|
|
249
|
+
...initialState,
|
|
250
|
+
// ========== 数值属性操作实现 ==========
|
|
251
|
+
/**
|
|
252
|
+
* 实现:更新单个数值属性
|
|
253
|
+
* 使用增量更新,保持其他属性不变
|
|
254
|
+
* 发射 STAT_CHANGE 事件通知监听器
|
|
255
|
+
*/
|
|
256
|
+
updateStat: (stat, delta) => set((state) => {
|
|
257
|
+
const currentVal = state.stats[stat] || 0;
|
|
258
|
+
const newVal = currentVal + delta;
|
|
259
|
+
gameEvents.emit(EngineEvents.STAT_CHANGE, {
|
|
260
|
+
stat,
|
|
261
|
+
delta,
|
|
262
|
+
current: newVal
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
stats: {
|
|
266
|
+
...state.stats,
|
|
267
|
+
[stat]: newVal
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}),
|
|
271
|
+
/**
|
|
272
|
+
* 实现:批量更新多个数值属性
|
|
273
|
+
* 遍历所有变化并应用到新的stats对象
|
|
274
|
+
*/
|
|
275
|
+
setStats: (changes) => set((state) => {
|
|
276
|
+
const newStats = { ...state.stats };
|
|
277
|
+
Object.entries(changes).forEach(([k, v]) => {
|
|
278
|
+
newStats[k] = (newStats[k] || 0) + v;
|
|
279
|
+
});
|
|
280
|
+
return { stats: newStats };
|
|
281
|
+
}),
|
|
282
|
+
// ========== 扩展数据操作实现 ==========
|
|
283
|
+
/**
|
|
284
|
+
* 实现:更新扩展数据
|
|
285
|
+
*
|
|
286
|
+
* 支持两种更新方式:
|
|
287
|
+
* 1. 直接传入部分更新对象
|
|
288
|
+
* 2. 传入更新函数,接收当前值返回部分更新
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```typescript
|
|
292
|
+
* // 方式1:直接更新
|
|
293
|
+
* setExtra({ reputation: 100 });
|
|
294
|
+
*
|
|
295
|
+
* // 方式2:基于当前值更新
|
|
296
|
+
* setExtra((prev) => ({ reputation: prev.reputation + 10 }));
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
setExtra: (updater) => set((state) => {
|
|
300
|
+
const update = typeof updater === "function" ? updater(state.extra) : updater;
|
|
301
|
+
return {
|
|
302
|
+
extra: { ...state.extra, ...update }
|
|
303
|
+
};
|
|
304
|
+
}),
|
|
305
|
+
// ========== 物品操作实现 ==========
|
|
306
|
+
/**
|
|
307
|
+
* 实现:添加物品到库存
|
|
308
|
+
* 创建新数组,保持不可变性
|
|
309
|
+
* 发射 ITEM_ADD 事件通知监听器
|
|
310
|
+
*/
|
|
311
|
+
addItem: (item) => set((state) => {
|
|
312
|
+
gameEvents.emit(EngineEvents.ITEM_ADD, { item });
|
|
313
|
+
return {
|
|
314
|
+
inventory: [...state.inventory, item]
|
|
315
|
+
};
|
|
316
|
+
}),
|
|
317
|
+
/**
|
|
318
|
+
* 实现:从库存移除物品
|
|
319
|
+
* 使用filter移除第一个匹配的物品
|
|
320
|
+
* 发射 ITEM_REMOVE 事件通知监听器
|
|
321
|
+
*/
|
|
322
|
+
removeItem: (item) => set((state) => {
|
|
323
|
+
gameEvents.emit(EngineEvents.ITEM_REMOVE, { item });
|
|
324
|
+
return {
|
|
325
|
+
inventory: state.inventory.filter((i) => i !== item)
|
|
326
|
+
};
|
|
327
|
+
}),
|
|
328
|
+
// ========== 标记操作实现 ==========
|
|
329
|
+
/**
|
|
330
|
+
* 实现:设置布尔标记
|
|
331
|
+
* 创建新的flags对象,更新指定标记
|
|
332
|
+
* 发射 FLAG_CHANGE 事件通知监听器
|
|
333
|
+
*/
|
|
334
|
+
setFlag: (flag, value) => set((state) => {
|
|
335
|
+
gameEvents.emit(EngineEvents.FLAG_CHANGE, { flag, value });
|
|
336
|
+
return {
|
|
337
|
+
flags: { ...state.flags, [flag]: value }
|
|
338
|
+
};
|
|
339
|
+
}),
|
|
340
|
+
// ========== 系统操作实现 ==========
|
|
341
|
+
/**
|
|
342
|
+
* 实现:添加日志条目
|
|
343
|
+
*
|
|
344
|
+
* 创建新日志并添加到日志数组开头。
|
|
345
|
+
* 使用 MAX_LOG_ENTRIES 限制日志数量,防止内存泄漏。
|
|
346
|
+
* 日志ID使用时间戳+随机数确保唯一性。
|
|
347
|
+
*
|
|
348
|
+
* ⚠️ 注意:日志会被持久化到 localStorage
|
|
349
|
+
*/
|
|
350
|
+
addLog: (text, type = "info") => set((state) => {
|
|
351
|
+
const newLog = {
|
|
352
|
+
id: generateId(),
|
|
353
|
+
text,
|
|
354
|
+
type,
|
|
355
|
+
timestamp: state.world.day
|
|
356
|
+
};
|
|
357
|
+
return {
|
|
358
|
+
logs: [newLog, ...state.logs].slice(0, DEFAULT_CONFIG.MAX_LOG_ENTRIES)
|
|
359
|
+
};
|
|
360
|
+
}),
|
|
361
|
+
/**
|
|
362
|
+
* 实现:显示飘字通知
|
|
363
|
+
*
|
|
364
|
+
* 发射 NOTIFICATION 事件,UI 层监听后显示飘字效果。
|
|
365
|
+
*
|
|
366
|
+
* ⚠️ 注意:通知不会被持久化,仅在当前会话中显示
|
|
367
|
+
*/
|
|
368
|
+
showToast: (text, type = "info") => {
|
|
369
|
+
const notification = {
|
|
370
|
+
text,
|
|
371
|
+
type,
|
|
372
|
+
notificationType: "toast",
|
|
373
|
+
timestamp: Date.now()
|
|
374
|
+
};
|
|
375
|
+
gameEvents.emit(EngineEvents.NOTIFICATION, notification);
|
|
376
|
+
},
|
|
377
|
+
/**
|
|
378
|
+
* 实现:显示弹窗通知
|
|
379
|
+
*
|
|
380
|
+
* 发射 NOTIFICATION 事件,UI 层监听后显示模态弹窗。
|
|
381
|
+
*
|
|
382
|
+
* ⚠️ 注意:通知不会被持久化,仅在当前会话中显示
|
|
383
|
+
*/
|
|
384
|
+
showModal: (text, type = "info") => {
|
|
385
|
+
const notification = {
|
|
386
|
+
text,
|
|
387
|
+
type,
|
|
388
|
+
notificationType: "modal",
|
|
389
|
+
timestamp: Date.now()
|
|
390
|
+
};
|
|
391
|
+
gameEvents.emit(EngineEvents.NOTIFICATION, notification);
|
|
392
|
+
},
|
|
393
|
+
/**
|
|
394
|
+
* 实现:推进游戏时间
|
|
395
|
+
* 增加world.day的值
|
|
396
|
+
*/
|
|
397
|
+
advanceTime: (amount = 1) => set((state) => ({
|
|
398
|
+
world: { ...state.world, day: state.world.day + amount }
|
|
399
|
+
})),
|
|
400
|
+
/**
|
|
401
|
+
* 实现:传送到指定地点
|
|
402
|
+
* 更新world.currentLocationId
|
|
403
|
+
*/
|
|
404
|
+
teleport: (locationId) => set((state) => ({
|
|
405
|
+
world: { ...state.world, currentLocationId: locationId }
|
|
406
|
+
})),
|
|
407
|
+
// ========== 存档操作实现 ==========
|
|
408
|
+
/**
|
|
409
|
+
* 实现:重置游戏状态
|
|
410
|
+
* 恢复到初始状态,清空日志
|
|
411
|
+
*/
|
|
412
|
+
reset: () => set({ ...initialState, logs: [] }),
|
|
413
|
+
// ========== 历史记录操作实现 ==========
|
|
414
|
+
/**
|
|
415
|
+
* 实现:保存当前状态快照
|
|
416
|
+
*
|
|
417
|
+
* 保存除日志外的所有状态数据到历史管理器。
|
|
418
|
+
* 日志通常不需要回退,因此被排除以节省内存。
|
|
419
|
+
*/
|
|
420
|
+
saveSnapshot: () => {
|
|
421
|
+
const currentState = get();
|
|
422
|
+
history.push(currentState);
|
|
423
|
+
},
|
|
424
|
+
/**
|
|
425
|
+
* 实现:撤销到上一个状态
|
|
426
|
+
*
|
|
427
|
+
* 从历史管理器中恢复上一个快照。
|
|
428
|
+
* 恢复时保留当前的日志,并添加系统提示。
|
|
429
|
+
*
|
|
430
|
+
* @returns 是否成功撤销
|
|
431
|
+
*/
|
|
432
|
+
undo: () => {
|
|
433
|
+
const prev = history.pop();
|
|
434
|
+
if (prev) {
|
|
435
|
+
set({
|
|
436
|
+
stats: prev.stats,
|
|
437
|
+
inventory: prev.inventory,
|
|
438
|
+
flags: prev.flags,
|
|
439
|
+
world: prev.world,
|
|
440
|
+
extra: prev.extra
|
|
441
|
+
// logs 保留当前的,不回退日志
|
|
442
|
+
});
|
|
443
|
+
get().addLog(SystemMessages.UNDO_SUCCESS, "info");
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}),
|
|
449
|
+
{
|
|
450
|
+
// 持久化配置
|
|
451
|
+
name: persistName,
|
|
452
|
+
// localStorage中的键名
|
|
453
|
+
storage: createJSONStorage(() => localStorage),
|
|
454
|
+
// 使用localStorage存储
|
|
455
|
+
/**
|
|
456
|
+
* 部分持久化 - 只保存数据字段,排除方法
|
|
457
|
+
*
|
|
458
|
+
* 这很重要,因为方法不能被序列化到JSON。
|
|
459
|
+
* 我们只持久化游戏状态数据,方法会在store重新创建时自动添加。
|
|
460
|
+
*/
|
|
461
|
+
partialize: (state) => ({
|
|
462
|
+
stats: state.stats,
|
|
463
|
+
inventory: state.inventory,
|
|
464
|
+
flags: state.flags,
|
|
465
|
+
world: state.world,
|
|
466
|
+
logs: state.logs,
|
|
467
|
+
extra: state.extra
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
)
|
|
471
|
+
);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
export { HistoryManager, createGameEngineStore };
|
|
475
|
+
//# sourceMappingURL=index.mjs.map
|
|
476
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../systems/events.ts","../../core/constants.ts","../../state/history.ts","../../core/messages.ts","../../core/utils.ts","../../state/store.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// src/engine/systems/events.ts\n\n/**\n * 类型安全的事件监听器回调函数\n * 使用 `unknown` 作为默认类型以强制类型安全\n * @template T - 监听器期望接收的数据类型\n */\ntype Listener<T = unknown> = (data: T) => void;\n\n/**\n * EventBus - 类型安全的发布-订阅事件系统\n *\n * 提供了一个集中式机制,让组件通过事件进行通信而无需紧密耦合。\n * 支持泛型类型参数以实现类型安全的载荷数据。\n *\n * @example\n * ```typescript\n * // 订阅事件并保证类型安全\n * const unsubscribe = eventBus.on<{ count: number }>('update', (data) => {\n * console.log(data.count); // TypeScript 知道 data 有 count 属性\n * });\n *\n * // 发射事件\n * eventBus.emit('update', { count: 42 });\n *\n * // 取消订阅\n * unsubscribe();\n * ```\n */\nexport class EventBus {\n /**\n * 事件监听器的内部存储\n * 将事件名映射到监听器回调函数数组\n * 使用 Listener[](默认为 Listener<unknown>[])以允许同一事件名有不同类型的监听器\n * 类型安全在订阅/发射时强制执行\n */\n private listeners: Map<string, Listener[]> = new Map();\n\n /**\n * 订阅事件\n *\n * @template T - 事件载荷的预期类型\n * @param event - 要订阅的事件名称\n * @param callback - 事件触发时调用的监听器函数\n * @returns 取消订阅的函数\n *\n * @example\n * ```typescript\n * const handler = (data: { count: number }) => console.log(data.count);\n * const unsubscribe = eventBus.on('update', handler);\n * // 稍后取消订阅\n * unsubscribe();\n * ```\n */\n on<T>(event: string, callback: Listener<T>): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, []);\n }\n this.listeners.get(event)!.push(callback as Listener);\n\n // 返回 unsubscribe 函数\n return () => this.off(event, callback);\n }\n\n /**\n * 取消订阅事件\n *\n * @template T - 事件载荷的预期类型\n * @param event - 要取消订阅的事件名称\n * @param callback - 要移除的监听器函数\n *\n * @example\n * ```typescript\n * const handler = (data: number) => console.log(data);\n * eventBus.on('count', handler);\n * eventBus.off('count', handler); // 移除监听器\n * ```\n */\n off<T>(event: string, callback: Listener<T>) {\n const callbacks = this.listeners.get(event);\n if (callbacks) {\n this.listeners.set(\n event,\n callbacks.filter((cb) => cb !== (callback as Listener))\n );\n }\n }\n\n /**\n * 发射事件并携带可选的类型化载荷数据\n * 该事件的所有已注册监听器都将被调用并传入数据\n * 单个监听器中的错误会被捕获并记录,不会影响其他监听器\n *\n * @template T - 事件载荷的类型\n * @param event - 要发射的事件名称\n * @param data - 传递给监听器的可选载荷数据\n *\n * @example\n * ```typescript\n * eventBus.emit<{ userId: string }>('login', { userId: '123' });\n * eventBus.emit('logout'); // 无载荷\n * ```\n */\n emit<T>(event: string, data?: T) {\n const callbacks = this.listeners.get(event);\n if (callbacks) {\n callbacks.forEach((cb) => {\n try {\n cb(data);\n } catch (error) {\n console.error(`Error in event listener for \"${event}\":`, error);\n }\n });\n }\n }\n\n /**\n * 清空所有事件的所有监听器\n * 用于清理或重置事件系统\n *\n * @example\n * ```typescript\n * eventBus.clear(); // 所有监听器被移除\n * ```\n */\n clear() {\n this.listeners.clear();\n }\n}\n\n/**\n * 全局单例 EventBus 实例,用于游戏范围内的事件通信\n * 在整个应用中使用此实例以保持一致的事件处理\n *\n * @example\n * ```typescript\n * import { gameEvents, EngineEvents } from './events';\n *\n * gameEvents.on(EngineEvents.STAT_CHANGE, (data) => {\n * console.log(`属性 ${data.stat} 变化了 ${data.delta}`);\n * });\n * ```\n */\nexport const gameEvents = new EventBus();\n\n/**\n * 预定义的系统事件名称常量,用于常见的游戏引擎事件\n * 使用这些常量而不是字符串字面量,以保证类型安全和一致性\n *\n * 事件载荷类型:\n * - STAT_CHANGE: { stat: string, delta: number, current: number }\n * - ITEM_ADD: { item: string }\n * - ITEM_REMOVE: { item: string }\n * - FLAG_CHANGE: { flag: string, value: boolean }\n * - ACTION_EXECUTED: { actionId: string }\n * - TIME_PASS: { day: number, time: number }\n * - NOTIFICATION: { text: string, type: string, notificationType: string, timestamp: number }\n * - CUSTOM: { id: string } (对应 effects.triggerEvent)\n */\nexport const EngineEvents = {\n /** 当角色属性变化时触发 */\n STAT_CHANGE: \"engine:stat_change\",\n /** 当物品添加到背包时触发 */\n ITEM_ADD: \"engine:item_add\",\n /** 当物品从背包移除时触发 */\n ITEM_REMOVE: \"engine:item_remove\",\n /** 当游戏标志变化时触发 */\n FLAG_CHANGE: \"engine:flag_change\",\n /** 当动作执行时触发 */\n ACTION_EXECUTED: \"engine:action_exec\",\n /** 当游戏时间推进时触发 */\n TIME_PASS: \"engine:time_pass\",\n /** 当需要显示瞬时通知时触发(Toast/Modal,不持久化) */\n NOTIFICATION: \"engine:notification\",\n /** 用于自定义触发事件 */\n CUSTOM: \"engine:custom_trigger\",\n} as const;\n","// src/engine/core/constants.ts\n/**\n * 全局常量定义\n */\n\n/**\n * 引擎版本\n */\nexport const ENGINE_VERSION = '1.0.0';\n\n/**\n * 默认配置\n */\nexport const DEFAULT_CONFIG = {\n /** 默认存档键名 */\n DEFAULT_SAVE_KEY: 'game-save',\n /** 最大日志条数 */\n MAX_LOG_ENTRIES: 50,\n /** 最大历史快照数 */\n MAX_HISTORY_SNAPSHOTS: 20,\n /** 飘字显示时长(毫秒) */\n TOAST_DURATION: 3000,\n} as const;\n\n/**\n * 日志类型颜色映射\n */\nexport const LOG_TYPE_COLORS = {\n info: '#3b82f6',\n success: '#10b981',\n warn: '#f59e0b',\n error: '#ef4444',\n result: '#6366f1',\n} as const;\n\n/**\n * 默认属性值\n */\nexport const DEFAULT_STATS = {\n /** 默认生命值 */\n DEFAULT_HP: 100,\n /** 默认金币 */\n DEFAULT_GOLD: 0,\n} as const;\n\n/**\n * 游戏时间常量\n */\nexport const TIME_CONSTANTS = {\n /** 一天的小时数 */\n HOURS_PER_DAY: 24,\n /** 早晨开始时间 */\n MORNING_START: 6,\n /** 中午开始时间 */\n NOON_START: 12,\n /** 傍晚开始时间 */\n EVENING_START: 18,\n /** 夜晚开始时间 */\n NIGHT_START: 22,\n} as const;\n\n/**\n * UI 常量\n */\nexport const UI_CONSTANTS = {\n /** 侧边栏宽度 */\n SIDEBAR_WIDTH: 288, // 72 * 4 = 288px (w-72)\n /** 日志栏宽度 */\n LOG_WIDTH: 320, // 80 * 4 = 320px (w-80)\n /** Toast 容器 z-index */\n TOAST_Z_INDEX: 1000,\n /** Modal 容器 z-index */\n MODAL_Z_INDEX: 2000,\n} as const;\n\n/**\n * 验证常量\n */\nexport const VALIDATION = {\n /** 最小属性值 */\n MIN_STAT_VALUE: 0,\n /** 最大属性值 */\n MAX_STAT_VALUE: 9999,\n /** 最大背包容量 */\n MAX_INVENTORY_SIZE: 100,\n /** 最小动作 ID 长度 */\n MIN_ACTION_ID_LENGTH: 1,\n /** 最大动作 ID 长度 */\n MAX_ACTION_ID_LENGTH: 50,\n} as const;","// src/engine/state/history.ts\nimport type { GameState } from \"../core/types\";\nimport { DEFAULT_CONFIG } from \"../core/constants\";\n\n/**\n * 历史管理器 - 管理游戏状态的历史快照\n * \n * 提供撤销/重做功能,通过保存游戏状态的快照来实现时间旅行\n * \n * @template S - 属性名称的字符串字面量类型\n * @template I - 物品名称的字符串字面量类型\n * @template F - 标志名称的字符串字面量类型\n * @template X - 扩展数据的类型\n * \n * @example\n * ```typescript\n * const history = new HistoryManager<'health' | 'mana', 'sword', 'hasKey'>(20);\n * history.push(currentState);\n * const previousState = history.pop(); // 撤销到上一个状态\n * ```\n */\nexport class HistoryManager<\n S extends string,\n I extends string,\n F extends string,\n X = Record<string, unknown>\n> {\n /** 存储的状态快照数组 */\n private snapshots: GameState<S, I, F, X>[] = [];\n /** 最大快照数量限制 */\n private maxSnapshots: number;\n\n /**\n * 创建历史管理器实例\n * @param maxSnapshots - 最大保存的快照数量,默认使用配置中的值\n */\n constructor(maxSnapshots = DEFAULT_CONFIG.MAX_HISTORY_SNAPSHOTS) {\n this.maxSnapshots = maxSnapshots;\n }\n\n /**\n * 保存当前状态快照\n * \n * 通过深拷贝保存状态,防止后续修改影响历史记录\n * 当快照数量超过最大限制时,自动移除最旧的快照\n * \n * @param state - 要保存的游戏状态\n * \n * @example\n * ```typescript\n * history.push(gameState);\n * ```\n */\n push(state: GameState<S, I, F, X>) {\n // 深拷贝状态,防止引用污染\n const snapshot = JSON.parse(JSON.stringify(state));\n this.snapshots.push(snapshot);\n\n // 限制历史记录长度\n if (this.snapshots.length > this.maxSnapshots) {\n this.snapshots.shift();\n }\n }\n\n /**\n * 回退到上一个状态(撤销操作)\n * \n * 移除并返回最近的一个快照\n * 如果没有可用的快照,返回 undefined\n * \n * @returns 上一个状态快照,如果历史为空则返回 undefined\n * \n * @example\n * ```typescript\n * const previousState = history.pop();\n * if (previousState) {\n * // 恢复到上一个状态\n * restoreState(previousState);\n * }\n * ```\n */\n pop(): GameState<S, I, F, X> | undefined {\n return this.snapshots.pop();\n }\n\n /**\n * 查看最近的快照但不移除\n * \n * 返回最新的快照但不从历史记录中删除它\n * 用于预览上一个状态而不实际执行撤销操作\n * \n * @returns 最近的状态快照,如果历史为空则返回 undefined\n * \n * @example\n * ```typescript\n * const lastState = history.peek();\n * if (lastState) {\n * console.log('上一个状态:', lastState);\n * }\n * ```\n */\n peek(): GameState<S, I, F, X> | undefined {\n return this.snapshots[this.snapshots.length - 1];\n }\n\n /**\n * 清空所有历史记录\n * \n * 移除所有保存的快照,释放内存\n * 通常在开始新游戏或重置时使用\n * \n * @example\n * ```typescript\n * history.clear(); // 清空所有历史\n * ```\n */\n clear() {\n this.snapshots = [];\n }\n\n /**\n * 获取当前历史记录数量\n * \n * @returns 当前保存的快照数量\n * \n * @example\n * ```typescript\n * console.log(`历史记录数量: ${history.size}`);\n * if (history.size > 0) {\n * // 可以执行撤销操作\n * }\n * ```\n */\n get size() {\n return this.snapshots.length;\n }\n}\n","// src/engine/core/messages.ts\n\n/**\n * 系统消息常量\n * \n * 使用特殊的标识符(sys: 前缀),让 UI 层知道这需要翻译。\n * 这些常量用于系统级别的提示消息,应该在 UI 层进行本地化处理。\n */\nexport const SystemMessages = {\n /** 资源不足提示 */\n NOT_ENOUGH_RESOURCES: 'sys:not_enough_resources',\n /** 条件不满足提示 */\n REQUIREMENT_NOT_MET: 'sys:requirement_not_met',\n /** 操作成功提示 */\n ACTION_SUCCESS: 'sys:action_success',\n /** 时间回溯成功提示 */\n UNDO_SUCCESS: 'sys:undo_success',\n} as const;","// src/engine/core/utils.ts\n/**\n * 工具函数集合\n */\n\nimport type { GameState } from './types';\nimport { TIME_CONSTANTS } from './constants';\n\n/**\n * 生成唯一 ID\n * \n * @returns 唯一标识符字符串\n * \n * @example\n * ```typescript\n * const id = generateId(); // \"1234567890-abc123\"\n * ```\n */\nexport function generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;\n}\n\n/**\n * 深拷贝对象\n * \n * 使用 JSON 序列化实现深拷贝,注意会丢失函数、Symbol 等特殊类型\n * \n * @param obj - 要拷贝的对象\n * @returns 深拷贝后的对象\n * \n * @example\n * ```typescript\n * const copy = deepClone(originalObject);\n * ```\n */\nexport function deepClone<T>(obj: T): T {\n return JSON.parse(JSON.stringify(obj));\n}\n\n/**\n * 限制数值在指定范围内\n * \n * @param value - 要限制的值\n * @param min - 最小值\n * @param max - 最大值\n * @returns 限制后的值\n * \n * @example\n * ```typescript\n * clamp(150, 0, 100); // 100\n * clamp(-10, 0, 100); // 0\n * clamp(50, 0, 100); // 50\n * ```\n */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/**\n * 格式化数字,添加千位分隔符\n * \n * @param num - 要格式化的数字\n * @returns 格式化后的字符串\n * \n * @example\n * ```typescript\n * formatNumber(1234567); // \"1,234,567\"\n * ```\n */\nexport function formatNumber(num: number): string {\n return num.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n}\n\n/**\n * 获取时间段描述\n * \n * @param hour - 小时数 (0-23)\n * @returns 时间段描述\n * \n * @example\n * ```typescript\n * getTimeOfDay(8); // \"早晨\"\n * getTimeOfDay(14); // \"下午\"\n * getTimeOfDay(20); // \"傍晚\"\n * ```\n */\nexport function getTimeOfDay(hour: number): string {\n if (hour >= TIME_CONSTANTS.MORNING_START && hour < TIME_CONSTANTS.NOON_START) {\n return '早晨';\n } else if (hour >= TIME_CONSTANTS.NOON_START && hour < TIME_CONSTANTS.EVENING_START) {\n return '下午';\n } else if (hour >= TIME_CONSTANTS.EVENING_START && hour < TIME_CONSTANTS.NIGHT_START) {\n return '傍晚';\n } else {\n return '夜晚';\n }\n}\n\n/**\n * 计算百分比\n * \n * @param current - 当前值\n * @param max - 最大值\n * @returns 百分比 (0-100)\n * \n * @example\n * ```typescript\n * getPercentage(75, 100); // 75\n * getPercentage(0, 100); // 0\n * ```\n */\nexport function getPercentage(current: number, max: number): number {\n if (max === 0) return 0;\n return Math.round((current / max) * 100);\n}\n\n/**\n * 随机整数\n * \n * @param min - 最小值(包含)\n * @param max - 最大值(包含)\n * @returns 随机整数\n * \n * @example\n * ```typescript\n * randomInt(1, 6); // 1-6 之间的随机整数(模拟骰子)\n * ```\n */\nexport function randomInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n/**\n * 从数组中随机选择一个元素\n * \n * @param array - 数组\n * @returns 随机选择的元素,如果数组为空则返回 undefined\n * \n * @example\n * ```typescript\n * randomChoice(['剑', '盾', '药水']); // 随机返回一个\n * ```\n */\nexport function randomChoice<T>(array: T[]): T | undefined {\n if (array.length === 0) return undefined;\n return array[Math.floor(Math.random() * array.length)];\n}\n\n/**\n * 打乱数组顺序(Fisher-Yates 算法)\n * \n * @param array - 要打乱的数组\n * @returns 打乱后的新数组\n * \n * @example\n * ```typescript\n * shuffle([1, 2, 3, 4, 5]); // [3, 1, 5, 2, 4]\n * ```\n */\nexport function shuffle<T>(array: T[]): T[] {\n const result = [...array];\n for (let i = result.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [result[i], result[j]] = [result[j], result[i]];\n }\n return result;\n}\n\n/**\n * 延迟执行\n * \n * @param ms - 延迟毫秒数\n * @returns Promise\n * \n * @example\n * ```typescript\n * await delay(1000); // 延迟 1 秒\n * ```\n */\nexport function delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * 检查对象是否为空\n * \n * @param obj - 要检查的对象\n * @returns 是否为空\n * \n * @example\n * ```typescript\n * isEmpty({}); // true\n * isEmpty({ a: 1 }); // false\n * ```\n */\nexport function isEmpty(obj: object): boolean {\n return Object.keys(obj).length === 0;\n}\n\n/**\n * 安全地获取嵌套属性\n * \n * @param obj - 对象\n * @param path - 属性路径,用点分隔\n * @param defaultValue - 默认值\n * @returns 属性值或默认值\n * \n * @example\n * ```typescript\n * const obj = { a: { b: { c: 123 } } };\n * get(obj, 'a.b.c', 0); // 123\n * get(obj, 'a.x.y', 0); // 0\n * ```\n */\nexport function get<T>(obj: unknown, path: string, defaultValue?: T): T {\n const keys = path.split('.');\n let result: unknown = obj;\n\n for (const key of keys) {\n if (result && typeof result === 'object' && key in result) {\n result = (result as Record<string, unknown>)[key];\n } else {\n return defaultValue as T;\n }\n }\n\n return result as T;\n}\n\n/**\n * 防抖函数\n * \n * @param fn - 要防抖的函数\n * @param wait - 等待时间(毫秒)\n * @returns 防抖后的函数\n * \n * @example\n * ```typescript\n * const debouncedSave = debounce(() => saveGame(), 1000);\n * debouncedSave(); // 1秒内多次调用只执行最后一次\n * ```\n */\nexport function debounce<T extends (...args: never[]) => unknown>(\n fn: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n\n return function (this: unknown, ...args: Parameters<T>) {\n if (timeout) clearTimeout(timeout);\n timeout = setTimeout(() => fn.apply(this, args), wait);\n };\n}\n\n/**\n * 节流函数\n * \n * @param fn - 要节流的函数\n * @param wait - 等待时间(毫秒)\n * @returns 节流后的函数\n * \n * @example\n * ```typescript\n * const throttledUpdate = throttle(() => updateUI(), 100);\n * throttledUpdate(); // 100ms 内只执行一次\n * ```\n */\nexport function throttle<T extends (...args: never[]) => unknown>(\n fn: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let lastTime = 0;\n\n return function (this: unknown, ...args: Parameters<T>) {\n const now = Date.now();\n if (now - lastTime >= wait) {\n lastTime = now;\n fn.apply(this, args);\n }\n };\n}\n\n/**\n * 格式化游戏时间\n * \n * @param day - 天数\n * @param time - 小时数\n * @returns 格式化的时间字符串\n * \n * @example\n * ```typescript\n * formatGameTime(5, 14); // \"第 5 天 下午 2:00\"\n * ```\n */\nexport function formatGameTime(day: number, time: number): string {\n const hour = time % TIME_CONSTANTS.HOURS_PER_DAY;\n const period = getTimeOfDay(hour);\n const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;\n return `第 ${day} 天 ${period} ${displayHour}:00`;\n}\n\n/**\n * 计算两个状态之间的差异\n * \n * @param oldState - 旧状态\n * @param newState - 新状态\n * @returns 差异对象\n * \n * @example\n * ```typescript\n * const diff = getStateDiff(oldState, newState);\n * console.log(diff.stats); // { hp: -10, gold: 5 }\n * ```\n */\nexport function getStateDiff<S extends string, I extends string, F extends string, X>(\n oldState: GameState<S, I, F, X>,\n newState: GameState<S, I, F, X>\n): {\n stats: Partial<Record<S, number>>;\n itemsAdded: I[];\n itemsRemoved: I[];\n flagsChanged: Partial<Record<F, boolean>>;\n} {\n const diff = {\n stats: {} as Partial<Record<S, number>>,\n itemsAdded: [] as I[],\n itemsRemoved: [] as I[],\n flagsChanged: {} as Partial<Record<F, boolean>>,\n };\n\n // 计算属性差异\n for (const key in newState.stats) {\n const oldValue = oldState.stats[key] || 0;\n const newValue = newState.stats[key] || 0;\n if (oldValue !== newValue) {\n diff.stats[key] = newValue - oldValue;\n }\n }\n\n // 计算物品差异\n const oldItems = new Set(oldState.inventory);\n const newItems = new Set(newState.inventory);\n\n for (const item of newState.inventory) {\n if (!oldItems.has(item)) {\n diff.itemsAdded.push(item);\n }\n }\n\n for (const item of oldState.inventory) {\n if (!newItems.has(item)) {\n diff.itemsRemoved.push(item);\n }\n }\n\n // 计算标记差异\n for (const key in newState.flags) {\n if (oldState.flags[key] !== newState.flags[key]) {\n diff.flagsChanged[key] = newState.flags[key];\n }\n }\n\n return diff;\n}","// src/engine/state/store.ts\n// ============================================================================\n// 游戏状态管理 - 使用Zustand实现全局状态管理和持久化\n// ============================================================================\n//\n// 这个文件提供了游戏引擎的状态管理解决方案,基于Zustand库实现。\n// 主要功能:\n// - 全局状态管理:统一管理游戏的所有状态数据\n// - 状态持久化:自动保存到localStorage,支持游戏存档\n// - 类型安全:完全的TypeScript类型支持\n// - 原子操作:提供细粒度的状态更新方法\n//\n// ============================================================================\n\nimport { create } from 'zustand';\nimport { persist, createJSONStorage } from 'zustand/middleware';\nimport type { GameState, LogEntry, NotificationPayload } from '../core/types';\nimport { gameEvents, EngineEvents } from '../systems/events';\nimport { HistoryManager } from './history';\nimport { SystemMessages } from '../core/messages';\nimport { DEFAULT_CONFIG } from '../core/constants';\nimport { generateId } from '../core/utils';\n\n/**\n * 游戏Store操作方法接口 - 定义所有可用的状态操作\n * \n * 这个接口定义了修改游戏状态的所有方法。所有方法都是原子操作,\n * 确保状态更新的一致性和可预测性。\n * \n * @template S - 数值属性键的联合类型\n * @template I - 物品ID的联合类型\n * @template F - 标记键的联合类型\n * \n * @example\n * ```typescript\n * const useStore = createGameEngineStore(initialState);\n * \n * // 在组件中使用\n * function GameComponent() {\n * const updateStat = useStore(state => state.updateStat);\n * const addItem = useStore(state => state.addItem);\n * \n * const handleAction = () => {\n * updateStat('hp', -10); // 减少10点生命值\n * addItem('potion'); // 获得药水\n * };\n * }\n * ```\n */\nexport interface GameStoreActions<\n S extends string,\n I extends string,\n F extends string,\n X = Record<string, unknown>\n> {\n // ========== 数值属性操作 ==========\n \n /**\n * 更新单个数值属性\n * \n * 对指定属性进行增量更新(正数增加,负数减少)。\n * 这是最常用的属性修改方法。\n * \n * @param stat - 要更新的属性键\n * @param delta - 变化量(正数增加,负数减少)\n * \n * @example\n * ```typescript\n * updateStat('hp', 20); // 恢复20点生命值\n * updateStat('gold', -50); // 花费50金币\n * ```\n */\n updateStat: (stat: S, delta: number) => void;\n\n /**\n * 批量更新多个数值属性\n * \n * 一次性更新多个属性,所有更新在同一个状态变更中完成。\n * 适用于需要同时修改多个属性的场景。\n * \n * @param stats - 属性变化的部分记录\n * \n * @example\n * ```typescript\n * setStats({ hp: 10, mp: -5, gold: 100 });\n * // 同时增加10点生命值,减少5点魔法值,增加100金币\n * ```\n */\n setStats: (stats: Partial<Record<S, number>>) => void;\n\n setExtra: (updater: Partial<X> | ((prev: X) => Partial<X>)) => void;\n\n // ========== 物品操作 ==========\n\n /**\n * 添加物品到库存\n * \n * 将指定物品添加到玩家的库存中。\n * 同一物品可以多次添加(如多个药水)。\n * \n * @param item - 要添加的物品ID\n * \n * @example\n * ```typescript\n * addItem('health_potion');\n * addItem('health_potion'); // 可以添加多个相同物品\n * ```\n */\n addItem: (item: I) => void;\n\n /**\n * 从库存移除物品\n * \n * 从玩家库存中移除指定物品的一个实例。\n * 如果有多个相同物品,只移除第一个匹配的。\n * \n * @param item - 要移除的物品ID\n * \n * @example\n * ```typescript\n * removeItem('health_potion'); // 移除一个生命药水\n * ```\n */\n removeItem: (item: I) => void;\n\n // ========== 标记操作 ==========\n\n /**\n * 设置布尔标记\n * \n * 设置或清除游戏标记,用于追踪任务进度、解锁状态等。\n * 标记是游戏逻辑的重要组成部分。\n * \n * @param flag - 标记键\n * @param value - 标记值(true或false)\n * \n * @example\n * ```typescript\n * setFlag('quest_completed', true); // 标记任务完成\n * setFlag('door_locked', false); // 解锁门\n * ```\n */\n setFlag: (flag: F, value: boolean) => void;\n\n // ========== 系统操作 ==========\n\n /**\n * 添加日志条目\n * \n * ⚠️ 重要:日志是持久化的历史记录\n * \n * 向游戏日志中添加新的消息,用于记录游戏事件。\n * 日志会:\n * - 被保存到 localStorage\n * - 在页面刷新后重新加载\n * - 永久显示在日志面板中\n * - 自动限制在最近50条\n * \n * @param text - 日志文本内容\n * @param type - 日志类型(默认为'info')\n * \n * @example\n * ```typescript\n * addLog('你进入了森林', 'info');\n * addLog('击败了哥布林', 'result');\n * addLog('获得了传说武器', 'success');\n * ```\n */\n addLog: (text: string, type?: LogEntry['type']) => void;\n\n /**\n * 显示飘字通知\n * \n * ⚠️ 重要:飘字是瞬时通知,不会被持久化\n * \n * 显示一个轻量级的飘字提示,会自动消失。\n * 飘字通知:\n * - 不会被保存到 localStorage\n * - 不会在页面刷新后重新出现\n * - 仅在当前会话中显示\n * - 适用于轻量级反馈\n * \n * @param text - 通知文本\n * @param type - 通知类型(默认为'info')\n * \n * @example\n * ```typescript\n * showToast('获得 10 金币', 'success');\n * showToast('体力不足', 'warn');\n * ```\n */\n showToast: (text: string, type?: LogEntry['type']) => void;\n\n /**\n * 显示弹窗通知\n * \n * ⚠️ 重要:弹窗是瞬时通知,不会被持久化\n * \n * 显示一个模态弹窗,需要用户确认才能关闭。\n * 弹窗通知:\n * - 不会被保存到 localStorage\n * - 不会在页面刷新后重新出现\n * - 仅在当前会话中显示\n * - 适用于重要信息和阻塞式交互\n * \n * @param text - 通知文本\n * @param type - 通知类型(默认为'info')\n * \n * @example\n * ```typescript\n * showModal('任务完成!获得传说装备', 'success');\n * showModal('警告:此操作不可撤销', 'warn');\n * ```\n */\n showModal: (text: string, type?: LogEntry['type']) => void;\n\n /**\n * 推进游戏时间\n * \n * 增加游戏内的天数,用于时间相关的游戏机制。\n * \n * @param amount - 要增加的天数(默认为1)\n * \n * @example\n * ```typescript\n * advanceTime(); // 推进1天\n * advanceTime(7); // 推进7天\n * ```\n */\n advanceTime: (amount?: number) => void;\n\n /**\n * 传送到指定地点\n * \n * 将玩家传送到指定的地点ID。\n * 这是改变玩家位置的主要方法。\n * \n * @param locationId - 目标地点的ID\n * \n * @example\n * ```typescript\n * teleport('town_square'); // 传送到城镇广场\n * teleport('dungeon_1'); // 传送到地牢\n * ```\n */\n teleport: (locationId: string) => void;\n\n // ========== 存档操作 ==========\n\n /**\n * 重置游戏状态\n * \n * 将游戏状态重置为初始状态,清空所有日志。\n * 用于开始新游戏或重置当前进度。\n * \n * @example\n * ```typescript\n * reset(); // 开始新游戏\n * ```\n */\n reset: () => void;\n\n // ========== 历史记录操作 ==========\n\n /**\n * 保存当前状态快照\n * \n * 将当前游戏状态保存到历史记录中,用于后续的撤销操作。\n * 快照会自动排除日志数据以节省内存。\n * \n * @example\n * ```typescript\n * saveSnapshot(); // 在重要操作前保存快照\n * ```\n */\n saveSnapshot: () => void;\n\n /**\n * 撤销到上一个状态\n * \n * 恢复到上一个保存的快照状态。\n * 如果没有可用的历史记录,操作失败并返回false。\n * \n * @returns 是否成功撤销\n * \n * @example\n * ```typescript\n * if (undo()) {\n * console.log('撤销成功');\n * } else {\n * console.log('没有可撤销的历史记录');\n * }\n * ```\n */\n undo: () => boolean;\n}\n\n/**\n * 游戏Store类型 - 组合数据和操作方法\n * \n * 这是完整的Store类型,包含游戏状态数据和所有操作方法。\n * 使用交叉类型将GameState和GameStoreActions合并。\n * \n * @template S - 数值属性键的联合类型\n * @template I - 物品ID的联合类型\n * @template F - 标记键的联合类型\n * \n * @example\n * ```typescript\n * const useGameStore: GameStore<'hp' | 'mp', 'sword', 'quest_done'>;\n * ```\n */\nexport type GameStore<\n S extends string,\n I extends string,\n F extends string,\n X = Record<string, unknown>\n> = GameState<S, I, F, X> & GameStoreActions<S, I, F, X>;\n\n/**\n * 创建游戏引擎Store的工厂函数\n * \n * 这是创建游戏状态管理Store的核心函数。它使用Zustand创建一个\n * 带有持久化功能的状态管理器,自动将游戏状态保存到localStorage。\n * \n * 工厂模式的优势:\n * - 可以创建多个独立的游戏实例\n * - 每个实例有自己的初始状态和存档名称\n * - 完全的类型安全和泛型支持\n * \n * @template S - 数值属性键的联合类型\n * @template I - 物品ID的联合类型\n * @template F - 标记键的联合类型\n * \n * @param initialState - 游戏的初始状态\n * @param persistName - localStorage中的存档键名(默认:'generic-rpg-save')\n * \n * @returns Zustand store hook,可在React组件中使用\n * \n * @example\n * ```typescript\n * // 定义游戏类型\n * type Stats = 'hp' | 'mp' | 'gold';\n * type Items = 'sword' | 'potion';\n * type Flags = 'quest_completed';\n * \n * // 创建初始状态\n * const initialState: GameState<Stats, Items, Flags> = {\n * stats: { hp: 100, mp: 50, gold: 0 },\n * inventory: [],\n * flags: { quest_completed: false },\n * world: { currentLocationId: 'start', day: 1, time: 0 },\n * logs: []\n * };\n * \n * // 创建store\n * export const useGameStore = createGameEngineStore(\n * initialState,\n * 'my-rpg-save'\n * );\n * \n * // 在组件中使用\n * function GameUI() {\n * const hp = useGameStore(state => state.stats.hp);\n * const updateStat = useGameStore(state => state.updateStat);\n * \n * return <button onClick={() => updateStat('hp', -10)}>受伤</button>;\n * }\n * ```\n */\nexport const createGameEngineStore = <\n S extends string,\n I extends string,\n F extends string,\n X = Record<string, unknown>\n>(\n initialState: GameState<S, I, F, X>,\n persistName: string = 'generic-rpg-save'\n) => {\n // 创建历史管理器实例(闭包内,每个store独立)\n const history = new HistoryManager<S, I, F, X>(\n DEFAULT_CONFIG.MAX_HISTORY_SNAPSHOTS\n );\n\n return create<GameStore<S, I, F, X>>()(\n persist(\n (set, get) => ({\n // 展开初始状态,包含所有游戏数据\n ...initialState,\n\n // ========== 数值属性操作实现 ==========\n\n /**\n * 实现:更新单个数值属性\n * 使用增量更新,保持其他属性不变\n * 发射 STAT_CHANGE 事件通知监听器\n */\n updateStat: (stat, delta) =>\n set((state) => {\n const currentVal = state.stats[stat] || 0;\n const newVal = currentVal + delta;\n\n // 发射事件\n gameEvents.emit(EngineEvents.STAT_CHANGE, {\n stat,\n delta,\n current: newVal,\n });\n\n return {\n stats: {\n ...state.stats,\n [stat]: newVal,\n },\n };\n }),\n\n /**\n * 实现:批量更新多个数值属性\n * 遍历所有变化并应用到新的stats对象\n */\n setStats: (changes) =>\n set((state) => {\n const newStats = { ...state.stats };\n Object.entries(changes).forEach(([k, v]) => {\n newStats[k as S] = (newStats[k as S] || 0) + (v as number);\n });\n return { stats: newStats };\n }),\n\n // ========== 扩展数据操作实现 ==========\n\n /**\n * 实现:更新扩展数据\n * \n * 支持两种更新方式:\n * 1. 直接传入部分更新对象\n * 2. 传入更新函数,接收当前值返回部分更新\n * \n * @example\n * ```typescript\n * // 方式1:直接更新\n * setExtra({ reputation: 100 });\n * \n * // 方式2:基于当前值更新\n * setExtra((prev) => ({ reputation: prev.reputation + 10 }));\n * ```\n */\n setExtra: (updater) =>\n set((state) => {\n const update =\n typeof updater === 'function' ? updater(state.extra) : updater;\n return {\n extra: { ...state.extra, ...update } as X,\n };\n }),\n\n // ========== 物品操作实现 ==========\n\n /**\n * 实现:添加物品到库存\n * 创建新数组,保持不可变性\n * 发射 ITEM_ADD 事件通知监听器\n */\n addItem: (item) =>\n set((state) => {\n // 发射事件\n gameEvents.emit(EngineEvents.ITEM_ADD, { item });\n\n return {\n inventory: [...state.inventory, item],\n };\n }),\n\n /**\n * 实现:从库存移除物品\n * 使用filter移除第一个匹配的物品\n * 发射 ITEM_REMOVE 事件通知监听器\n */\n removeItem: (item) =>\n set((state) => {\n // 发射事件\n gameEvents.emit(EngineEvents.ITEM_REMOVE, { item });\n\n return {\n inventory: state.inventory.filter((i) => i !== item),\n };\n }),\n\n // ========== 标记操作实现 ==========\n\n /**\n * 实现:设置布尔标记\n * 创建新的flags对象,更新指定标记\n * 发射 FLAG_CHANGE 事件通知监听器\n */\n setFlag: (flag, value) =>\n set((state) => {\n // 发射事件\n gameEvents.emit(EngineEvents.FLAG_CHANGE, { flag, value });\n\n return {\n flags: { ...state.flags, [flag]: value },\n };\n }),\n\n // ========== 系统操作实现 ==========\n\n /**\n * 实现:添加日志条目\n * \n * 创建新日志并添加到日志数组开头。\n * 使用 MAX_LOG_ENTRIES 限制日志数量,防止内存泄漏。\n * 日志ID使用时间戳+随机数确保唯一性。\n * \n * ⚠️ 注意:日志会被持久化到 localStorage\n */\n addLog: (text, type = 'info') =>\n set((state) => {\n const newLog: LogEntry = {\n id: generateId(),\n text,\n type,\n timestamp: state.world.day,\n };\n\n return { \n logs: [newLog, ...state.logs].slice(0, DEFAULT_CONFIG.MAX_LOG_ENTRIES) \n };\n }),\n\n /**\n * 实现:显示飘字通知\n * \n * 发射 NOTIFICATION 事件,UI 层监听后显示飘字效果。\n * \n * ⚠️ 注意:通知不会被持久化,仅在当前会话中显示\n */\n showToast: (text, type = 'info') => {\n const notification: NotificationPayload = {\n text,\n type,\n notificationType: 'toast',\n timestamp: Date.now(),\n };\n gameEvents.emit(EngineEvents.NOTIFICATION, notification);\n },\n\n /**\n * 实现:显示弹窗通知\n * \n * 发射 NOTIFICATION 事件,UI 层监听后显示模态弹窗。\n * \n * ⚠️ 注意:通知不会被持久化,仅在当前会话中显示\n */\n showModal: (text, type = 'info') => {\n const notification: NotificationPayload = {\n text,\n type,\n notificationType: 'modal',\n timestamp: Date.now(),\n };\n gameEvents.emit(EngineEvents.NOTIFICATION, notification);\n },\n\n /**\n * 实现:推进游戏时间\n * 增加world.day的值\n */\n advanceTime: (amount = 1) =>\n set((state) => ({\n world: { ...state.world, day: state.world.day + amount },\n })),\n\n /**\n * 实现:传送到指定地点\n * 更新world.currentLocationId\n */\n teleport: (locationId) =>\n set((state) => ({\n world: { ...state.world, currentLocationId: locationId },\n })),\n\n // ========== 存档操作实现 ==========\n\n /**\n * 实现:重置游戏状态\n * 恢复到初始状态,清空日志\n */\n reset: () => set({ ...initialState, logs: [] }),\n\n // ========== 历史记录操作实现 ==========\n\n /**\n * 实现:保存当前状态快照\n * \n * 保存除日志外的所有状态数据到历史管理器。\n * 日志通常不需要回退,因此被排除以节省内存。\n */\n saveSnapshot: () => {\n const currentState = get();\n // 保存完整状态(HistoryManager 会通过 JSON 序列化自动过滤掉方法)\n history.push(currentState);\n },\n\n /**\n * 实现:撤销到上一个状态\n * \n * 从历史管理器中恢复上一个快照。\n * 恢复时保留当前的日志,并添加系统提示。\n * \n * @returns 是否成功撤销\n */\n undo: () => {\n const prev = history.pop();\n if (prev) {\n set({\n stats: prev.stats,\n inventory: prev.inventory,\n flags: prev.flags,\n world: prev.world,\n extra: prev.extra,\n // logs 保留当前的,不回退日志\n });\n get().addLog(SystemMessages.UNDO_SUCCESS, 'info');\n return true;\n }\n return false;\n },\n }),\n {\n // 持久化配置\n name: persistName, // localStorage中的键名\n storage: createJSONStorage(() => localStorage), // 使用localStorage存储\n\n /**\n * 部分持久化 - 只保存数据字段,排除方法\n * \n * 这很重要,因为方法不能被序列化到JSON。\n * 我们只持久化游戏状态数据,方法会在store重新创建时自动添加。\n */\n partialize: (state) =>\n ({\n stats: state.stats,\n inventory: state.inventory,\n flags: state.flags,\n world: state.world,\n logs: state.logs,\n extra: state.extra,\n }) as GameState<S, I, F, X>,\n }\n )\n );\n};"]}
|