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 ADDED
@@ -0,0 +1,1087 @@
1
+ # 游戏引擎框架使用指南
2
+
3
+ ## 📋 更新日志
4
+
5
+ ### 最新更新 (2024)
6
+
7
+ #### ✨ 新增功能
8
+
9
+ 1. **事件系统 (Event System)**
10
+ - 新增 `EventBus` 类,实现发布-订阅模式
11
+ - 新增 `EngineEvents` 常量,预定义系统事件
12
+ - 支持类型安全的事件监听和发射
13
+ - 自动发射状态变更事件(属性、物品、标记等)
14
+
15
+ 2. **历史管理 (History Manager)**
16
+ - 新增 `HistoryManager` 类,支持撤销/重做
17
+ - Store 新增 `saveSnapshot()` 和 `undo()` 方法
18
+ - 支持最多保存 20 个历史快照
19
+ - 深拷贝状态,防止引用污染
20
+
21
+ 3. **覆盖层系统 (Overlay System)**
22
+ - 新增 `OverlaySystem` 组件,处理瞬时通知
23
+ - 支持飘字 (Toast) 和弹窗 (Modal)
24
+ - 完全分离持久化日志和瞬时通知
25
+ - 自动动画和样式
26
+
27
+ 4. **消息通知系统**
28
+ - Store 新增 `showToast()` 方法 - 显示飘字通知
29
+ - Store 新增 `showModal()` 方法 - 显示弹窗通知
30
+ - 新增 `NotificationPayload` 类型
31
+ - 新增 `NOTIFICATION` 事件
32
+
33
+ #### 🔧 重要修改
34
+
35
+ 1. **架构优化 - 分离记录与通知**
36
+ - ⚠️ **重要**:`LogEntry` 现在仅用于持久化的历史记录
37
+ - 移除了 `LogEntry.display` 字段(之前的错误设计)
38
+ - 飘字和弹窗不再存入日志,避免持久化灾难
39
+ - 通过事件系统实现通知,不污染历史记录
40
+
41
+ 2. **Store 方法更新**
42
+ - `addLog()` - 移除 `display` 参数,仅用于添加历史记录
43
+ - 所有状态变更方法自动发射对应事件
44
+ - 新增历史管理相关方法
45
+
46
+ 3. **Flow 系统集成**
47
+ - 动作执行时自动发射 `ACTION_EXECUTED` 事件
48
+ - 支持 `effects.triggerEvent` 触发自定义事件
49
+ - 可选的自动快照功能(注释中)
50
+
51
+ #### 📚 文档更新
52
+
53
+ - 新增完整的中文注释
54
+ - 新增架构说明和最佳实践
55
+ - 新增事件系统使用指南
56
+ - 新增消息通知系统说明
57
+ - 更新完整示例代码
58
+
59
+ ---
60
+
61
+ ## 目录
62
+
63
+ - [更新日志](#-更新日志)
64
+ - [概述](#概述)
65
+ - [核心概念](#核心概念)
66
+ - [快速开始](#快速开始)
67
+ - [核心系统](#核心系统)
68
+ - [状态管理 (Store)](#状态管理-store)
69
+ - [事件系统 (Events)](#事件系统-events)
70
+ - [历史管理 (History)](#历史管理-history)
71
+ - [流程系统 (Flow)](#流程系统-flow)
72
+ - [查询系统 (Query)](#查询系统-query)
73
+ - [消息通知系统](#消息通知系统)
74
+ - [完整示例](#完整示例)
75
+ - [最佳实践](#最佳实践)
76
+
77
+ ---
78
+
79
+ ## 概述
80
+
81
+ 这是一个基于 TypeScript 和 Zustand 构建的通用游戏引擎框架,专为文字冒险、RPG 等回合制游戏设计。
82
+
83
+ ### 核心特性
84
+
85
+ - ✅ **完全类型安全** - 使用 TypeScript 泛型实现类型安全
86
+ - ✅ **状态持久化** - 自动保存到 localStorage
87
+ - ✅ **事件驱动** - 解耦的事件系统
88
+ - ✅ **撤销/重做** - 内置历史管理
89
+ - ✅ **多级消息** - 支持日志/飘字/弹窗
90
+ - ✅ **灵活扩展** - 易于扩展自定义功能
91
+
92
+ ### 架构图
93
+
94
+ ```
95
+ ┌─────────────────────────────────────────────────────────┐
96
+ │ UI Layer │
97
+ │ (React Components, Event Listeners) │
98
+ └────────────────┬────────────────────────────────────────┘
99
+
100
+ ┌────────────────▼────────────────────────────────────────┐
101
+ │ Engine Layer │
102
+ │ │
103
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
104
+ │ │ Store │ │ Events │ │ History │ │
105
+ │ │ (Zustand)│◄─┤ (Bus) │ │(Manager) │ │
106
+ │ └────┬─────┘ └──────────┘ └──────────┘ │
107
+ │ │ │
108
+ │ ┌────▼─────┐ ┌──────────┐ ┌──────────-┐ │
109
+ │ │ Flow │ │ Query │ │ Messages │ │
110
+ │ │ (System) │ │ (System) │ │(Constants)│ │
111
+ │ └──────────┘ └──────────┘ └──────────—┘ │
112
+ └─────────────────────────────────────────────────────────┘
113
+ ```
114
+
115
+ ---
116
+
117
+ ## 核心概念
118
+
119
+ ### 1. 游戏状态 (GameState)
120
+
121
+ 游戏状态包含所有游戏数据:
122
+
123
+ ```typescript
124
+ interface GameState<S, I, F, X> {
125
+ stats: Record<S, number>; // 数值属性(生命值、金币等)
126
+ inventory: I[]; // 物品清单
127
+ flags: Record<F, boolean>; // 布尔标记(任务进度等)
128
+ world: WorldState; // 世界状态(位置、时间)
129
+ logs: LogEntry[]; // 日志记录
130
+ extra: X; // 扩展数据
131
+ }
132
+ ```
133
+
134
+ ### 2. 动作 (Action)
135
+
136
+ 动作是玩家可以执行的操作:
137
+
138
+ ```typescript
139
+ interface ActionDef<S, I, F, X> {
140
+ id: string; // 唯一标识
141
+ label: string; // 显示名称
142
+ costs?: Partial<Record<S, number>>; // 执行成本
143
+ requirements?: RequirementDef; // 执行条件
144
+ effects?: EffectDef; // 执行效果
145
+ resultText: string; // 结果文本
146
+ }
147
+ ```
148
+
149
+ ### 3. 事件 (Event)
150
+
151
+ 事件用于系统间通信,解耦组件:
152
+
153
+ ```typescript
154
+ // 发射事件
155
+ gameEvents.emit(EngineEvents.STAT_CHANGE, { stat: 'hp', delta: -10 });
156
+
157
+ // 监听事件
158
+ gameEvents.on(EngineEvents.STAT_CHANGE, (data) => {
159
+ console.log(`${data.stat} 变化了 ${data.delta}`);
160
+ });
161
+ ```
162
+
163
+ ---
164
+
165
+ ## 快速开始
166
+
167
+ ### 步骤 1: 定义游戏类型
168
+
169
+ ```typescript
170
+ // src/game/types.ts
171
+
172
+ // 定义你的游戏属性
173
+ type Stats = 'hp' | 'mp' | 'gold' | 'exp';
174
+
175
+ // 定义你的物品
176
+ type Items = 'sword' | 'potion' | 'key';
177
+
178
+ // 定义你的标记
179
+ type Flags = 'quest_completed' | 'door_unlocked';
180
+
181
+ // 定义扩展数据(可选)
182
+ interface ExtraData {
183
+ reputation: number;
184
+ guild: string;
185
+ }
186
+ ```
187
+
188
+ ### 步骤 2: 创建初始状态
189
+
190
+ ```typescript
191
+ // src/game/config/init.ts
192
+ import type { GameState } from '@/engine/core/types';
193
+
194
+ export const initialState: GameState<Stats, Items, Flags, ExtraData> = {
195
+ stats: {
196
+ hp: 100,
197
+ mp: 50,
198
+ gold: 0,
199
+ exp: 0,
200
+ },
201
+ inventory: [],
202
+ flags: {
203
+ quest_completed: false,
204
+ door_unlocked: false,
205
+ },
206
+ world: {
207
+ currentLocationId: 'town',
208
+ day: 1,
209
+ time: 0,
210
+ },
211
+ logs: [],
212
+ extra: {
213
+ reputation: 0,
214
+ guild: 'none',
215
+ },
216
+ };
217
+ ```
218
+
219
+ ### 步骤 3: 创建 Store
220
+
221
+ ```typescript
222
+ // src/game/store.ts
223
+ import { createGameEngineStore } from '@/engine/state/store';
224
+ import { initialState } from './config/init';
225
+
226
+ export const useGameStore = createGameEngineStore(
227
+ initialState,
228
+ 'my-game-save' // localStorage 键名
229
+ );
230
+
231
+ export type GameStore = ReturnType<typeof useGameStore>;
232
+ ```
233
+
234
+ ### 步骤 4: 定义动作
235
+
236
+ ```typescript
237
+ // src/game/content/actions.ts
238
+ import type { ActionDef } from '@/engine/core/types';
239
+
240
+ export const attackAction: ActionDef<Stats, Items, Flags, ExtraData> = {
241
+ id: 'attack_goblin',
242
+ label: '攻击哥布林',
243
+ description: '用你的武器攻击哥布林',
244
+
245
+ // 执行成本
246
+ costs: {
247
+ mp: 10,
248
+ },
249
+
250
+ // 执行条件
251
+ requirements: {
252
+ hasItems: ['sword'],
253
+ stats: {
254
+ hp: { min: 20 }, // 至少需要 20 点生命值
255
+ },
256
+ },
257
+
258
+ // 执行效果
259
+ effects: {
260
+ statsChange: {
261
+ exp: 10,
262
+ gold: 5,
263
+ },
264
+ flagsSet: {
265
+ quest_completed: true,
266
+ },
267
+ triggerEvent: 'goblin_defeated', // 触发自定义事件
268
+ },
269
+
270
+ resultText: '你击败了哥布林!获得 10 经验和 5 金币。',
271
+ };
272
+ ```
273
+
274
+ ### 步骤 5: 在 UI 中使用
275
+
276
+ ```typescript
277
+ // src/components/GameUI.tsx
278
+ import { useGameStore } from '@/game/store';
279
+ import { FlowSystem } from '@/engine/systems/flow';
280
+ import { QuerySystem } from '@/engine/systems/query';
281
+ import { attackAction } from '@/game/content/actions';
282
+
283
+ function GameUI() {
284
+ const hp = useGameStore((state) => state.stats.hp);
285
+ const gold = useGameStore((state) => state.stats.gold);
286
+ const inventory = useGameStore((state) => state.inventory);
287
+
288
+ const handleAttack = () => {
289
+ const store = useGameStore.getState();
290
+
291
+ // 检查是否可以执行
292
+ const canExecute = QuerySystem.canExecuteAction(store, attackAction);
293
+
294
+ if (canExecute) {
295
+ // 执行动作
296
+ FlowSystem.executeAction(store, attackAction);
297
+ } else {
298
+ store.showToast('无法执行该动作', 'warn');
299
+ }
300
+ };
301
+
302
+ return (
303
+ <div>
304
+ <div>生命值: {hp}</div>
305
+ <div>金币: {gold}</div>
306
+ <div>背包: {inventory.join(', ')}</div>
307
+ <button onClick={handleAttack}>攻击哥布林</button>
308
+ </div>
309
+ );
310
+ }
311
+ ```
312
+
313
+ ---
314
+
315
+ ## 核心系统
316
+
317
+ ### 状态管理 (Store)
318
+
319
+ Store 是游戏状态的中心,提供所有状态操作方法。
320
+
321
+ #### 基本操作
322
+
323
+ ```typescript
324
+ const store = useGameStore.getState();
325
+
326
+ // 更新属性
327
+ store.updateStat('hp', -10); // 减少 10 点生命值
328
+ store.updateStat('gold', 50); // 增加 50 金币
329
+
330
+ // 批量更新
331
+ store.setStats({ hp: 10, mp: -5, gold: 100 });
332
+
333
+ // 物品操作
334
+ store.addItem('potion');
335
+ store.removeItem('potion');
336
+
337
+ // 标记操作
338
+ store.setFlag('quest_completed', true);
339
+
340
+ // 日志操作
341
+ store.addLog('你进入了森林', 'info');
342
+
343
+ // 时间操作
344
+ store.advanceTime(1); // 推进 1 天
345
+
346
+ // 传送
347
+ store.teleport('dungeon');
348
+
349
+ // 重置游戏
350
+ store.reset();
351
+ ```
352
+
353
+ #### 扩展数据操作
354
+
355
+ ```typescript
356
+ // 直接更新
357
+ store.setExtra({ reputation: 100 });
358
+
359
+ // 基于当前值更新
360
+ store.setExtra((prev) => ({
361
+ reputation: prev.reputation + 10,
362
+ }));
363
+ ```
364
+
365
+ #### 历史记录操作
366
+
367
+ ```typescript
368
+ // 保存快照
369
+ store.saveSnapshot();
370
+
371
+ // 撤销到上一个状态
372
+ const success = store.undo();
373
+ if (success) {
374
+ console.log('撤销成功');
375
+ }
376
+ ```
377
+
378
+ ---
379
+
380
+ ### 事件系统 (Events)
381
+
382
+ 事件系统用于解耦组件,实现发布-订阅模式。
383
+
384
+ #### 预定义事件
385
+
386
+ ```typescript
387
+ export const EngineEvents = {
388
+ STAT_CHANGE: 'engine:stat_change', // 属性变化
389
+ ITEM_ADD: 'engine:item_add', // 物品添加
390
+ ITEM_REMOVE: 'engine:item_remove', // 物品移除
391
+ FLAG_CHANGE: 'engine:flag_change', // 标记变化
392
+ ACTION_EXECUTED: 'engine:action_exec', // 动作执行
393
+ TIME_PASS: 'engine:time_pass', // 时间推进
394
+ MESSAGE: 'engine:message', // 消息通知
395
+ CUSTOM: 'engine:custom_trigger', // 自定义事件
396
+ };
397
+ ```
398
+
399
+ #### 监听事件
400
+
401
+ ```typescript
402
+ import { gameEvents, EngineEvents } from '@/engine/systems/events';
403
+
404
+ // 监听属性变化
405
+ const unsubscribe = gameEvents.on(EngineEvents.STAT_CHANGE, (data) => {
406
+ console.log(`${data.stat} 变化了 ${data.delta},当前值: ${data.current}`);
407
+
408
+ // 生命值过低警告
409
+ if (data.stat === 'hp' && data.current < 20) {
410
+ playSound('warning');
411
+ }
412
+ });
413
+
414
+ // 取消监听
415
+ unsubscribe();
416
+ ```
417
+
418
+ #### 自定义事件
419
+
420
+ ```typescript
421
+ // 在动作中触发
422
+ const action: ActionDef = {
423
+ id: 'open_chest',
424
+ effects: {
425
+ triggerEvent: 'chest_opened',
426
+ },
427
+ // ...
428
+ };
429
+
430
+ // 监听自定义事件
431
+ gameEvents.on(EngineEvents.CUSTOM, (data) => {
432
+ if (data.id === 'chest_opened') {
433
+ playSound('treasure');
434
+ showAnimation('sparkle');
435
+ }
436
+ });
437
+ ```
438
+
439
+ ---
440
+
441
+ ### 历史管理 (History)
442
+
443
+ 历史管理器提供撤销/重做功能。
444
+
445
+ #### 基本用法
446
+
447
+ ```typescript
448
+ // 在重要操作前保存快照
449
+ store.saveSnapshot();
450
+
451
+ // 执行操作
452
+ store.updateStat('hp', -50);
453
+ store.addItem('cursed_item');
454
+
455
+ // 撤销操作
456
+ if (store.undo()) {
457
+ console.log('已撤销');
458
+ }
459
+ ```
460
+
461
+ #### 自动快照
462
+
463
+ 可以在 `flow.ts` 中启用自动快照:
464
+
465
+ ```typescript
466
+ // src/engine/systems/flow.ts
467
+ static executeAction(...) {
468
+ // 取消注释以启用自动快照
469
+ store.saveSnapshot();
470
+
471
+ // ... 执行动作
472
+ }
473
+ ```
474
+
475
+ ---
476
+
477
+ ### 流程系统 (Flow)
478
+
479
+ 流程系统负责执行动作的完整流程。
480
+
481
+ #### 执行流程
482
+
483
+ 1. **验证条件** - 检查需求和成本
484
+ 2. **扣除成本** - 消耗资源
485
+ 3. **应用效果** - 修改游戏状态
486
+ 4. **记录日志** - 反馈给玩家
487
+ 5. **发射事件** - 通知其他系统
488
+
489
+ #### 使用方法
490
+
491
+ ```typescript
492
+ import { FlowSystem } from '@/engine/systems/flow';
493
+
494
+ const success = FlowSystem.executeAction(store, action);
495
+
496
+ if (success) {
497
+ console.log('动作执行成功');
498
+ } else {
499
+ console.log('动作执行失败');
500
+ }
501
+ ```
502
+
503
+ ---
504
+
505
+ ### 查询系统 (Query)
506
+
507
+ 查询系统提供各种状态查询方法。
508
+
509
+ #### 常用查询
510
+
511
+ ```typescript
512
+ import { QuerySystem } from '@/engine/systems/query';
513
+
514
+ // 检查是否有物品
515
+ const hasKey = QuerySystem.hasItem(state, 'key');
516
+
517
+ // 检查是否有足够的资源
518
+ const canAfford = QuerySystem.canAfford(state, { gold: 100 });
519
+
520
+ // 检查需求
521
+ const meetsRequirements = QuerySystem.checkRequirements(state, {
522
+ hasItems: ['sword'],
523
+ stats: { hp: { min: 20 } },
524
+ });
525
+
526
+ // 检查是否可以执行动作
527
+ const canExecute = QuerySystem.canExecuteAction(state, action);
528
+
529
+ // 获取可用动作列表
530
+ const availableActions = QuerySystem.getAvailableActions(state, allActions);
531
+ ```
532
+
533
+ ---
534
+
535
+ ## 消息通知系统
536
+
537
+ ### ⚠️ 重要架构说明
538
+
539
+ 框架严格区分 **持久化记录** 和 **瞬时通知**:
540
+
541
+ | 类型 | 持久化 | 刷新后 | 撤销后 | 用途 |
542
+ |------|--------|--------|--------|------|
543
+ | **Logs (日志)** | ✅ 是 | ✅ 重新加载 | ✅ 保留 | 历史记录,可回顾 |
544
+ | **Toasts (飘字)** | ❌ 否 | ❌ 消失 | ❌ 消失 | 轻量级反馈 |
545
+ | **Modals (弹窗)** | ❌ 否 | ❌ 消失 | ❌ 消失 | 重要信息,阻塞式 |
546
+
547
+ ### 为什么要分离?
548
+
549
+ **错误的做法**(将弹窗存入日志):
550
+ ```typescript
551
+ // ❌ 错误:弹窗会被持久化
552
+ store.addLog('打开宝箱', 'info', 'modal');
553
+
554
+ // Bug 1: 页面刷新后,弹窗再次出现!
555
+ // Bug 2: 撤销操作后,弹窗记录还在,逻辑混乱!
556
+ ```
557
+
558
+ **正确的做法**(分离记录和通知):
559
+ ```typescript
560
+ // ✅ 正确:日志用于记录
561
+ store.addLog('你打开了宝箱', 'result');
562
+
563
+ // ✅ 正确:弹窗用于通知(不持久化)
564
+ store.showModal('获得传说武器!', 'success');
565
+ ```
566
+
567
+ ### 1. 日志 (Logs)
568
+
569
+ **持久化的历史记录**,会被保存到 localStorage:
570
+
571
+ ```typescript
572
+ // 记录游戏事件
573
+ store.addLog('你进入了森林', 'info');
574
+ store.addLog('击败了哥布林', 'result');
575
+ store.addLog('获得了传说武器', 'success');
576
+
577
+ // 这些日志会:
578
+ // - 保存到 localStorage
579
+ // - 页面刷新后重新加载
580
+ // - 永久显示在日志面板
581
+ // - 可以被玩家回顾
582
+ ```
583
+
584
+ ### 2. 飘字 (Toasts)
585
+
586
+ **瞬时通知**,不会被持久化:
587
+
588
+ ```typescript
589
+ // 轻量级反馈
590
+ store.showToast('获得 10 经验值', 'success');
591
+ store.showToast('体力不足', 'warn');
592
+
593
+ // 飘字会:
594
+ // - 显示 3 秒后自动消失
595
+ // - 不保存到 localStorage
596
+ // - 页面刷新后不会重新出现
597
+ // - 不阻塞用户操作
598
+ ```
599
+
600
+ ### 3. 弹窗 (Modals)
601
+
602
+ **瞬时通知**,不会被持久化:
603
+
604
+ ```typescript
605
+ // 重要信息
606
+ store.showModal('恭喜!你升级了!', 'success');
607
+ store.showModal('警告:此操作不可撤销', 'warn');
608
+
609
+ // 弹窗会:
610
+ // - 需要用户点击确认
611
+ // - 不保存到 localStorage
612
+ // - 页面刷新后不会重新出现
613
+ // - 阻塞用户操作
614
+ ```
615
+
616
+ ### UI 层实现
617
+
618
+ 使用 `OverlaySystem` 组件处理瞬时通知:
619
+
620
+ ```typescript
621
+ // src/App.tsx
622
+ import { OverlaySystem } from '@/engine/ui/OverlaySystem';
623
+ import { LogStream } from '@/engine/ui/LogStream';
624
+
625
+ function App() {
626
+ return (
627
+ <div className="app">
628
+ {/* 日志面板(持久化) */}
629
+ <LogStream />
630
+
631
+ {/* 覆盖层系统(瞬时通知) */}
632
+ <OverlaySystem />
633
+
634
+ {/* 游戏内容 */}
635
+ <GameContent />
636
+ </div>
637
+ );
638
+ }
639
+ ```
640
+
641
+ `OverlaySystem` 会自动监听 `NOTIFICATION` 事件并显示对应的 UI。
642
+
643
+ ---
644
+
645
+ ## 完整示例
646
+
647
+ ### 创建一个简单的 RPG 游戏
648
+
649
+ ```typescript
650
+ // 1. 定义类型
651
+ type Stats = 'hp' | 'mp' | 'gold' | 'exp' | 'level';
652
+ type Items = 'sword' | 'potion' | 'key';
653
+ type Flags = 'tutorial_done' | 'boss_defeated';
654
+
655
+ // 2. 创建初始状态
656
+ const initialState: GameState<Stats, Items, Flags> = {
657
+ stats: { hp: 100, mp: 50, gold: 0, exp: 0, level: 1 },
658
+ inventory: ['potion'],
659
+ flags: { tutorial_done: false, boss_defeated: false },
660
+ world: { currentLocationId: 'town', day: 1, time: 0 },
661
+ logs: [],
662
+ extra: {},
663
+ };
664
+
665
+ // 3. 创建 Store
666
+ export const useGameStore = createGameEngineStore(initialState, 'rpg-save');
667
+
668
+ // 4. 定义动作
669
+ const buyPotionAction: ActionDef<Stats, Items, Flags> = {
670
+ id: 'buy_potion',
671
+ label: '购买药水',
672
+ costs: { gold: 10 },
673
+ effects: {
674
+ itemsAdd: ['potion'],
675
+ },
676
+ resultText: '你购买了一瓶药水。',
677
+ };
678
+
679
+ const usePotionAction: ActionDef<Stats, Items, Flags> = {
680
+ id: 'use_potion',
681
+ label: '使用药水',
682
+ requirements: {
683
+ hasItems: ['potion'],
684
+ stats: { hp: { max: 99 } }, // 生命值未满
685
+ },
686
+ effects: {
687
+ statsChange: { hp: 30 },
688
+ itemsRemove: ['potion'],
689
+ },
690
+ resultText: (state) => `你使用了药水,恢复了 30 点生命值。当前生命值: ${state.stats.hp + 30}`,
691
+ };
692
+
693
+ const attackBossAction: ActionDef<Stats, Items, Flags> = {
694
+ id: 'attack_boss',
695
+ label: '挑战 Boss',
696
+ requirements: {
697
+ hasItems: ['sword'],
698
+ stats: {
699
+ level: { min: 5 },
700
+ hp: { min: 50 },
701
+ },
702
+ },
703
+ costs: {
704
+ mp: 20,
705
+ },
706
+ effects: {
707
+ statsChange: { exp: 100, gold: 50 },
708
+ flagsSet: { boss_defeated: true },
709
+ triggerEvent: 'boss_victory',
710
+ },
711
+ resultText: '你击败了 Boss!获得大量奖励!',
712
+ };
713
+
714
+ // 5. 设置事件监听
715
+ function setupGameEvents() {
716
+ // 升级检查
717
+ gameEvents.on(EngineEvents.STAT_CHANGE, (data) => {
718
+ if (data.stat === 'exp') {
719
+ const store = useGameStore.getState();
720
+ const expNeeded = store.stats.level * 100;
721
+
722
+ if (store.stats.exp >= expNeeded) {
723
+ store.updateStat('level', 1);
724
+ store.updateStat('exp', -expNeeded);
725
+ store.updateStat('hp', 20);
726
+ store.updateStat('mp', 10);
727
+ store.showModal('恭喜升级!', 'success');
728
+ }
729
+ }
730
+ });
731
+
732
+ // Boss 胜利
733
+ gameEvents.on(EngineEvents.CUSTOM, (data) => {
734
+ if (data.id === 'boss_victory') {
735
+ playSound('victory');
736
+ showCutscene('ending');
737
+ }
738
+ });
739
+ }
740
+
741
+ // 6. UI 组件
742
+ function GameUI() {
743
+ const stats = useGameStore((state) => state.stats);
744
+ const inventory = useGameStore((state) => state.inventory);
745
+ const flags = useGameStore((state) => state.flags);
746
+
747
+ const handleAction = (action: ActionDef) => {
748
+ const store = useGameStore.getState();
749
+ const success = FlowSystem.executeAction(store, action);
750
+
751
+ if (!success) {
752
+ store.showToast('无法执行该动作', 'warn');
753
+ }
754
+ };
755
+
756
+ return (
757
+ <div className="game-ui">
758
+ {/* 状态栏 */}
759
+ <div className="stats">
760
+ <div>等级: {stats.level}</div>
761
+ <div>生命值: {stats.hp}/100</div>
762
+ <div>魔法值: {stats.mp}/50</div>
763
+ <div>金币: {stats.gold}</div>
764
+ <div>经验值: {stats.exp}</div>
765
+ </div>
766
+
767
+ {/* 背包 */}
768
+ <div className="inventory">
769
+ <h3>背包</h3>
770
+ {inventory.map((item, i) => (
771
+ <div key={i}>{item}</div>
772
+ ))}
773
+ </div>
774
+
775
+ {/* 动作按钮 */}
776
+ <div className="actions">
777
+ <button onClick={() => handleAction(buyPotionAction)}>
778
+ 购买药水 (10 金币)
779
+ </button>
780
+ <button onClick={() => handleAction(usePotionAction)}>
781
+ 使用药水
782
+ </button>
783
+ {flags.tutorial_done && (
784
+ <button onClick={() => handleAction(attackBossAction)}>
785
+ 挑战 Boss
786
+ </button>
787
+ )}
788
+ </div>
789
+
790
+ {/* 撤销按钮 */}
791
+ <button onClick={() => useGameStore.getState().undo()}>
792
+ 撤销上一步
793
+ </button>
794
+ </div>
795
+ );
796
+ }
797
+ ```
798
+
799
+ ---
800
+
801
+ ## 最佳实践
802
+
803
+ ### 1. 类型定义
804
+
805
+ ```typescript
806
+ // ✅ 好的做法:使用字符串字面量联合类型
807
+ type Stats = 'hp' | 'mp' | 'gold';
808
+
809
+ // ❌ 避免:使用 string 类型
810
+ type Stats = string;
811
+ ```
812
+
813
+ ### 2. 动作设计
814
+
815
+ ```typescript
816
+ // ✅ 好的做法:清晰的动作定义
817
+ const action: ActionDef = {
818
+ id: 'buy_sword',
819
+ label: '购买剑',
820
+ costs: { gold: 100 },
821
+ effects: { itemsAdd: ['sword'] },
822
+ resultText: '你购买了一把剑。',
823
+ };
824
+
825
+ // ❌ 避免:过于复杂的动作
826
+ const action: ActionDef = {
827
+ id: 'complex_action',
828
+ // 太多效果,应该拆分成多个动作
829
+ effects: {
830
+ statsChange: { hp: 10, mp: -5, gold: -50, exp: 20 },
831
+ itemsAdd: ['item1', 'item2', 'item3'],
832
+ flagsSet: { flag1: true, flag2: false, flag3: true },
833
+ },
834
+ };
835
+ ```
836
+
837
+ ### 3. 需求检查
838
+
839
+ ```typescript
840
+ // ✅ 好的做法:使用 QuerySystem
841
+ if (QuerySystem.canExecuteAction(store, action)) {
842
+ FlowSystem.executeAction(store, action);
843
+ }
844
+
845
+ // ❌ 避免:手动检查
846
+ if (store.stats.gold >= 100 && store.inventory.includes('key')) {
847
+ FlowSystem.executeAction(store, action);
848
+ }
849
+ ```
850
+
851
+ ### 4. 事件监听
852
+
853
+ ```typescript
854
+ // ✅ 好的做法:在组件卸载时取消监听
855
+ useEffect(() => {
856
+ const unsubscribe = gameEvents.on(EngineEvents.STAT_CHANGE, handler);
857
+ return unsubscribe; // 清理
858
+ }, []);
859
+
860
+ // ❌ 避免:忘记取消监听(内存泄漏)
861
+ useEffect(() => {
862
+ gameEvents.on(EngineEvents.STAT_CHANGE, handler);
863
+ }, []);
864
+ ```
865
+
866
+ ### 5. 状态更新
867
+
868
+ ```typescript
869
+ // ✅ 好的做法:使用 Store 提供的方法
870
+ store.updateStat('hp', -10);
871
+
872
+ // ❌ 避免:直接修改状态(不会触发更新)
873
+ store.stats.hp -= 10;
874
+ ```
875
+
876
+ ### 6. 消息使用
877
+
878
+ ```typescript
879
+ // ✅ 好的做法:根据重要性选择合适的展示方式
880
+ store.addLog('你走进了房间', 'info'); // 普通信息
881
+ store.showToast('获得 10 金币', 'success'); // 轻量提示
882
+ store.showModal('任务完成!', 'success'); // 重要信息
883
+
884
+ // ❌ 避免:所有消息都用弹窗(打扰用户)
885
+ store.showModal('你走了一步', 'info');
886
+ ```
887
+
888
+ ### 7. 扩展数据
889
+
890
+ ```typescript
891
+ // ✅ 好的做法:定义明确的扩展数据类型
892
+ interface ExtraData {
893
+ reputation: number;
894
+ guild: string;
895
+ achievements: string[];
896
+ }
897
+
898
+ // ❌ 避免:使用 any 或过于宽泛的类型
899
+ type ExtraData = any;
900
+ ```
901
+
902
+ ---
903
+
904
+ ## 常见问题
905
+
906
+ ### Q: 如何添加新的属性类型?
907
+
908
+ A: 在类型定义中添加新的字符串字面量:
909
+
910
+ ```typescript
911
+ // 添加 'stamina' 属性
912
+ type Stats = 'hp' | 'mp' | 'gold' | 'stamina';
913
+ ```
914
+
915
+ ### Q: 如何实现复杂的条件判断?
916
+
917
+ A: 使用 `custom` 函数:
918
+
919
+ ```typescript
920
+ requirements: {
921
+ custom: (state) => {
922
+ // 复杂逻辑:(有钥匙 AND 力量>=5) OR 有万能钥匙
923
+ const hasKeyAndStrength =
924
+ state.inventory.includes('key') && state.stats.strength >= 5;
925
+ const hasMasterKey = state.inventory.includes('master_key');
926
+ return hasKeyAndStrength || hasMasterKey;
927
+ }
928
+ }
929
+ ```
930
+
931
+ ### Q: 如何保存多个存档?
932
+
933
+ A: 使用不同的 `persistName`:
934
+
935
+ ```typescript
936
+ const slot1 = createGameEngineStore(initialState, 'save-slot-1');
937
+ const slot2 = createGameEngineStore(initialState, 'save-slot-2');
938
+ const slot3 = createGameEngineStore(initialState, 'save-slot-3');
939
+ ```
940
+
941
+ ### Q: 如何实现自动保存?
942
+
943
+ A: 监听关键事件并保存快照:
944
+
945
+ ```typescript
946
+ gameEvents.on(EngineEvents.ACTION_EXECUTED, () => {
947
+ useGameStore.getState().saveSnapshot();
948
+ });
949
+ ```
950
+
951
+ ---
952
+
953
+ ## 进阶主题
954
+
955
+ ### 自定义系统扩展
956
+
957
+ 你可以创建自己的系统来扩展引擎功能:
958
+
959
+ ```typescript
960
+ // src/game/systems/achievement.ts
961
+ export class AchievementSystem {
962
+ static checkAchievements(store: GameStore) {
963
+ // 检查成就解锁条件
964
+ if (store.stats.gold >= 1000 && !store.flags.rich_achievement) {
965
+ store.setFlag('rich_achievement', true);
966
+ store.showModal('成就解锁:富豪', 'success');
967
+ }
968
+ }
969
+ }
970
+
971
+ // 在事件监听中使用
972
+ gameEvents.on(EngineEvents.STAT_CHANGE, (data) => {
973
+ if (data.stat === 'gold') {
974
+ AchievementSystem.checkAchievements(useGameStore.getState());
975
+ }
976
+ });
977
+ ```
978
+
979
+ ### 动态内容加载
980
+
981
+ ```typescript
982
+ // 从 JSON 文件加载动作
983
+ import actionsData from './data/actions.json';
984
+
985
+ const actions: ActionDef[] = actionsData.map(data => ({
986
+ ...data,
987
+ // 转换 JSON 数据为 ActionDef
988
+ }));
989
+ ```
990
+
991
+ ---
992
+
993
+ ## 总结
994
+
995
+ 这个游戏引擎框架提供了:
996
+
997
+ - 🎯 **类型安全** - 完整的 TypeScript 支持
998
+ - 🔄 **状态管理** - 基于 Zustand 的响应式状态
999
+ - 📡 **事件系统** - 解耦的发布-订阅模式
1000
+ - ⏮️ **历史管理** - 撤销/重做功能
1001
+ - 💬 **消息系统** - 多级消息通知
1002
+ - 🎮 **流程控制** - 完整的动作执行流程
1003
+ - 🔍 **查询系统** - 便捷的状态查询
1004
+
1005
+ 通过这些系统的组合,你可以快速构建各种类型的文字冒险和 RPG 游戏!
1006
+
1007
+ ---
1008
+
1009
+ ## 迁移指南
1010
+
1011
+ ### 从旧版本迁移
1012
+
1013
+ 如果你之前使用了错误的 `addLog` 设计(带 `display` 参数),需要进行以下修改:
1014
+
1015
+ #### ❌ 旧的错误做法
1016
+
1017
+ ```typescript
1018
+ // 错误:将弹窗存入日志(会导致持久化问题)
1019
+ store.addLog('打开宝箱', 'info', 'modal');
1020
+ store.addLog('获得金币', 'success', 'toast');
1021
+ ```
1022
+
1023
+ **问题**:
1024
+ - 页面刷新后,弹窗会再次出现
1025
+ - 撤销操作后,弹窗记录仍然存在
1026
+ - 日志面板会显示不应该显示的内容
1027
+
1028
+ #### ✅ 新的正确做法
1029
+
1030
+ ```typescript
1031
+ // 正确:分离记录和通知
1032
+
1033
+ // 1. 添加历史记录(持久化)
1034
+ store.addLog('你打开了宝箱', 'result');
1035
+
1036
+ // 2. 显示瞬时通知(不持久化)
1037
+ store.showModal('获得传说武器!', 'success');
1038
+ store.showToast('获得 10 金币', 'success');
1039
+ ```
1040
+
1041
+ #### 需要修改的地方
1042
+
1043
+ 1. **移除 `addLog` 的第三个参数**
1044
+ ```typescript
1045
+ // 旧代码
1046
+ store.addLog('消息', 'info', 'toast');
1047
+
1048
+ // 新代码
1049
+ store.addLog('消息', 'info'); // 仅用于历史记录
1050
+ store.showToast('消息', 'info'); // 用于通知
1051
+ ```
1052
+
1053
+ 2. **添加 `OverlaySystem` 组件**
1054
+ ```typescript
1055
+ // src/App.tsx
1056
+ import { OverlaySystem } from '@/engine/ui/OverlaySystem';
1057
+
1058
+ function App() {
1059
+ return (
1060
+ <>
1061
+ <OverlaySystem /> {/* 新增:处理飘字和弹窗 */}
1062
+ <GameContent />
1063
+ </>
1064
+ );
1065
+ }
1066
+ ```
1067
+
1068
+ 3. **更新类型定义**
1069
+ - `LogEntry` 不再有 `display` 字段
1070
+ - 使用 `NotificationPayload` 处理通知
1071
+ - 监听 `NOTIFICATION` 事件而不是 `MESSAGE` 事件
1072
+
1073
+ ---
1074
+
1075
+ ## 相关文档
1076
+
1077
+ - [核心类型定义](./core/types.ts)
1078
+ - [状态管理](./state/store.ts)
1079
+ - [事件系统](./systems/events.ts)
1080
+ - [历史管理](./state/history.ts)
1081
+ - [流程系统](./systems/flow.ts)
1082
+ - [查询系统](./systems/query.ts)
1083
+ - [覆盖层系统](./ui/OverlaySystem.tsx)
1084
+
1085
+ ## 许可证
1086
+
1087
+ MIT