steamsheep-ts-game-engine 2.0.0 → 3.1.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 +427 -962
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +562 -68
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +562 -68
- package/dist/index.mjs.map +1 -1
- package/dist/state/index.d.mts +204 -21
- package/dist/state/index.d.ts +204 -21
- package/dist/state/index.js +290 -23
- package/dist/state/index.js.map +1 -1
- package/dist/state/index.mjs +290 -23
- package/dist/state/index.mjs.map +1 -1
- package/dist/{store-xBiJ2MvB.d.mts → store-5-3GQpi9.d.mts} +100 -5
- package/dist/{store-D0SE7zJK.d.ts → store-PPh__zkF.d.ts} +100 -5
- package/dist/systems/index.d.mts +34 -3
- package/dist/systems/index.d.ts +34 -3
- package/dist/systems/index.js +272 -45
- package/dist/systems/index.js.map +1 -1
- package/dist/systems/index.mjs +272 -45
- package/dist/systems/index.mjs.map +1 -1
- package/dist/types-D-nDlnv3.d.mts +1650 -0
- package/dist/types-D-nDlnv3.d.ts +1650 -0
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/types-BLjkeE3R.d.mts +0 -536
- package/dist/types-BLjkeE3R.d.ts +0 -536
package/README.md
CHANGED
|
@@ -1,1087 +1,552 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SteamSheep TypeScript Game Engine - 完整 API 参考
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
一个功能强大、类型安全的文字冒险/RPG 游戏引擎框架。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 📚 目录
|
|
6
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
7
|
- [快速开始](#快速开始)
|
|
67
|
-
- [
|
|
68
|
-
|
|
69
|
-
- [事件系统 (Events)](#事件系统-events)
|
|
70
|
-
- [历史管理 (History)](#历史管理-history)
|
|
71
|
-
- [流程系统 (Flow)](#流程系统-flow)
|
|
72
|
-
- [查询系统 (Query)](#查询系统-query)
|
|
73
|
-
- [消息通知系统](#消息通知系统)
|
|
8
|
+
- [核心 API 列表](#核心-api-列表)
|
|
9
|
+
- [详细用法](#详细用法)
|
|
74
10
|
- [完整示例](#完整示例)
|
|
75
|
-
- [最佳实践](#最佳实践)
|
|
76
11
|
|
|
77
12
|
---
|
|
78
13
|
|
|
79
|
-
##
|
|
80
|
-
|
|
81
|
-
这是一个基于 TypeScript 和 Zustand 构建的通用游戏引擎框架,专为文字冒险、RPG 等回合制游戏设计。
|
|
82
|
-
|
|
83
|
-
### 核心特性
|
|
84
|
-
|
|
85
|
-
- ✅ **完全类型安全** - 使用 TypeScript 泛型实现类型安全
|
|
86
|
-
- ✅ **状态持久化** - 自动保存到 localStorage
|
|
87
|
-
- ✅ **事件驱动** - 解耦的事件系统
|
|
88
|
-
- ✅ **撤销/重做** - 内置历史管理
|
|
89
|
-
- ✅ **多级消息** - 支持日志/飘字/弹窗
|
|
90
|
-
- ✅ **灵活扩展** - 易于扩展自定义功能
|
|
14
|
+
## 🚀 快速开始
|
|
91
15
|
|
|
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
|
-
└─────────────────────────────────────────────────────────┘
|
|
16
|
+
```bash
|
|
17
|
+
npm install steamsheep-ts-game-engine
|
|
113
18
|
```
|
|
114
19
|
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## 核心概念
|
|
118
|
-
|
|
119
|
-
### 1. 游戏状态 (GameState)
|
|
120
|
-
|
|
121
|
-
游戏状态包含所有游戏数据:
|
|
122
|
-
|
|
123
20
|
```typescript
|
|
124
|
-
|
|
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)
|
|
21
|
+
import { createGameEngineStore } from 'steamsheep-ts-game-engine';
|
|
135
22
|
|
|
136
|
-
|
|
23
|
+
// 定义游戏类型
|
|
24
|
+
type Stats = 'hp' | 'mp' | 'gold';
|
|
25
|
+
type Items = 'sword' | 'potion';
|
|
26
|
+
type Flags = 'quest_done';
|
|
137
27
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
id: string; // 唯一标识
|
|
141
|
-
label: string; // 显示名称
|
|
142
|
-
costs?: Partial<Record<S, number>>; // 执行成本
|
|
143
|
-
requirements?: RequirementDef; // 执行条件
|
|
144
|
-
effects?: EffectDef; // 执行效果
|
|
145
|
-
resultText: string; // 结果文本
|
|
146
|
-
}
|
|
28
|
+
// 创建 Store
|
|
29
|
+
const useGameStore = createGameEngineStore(initialState, 'my-game');
|
|
147
30
|
```
|
|
148
31
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
事件用于系统间通信,解耦组件:
|
|
32
|
+
---
|
|
152
33
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
34
|
+
## 📋 核心 API 列表
|
|
35
|
+
|
|
36
|
+
### 1. 类型定义 (core/types.ts)
|
|
37
|
+
|
|
38
|
+
#### 核心接口
|
|
39
|
+
- `GameState<S, I, F, X>` - 游戏状态
|
|
40
|
+
- `ActionDef<S, I, F, X>` - 动作定义
|
|
41
|
+
- `EffectDef<S, I, F, X>` - 效果定义
|
|
42
|
+
- `RequirementDef<S, I, F, X>` - 需求定义
|
|
43
|
+
- `LocationDef<S, I, F, X>` - 地点定义
|
|
44
|
+
- `RequirementCheckResult` - 需求检查结果
|
|
45
|
+
- `FlagsBatchOperation<F>` - 批量标记操作
|
|
46
|
+
- `GameStoreActions<S, I, F, X>` - Store 操作方法
|
|
47
|
+
- `SnapshotInfo<S, I, F, X>` - 快照信息
|
|
48
|
+
|
|
49
|
+
### 2. Store API (state/store.ts)
|
|
50
|
+
|
|
51
|
+
#### 状态操作
|
|
52
|
+
- `updateStat(stat, delta)` - 更新单个属性
|
|
53
|
+
- `setStats(stats)` - 批量更新属性
|
|
54
|
+
- `setExtra(updater)` - 更新扩展数据
|
|
55
|
+
- `addItem(item)` - 添加物品
|
|
56
|
+
- `removeItem(item)` - 移除物品
|
|
57
|
+
- `setFlag(flag, value)` - 设置标记
|
|
58
|
+
|
|
59
|
+
#### 系统操作
|
|
60
|
+
- `addLog(text, type?)` - 添加日志(持久化)
|
|
61
|
+
- `showToast(text, type?)` - 显示飘字(瞬时)
|
|
62
|
+
- `showModal(text, type?)` - 显示弹窗(瞬时)
|
|
63
|
+
- `advanceTime(amount?)` - 推进时间
|
|
64
|
+
- `teleport(locationId)` - 传送
|
|
65
|
+
- `reset()` - 重置游戏
|
|
66
|
+
|
|
67
|
+
#### 快照操作
|
|
68
|
+
- `saveSnapshot(description?)` - 保存匿名快照
|
|
69
|
+
- `saveNamedSnapshot(name, description?)` - 保存命名快照
|
|
70
|
+
- `undo()` - 撤销到上一个状态
|
|
71
|
+
- `restoreSnapshot(name)` - 恢复到命名快照
|
|
72
|
+
- `deleteSnapshot(name)` - 删除命名快照
|
|
73
|
+
- `listSnapshots()` - 列出所有快照
|
|
74
|
+
- `hasSnapshot(name)` - 检查快照是否存在
|
|
75
|
+
|
|
76
|
+
### 3. 效果系统 (EffectDef)
|
|
77
|
+
|
|
78
|
+
#### 静态效果
|
|
79
|
+
- `statsChange` - 数值属性变更(支持函数)
|
|
80
|
+
- `itemsAdd` - 添加物品
|
|
81
|
+
- `itemsRemove` - 移除物品
|
|
82
|
+
- `flagsSet` - 设置标记(支持函数)
|
|
83
|
+
- `flagsBatch` - 批量标记操作(支持函数)
|
|
84
|
+
- `teleport` - 传送
|
|
85
|
+
- `timeAdvance` - 时间推进(支持函数)
|
|
86
|
+
- `onTimeAdvance` - 时间推进钩子
|
|
87
|
+
- `triggerEvent` - 触发自定义事件
|
|
88
|
+
|
|
89
|
+
#### 动态效果
|
|
90
|
+
- `conditionalEffects(state)` - 条件性效果
|
|
91
|
+
- `custom(draft, state)` - 修改 extra 数据
|
|
92
|
+
- `customFull(draft, originalState)` - 修改完整状态
|
|
93
|
+
|
|
94
|
+
#### 后置钩子
|
|
95
|
+
- `afterEffects(state, originalState, actions)` - 后置效果钩子
|
|
96
|
+
|
|
97
|
+
### 4. 需求系统 (RequirementDef)
|
|
98
|
+
|
|
99
|
+
- `stats` - 数值属性需求
|
|
100
|
+
- `hasItems` - 必须拥有的物品
|
|
101
|
+
- `noItems` - 不能拥有的物品
|
|
102
|
+
- `hasFlags` - 必须为 true 的标记
|
|
103
|
+
- `noFlags` - 必须为 false 的标记
|
|
104
|
+
- `custom(state)` - 自定义需求(支持返回失败原因)
|
|
105
|
+
|
|
106
|
+
### 5. 查询系统 (systems/query.ts)
|
|
107
|
+
|
|
108
|
+
- `QuerySystem.checkRequirements(state, reqs)` - 检查需求(boolean)
|
|
109
|
+
- `QuerySystem.checkRequirementsWithReason(state, reqs)` - 检查需求(带原因)
|
|
110
|
+
- `QuerySystem.canAfford(state, costs)` - 检查成本
|
|
111
|
+
|
|
112
|
+
### 6. 流程系统 (systems/flow.ts)
|
|
113
|
+
|
|
114
|
+
- `FlowSystem.executeAction(store, action)` - 执行动作
|
|
115
|
+
|
|
116
|
+
### 7. 事件系统 (systems/events.ts)
|
|
117
|
+
|
|
118
|
+
- `gameEvents.on(event, handler)` - 监听事件
|
|
119
|
+
- `gameEvents.emit(event, data)` - 发射事件
|
|
120
|
+
- `gameEvents.off(event, handler)` - 取消监听
|
|
156
121
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
122
|
+
#### 预定义事件
|
|
123
|
+
- `EngineEvents.STAT_CHANGE` - 属性变化
|
|
124
|
+
- `EngineEvents.ITEM_ADD` - 物品添加
|
|
125
|
+
- `EngineEvents.ITEM_REMOVE` - 物品移除
|
|
126
|
+
- `EngineEvents.FLAG_CHANGE` - 标记变化
|
|
127
|
+
- `EngineEvents.ACTION_EXECUTED` - 动作执行
|
|
128
|
+
- `EngineEvents.NOTIFICATION` - 通知
|
|
129
|
+
- `EngineEvents.CUSTOM` - 自定义事件
|
|
130
|
+
|
|
131
|
+
### 8. 历史管理 (state/history.ts)
|
|
132
|
+
|
|
133
|
+
- `history.push(state, description?)` - 保存匿名快照
|
|
134
|
+
- `history.pushNamed(state, name, description?)` - 保存命名快照
|
|
135
|
+
- `history.pop()` - 撤销
|
|
136
|
+
- `history.restoreNamed(name)` - 恢复命名快照
|
|
137
|
+
- `history.deleteNamed(name)` - 删除命名快照
|
|
138
|
+
- `history.hasNamed(name)` - 检查快照是否存在
|
|
139
|
+
- `history.listSnapshots()` - 列出所有快照
|
|
140
|
+
- `history.listNamedSnapshots()` - 列出命名快照
|
|
141
|
+
- `history.peek()` - 查看最近快照
|
|
142
|
+
- `history.clear(includeNamed?)` - 清空快照
|
|
143
|
+
- `history.size` - 匿名快照数量
|
|
144
|
+
- `history.namedSize` - 命名快照数量
|
|
145
|
+
- `history.totalSize` - 总快照数量
|
|
162
146
|
|
|
163
147
|
---
|
|
164
148
|
|
|
165
|
-
##
|
|
149
|
+
## 📖 详细用法
|
|
166
150
|
|
|
167
|
-
###
|
|
151
|
+
### 1. 动态 Flag 支持
|
|
168
152
|
|
|
169
153
|
```typescript
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
type Flags = 'quest_completed' | 'door_unlocked';
|
|
180
|
-
|
|
181
|
-
// 定义扩展数据(可选)
|
|
182
|
-
interface ExtraData {
|
|
183
|
-
reputation: number;
|
|
184
|
-
guild: string;
|
|
154
|
+
effects: {
|
|
155
|
+
// 动态生成 flag 名称
|
|
156
|
+
flagsSet: (state) => {
|
|
157
|
+
const day = state.world.day;
|
|
158
|
+
return {
|
|
159
|
+
[`event_day_${day}`]: true,
|
|
160
|
+
[`gazed_stars_day_${day}`]: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
185
163
|
}
|
|
186
164
|
```
|
|
187
165
|
|
|
188
|
-
###
|
|
166
|
+
### 2. 完整状态访问 (customFull)
|
|
189
167
|
|
|
190
168
|
```typescript
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
};
|
|
169
|
+
effects: {
|
|
170
|
+
customFull: (draft, original) => {
|
|
171
|
+
// 可以修改所有状态
|
|
172
|
+
draft.stats.sanity -= 10;
|
|
173
|
+
draft.flags.some_flag = true;
|
|
174
|
+
draft.inventory.push('new_item');
|
|
175
|
+
draft.extra.customData = 'value';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
217
178
|
```
|
|
218
179
|
|
|
219
|
-
###
|
|
180
|
+
### 3. 条件性效果
|
|
220
181
|
|
|
221
182
|
```typescript
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
183
|
+
effects: {
|
|
184
|
+
conditionalEffects: (state) => {
|
|
185
|
+
if (state.flags.some_condition) {
|
|
186
|
+
return {
|
|
187
|
+
statsChange: { sanity: -10 },
|
|
188
|
+
flagsSet: { flag_a: true }
|
|
189
|
+
};
|
|
190
|
+
} else {
|
|
191
|
+
return {
|
|
192
|
+
statsChange: { sanity: -5 },
|
|
193
|
+
flagsSet: { flag_b: true }
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
232
198
|
```
|
|
233
199
|
|
|
234
|
-
###
|
|
200
|
+
### 4. 职责分离 (afterEffects + resultText 双参数)
|
|
235
201
|
|
|
236
202
|
```typescript
|
|
237
|
-
|
|
238
|
-
import type { ActionDef } from 'steamsheep-ts-game-engine/core';
|
|
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
|
-
// 执行效果
|
|
203
|
+
{
|
|
259
204
|
effects: {
|
|
260
|
-
statsChange: {
|
|
261
|
-
exp: 10,
|
|
262
|
-
gold: 5,
|
|
263
|
-
},
|
|
264
|
-
flagsSet: {
|
|
265
|
-
quest_completed: true,
|
|
266
|
-
},
|
|
267
|
-
triggerEvent: 'goblin_defeated', // 触发自定义事件
|
|
205
|
+
statsChange: { sanity: -10 }
|
|
268
206
|
},
|
|
269
207
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
// src/components/GameUI.tsx
|
|
278
|
-
import { useGameStore } from '@/game/store';
|
|
279
|
-
import { FlowSystem } from 'steamsheep-ts-game-engine/systems';
|
|
280
|
-
import { QuerySystem } from 'steamsheep-ts-game-engine/systems';
|
|
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');
|
|
208
|
+
// 副作用在这里执行
|
|
209
|
+
afterEffects: (state, original, actions) => {
|
|
210
|
+
if (state.stats.sanity < 20) {
|
|
211
|
+
actions.setFlag('going_mad', true);
|
|
212
|
+
actions.showToast('理智危险!', 'warn');
|
|
299
213
|
}
|
|
300
|
-
}
|
|
214
|
+
},
|
|
301
215
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
216
|
+
// 文本生成 - 可以比较前后状态
|
|
217
|
+
resultText: (state, original) => {
|
|
218
|
+
const sanityLoss = original.stats.sanity - state.stats.sanity;
|
|
219
|
+
return sanityLoss > 15
|
|
220
|
+
? `你失去了 ${sanityLoss} 点理智,感到崩溃...`
|
|
221
|
+
: '仪式完成了。';
|
|
222
|
+
}
|
|
310
223
|
}
|
|
311
224
|
```
|
|
312
225
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
## 核心系统
|
|
316
|
-
|
|
317
|
-
### 状态管理 (Store)
|
|
318
|
-
|
|
319
|
-
Store 是游戏状态的中心,提供所有状态操作方法。
|
|
320
|
-
|
|
321
|
-
#### 基本操作
|
|
226
|
+
### 5. 动态 StatsChange
|
|
322
227
|
|
|
323
228
|
```typescript
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
}));
|
|
229
|
+
effects: {
|
|
230
|
+
statsChange: (state) => {
|
|
231
|
+
const maxAP = state.stats.max_action_point || 4;
|
|
232
|
+
return {
|
|
233
|
+
action_point: maxAP - state.stats.action_point
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
363
237
|
```
|
|
364
238
|
|
|
365
|
-
|
|
239
|
+
### 6. 批量 Flag 操作
|
|
366
240
|
|
|
367
241
|
```typescript
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
console.log('撤销成功');
|
|
242
|
+
effects: {
|
|
243
|
+
// 清除所有 daily_ 开头的 flags
|
|
244
|
+
flagsBatch: {
|
|
245
|
+
clear: /^daily_/,
|
|
246
|
+
set: { new_day: true }
|
|
247
|
+
}
|
|
375
248
|
}
|
|
376
|
-
```
|
|
377
249
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
};
|
|
250
|
+
// 或使用函数
|
|
251
|
+
effects: {
|
|
252
|
+
flagsBatch: (state) => ({
|
|
253
|
+
clear: Object.keys(state.flags).filter(k => k.startsWith('daily_')),
|
|
254
|
+
set: { new_day: true }
|
|
255
|
+
})
|
|
256
|
+
}
|
|
397
257
|
```
|
|
398
258
|
|
|
399
|
-
|
|
259
|
+
### 7. 时间推进副作用
|
|
400
260
|
|
|
401
261
|
```typescript
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// 监听属性变化
|
|
405
|
-
const unsubscribe = gameEvents.on(EngineEvents.STAT_CHANGE, (data) => {
|
|
406
|
-
console.log(`${data.stat} 变化了 ${data.delta},当前值: ${data.current}`);
|
|
262
|
+
effects: {
|
|
263
|
+
timeAdvance: 1,
|
|
407
264
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
265
|
+
onTimeAdvance: (currentDay, previousDay, state, actions) => {
|
|
266
|
+
// 恢复每日资源
|
|
267
|
+
const maxAP = state.stats.max_action_point || 4;
|
|
268
|
+
actions.setStats({
|
|
269
|
+
action_point: maxAP
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// 清除每日标记
|
|
273
|
+
Object.keys(state.flags)
|
|
274
|
+
.filter(f => f.startsWith('daily_'))
|
|
275
|
+
.forEach(f => actions.setFlag(f as F, false));
|
|
411
276
|
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// 取消监听
|
|
415
|
-
unsubscribe();
|
|
277
|
+
}
|
|
416
278
|
```
|
|
417
279
|
|
|
418
|
-
|
|
280
|
+
### 8. 灵活的 Requirements
|
|
419
281
|
|
|
420
282
|
```typescript
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
283
|
+
requirements: {
|
|
284
|
+
custom: (state) => {
|
|
285
|
+
if (!state.inventory.includes('key')) {
|
|
286
|
+
return {
|
|
287
|
+
passed: false,
|
|
288
|
+
reason: "需要钥匙才能打开"
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (state.stats.sanity < 20) {
|
|
293
|
+
return {
|
|
294
|
+
passed: false,
|
|
295
|
+
reason: "理智太低,无法集中精神"
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { passed: true };
|
|
435
300
|
}
|
|
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
301
|
}
|
|
459
302
|
```
|
|
460
303
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
可以在 `flow.ts` 中启用自动快照:
|
|
304
|
+
### 9. 效果执行顺序
|
|
464
305
|
|
|
465
306
|
```typescript
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
//
|
|
469
|
-
|
|
307
|
+
effects: {
|
|
308
|
+
// 【阶段 1:静态效果】按以下顺序执行
|
|
309
|
+
statsChange: { sanity: -10 }, // 1
|
|
310
|
+
itemsAdd: ['item'], // 2
|
|
311
|
+
itemsRemove: ['old_item'], // 3
|
|
312
|
+
flagsSet: { flag: true }, // 4
|
|
313
|
+
flagsBatch: { clear: /^temp_/ }, // 5
|
|
314
|
+
teleport: 'new_location', // 6
|
|
315
|
+
timeAdvance: 1, // 7
|
|
316
|
+
onTimeAdvance: (day) => {}, // 7.1
|
|
317
|
+
triggerEvent: 'custom_event', // 8
|
|
318
|
+
|
|
319
|
+
// 【阶段 2:条件效果】
|
|
320
|
+
conditionalEffects: (state) => { // 9
|
|
321
|
+
// 基于阶段1后的状态
|
|
322
|
+
},
|
|
470
323
|
|
|
471
|
-
//
|
|
324
|
+
// 【阶段 3:自定义效果】
|
|
325
|
+
custom: (draft, state) => {}, // 10
|
|
326
|
+
customFull: (draft, original) => {}, // 11
|
|
472
327
|
}
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
---
|
|
476
|
-
|
|
477
|
-
### 流程系统 (Flow)
|
|
478
|
-
|
|
479
|
-
流程系统负责执行动作的完整流程。
|
|
480
328
|
|
|
481
|
-
|
|
329
|
+
// 【阶段 4:后置钩子】
|
|
330
|
+
afterEffects: (state, original, actions) => {}, // 12
|
|
482
331
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
3. **应用效果** - 修改游戏状态
|
|
486
|
-
4. **记录日志** - 反馈给玩家
|
|
487
|
-
5. **发射事件** - 通知其他系统
|
|
488
|
-
|
|
489
|
-
#### 使用方法
|
|
490
|
-
|
|
491
|
-
```typescript
|
|
492
|
-
import { FlowSystem } from 'steamsheep-ts-game-engine/systems';
|
|
493
|
-
|
|
494
|
-
const success = FlowSystem.executeAction(store, action);
|
|
495
|
-
|
|
496
|
-
if (success) {
|
|
497
|
-
console.log('动作执行成功');
|
|
498
|
-
} else {
|
|
499
|
-
console.log('动作执行失败');
|
|
500
|
-
}
|
|
332
|
+
// 【阶段 5:结果文本】
|
|
333
|
+
resultText: (state) => {} // 13
|
|
501
334
|
```
|
|
502
335
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
### 查询系统 (Query)
|
|
506
|
-
|
|
507
|
-
查询系统提供各种状态查询方法。
|
|
508
|
-
|
|
509
|
-
#### 常用查询
|
|
336
|
+
### 10. 命名快照系统
|
|
510
337
|
|
|
511
338
|
```typescript
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
// 检查是否有物品
|
|
515
|
-
const hasKey = QuerySystem.hasItem(state, 'key');
|
|
339
|
+
// 保存命名快照
|
|
340
|
+
store.saveNamedSnapshot('before_boss_fight', '挑战Boss前');
|
|
516
341
|
|
|
517
|
-
//
|
|
518
|
-
|
|
342
|
+
// 恢复到指定快照
|
|
343
|
+
if (store.restoreSnapshot('before_boss_fight')) {
|
|
344
|
+
console.log('已恢复到Boss战前');
|
|
345
|
+
}
|
|
519
346
|
|
|
520
|
-
//
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
347
|
+
// 查看所有快照
|
|
348
|
+
const snapshots = store.listSnapshots();
|
|
349
|
+
snapshots.forEach(snap => {
|
|
350
|
+
console.log(`${snap.name || '匿名'}: ${snap.description}`);
|
|
351
|
+
console.log(`时间: ${new Date(snap.timestamp).toLocaleString()}`);
|
|
524
352
|
});
|
|
525
353
|
|
|
526
|
-
//
|
|
527
|
-
|
|
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
|
-
**瞬时通知**,不会被持久化:
|
|
354
|
+
// 检查快照是否存在
|
|
355
|
+
if (store.hasSnapshot('before_boss_fight')) {
|
|
356
|
+
// 可以恢复
|
|
357
|
+
}
|
|
587
358
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
store.showToast('获得 10 经验值', 'success');
|
|
591
|
-
store.showToast('体力不足', 'warn');
|
|
592
|
-
|
|
593
|
-
// 飘字会:
|
|
594
|
-
// - 显示 3 秒后自动消失
|
|
595
|
-
// - 不保存到 localStorage
|
|
596
|
-
// - 页面刷新后不会重新出现
|
|
597
|
-
// - 不阻塞用户操作
|
|
359
|
+
// 删除快照
|
|
360
|
+
store.deleteSnapshot('before_boss_fight');
|
|
598
361
|
```
|
|
599
362
|
|
|
600
|
-
###
|
|
601
|
-
|
|
602
|
-
**瞬时通知**,不会被持久化:
|
|
363
|
+
### 11. ResultText 比较前后状态
|
|
603
364
|
|
|
604
365
|
```typescript
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
366
|
+
// 简化前:需要通过 afterEffects + extra 传递信息
|
|
367
|
+
{
|
|
368
|
+
effects: {
|
|
369
|
+
conditionalEffects: () => {
|
|
370
|
+
if (Math.random() < 0.15) {
|
|
371
|
+
return { statsChange: { gnosis: 1 } };
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
afterEffects: (state, original, actions) => {
|
|
377
|
+
const gainedGnosis = state.stats.gnosis > original.stats.gnosis;
|
|
378
|
+
actions.setExtra({ lastMeditateGainedGnosis: gainedGnosis });
|
|
379
|
+
},
|
|
380
|
+
resultText: (state) => {
|
|
381
|
+
return state.extra.lastMeditateGainedGnosis
|
|
382
|
+
? "获得了灵知!"
|
|
383
|
+
: "平静地冥想。";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
617
386
|
|
|
618
|
-
|
|
387
|
+
// 简化后:直接比较前后状态
|
|
388
|
+
{
|
|
389
|
+
effects: {
|
|
390
|
+
conditionalEffects: () => {
|
|
391
|
+
if (Math.random() < 0.15) {
|
|
392
|
+
return { statsChange: { gnosis: 1 } };
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
resultText: (state, original) => {
|
|
398
|
+
const gainedGnosis = state.stats.gnosis > original.stats.gnosis;
|
|
399
|
+
return gainedGnosis
|
|
400
|
+
? "获得了灵知!"
|
|
401
|
+
: "平静地冥想。";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
619
404
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
{/* 覆盖层系统(瞬时通知) */}
|
|
632
|
-
<OverlaySystem />
|
|
633
|
-
|
|
634
|
-
{/* 游戏内容 */}
|
|
635
|
-
<GameContent />
|
|
636
|
-
</div>
|
|
637
|
-
);
|
|
405
|
+
// 显示具体变化量
|
|
406
|
+
resultText: (state, original) => {
|
|
407
|
+
const hpChange = state.stats.hp - original.stats.hp;
|
|
408
|
+
const goldChange = state.stats.gold - original.stats.gold;
|
|
409
|
+
|
|
410
|
+
const parts = [];
|
|
411
|
+
if (hpChange < 0) parts.push(`失去 ${-hpChange} HP`);
|
|
412
|
+
if (goldChange > 0) parts.push(`获得 ${goldChange} 金币`);
|
|
413
|
+
|
|
414
|
+
return parts.join(',') || '什么都没发生。';
|
|
638
415
|
}
|
|
639
416
|
```
|
|
640
417
|
|
|
641
|
-
`OverlaySystem` 会自动监听 `NOTIFICATION` 事件并显示对应的 UI。
|
|
642
|
-
|
|
643
418
|
---
|
|
644
419
|
|
|
645
|
-
## 完整示例
|
|
646
|
-
|
|
647
|
-
### 创建一个简单的 RPG 游戏
|
|
420
|
+
## 🎮 完整示例
|
|
648
421
|
|
|
649
422
|
```typescript
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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'],
|
|
423
|
+
// 克苏鲁风格的仪式动作
|
|
424
|
+
const performRitualAction: ActionDef<Stats, Items, Flags, Extra> = {
|
|
425
|
+
id: 'perform_ritual',
|
|
426
|
+
label: '进行禁忌仪式',
|
|
427
|
+
|
|
428
|
+
costs: {
|
|
429
|
+
action_point: 2,
|
|
430
|
+
sanity: 5
|
|
689
431
|
},
|
|
690
|
-
|
|
691
|
-
};
|
|
692
|
-
|
|
693
|
-
const attackBossAction: ActionDef<Stats, Items, Flags> = {
|
|
694
|
-
id: 'attack_boss',
|
|
695
|
-
label: '挑战 Boss',
|
|
432
|
+
|
|
696
433
|
requirements: {
|
|
697
|
-
hasItems: ['
|
|
698
|
-
stats: {
|
|
699
|
-
|
|
700
|
-
|
|
434
|
+
hasItems: ['ancient_book'],
|
|
435
|
+
stats: {
|
|
436
|
+
knowledge: { min: 10 },
|
|
437
|
+
sanity: { min: 20 }
|
|
701
438
|
},
|
|
439
|
+
custom: (state) => {
|
|
440
|
+
const time = state.world.time;
|
|
441
|
+
if (time < 18 && time > 6) {
|
|
442
|
+
return {
|
|
443
|
+
passed: false,
|
|
444
|
+
reason: '禁忌仪式只能在夜晚进行'
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
return { passed: true };
|
|
448
|
+
}
|
|
702
449
|
},
|
|
703
|
-
|
|
704
|
-
mp: 20,
|
|
705
|
-
},
|
|
450
|
+
|
|
706
451
|
effects: {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
452
|
+
// 动态属性变更
|
|
453
|
+
statsChange: (state) => ({
|
|
454
|
+
knowledge: state.world.time === 0 ? 5 : 3,
|
|
455
|
+
sanity: state.world.time === 0 ? -15 : -10
|
|
456
|
+
}),
|
|
457
|
+
|
|
458
|
+
// 动态标记
|
|
459
|
+
flagsSet: (state) => ({
|
|
460
|
+
[`ritual_day_${state.world.day}`]: true,
|
|
461
|
+
'has_performed_ritual': true
|
|
462
|
+
}),
|
|
463
|
+
|
|
464
|
+
// 条件效果
|
|
465
|
+
conditionalEffects: (state) => {
|
|
466
|
+
if (state.stats.sanity < 30) {
|
|
467
|
+
return {
|
|
468
|
+
statsChange: { sanity: -5 },
|
|
469
|
+
flagsSet: { 'going_mad': true },
|
|
470
|
+
itemsAdd: ['cursed_artifact']
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
// 修改扩展数据
|
|
477
|
+
custom: (draft) => {
|
|
478
|
+
draft.lastActionTime = Date.now();
|
|
479
|
+
draft.reputation += 5;
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
// 完整状态修改
|
|
483
|
+
customFull: (draft, original) => {
|
|
484
|
+
if (original.inventory.includes('protective_charm')) {
|
|
485
|
+
const sanityLoss = original.stats.sanity - draft.stats.sanity;
|
|
486
|
+
draft.stats.sanity = original.stats.sanity - Math.floor(sanityLoss * 0.5);
|
|
487
|
+
draft.inventory = draft.inventory.filter(i => i !== 'protective_charm');
|
|
488
|
+
draft.flags.charm_used = true;
|
|
728
489
|
}
|
|
729
490
|
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
showCutscene('ending');
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
afterEffects: (state, original, actions) => {
|
|
494
|
+
if (state.stats.knowledge >= 100 && !state.flags.master_scholar) {
|
|
495
|
+
actions.setFlag('master_scholar', true);
|
|
496
|
+
actions.showModal('成就解锁:博学大师!', 'success');
|
|
737
497
|
}
|
|
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
498
|
|
|
751
|
-
if (
|
|
752
|
-
|
|
499
|
+
if (state.flags.midnight_ritual) {
|
|
500
|
+
actions.saveNamedSnapshot('after_midnight_ritual', '午夜仪式后');
|
|
753
501
|
}
|
|
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
502
|
},
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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');
|
|
503
|
+
|
|
504
|
+
resultText: (state) => {
|
|
505
|
+
const parts = [];
|
|
506
|
+
if (state.flags.midnight_ritual) {
|
|
507
|
+
parts.push('午夜时分,仪式达到了高潮');
|
|
967
508
|
}
|
|
509
|
+
if (state.stats.sanity < 20) {
|
|
510
|
+
parts.push('你的理智岌岌可危');
|
|
511
|
+
}
|
|
512
|
+
if (state.flags.charm_used) {
|
|
513
|
+
parts.push('护符保护了你,但它已经碎裂了');
|
|
514
|
+
}
|
|
515
|
+
return parts.join(',') + '。';
|
|
968
516
|
}
|
|
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
|
-
}));
|
|
517
|
+
};
|
|
989
518
|
```
|
|
990
519
|
|
|
991
520
|
---
|
|
992
521
|
|
|
993
|
-
## 总结
|
|
994
|
-
|
|
995
|
-
这个游戏引擎框架提供了:
|
|
996
|
-
|
|
997
|
-
- 🎯 **类型安全** - 完整的 TypeScript 支持
|
|
998
|
-
- 🔄 **状态管理** - 基于 Zustand 的响应式状态
|
|
999
|
-
- 📡 **事件系统** - 解耦的发布-订阅模式
|
|
1000
|
-
- ⏮️ **历史管理** - 撤销/重做功能
|
|
1001
|
-
- 💬 **消息系统** - 多级消息通知
|
|
1002
|
-
- 🎮 **流程控制** - 完整的动作执行流程
|
|
1003
|
-
- 🔍 **查询系统** - 便捷的状态查询
|
|
1004
|
-
|
|
1005
|
-
通过这些系统的组合,你可以快速构建各种类型的文字冒险和 RPG 游戏!
|
|
1006
|
-
|
|
1007
|
-
---
|
|
1008
|
-
|
|
1009
|
-
## 迁移指南
|
|
1010
|
-
|
|
1011
|
-
### 从旧版本迁移
|
|
522
|
+
## 📝 总结
|
|
1012
523
|
|
|
1013
|
-
|
|
524
|
+
本引擎提供了以下增强功能:
|
|
1014
525
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
```
|
|
526
|
+
1. ✅ **动态 Flag 支持** - flagsSet 支持函数形式
|
|
527
|
+
2. ✅ **完整状态访问** - customFull 可修改所有状态
|
|
528
|
+
3. ✅ **条件性效果** - conditionalEffects 根据状态决定效果
|
|
529
|
+
4. ✅ **职责分离** - afterEffects 和 resultText 分离副作用和文本
|
|
530
|
+
5. ✅ **动态 StatsChange** - statsChange 支持函数形式
|
|
531
|
+
6. ✅ **批量 Flag 操作** - flagsBatch 支持正则、前缀、列表
|
|
532
|
+
7. ✅ **时间推进副作用** - onTimeAdvance 自动处理每日重置
|
|
533
|
+
8. ✅ **灵活的 Requirements** - custom 可返回失败原因
|
|
534
|
+
9. ✅ **明确的执行顺序** - 5 个阶段,清晰的文档说明
|
|
535
|
+
10. ✅ **命名快照系统** - 支持保存和恢复命名快照
|
|
536
|
+
11. ✅ **ResultText 双参数** - 可以比较前后状态,无需 extra 传递
|
|
1040
537
|
|
|
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 'steamsheep-ts-game-engine/ui';
|
|
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` 事件
|
|
538
|
+
所有功能都是类型安全的,并且完全向后兼容!
|
|
1072
539
|
|
|
1073
540
|
---
|
|
1074
541
|
|
|
1075
|
-
##
|
|
542
|
+
## 📚 更多文档
|
|
1076
543
|
|
|
544
|
+
- [完整功能示例](./examples/complete-features-example.ts)
|
|
545
|
+
- [动态 Flag 示例](./examples/dynamic-flags-example.ts)
|
|
1077
546
|
- [核心类型定义](./core/types.ts)
|
|
1078
547
|
- [状态管理](./state/store.ts)
|
|
1079
|
-
- [事件系统](./systems/events.ts)
|
|
1080
548
|
- [历史管理](./state/history.ts)
|
|
1081
|
-
- [流程系统](./systems/flow.ts)
|
|
1082
|
-
- [查询系统](./systems/query.ts)
|
|
1083
|
-
- [覆盖层系统](./ui/OverlaySystem.tsx)
|
|
1084
549
|
|
|
1085
|
-
## 许可证
|
|
550
|
+
## 📄 许可证
|
|
1086
551
|
|
|
1087
552
|
MIT
|