steamsheep-ts-game-engine 1.1.0 → 3.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 +377 -969
- 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-B7yMYHpB.d.mts} +100 -5
- package/dist/{store-D0SE7zJK.d.ts → store-BKROShPq.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-C3Q7UPWy.d.mts +1601 -0
- package/dist/types-C3Q7UPWy.d.ts +1601 -0
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/package.json +4 -4
- package/dist/types-BLjkeE3R.d.mts +0 -536
- package/dist/types-BLjkeE3R.d.ts +0 -536
|
@@ -0,0 +1,1601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 批量标记操作定义
|
|
3
|
+
*
|
|
4
|
+
* 定义批量清除和设置标记的操作。
|
|
5
|
+
*
|
|
6
|
+
* @template F - 标记键的联合类型
|
|
7
|
+
*/
|
|
8
|
+
interface FlagsBatchOperation<_F extends string = string> {
|
|
9
|
+
/**
|
|
10
|
+
* 要清除的标记
|
|
11
|
+
*
|
|
12
|
+
* 支持三种形式:
|
|
13
|
+
* - RegExp: 正则表达式,匹配的标记会被设置为 false
|
|
14
|
+
* - string: 前缀字符串,以此开头的标记会被设置为 false
|
|
15
|
+
* - string[]: 标记列表,列出的标记会被设置为 false
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // 正则表达式
|
|
20
|
+
* clear: /^daily_.*$/
|
|
21
|
+
*
|
|
22
|
+
* // 前缀字符串
|
|
23
|
+
* clear: 'temp_'
|
|
24
|
+
*
|
|
25
|
+
* // 标记列表
|
|
26
|
+
* clear: ['flag1', 'flag2', 'flag3']
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
clear?: RegExp | string | string[];
|
|
30
|
+
/**
|
|
31
|
+
* 要设置的标记
|
|
32
|
+
*
|
|
33
|
+
* 键值对形式,指定要设置的标记及其值。
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* set: {
|
|
38
|
+
* new_day: true,
|
|
39
|
+
* old_flag: false,
|
|
40
|
+
* dynamic_flag: true
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
set?: Record<string, boolean>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 游戏Store操作方法接口 - 用于 afterEffects 钩子
|
|
48
|
+
*
|
|
49
|
+
* 这是一个简化的接口定义,包含了在 afterEffects 中常用的操作方法。
|
|
50
|
+
* 完整的接口定义在 state/store.ts 中。
|
|
51
|
+
*
|
|
52
|
+
* @template S - 数值属性键的联合类型
|
|
53
|
+
* @template I - 物品ID的联合类型
|
|
54
|
+
* @template F - 标记键的联合类型
|
|
55
|
+
* @template X - 扩展数据类型
|
|
56
|
+
*/
|
|
57
|
+
interface GameStoreActions<S extends string, I extends string, F extends string, X = Record<string, unknown>> {
|
|
58
|
+
updateStat: (stat: S, delta: number) => void;
|
|
59
|
+
setStats: (stats: Partial<Record<S, number>>) => void;
|
|
60
|
+
setExtra: (updater: Partial<X> | ((prev: X) => Partial<X>)) => void;
|
|
61
|
+
addItem: (item: I) => void;
|
|
62
|
+
removeItem: (item: I) => void;
|
|
63
|
+
setFlag: (flag: F, value: boolean) => void;
|
|
64
|
+
addLog: (text: string, type?: LogEntry['type']) => void;
|
|
65
|
+
showToast: (text: string, type?: LogEntry['type']) => void;
|
|
66
|
+
showModal: (text: string, type?: LogEntry['type']) => void;
|
|
67
|
+
advanceTime: (amount?: number) => void;
|
|
68
|
+
teleport: (locationId: string) => void;
|
|
69
|
+
reset: () => void;
|
|
70
|
+
saveSnapshot: () => void;
|
|
71
|
+
undo: () => boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 游戏状态接口 - 存储游戏的完整状态
|
|
75
|
+
*
|
|
76
|
+
* 这是整个游戏引擎的核心数据结构,包含了游戏运行所需的所有状态信息。
|
|
77
|
+
* 使用泛型使其适用于任何类型的游戏,无需修改引擎代码。
|
|
78
|
+
*
|
|
79
|
+
* @template S - 数值属性键的联合类型
|
|
80
|
+
* @template I - 物品ID的联合类型
|
|
81
|
+
* @template F - 标记键的联合类型
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* type RPGState = GameState<
|
|
86
|
+
* 'hp' | 'mp' | 'gold',
|
|
87
|
+
* 'sword' | 'potion',
|
|
88
|
+
* 'dragon_defeated' | 'has_key'
|
|
89
|
+
* >;
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
interface GameState<S extends string, I extends string, F extends string, X = Record<string, unknown>> {
|
|
93
|
+
/**
|
|
94
|
+
* 数值属性记录 (如生命值、金币、经验等)
|
|
95
|
+
* 使用Record类型确保所有定义的属性都有对应的数值
|
|
96
|
+
*/
|
|
97
|
+
stats: Record<S, number>;
|
|
98
|
+
/**
|
|
99
|
+
* 物品清单 - 存储玩家拥有的物品ID
|
|
100
|
+
* 使用数组允许同一物品多次出现(如多个药水)
|
|
101
|
+
*/
|
|
102
|
+
inventory: I[];
|
|
103
|
+
/**
|
|
104
|
+
* 布尔标记 - 用于追踪游戏状态、任务进度等
|
|
105
|
+
* 常用于记录剧情进度、解锁状态、触发条件等
|
|
106
|
+
*/
|
|
107
|
+
flags: Record<F, boolean>;
|
|
108
|
+
/**
|
|
109
|
+
* 世界状态 - 包含位置、时间等世界级信息
|
|
110
|
+
*/
|
|
111
|
+
world: WorldState;
|
|
112
|
+
/**
|
|
113
|
+
* 日志条目数组 - 记录游戏事件
|
|
114
|
+
* 用于显示游戏历史和玩家操作反馈
|
|
115
|
+
*/
|
|
116
|
+
logs: LogEntry[];
|
|
117
|
+
/**
|
|
118
|
+
* 扩展数据 - 存储游戏特定的额外状态
|
|
119
|
+
*
|
|
120
|
+
* 这是一个灵活的字段,允许不同游戏存储自定义数据。
|
|
121
|
+
* 使用泛型 X 来定义具体的扩展数据类型。
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* // 定义扩展数据类型
|
|
126
|
+
* interface MyGameExtra {
|
|
127
|
+
* reputation: number;
|
|
128
|
+
* guild: string;
|
|
129
|
+
* achievements: string[];
|
|
130
|
+
* }
|
|
131
|
+
*
|
|
132
|
+
* // 使用扩展数据
|
|
133
|
+
* type MyGameState = GameState<Stats, Items, Flags, MyGameExtra>;
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
extra: X;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 世界状态接口 - 追踪游戏世界的时间和位置信息
|
|
140
|
+
*/
|
|
141
|
+
interface WorldState {
|
|
142
|
+
/** 当前所在位置的唯一标识符 */
|
|
143
|
+
currentLocationId: string;
|
|
144
|
+
/** 游戏内的天数 - 用于追踪游戏进度 */
|
|
145
|
+
day: number;
|
|
146
|
+
/** 当前时间 - 可以是0-24的小时制或自定义时间单位 */
|
|
147
|
+
time: number;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 日志条目接口 - 记录游戏中发生的事件
|
|
151
|
+
*
|
|
152
|
+
* ⚠️ 重要:LogEntry 是持久化的历史记录,会被保存到 localStorage
|
|
153
|
+
*
|
|
154
|
+
* 用于构建游戏日志系统,记录玩家操作、系统事件等信息。
|
|
155
|
+
* 每条日志都有类型标记,便于UI层进行样式区分和过滤。
|
|
156
|
+
*
|
|
157
|
+
* 日志会:
|
|
158
|
+
* - 被保存到 localStorage
|
|
159
|
+
* - 在页面刷新后重新加载
|
|
160
|
+
* - 永久显示在日志面板中
|
|
161
|
+
* - 可以被玩家回顾
|
|
162
|
+
*/
|
|
163
|
+
interface LogEntry {
|
|
164
|
+
/**
|
|
165
|
+
* 唯一标识符 - 用于React列表渲染的key
|
|
166
|
+
* 建议使用UUID或时间戳+随机数生成
|
|
167
|
+
*/
|
|
168
|
+
id: string;
|
|
169
|
+
/**
|
|
170
|
+
* 日志文本内容 - 显示给玩家的消息
|
|
171
|
+
* 可以包含动态内容,如玩家名称、物品名称等
|
|
172
|
+
*/
|
|
173
|
+
text: string;
|
|
174
|
+
/**
|
|
175
|
+
* 日志类型 - 用于UI显示不同样式和图标
|
|
176
|
+
* - info: 普通信息(灰色)
|
|
177
|
+
* - warn: 警告信息(黄色)
|
|
178
|
+
* - error: 错误信息(红色)
|
|
179
|
+
* - success: 成功信息(绿色)
|
|
180
|
+
* - result: 动作执行结果(黑色)
|
|
181
|
+
*/
|
|
182
|
+
type: "info" | "warn" | "error" | "success" | "result";
|
|
183
|
+
/**
|
|
184
|
+
* 日志产生的时间戳 - 游戏内时间或真实时间戳
|
|
185
|
+
* 用于排序和显示时间信息
|
|
186
|
+
*/
|
|
187
|
+
timestamp: number;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 通知类型枚举
|
|
191
|
+
*
|
|
192
|
+
* ⚠️ 重要:通知是瞬时的 UI 状态,不会被持久化
|
|
193
|
+
*
|
|
194
|
+
* 定义不同强度的通知方式:
|
|
195
|
+
* - toast: 飘字提示(轻量级,自动消失,不阻塞)
|
|
196
|
+
* - modal: 弹窗消息(强提示,需要用户确认,阻塞式)
|
|
197
|
+
*/
|
|
198
|
+
type NotificationType = 'toast' | 'modal';
|
|
199
|
+
/**
|
|
200
|
+
* 通知载荷 - 用于事件系统传递瞬时通知
|
|
201
|
+
*
|
|
202
|
+
* ⚠️ 与 LogEntry 的区别:
|
|
203
|
+
* - NotificationPayload: 瞬时通知,不持久化,用于 UI 反馈
|
|
204
|
+
* - LogEntry: 历史记录,持久化,用于回顾
|
|
205
|
+
*
|
|
206
|
+
* 通知会:
|
|
207
|
+
* - 通过事件系统发送
|
|
208
|
+
* - 仅在当前会话中显示
|
|
209
|
+
* - 不会被保存到 localStorage
|
|
210
|
+
* - 不会在页面刷新后重新出现
|
|
211
|
+
*/
|
|
212
|
+
interface NotificationPayload {
|
|
213
|
+
/** 通知文本 */
|
|
214
|
+
text: string;
|
|
215
|
+
/** 通知类型(影响样式) */
|
|
216
|
+
type: LogEntry['type'];
|
|
217
|
+
/** 通知方式(toast 或 modal) */
|
|
218
|
+
notificationType: NotificationType;
|
|
219
|
+
/** 时间戳(仅用于生成唯一 ID 和去重,不在 UI 中显示) */
|
|
220
|
+
timestamp: number;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 数值需求类型 - 定义属性检查的方式
|
|
224
|
+
*
|
|
225
|
+
* 可以是简单的数值(精确匹配)或范围对象(最小/最大值)
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* // 精确需求: 必须有10点体力
|
|
230
|
+
* const exact: StatRequirement = 10;
|
|
231
|
+
*
|
|
232
|
+
* // 范围需求: 体力必须在5-20之间
|
|
233
|
+
* const range: StatRequirement = { min: 5, max: 20 };
|
|
234
|
+
*
|
|
235
|
+
* // 最小需求: 至少需要5点体力
|
|
236
|
+
* const minimum: StatRequirement = { min: 5 };
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
type StatRequirement = number | {
|
|
240
|
+
min?: number;
|
|
241
|
+
max?: number;
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* 需求检查结果
|
|
245
|
+
*
|
|
246
|
+
* 用于返回需求检查的结果,包括是否通过和失败原因。
|
|
247
|
+
*/
|
|
248
|
+
interface RequirementCheckResult {
|
|
249
|
+
/**
|
|
250
|
+
* 是否通过需求检查
|
|
251
|
+
*/
|
|
252
|
+
passed: boolean;
|
|
253
|
+
/**
|
|
254
|
+
* 失败原因(可选)
|
|
255
|
+
*
|
|
256
|
+
* 当 passed 为 false 时,可以提供失败原因。
|
|
257
|
+
* UI 层可以显示这个原因给玩家。
|
|
258
|
+
*/
|
|
259
|
+
reason?: string;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 需求定义接口 - 定义执行动作前必须满足的条件
|
|
263
|
+
*
|
|
264
|
+
* 用于检查玩家是否有资格执行某个动作。
|
|
265
|
+
* 需求检查不会消耗资源,只是判断是否满足条件。
|
|
266
|
+
*
|
|
267
|
+
* @template S - 数值属性键的联合类型
|
|
268
|
+
* @template I - 物品ID的联合类型
|
|
269
|
+
* @template F - 标记键的联合类型
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* // 简单需求
|
|
274
|
+
* const simpleReqs: RequirementDef<Stats, Items, Flags> = {
|
|
275
|
+
* hasItems: ['key'],
|
|
276
|
+
* stats: { strength: { min: 5 } }
|
|
277
|
+
* };
|
|
278
|
+
*
|
|
279
|
+
* // 复杂逻辑使用 custom 函数
|
|
280
|
+
* const complexReqs: RequirementDef<Stats, Items, Flags> = {
|
|
281
|
+
* custom: (state) => {
|
|
282
|
+
* // (有钥匙 AND 力量>=5) OR 有万能钥匙
|
|
283
|
+
* const hasKeyAndStrength =
|
|
284
|
+
* state.inventory.includes('key') && state.stats.strength >= 5;
|
|
285
|
+
* const hasMasterKey = state.inventory.includes('master_key');
|
|
286
|
+
* return hasKeyAndStrength || hasMasterKey;
|
|
287
|
+
* }
|
|
288
|
+
* };
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
interface RequirementDef<S extends string, I extends string, F extends string, X = Record<string, unknown>> {
|
|
292
|
+
/**
|
|
293
|
+
* 数值属性需求 - 检查玩家属性是否满足条件
|
|
294
|
+
* 使用Partial允许只检查部分属性
|
|
295
|
+
* 所有指定的属性必须同时满足(AND逻辑)
|
|
296
|
+
*/
|
|
297
|
+
stats?: Partial<Record<S, StatRequirement>>;
|
|
298
|
+
/**
|
|
299
|
+
* 必须拥有的物品列表
|
|
300
|
+
* 玩家库存中必须包含所有列出的物品(AND逻辑)
|
|
301
|
+
*/
|
|
302
|
+
hasItems?: I[];
|
|
303
|
+
/**
|
|
304
|
+
* 不能拥有的物品列表
|
|
305
|
+
* 玩家库存中不能包含任何列出的物品(OR逻辑)
|
|
306
|
+
*/
|
|
307
|
+
noItems?: I[];
|
|
308
|
+
/**
|
|
309
|
+
* 必须为true的标记列表
|
|
310
|
+
* 所有列出的标记必须已设置为true(AND逻辑)
|
|
311
|
+
*/
|
|
312
|
+
hasFlags?: F[];
|
|
313
|
+
/**
|
|
314
|
+
* 必须为false的标记列表
|
|
315
|
+
* 所有列出的标记必须为false或未设置(OR逻辑)
|
|
316
|
+
*/
|
|
317
|
+
noFlags?: F[];
|
|
318
|
+
/**
|
|
319
|
+
* 自定义需求函数 - 用于复杂逻辑
|
|
320
|
+
*
|
|
321
|
+
* ⚠️ 增强功能:支持返回失败原因
|
|
322
|
+
*
|
|
323
|
+
* 当需要表达复杂的逻辑关系时使用此函数。
|
|
324
|
+
* 函数接收完整的游戏状态,可以返回:
|
|
325
|
+
* - boolean: true 表示满足需求,false 表示不满足
|
|
326
|
+
* - RequirementCheckResult: 包含是否通过和失败原因
|
|
327
|
+
*
|
|
328
|
+
* 返回失败原因的优势:
|
|
329
|
+
* - UI 可以显示具体的失败原因
|
|
330
|
+
* - 提供更好的用户体验
|
|
331
|
+
* - 帮助玩家理解为什么不能执行动作
|
|
332
|
+
*
|
|
333
|
+
* 可以在函数中实现任意复杂的逻辑,包括:
|
|
334
|
+
* - OR 逻辑:A || B
|
|
335
|
+
* - 复杂组合:(A && B) || C
|
|
336
|
+
* - 嵌套逻辑:((A || B) && C) || D
|
|
337
|
+
* - 动态计算:基于多个状态的复杂判断
|
|
338
|
+
*
|
|
339
|
+
* @param state - 当前游戏状态
|
|
340
|
+
* @returns boolean 或 RequirementCheckResult
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```typescript
|
|
344
|
+
* // 示例1:简单 boolean 返回
|
|
345
|
+
* custom: (state) =>
|
|
346
|
+
* state.inventory.includes('key') ||
|
|
347
|
+
* state.inventory.includes('lockpick')
|
|
348
|
+
*
|
|
349
|
+
* // 示例2:返回失败原因
|
|
350
|
+
* custom: (state) => {
|
|
351
|
+
* if (!state.inventory.includes('key')) {
|
|
352
|
+
* return {
|
|
353
|
+
* passed: false,
|
|
354
|
+
* reason: "需要钥匙才能打开这扇门"
|
|
355
|
+
* };
|
|
356
|
+
* }
|
|
357
|
+
*
|
|
358
|
+
* if (state.stats.strength < 5) {
|
|
359
|
+
* return {
|
|
360
|
+
* passed: false,
|
|
361
|
+
* reason: "力量不足(需要 5 点力量)"
|
|
362
|
+
* };
|
|
363
|
+
* }
|
|
364
|
+
*
|
|
365
|
+
* return { passed: true };
|
|
366
|
+
* }
|
|
367
|
+
*
|
|
368
|
+
* // 示例3:多个条件的详细提示
|
|
369
|
+
* custom: (state) => {
|
|
370
|
+
* const hasKey = state.inventory.includes('key');
|
|
371
|
+
* const hasLockpick = state.inventory.includes('lockpick');
|
|
372
|
+
* const hasStrength = state.stats.strength >= 10;
|
|
373
|
+
*
|
|
374
|
+
* if (!hasKey && !hasLockpick && !hasStrength) {
|
|
375
|
+
* return {
|
|
376
|
+
* passed: false,
|
|
377
|
+
* reason: "需要钥匙、撬锁工具或 10 点力量"
|
|
378
|
+
* };
|
|
379
|
+
* }
|
|
380
|
+
*
|
|
381
|
+
* if (hasLockpick && state.stats.dexterity < 5) {
|
|
382
|
+
* return {
|
|
383
|
+
* passed: false,
|
|
384
|
+
* reason: "使用撬锁工具需要 5 点敏捷"
|
|
385
|
+
* };
|
|
386
|
+
* }
|
|
387
|
+
*
|
|
388
|
+
* return { passed: true };
|
|
389
|
+
* }
|
|
390
|
+
*
|
|
391
|
+
* // 示例4:基于理智值的检查
|
|
392
|
+
* custom: (state) => {
|
|
393
|
+
* if (state.stats.sanity < 20) {
|
|
394
|
+
* return {
|
|
395
|
+
* passed: false,
|
|
396
|
+
* reason: "理智太低,无法集中精神进行仪式"
|
|
397
|
+
* };
|
|
398
|
+
* }
|
|
399
|
+
*
|
|
400
|
+
* if (state.stats.sanity < 50) {
|
|
401
|
+
* return {
|
|
402
|
+
* passed: true,
|
|
403
|
+
* reason: "你勉强能够进行仪式,但风险很大"
|
|
404
|
+
* };
|
|
405
|
+
* }
|
|
406
|
+
*
|
|
407
|
+
* return { passed: true };
|
|
408
|
+
* }
|
|
409
|
+
*
|
|
410
|
+
* // 示例5:时间相关的检查
|
|
411
|
+
* custom: (state) => {
|
|
412
|
+
* const time = state.world.time;
|
|
413
|
+
*
|
|
414
|
+
* if (time < 18 && time > 6) {
|
|
415
|
+
* return {
|
|
416
|
+
* passed: false,
|
|
417
|
+
* reason: "只能在夜晚(18:00-6:00)进行此仪式"
|
|
418
|
+
* };
|
|
419
|
+
* }
|
|
420
|
+
*
|
|
421
|
+
* return { passed: true };
|
|
422
|
+
* }
|
|
423
|
+
*
|
|
424
|
+
* // 示例6:组合多个条件
|
|
425
|
+
* custom: (state) => {
|
|
426
|
+
* const missingItems: string[] = [];
|
|
427
|
+
*
|
|
428
|
+
* if (!state.inventory.includes('candle')) {
|
|
429
|
+
* missingItems.push('蜡烛');
|
|
430
|
+
* }
|
|
431
|
+
* if (!state.inventory.includes('chalk')) {
|
|
432
|
+
* missingItems.push('粉笔');
|
|
433
|
+
* }
|
|
434
|
+
* if (!state.inventory.includes('book')) {
|
|
435
|
+
* missingItems.push('古书');
|
|
436
|
+
* }
|
|
437
|
+
*
|
|
438
|
+
* if (missingItems.length > 0) {
|
|
439
|
+
* return {
|
|
440
|
+
* passed: false,
|
|
441
|
+
* reason: `缺少必需物品:${missingItems.join('、')}`
|
|
442
|
+
* };
|
|
443
|
+
* }
|
|
444
|
+
*
|
|
445
|
+
* return { passed: true };
|
|
446
|
+
* }
|
|
447
|
+
*
|
|
448
|
+
* // 示例7:向后兼容 - 仍然可以返回 boolean
|
|
449
|
+
* custom: (state) => {
|
|
450
|
+
* return state.stats.level >= 10;
|
|
451
|
+
* }
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
custom?: (state: GameState<S, I, F, X>) => boolean | RequirementCheckResult;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* 效果定义接口 - 定义动作执行后对游戏状态的改变
|
|
458
|
+
*
|
|
459
|
+
* 描述执行动作后会产生的所有效果,包括属性变化、物品变化、
|
|
460
|
+
* 标记设置、位置传送等。
|
|
461
|
+
*
|
|
462
|
+
* ⚠️ 重要:效果执行顺序
|
|
463
|
+
*
|
|
464
|
+
* 效果按以下顺序执行(从上到下):
|
|
465
|
+
*
|
|
466
|
+
* 【阶段 1:静态效果】
|
|
467
|
+
* 1. statsChange - 数值属性变更
|
|
468
|
+
* 2. itemsAdd - 添加物品
|
|
469
|
+
* 3. itemsRemove - 移除物品
|
|
470
|
+
* 4. flagsSet - 设置标记
|
|
471
|
+
* 5. flagsBatch - 批量标记操作
|
|
472
|
+
* 6. teleport - 传送
|
|
473
|
+
* 7. timeAdvance + onTimeAdvance - 时间推进及其钩子
|
|
474
|
+
* 8. triggerEvent - 触发自定义事件
|
|
475
|
+
*
|
|
476
|
+
* 【阶段 2:条件效果】
|
|
477
|
+
* 9. conditionalEffects - 根据当前状态(包含阶段1的修改)决定额外效果
|
|
478
|
+
* - 返回的效果会按阶段1的顺序再次执行
|
|
479
|
+
*
|
|
480
|
+
* 【阶段 3:自定义效果】
|
|
481
|
+
* 10. custom - 修改 extra 扩展数据
|
|
482
|
+
* 11. customFull - 修改完整游戏状态(最强大,可覆盖之前的修改)
|
|
483
|
+
*
|
|
484
|
+
* 【阶段 4:后置钩子】
|
|
485
|
+
* 12. afterEffects - 在所有效果执行完成后调用(在 ActionDef 中定义)
|
|
486
|
+
*
|
|
487
|
+
* 【阶段 5:结果文本】
|
|
488
|
+
* 13. resultText - 生成反馈文本(在 ActionDef 中定义)
|
|
489
|
+
*
|
|
490
|
+
* 设计原则:
|
|
491
|
+
* - 简单效果在前,复杂效果在后
|
|
492
|
+
* - 静态效果在前,动态效果在后
|
|
493
|
+
* - 读取状态的效果在后(可以看到之前的修改)
|
|
494
|
+
* - 最强大的 customFull 在最后(可以覆盖一切)
|
|
495
|
+
*
|
|
496
|
+
* 使用建议:
|
|
497
|
+
* - 大多数情况使用静态效果(statsChange、itemsAdd 等)
|
|
498
|
+
* - 需要条件判断时使用 conditionalEffects
|
|
499
|
+
* - 需要修改 extra 时使用 custom
|
|
500
|
+
* - 需要复杂状态修改时使用 customFull
|
|
501
|
+
* - 需要基于最终状态执行副作用时使用 afterEffects
|
|
502
|
+
*
|
|
503
|
+
* @template S - 数值属性键的联合类型
|
|
504
|
+
* @template I - 物品ID的联合类型
|
|
505
|
+
* @template F - 标记键的联合类型
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```typescript
|
|
509
|
+
* // 示例1:简单效果(按顺序执行)
|
|
510
|
+
* const drinkPotionEffect: EffectDef<Stats, Items, Flags> = {
|
|
511
|
+
* statsChange: { hp: 20 }, // 1. 恢复20点生命值
|
|
512
|
+
* itemsRemove: ['potion'], // 2. 消耗药水
|
|
513
|
+
* flagsSet: { used_potion: true } // 3. 标记已使用过药水
|
|
514
|
+
* };
|
|
515
|
+
*
|
|
516
|
+
* // 示例2:利用执行顺序
|
|
517
|
+
* const complexEffect: EffectDef<Stats, Items, Flags> = {
|
|
518
|
+
* // 阶段1:先修改基础状态
|
|
519
|
+
* statsChange: { sanity: -10 },
|
|
520
|
+
* itemsAdd: ['cursed_item'],
|
|
521
|
+
*
|
|
522
|
+
* // 阶段2:基于修改后的状态决定额外效果
|
|
523
|
+
* conditionalEffects: (state) => {
|
|
524
|
+
* // 此时 sanity 已经减少了 10
|
|
525
|
+
* if (state.stats.sanity < 20) {
|
|
526
|
+
* return {
|
|
527
|
+
* flagsSet: { going_mad: true },
|
|
528
|
+
* statsChange: { sanity: -5 } // 额外减少
|
|
529
|
+
* };
|
|
530
|
+
* }
|
|
531
|
+
* return null;
|
|
532
|
+
* },
|
|
533
|
+
*
|
|
534
|
+
* // 阶段3:最后修改扩展数据
|
|
535
|
+
* custom: (draft, state) => {
|
|
536
|
+
* // 此时所有之前的效果都已执行
|
|
537
|
+
* draft.lastSanityLoss = Date.now();
|
|
538
|
+
* }
|
|
539
|
+
* };
|
|
540
|
+
*
|
|
541
|
+
* // 示例3:使用 customFull 覆盖之前的修改
|
|
542
|
+
* const overrideEffect: EffectDef<Stats, Items, Flags> = {
|
|
543
|
+
* statsChange: { hp: -50 }, // 先扣除50点生命
|
|
544
|
+
*
|
|
545
|
+
* customFull: (draft, original) => {
|
|
546
|
+
* // 如果有护盾,取消伤害
|
|
547
|
+
* if (original.flags.has_shield) {
|
|
548
|
+
* draft.stats.hp = original.stats.hp; // 恢复到原始值
|
|
549
|
+
* draft.flags.has_shield = false; // 消耗护盾
|
|
550
|
+
* }
|
|
551
|
+
* }
|
|
552
|
+
* };
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
interface EffectDef<S extends string, I extends string, F extends string, X = Record<string, unknown>> {
|
|
556
|
+
/**
|
|
557
|
+
* 数值属性变更
|
|
558
|
+
*
|
|
559
|
+
* 可以是静态对象或动态函数:
|
|
560
|
+
* - 静态对象:直接指定属性变化量(正数增加,负数减少)
|
|
561
|
+
* - 动态函数:根据游戏状态动态计算属性变化
|
|
562
|
+
*
|
|
563
|
+
* 函数形式的优势:
|
|
564
|
+
* - 类型自动推断,无需手动标注
|
|
565
|
+
* - 可以基于当前状态计算变化量
|
|
566
|
+
* - 支持复杂的计算逻辑
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```typescript
|
|
570
|
+
* // 静态变更
|
|
571
|
+
* statsChange: { hp: 10, gold: -5 }
|
|
572
|
+
*
|
|
573
|
+
* // 动态变更 - 基于当前状态计算
|
|
574
|
+
* statsChange: (state) => {
|
|
575
|
+
* // state 类型自动推断,无需手动标注
|
|
576
|
+
* const maxAP = state.stats.max_action_point || 4;
|
|
577
|
+
* const currentAP = state.stats.action_point || 0;
|
|
578
|
+
*
|
|
579
|
+
* return {
|
|
580
|
+
* action_point: maxAP - currentAP // 恢复到最大值
|
|
581
|
+
* };
|
|
582
|
+
* }
|
|
583
|
+
*
|
|
584
|
+
* // 基于时间的动态变更
|
|
585
|
+
* statsChange: (state) => {
|
|
586
|
+
* const isNight = state.world.time >= 18 || state.world.time <= 6;
|
|
587
|
+
*
|
|
588
|
+
* return {
|
|
589
|
+
* sanity: isNight ? -10 : -5, // 夜晚消耗更多理智
|
|
590
|
+
* knowledge: isNight ? 3 : 1 // 夜晚获得更多知识
|
|
591
|
+
* };
|
|
592
|
+
* }
|
|
593
|
+
*
|
|
594
|
+
* // 基于物品的动态变更
|
|
595
|
+
* statsChange: (state) => {
|
|
596
|
+
* const hasAmulet = state.inventory.includes('protective_amulet' as any);
|
|
597
|
+
* const baseDamage = -20;
|
|
598
|
+
*
|
|
599
|
+
* return {
|
|
600
|
+
* health: hasAmulet ? baseDamage / 2 : baseDamage // 护符减半伤害
|
|
601
|
+
* };
|
|
602
|
+
* }
|
|
603
|
+
*
|
|
604
|
+
* // 基于百分比的变更
|
|
605
|
+
* statsChange: (state) => {
|
|
606
|
+
* const currentHP = state.stats.health;
|
|
607
|
+
* const maxHP = state.stats.max_health || 100;
|
|
608
|
+
*
|
|
609
|
+
* return {
|
|
610
|
+
* health: Math.floor(maxHP * 0.5) - currentHP // 恢复到50%
|
|
611
|
+
* };
|
|
612
|
+
* }
|
|
613
|
+
*
|
|
614
|
+
* // 复杂的条件计算
|
|
615
|
+
* statsChange: (state) => {
|
|
616
|
+
* const changes: Partial<Record<S, number>> = {};
|
|
617
|
+
*
|
|
618
|
+
* // 基础消耗
|
|
619
|
+
* changes.stamina = -10;
|
|
620
|
+
*
|
|
621
|
+
* // 如果理智低,额外消耗
|
|
622
|
+
* if (state.stats.sanity < 30) {
|
|
623
|
+
* changes.stamina = -15;
|
|
624
|
+
* changes.sanity = -5;
|
|
625
|
+
* }
|
|
626
|
+
*
|
|
627
|
+
* // 如果有特殊状态,获得奖励
|
|
628
|
+
* if (state.flags.blessed) {
|
|
629
|
+
* changes.luck = 1;
|
|
630
|
+
* }
|
|
631
|
+
*
|
|
632
|
+
* return changes;
|
|
633
|
+
* }
|
|
634
|
+
* ```
|
|
635
|
+
*/
|
|
636
|
+
statsChange?: Partial<Record<S, number>> | ((state: GameState<S, I, F, X>) => Partial<Record<S, number>>);
|
|
637
|
+
/**
|
|
638
|
+
* 添加到库存的物品列表
|
|
639
|
+
* 物品会被添加到玩家的inventory数组中
|
|
640
|
+
*/
|
|
641
|
+
itemsAdd?: I[];
|
|
642
|
+
/**
|
|
643
|
+
* 从库存移除的物品列表
|
|
644
|
+
* 每个物品只会移除一次(如果有多个相同物品)
|
|
645
|
+
*/
|
|
646
|
+
itemsRemove?: I[];
|
|
647
|
+
/**
|
|
648
|
+
* 标记设置
|
|
649
|
+
*
|
|
650
|
+
* 可以是静态对象或动态函数:
|
|
651
|
+
* - 静态对象:直接设置指定标记的布尔值
|
|
652
|
+
* - 动态函数:根据游戏状态动态生成标记
|
|
653
|
+
*
|
|
654
|
+
* @example
|
|
655
|
+
* ```typescript
|
|
656
|
+
* // 静态标记
|
|
657
|
+
* flagsSet: { quest_completed: true, door_locked: false }
|
|
658
|
+
*
|
|
659
|
+
* // 动态标记 - 根据游戏状态生成
|
|
660
|
+
* flagsSet: (state) => {
|
|
661
|
+
* const day = state.world.day;
|
|
662
|
+
* return {
|
|
663
|
+
* [`event_day_${day}`]: true,
|
|
664
|
+
* [`gazed_stars_day_${day}`]: true
|
|
665
|
+
* };
|
|
666
|
+
* }
|
|
667
|
+
*
|
|
668
|
+
* // 混合使用 - 结合静态和动态逻辑
|
|
669
|
+
* flagsSet: (state) => {
|
|
670
|
+
* const flags: Record<string, boolean> = {
|
|
671
|
+
* action_taken: true // 静态标记
|
|
672
|
+
* };
|
|
673
|
+
*
|
|
674
|
+
* // 动态添加标记
|
|
675
|
+
* if (state.world.time >= 18) {
|
|
676
|
+
* flags.night_action = true;
|
|
677
|
+
* }
|
|
678
|
+
*
|
|
679
|
+
* return flags;
|
|
680
|
+
* }
|
|
681
|
+
* ```
|
|
682
|
+
*/
|
|
683
|
+
flagsSet?: Partial<Record<F, boolean>> | ((state: GameState<S, I, F, X>) => Record<string, boolean>);
|
|
684
|
+
/**
|
|
685
|
+
* 批量标记操作 - 支持模式匹配和批量设置/清除
|
|
686
|
+
*
|
|
687
|
+
* ⚠️ 高级功能:用于批量管理大量标记
|
|
688
|
+
*
|
|
689
|
+
* 提供更强大的标记操作能力:
|
|
690
|
+
* - 批量清除:根据模式或列表清除多个标记
|
|
691
|
+
* - 批量设置:同时设置多个标记
|
|
692
|
+
* - 模式匹配:使用正则表达式或前缀匹配
|
|
693
|
+
*
|
|
694
|
+
* 执行顺序:
|
|
695
|
+
* 1. 先执行 clear(清除操作)
|
|
696
|
+
* 2. 再执行 set(设置操作)
|
|
697
|
+
* 3. flagsBatch 在 flagsSet 之后执行
|
|
698
|
+
*
|
|
699
|
+
* @example
|
|
700
|
+
* ```typescript
|
|
701
|
+
* // 示例1:清除所有每日标记
|
|
702
|
+
* flagsBatch: {
|
|
703
|
+
* clear: /^daily_.*$/, // 清除所有 daily_ 开头的标记
|
|
704
|
+
* set: { new_day: true }
|
|
705
|
+
* }
|
|
706
|
+
*
|
|
707
|
+
* // 示例2:使用字符串前缀匹配
|
|
708
|
+
* flagsBatch: {
|
|
709
|
+
* clear: 'temp_', // 清除所有 temp_ 开头的标记
|
|
710
|
+
* set: { cleanup_done: true }
|
|
711
|
+
* }
|
|
712
|
+
*
|
|
713
|
+
* // 示例3:清除指定列表的标记
|
|
714
|
+
* flagsBatch: {
|
|
715
|
+
* clear: ['flag1', 'flag2', 'flag3'],
|
|
716
|
+
* set: { reset_complete: true }
|
|
717
|
+
* }
|
|
718
|
+
*
|
|
719
|
+
* // 示例4:动态批量操作
|
|
720
|
+
* flagsBatch: (state) => {
|
|
721
|
+
* const currentDay = state.world.day;
|
|
722
|
+
* const flagsToClear: string[] = [];
|
|
723
|
+
*
|
|
724
|
+
* // 清除7天前的事件标记
|
|
725
|
+
* for (let i = 1; i <= 7; i++) {
|
|
726
|
+
* const oldDay = currentDay - 7;
|
|
727
|
+
* if (oldDay > 0) {
|
|
728
|
+
* flagsToClear.push(`event_day_${oldDay}`);
|
|
729
|
+
* flagsToClear.push(`action_day_${oldDay}`);
|
|
730
|
+
* }
|
|
731
|
+
* }
|
|
732
|
+
*
|
|
733
|
+
* return {
|
|
734
|
+
* clear: flagsToClear,
|
|
735
|
+
* set: { [`day_${currentDay}_started`]: true }
|
|
736
|
+
* };
|
|
737
|
+
* }
|
|
738
|
+
*
|
|
739
|
+
* // 示例5:基于条件的批量清除
|
|
740
|
+
* flagsBatch: (state) => {
|
|
741
|
+
* if (state.flags.game_reset) {
|
|
742
|
+
* // 游戏重置时清除所有进度标记
|
|
743
|
+
* return {
|
|
744
|
+
* clear: /^(quest_|achievement_|unlock_)/,
|
|
745
|
+
* set: {
|
|
746
|
+
* game_reset: false,
|
|
747
|
+
* fresh_start: true
|
|
748
|
+
* }
|
|
749
|
+
* };
|
|
750
|
+
* }
|
|
751
|
+
* return null; // 不执行批量操作
|
|
752
|
+
* }
|
|
753
|
+
*
|
|
754
|
+
* // 示例6:清除过期的临时标记
|
|
755
|
+
* flagsBatch: (state) => {
|
|
756
|
+
* const allFlags = Object.keys(state.flags);
|
|
757
|
+
* const tempFlags = allFlags.filter(f => f.startsWith('temp_'));
|
|
758
|
+
*
|
|
759
|
+
* return {
|
|
760
|
+
* clear: tempFlags,
|
|
761
|
+
* set: { temp_cleanup_done: true }
|
|
762
|
+
* };
|
|
763
|
+
* }
|
|
764
|
+
*
|
|
765
|
+
* // 示例7:只清除不设置
|
|
766
|
+
* flagsBatch: {
|
|
767
|
+
* clear: /^cache_/ // 只清除缓存标记
|
|
768
|
+
* }
|
|
769
|
+
*
|
|
770
|
+
* // 示例8:只设置不清除
|
|
771
|
+
* flagsBatch: {
|
|
772
|
+
* set: {
|
|
773
|
+
* flag1: true,
|
|
774
|
+
* flag2: false,
|
|
775
|
+
* flag3: true
|
|
776
|
+
* }
|
|
777
|
+
* }
|
|
778
|
+
*
|
|
779
|
+
* // 示例9:组合多种模式
|
|
780
|
+
* flagsBatch: (state) => {
|
|
781
|
+
* const operations: FlagsBatchOperation<F> = {};
|
|
782
|
+
*
|
|
783
|
+
* // 清除所有临时标记
|
|
784
|
+
* if (state.world.day % 7 === 0) {
|
|
785
|
+
* operations.clear = /^temp_/;
|
|
786
|
+
* }
|
|
787
|
+
*
|
|
788
|
+
* // 设置新的周期标记
|
|
789
|
+
* operations.set = {
|
|
790
|
+
* [`week_${Math.floor(state.world.day / 7)}`]: true
|
|
791
|
+
* };
|
|
792
|
+
*
|
|
793
|
+
* return operations;
|
|
794
|
+
* }
|
|
795
|
+
* ```
|
|
796
|
+
*/
|
|
797
|
+
flagsBatch?: FlagsBatchOperation<F> | ((state: GameState<S, I, F, X>) => FlagsBatchOperation<F> | null | undefined);
|
|
798
|
+
/**
|
|
799
|
+
* 传送目标位置ID
|
|
800
|
+
* 如果设置,玩家会被传送到指定位置
|
|
801
|
+
*/
|
|
802
|
+
teleport?: string;
|
|
803
|
+
/**
|
|
804
|
+
* 时间推进
|
|
805
|
+
*
|
|
806
|
+
* 增加游戏内的天数。可以是静态数值或动态函数。
|
|
807
|
+
*
|
|
808
|
+
* @example
|
|
809
|
+
* ```typescript
|
|
810
|
+
* // 静态推进
|
|
811
|
+
* timeAdvance: 1 // 推进1天
|
|
812
|
+
*
|
|
813
|
+
* // 动态推进
|
|
814
|
+
* timeAdvance: (state) => {
|
|
815
|
+
* // 根据状态决定推进天数
|
|
816
|
+
* return state.flags.fast_travel ? 3 : 1;
|
|
817
|
+
* }
|
|
818
|
+
* ```
|
|
819
|
+
*/
|
|
820
|
+
timeAdvance?: number | ((state: GameState<S, I, F, X>) => number);
|
|
821
|
+
/**
|
|
822
|
+
* 时间推进钩子 - 在时间推进时自动执行
|
|
823
|
+
*
|
|
824
|
+
* ⚠️ 实用功能:自动处理时间相关的状态变化
|
|
825
|
+
*
|
|
826
|
+
* 当 timeAdvance 执行后,此钩子会被调用。
|
|
827
|
+
* 可以用于:
|
|
828
|
+
* - 清除每日标记
|
|
829
|
+
* - 恢复每日资源
|
|
830
|
+
* - 触发时间相关事件
|
|
831
|
+
* - 更新时间相关状态
|
|
832
|
+
*
|
|
833
|
+
* 执行时机:
|
|
834
|
+
* - 在 timeAdvance 修改 world.day 之后立即执行
|
|
835
|
+
* - 在其他效果之后执行
|
|
836
|
+
* - 每推进一天调用一次(如果推进多天,会多次调用)
|
|
837
|
+
*
|
|
838
|
+
* 注意事项:
|
|
839
|
+
* 1. 函数接收推进后的天数和推进前的天数
|
|
840
|
+
* 2. 如果推进多天,钩子会为每一天分别调用
|
|
841
|
+
* 3. 可以访问和修改游戏状态
|
|
842
|
+
* 4. 不应返回值,所有修改通过参数进行
|
|
843
|
+
*
|
|
844
|
+
* @param currentDay - 推进后的当前天数
|
|
845
|
+
* @param previousDay - 推进前的天数
|
|
846
|
+
* @param state - 当前游戏状态(可读)
|
|
847
|
+
* @param actions - Store 操作方法集合(用于修改状态)
|
|
848
|
+
*
|
|
849
|
+
* @example
|
|
850
|
+
* ```typescript
|
|
851
|
+
* // 示例1:清除每日标记
|
|
852
|
+
* {
|
|
853
|
+
* timeAdvance: 1,
|
|
854
|
+
* onTimeAdvance: (currentDay, previousDay, state, actions) => {
|
|
855
|
+
* // 清除所有 daily_ 开头的标记
|
|
856
|
+
* const dailyFlags = Object.keys(state.flags)
|
|
857
|
+
* .filter(f => f.startsWith('daily_'));
|
|
858
|
+
*
|
|
859
|
+
* dailyFlags.forEach(f => {
|
|
860
|
+
* actions.setFlag(f as F, false);
|
|
861
|
+
* });
|
|
862
|
+
*
|
|
863
|
+
* // 设置新的一天标记
|
|
864
|
+
* actions.setFlag(`day_${currentDay}_started` as F, true);
|
|
865
|
+
* }
|
|
866
|
+
* }
|
|
867
|
+
*
|
|
868
|
+
* // 示例2:恢复每日资源
|
|
869
|
+
* {
|
|
870
|
+
* timeAdvance: 1,
|
|
871
|
+
* onTimeAdvance: (currentDay, previousDay, state, actions) => {
|
|
872
|
+
* // 恢复每日行动点
|
|
873
|
+
* const maxAP = state.stats.max_action_point || 4;
|
|
874
|
+
* const currentAP = state.stats.action_point || 0;
|
|
875
|
+
* actions.setStats({
|
|
876
|
+
* action_point: maxAP - currentAP
|
|
877
|
+
* });
|
|
878
|
+
*
|
|
879
|
+
* // 恢复部分理智
|
|
880
|
+
* actions.setStats({ sanity: 10 });
|
|
881
|
+
*
|
|
882
|
+
* actions.addLog(`第 ${currentDay} 天开始`, 'info');
|
|
883
|
+
* }
|
|
884
|
+
* }
|
|
885
|
+
*
|
|
886
|
+
* // 示例3:触发周期性事件
|
|
887
|
+
* {
|
|
888
|
+
* timeAdvance: 1,
|
|
889
|
+
* onTimeAdvance: (currentDay, previousDay, state, actions) => {
|
|
890
|
+
* // 每7天触发一次特殊事件
|
|
891
|
+
* if (currentDay % 7 === 0) {
|
|
892
|
+
* actions.setFlag('weekly_event_available', true);
|
|
893
|
+
* actions.showToast('新的周期事件可用!', 'info');
|
|
894
|
+
* }
|
|
895
|
+
*
|
|
896
|
+
* // 每30天触发月度事件
|
|
897
|
+
* if (currentDay % 30 === 0) {
|
|
898
|
+
* actions.setFlag('monthly_event_available', true);
|
|
899
|
+
* actions.showModal('月圆之夜,诡异的事情即将发生...', 'warn');
|
|
900
|
+
* }
|
|
901
|
+
* }
|
|
902
|
+
* }
|
|
903
|
+
*
|
|
904
|
+
* // 示例4:清理过期数据
|
|
905
|
+
* {
|
|
906
|
+
* timeAdvance: 1,
|
|
907
|
+
* onTimeAdvance: (currentDay, previousDay, state, actions) => {
|
|
908
|
+
* // 清理7天前的临时标记
|
|
909
|
+
* const oldDay = currentDay - 7;
|
|
910
|
+
* if (oldDay > 0) {
|
|
911
|
+
* const oldFlags = [
|
|
912
|
+
* `temp_event_day_${oldDay}`,
|
|
913
|
+
* `daily_action_day_${oldDay}`,
|
|
914
|
+
* `cache_day_${oldDay}`
|
|
915
|
+
* ];
|
|
916
|
+
*
|
|
917
|
+
* oldFlags.forEach(f => {
|
|
918
|
+
* actions.setFlag(f as F, false);
|
|
919
|
+
* });
|
|
920
|
+
* }
|
|
921
|
+
* }
|
|
922
|
+
* }
|
|
923
|
+
*
|
|
924
|
+
* // 示例5:时间相关的状态衰减
|
|
925
|
+
* {
|
|
926
|
+
* timeAdvance: 1,
|
|
927
|
+
* onTimeAdvance: (currentDay, previousDay, state, actions) => {
|
|
928
|
+
* const daysPassed = currentDay - previousDay;
|
|
929
|
+
*
|
|
930
|
+
* // 每天衰减一些属性
|
|
931
|
+
* actions.setStats({
|
|
932
|
+
* hunger: daysPassed * 10, // 饥饿度增加
|
|
933
|
+
* fatigue: daysPassed * 5 // 疲劳度增加
|
|
934
|
+
* });
|
|
935
|
+
*
|
|
936
|
+
* // 如果有食物,自动消耗
|
|
937
|
+
* if (state.inventory.includes('food' as I)) {
|
|
938
|
+
* actions.removeItem('food' as I);
|
|
939
|
+
* actions.setStats({ hunger: -20 });
|
|
940
|
+
* }
|
|
941
|
+
* }
|
|
942
|
+
* }
|
|
943
|
+
*
|
|
944
|
+
* // 示例6:使用批量标记操作
|
|
945
|
+
* {
|
|
946
|
+
* timeAdvance: 1,
|
|
947
|
+
* onTimeAdvance: (currentDay, previousDay, state, actions) => {
|
|
948
|
+
* // 注意:这里不能直接使用 flagsBatch
|
|
949
|
+
* // 需要手动实现批量清除逻辑
|
|
950
|
+
* const dailyFlags = Object.keys(state.flags)
|
|
951
|
+
* .filter(f => f.startsWith('daily_'));
|
|
952
|
+
*
|
|
953
|
+
* dailyFlags.forEach(f => actions.setFlag(f as F, false));
|
|
954
|
+
*
|
|
955
|
+
* // 设置新的每日标记
|
|
956
|
+
* actions.setFlag('daily_reset_done', true);
|
|
957
|
+
* actions.setFlag(`day_${currentDay}`, true);
|
|
958
|
+
* }
|
|
959
|
+
* }
|
|
960
|
+
*
|
|
961
|
+
* // 示例7:多天推进的处理
|
|
962
|
+
* {
|
|
963
|
+
* timeAdvance: (state) => state.flags.fast_travel ? 3 : 1,
|
|
964
|
+
* onTimeAdvance: (currentDay, previousDay, state, actions) => {
|
|
965
|
+
* // 这个钩子会为每一天分别调用
|
|
966
|
+
* // 如果推进3天,会调用3次
|
|
967
|
+
*
|
|
968
|
+
* actions.addLog(`第 ${currentDay} 天`, 'info');
|
|
969
|
+
*
|
|
970
|
+
* // 每天恢复资源
|
|
971
|
+
* actions.setStats({ action_point: 4 });
|
|
972
|
+
* }
|
|
973
|
+
* }
|
|
974
|
+
* ```
|
|
975
|
+
*/
|
|
976
|
+
onTimeAdvance?: (currentDay: number, previousDay: number, state: GameState<S, I, F, X>, actions: GameStoreActions<S, I, F, X>) => void;
|
|
977
|
+
/**
|
|
978
|
+
* 触发特殊事件的标识符
|
|
979
|
+
* 用于触发游戏中的特殊事件或剧情
|
|
980
|
+
*/
|
|
981
|
+
triggerEvent?: string;
|
|
982
|
+
/**
|
|
983
|
+
* 自定义效果函数 - 用于修改扩展数据
|
|
984
|
+
*
|
|
985
|
+
* 当需要修改 extra 字段中的自定义数据时使用。
|
|
986
|
+
* 函数接收当前的 extra 数据和完整游戏状态。
|
|
987
|
+
* 可以直接修改 draft(Immer 风格),或返回部分更新。
|
|
988
|
+
*
|
|
989
|
+
* @param draft - 当前的扩展数据(可直接修改)
|
|
990
|
+
* @param state - 完整的游戏状态(只读)
|
|
991
|
+
* @returns void 或部分更新对象
|
|
992
|
+
*
|
|
993
|
+
* @example
|
|
994
|
+
* ```typescript
|
|
995
|
+
* // 示例1:直接修改 draft
|
|
996
|
+
* custom: (draft) => {
|
|
997
|
+
* draft.reputation += 10;
|
|
998
|
+
* draft.achievements.push('first_quest');
|
|
999
|
+
* }
|
|
1000
|
+
*
|
|
1001
|
+
* // 示例2:返回部分更新
|
|
1002
|
+
* custom: (draft, state) => {
|
|
1003
|
+
* return {
|
|
1004
|
+
* reputation: draft.reputation + 10,
|
|
1005
|
+
* lastAction: Date.now()
|
|
1006
|
+
* };
|
|
1007
|
+
* }
|
|
1008
|
+
* ```
|
|
1009
|
+
*/
|
|
1010
|
+
custom?: (draft: X, state: GameState<S, I, F, X>) => void | Partial<X>;
|
|
1011
|
+
/**
|
|
1012
|
+
* 条件性效果 - 根据游戏状态动态决定要应用的效果
|
|
1013
|
+
*
|
|
1014
|
+
* ⚠️ 实用功能:根据条件返回不同的效果配置
|
|
1015
|
+
*
|
|
1016
|
+
* 当需要根据游戏状态决定效果内容时使用。函数接收当前游戏状态,
|
|
1017
|
+
* 返回要应用的效果定义。这允许实现:
|
|
1018
|
+
* - 基于条件的不同效果
|
|
1019
|
+
* - 动态计算的效果数值
|
|
1020
|
+
* - 复杂的分支逻辑
|
|
1021
|
+
*
|
|
1022
|
+
* 执行顺序:
|
|
1023
|
+
* 1. 先执行静态效果(statsChange、itemsAdd 等)
|
|
1024
|
+
* 2. 再执行 conditionalEffects 返回的效果
|
|
1025
|
+
* 3. 最后执行 custom 和 customFull
|
|
1026
|
+
*
|
|
1027
|
+
* 注意事项:
|
|
1028
|
+
* 1. conditionalEffects 返回的效果会叠加到静态效果上
|
|
1029
|
+
* 2. 如果只需要条件效果,可以不定义静态效果字段
|
|
1030
|
+
* 3. 返回的效果对象支持所有标准效果字段(除了 conditionalEffects 本身)
|
|
1031
|
+
* 4. 可以返回 null 或 undefined 表示不应用额外效果
|
|
1032
|
+
*
|
|
1033
|
+
* @param state - 当前游戏状态(执行成本扣除后的状态)
|
|
1034
|
+
* @returns 要应用的效果定义,或 null/undefined 表示无额外效果
|
|
1035
|
+
*
|
|
1036
|
+
* @example
|
|
1037
|
+
* ```typescript
|
|
1038
|
+
* // 示例1:基于条件的不同效果
|
|
1039
|
+
* conditionalEffects: (state) => {
|
|
1040
|
+
* const today = `gazed_stars_day_${state.world.day}`;
|
|
1041
|
+
*
|
|
1042
|
+
* if (state.flags[today]) {
|
|
1043
|
+
* // 今天已经观星过,效果减弱
|
|
1044
|
+
* return {
|
|
1045
|
+
* statsChange: { sanity: -2, knowledge: 0 },
|
|
1046
|
+
* flagsSet: { repeated_action: true }
|
|
1047
|
+
* };
|
|
1048
|
+
* } else {
|
|
1049
|
+
* // 第一次观星,正常效果
|
|
1050
|
+
* return {
|
|
1051
|
+
* statsChange: { sanity: -5, knowledge: 1 },
|
|
1052
|
+
* flagsSet: { [today]: true }
|
|
1053
|
+
* };
|
|
1054
|
+
* }
|
|
1055
|
+
* }
|
|
1056
|
+
*
|
|
1057
|
+
* // 示例2:基于时间的动态效果
|
|
1058
|
+
* conditionalEffects: (state) => {
|
|
1059
|
+
* const time = state.world.time;
|
|
1060
|
+
*
|
|
1061
|
+
* if (time >= 22 || time <= 4) {
|
|
1062
|
+
* // 深夜时间,效果增强
|
|
1063
|
+
* return {
|
|
1064
|
+
* statsChange: { sanity: -10, knowledge: 3 },
|
|
1065
|
+
* flagsSet: { midnight_ritual: true }
|
|
1066
|
+
* };
|
|
1067
|
+
* } else if (time >= 6 && time <= 18) {
|
|
1068
|
+
* // 白天,效果减弱
|
|
1069
|
+
* return {
|
|
1070
|
+
* statsChange: { sanity: -2, knowledge: 1 }
|
|
1071
|
+
* };
|
|
1072
|
+
* } else {
|
|
1073
|
+
* // 黄昏,正常效果
|
|
1074
|
+
* return {
|
|
1075
|
+
* statsChange: { sanity: -5, knowledge: 2 }
|
|
1076
|
+
* };
|
|
1077
|
+
* }
|
|
1078
|
+
* }
|
|
1079
|
+
*
|
|
1080
|
+
* // 示例3:基于物品的效果变化
|
|
1081
|
+
* conditionalEffects: (state) => {
|
|
1082
|
+
* const hasProtection = state.inventory.includes('protective_charm' as any);
|
|
1083
|
+
* const hasCurse = state.flags.cursed;
|
|
1084
|
+
*
|
|
1085
|
+
* if (hasProtection && hasCurse) {
|
|
1086
|
+
* // 有护符但被诅咒,护符被摧毁
|
|
1087
|
+
* return {
|
|
1088
|
+
* itemsRemove: ['protective_charm' as any],
|
|
1089
|
+
* statsChange: { sanity: -15 },
|
|
1090
|
+
* flagsSet: { charm_destroyed: true }
|
|
1091
|
+
* };
|
|
1092
|
+
* } else if (hasProtection) {
|
|
1093
|
+
* // 有护符,减少伤害
|
|
1094
|
+
* return {
|
|
1095
|
+
* statsChange: { sanity: -3 }
|
|
1096
|
+
* };
|
|
1097
|
+
* } else {
|
|
1098
|
+
* // 无保护,正常伤害
|
|
1099
|
+
* return {
|
|
1100
|
+
* statsChange: { sanity: -10 }
|
|
1101
|
+
* };
|
|
1102
|
+
* }
|
|
1103
|
+
* }
|
|
1104
|
+
*
|
|
1105
|
+
* // 示例4:基于数值的动态效果
|
|
1106
|
+
* conditionalEffects: (state) => {
|
|
1107
|
+
* const sanity = state.stats.sanity;
|
|
1108
|
+
*
|
|
1109
|
+
* if (sanity < 20) {
|
|
1110
|
+
* // 理智极低,产生幻觉
|
|
1111
|
+
* return {
|
|
1112
|
+
* statsChange: { sanity: -5 },
|
|
1113
|
+
* itemsAdd: ['hallucination' as any],
|
|
1114
|
+
* flagsSet: { insane: true },
|
|
1115
|
+
* triggerEvent: 'madness_event'
|
|
1116
|
+
* };
|
|
1117
|
+
* } else if (sanity < 50) {
|
|
1118
|
+
* // 理智较低,轻微影响
|
|
1119
|
+
* return {
|
|
1120
|
+
* statsChange: { sanity: -3 },
|
|
1121
|
+
* flagsSet: { unstable: true }
|
|
1122
|
+
* };
|
|
1123
|
+
* } else {
|
|
1124
|
+
* // 理智正常
|
|
1125
|
+
* return {
|
|
1126
|
+
* statsChange: { sanity: -1 }
|
|
1127
|
+
* };
|
|
1128
|
+
* }
|
|
1129
|
+
* }
|
|
1130
|
+
*
|
|
1131
|
+
* // 示例5:无额外效果
|
|
1132
|
+
* conditionalEffects: (state) => {
|
|
1133
|
+
* // 某些条件下不产生额外效果
|
|
1134
|
+
* if (state.flags.immune) {
|
|
1135
|
+
* return null; // 或 undefined
|
|
1136
|
+
* }
|
|
1137
|
+
*
|
|
1138
|
+
* return {
|
|
1139
|
+
* statsChange: { health: -10 }
|
|
1140
|
+
* };
|
|
1141
|
+
* }
|
|
1142
|
+
*
|
|
1143
|
+
* // 示例6:与静态效果结合
|
|
1144
|
+
* // 静态效果总是执行,条件效果叠加
|
|
1145
|
+
* {
|
|
1146
|
+
* statsChange: { action_points: -1 }, // 总是消耗行动点
|
|
1147
|
+
* conditionalEffects: (state) => {
|
|
1148
|
+
* // 根据条件添加额外效果
|
|
1149
|
+
* if (state.world.time === 0) {
|
|
1150
|
+
* return {
|
|
1151
|
+
* statsChange: { sanity: -5 },
|
|
1152
|
+
* flagsSet: { midnight_action: true }
|
|
1153
|
+
* };
|
|
1154
|
+
* }
|
|
1155
|
+
* return null;
|
|
1156
|
+
* }
|
|
1157
|
+
* }
|
|
1158
|
+
* ```
|
|
1159
|
+
*/
|
|
1160
|
+
conditionalEffects?: (state: GameState<S, I, F, X>) => Omit<EffectDef<S, I, F, X>, 'conditionalEffects'> | null | undefined;
|
|
1161
|
+
/**
|
|
1162
|
+
* 完整自定义效果函数 - 用于修改完整游戏状态
|
|
1163
|
+
*
|
|
1164
|
+
* ⚠️ 高级功能:提供对完整游戏状态的访问和修改权限
|
|
1165
|
+
*
|
|
1166
|
+
* 当需要执行复杂的状态修改逻辑时使用,可以同时修改:
|
|
1167
|
+
* - stats: 数值属性
|
|
1168
|
+
* - inventory: 物品清单
|
|
1169
|
+
* - flags: 布尔标记
|
|
1170
|
+
* - world: 世界状态
|
|
1171
|
+
* - extra: 扩展数据
|
|
1172
|
+
*
|
|
1173
|
+
* 注意事项:
|
|
1174
|
+
* 1. 不要修改 logs 字段,日志应该通过 resultText 或单独的 addLog 调用添加
|
|
1175
|
+
* 2. 使用 customFull 时,其他效果字段(statsChange、itemsAdd 等)仍会执行
|
|
1176
|
+
* 3. customFull 在其他效果之后执行,可以覆盖或调整之前的修改
|
|
1177
|
+
* 4. 函数接收的是状态的副本,可以直接修改,无需返回值
|
|
1178
|
+
*
|
|
1179
|
+
* @param draft - 完整游戏状态的可修改副本(不包含 logs)
|
|
1180
|
+
* @param originalState - 执行效果前的原始状态(只读,用于参考)
|
|
1181
|
+
*
|
|
1182
|
+
* @example
|
|
1183
|
+
* ```typescript
|
|
1184
|
+
* // 示例1:复杂的条件逻辑
|
|
1185
|
+
* customFull: (draft, original) => {
|
|
1186
|
+
* // 根据理智值调整多个状态
|
|
1187
|
+
* if (draft.stats.sanity < 30) {
|
|
1188
|
+
* draft.stats.sanity -= 10;
|
|
1189
|
+
* draft.flags.going_insane = true;
|
|
1190
|
+
* draft.inventory.push('hallucination' as any);
|
|
1191
|
+
* draft.extra.mentalState = 'critical';
|
|
1192
|
+
* }
|
|
1193
|
+
* }
|
|
1194
|
+
*
|
|
1195
|
+
* // 示例2:基于时间的复杂效果
|
|
1196
|
+
* customFull: (draft) => {
|
|
1197
|
+
* const day = draft.world.day;
|
|
1198
|
+
*
|
|
1199
|
+
* // 每7天触发特殊事件
|
|
1200
|
+
* if (day % 7 === 0) {
|
|
1201
|
+
* draft.stats.sanity -= 5;
|
|
1202
|
+
* draft.flags[`week_${Math.floor(day / 7)}_event`] = true;
|
|
1203
|
+
* }
|
|
1204
|
+
*
|
|
1205
|
+
* // 根据物品数量调整属性
|
|
1206
|
+
* const itemCount = draft.inventory.length;
|
|
1207
|
+
* if (itemCount > 10) {
|
|
1208
|
+
* draft.stats.burden = itemCount - 10;
|
|
1209
|
+
* }
|
|
1210
|
+
* }
|
|
1211
|
+
*
|
|
1212
|
+
* // 示例3:连锁反应
|
|
1213
|
+
* customFull: (draft, original) => {
|
|
1214
|
+
* // 如果获得了特殊物品,触发连锁效果
|
|
1215
|
+
* if (draft.inventory.includes('cursed_artifact' as any)) {
|
|
1216
|
+
* draft.stats.sanity -= 20;
|
|
1217
|
+
* draft.stats.power += 50;
|
|
1218
|
+
* draft.flags.cursed = true;
|
|
1219
|
+
* draft.extra.curseLevel = (draft.extra.curseLevel || 0) + 1;
|
|
1220
|
+
*
|
|
1221
|
+
* // 移除其他物品
|
|
1222
|
+
* draft.inventory = draft.inventory.filter(
|
|
1223
|
+
* item => item !== 'holy_water'
|
|
1224
|
+
* );
|
|
1225
|
+
* }
|
|
1226
|
+
* }
|
|
1227
|
+
*
|
|
1228
|
+
* // 示例4:动态计算和状态同步
|
|
1229
|
+
* customFull: (draft) => {
|
|
1230
|
+
* // 计算总战斗力
|
|
1231
|
+
* const weaponCount = draft.inventory.filter(
|
|
1232
|
+
* i => i.toString().includes('weapon')
|
|
1233
|
+
* ).length;
|
|
1234
|
+
* draft.extra.combatPower = draft.stats.strength * (1 + weaponCount * 0.1);
|
|
1235
|
+
*
|
|
1236
|
+
* // 同步标记状态
|
|
1237
|
+
* draft.flags.well_equipped = weaponCount >= 3;
|
|
1238
|
+
* draft.flags.unarmed = weaponCount === 0;
|
|
1239
|
+
* }
|
|
1240
|
+
* ```
|
|
1241
|
+
*/
|
|
1242
|
+
customFull?: (draft: Omit<GameState<S, I, F, X>, 'logs'>, originalState: GameState<S, I, F, X>) => void;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* 动作定义接口 - 定义玩家可以执行的动作
|
|
1246
|
+
*
|
|
1247
|
+
* 动作是游戏交互的核心,包含了动作的显示信息、执行条件、
|
|
1248
|
+
* 消耗成本和产生的效果。
|
|
1249
|
+
*
|
|
1250
|
+
* @template S - 数值属性键的联合类型
|
|
1251
|
+
* @template I - 物品ID的联合类型
|
|
1252
|
+
* @template F - 标记键的联合类型
|
|
1253
|
+
*
|
|
1254
|
+
* @example
|
|
1255
|
+
* ```typescript
|
|
1256
|
+
* const attackAction: ActionDef<Stats, Items, Flags> = {
|
|
1257
|
+
* id: 'attack_goblin',
|
|
1258
|
+
* label: '攻击哥布林',
|
|
1259
|
+
* description: '用你的武器攻击哥布林',
|
|
1260
|
+
* costs: { stamina: 10 }, // 消耗10点体力
|
|
1261
|
+
* requirements: {
|
|
1262
|
+
* hasItems: ['weapon'] // 需要武器
|
|
1263
|
+
* },
|
|
1264
|
+
* effects: {
|
|
1265
|
+
* statsChange: { exp: 5 }, // 获得5点经验
|
|
1266
|
+
* flagsSet: { goblin_defeated: true }
|
|
1267
|
+
* },
|
|
1268
|
+
* resultText: '你击败了哥布林!'
|
|
1269
|
+
* };
|
|
1270
|
+
* ```
|
|
1271
|
+
*/
|
|
1272
|
+
interface ActionDef<S extends string, I extends string, F extends string, X = Record<string, unknown>> {
|
|
1273
|
+
/** 动作的唯一标识符 */
|
|
1274
|
+
id: string;
|
|
1275
|
+
/**
|
|
1276
|
+
* 动作标签 - 显示在UI上的动作名称
|
|
1277
|
+
* 可以是静态字符串或根据游戏状态动态生成的函数
|
|
1278
|
+
*/
|
|
1279
|
+
label: string | ((state: GameState<S, I, F>) => string);
|
|
1280
|
+
/**
|
|
1281
|
+
* 动作描述 - 详细说明动作的效果
|
|
1282
|
+
* 可选,可以是静态字符串或动态函数
|
|
1283
|
+
*/
|
|
1284
|
+
description?: string | ((state: GameState<S, I, F>) => string);
|
|
1285
|
+
/**
|
|
1286
|
+
* 执行成本 - 执行动作时刚性扣除的资源
|
|
1287
|
+
*
|
|
1288
|
+
* 与requirements不同,costs会实际消耗资源。
|
|
1289
|
+
* 例如: { stamina: 10, gold: 5 } 会扣除10点体力和5金币
|
|
1290
|
+
*/
|
|
1291
|
+
costs?: Partial<Record<S, number>>;
|
|
1292
|
+
/**
|
|
1293
|
+
* 执行需求 - 执行动作前必须满足的条件
|
|
1294
|
+
*
|
|
1295
|
+
* 需求检查不会消耗资源,只是判断是否有资格执行。
|
|
1296
|
+
* 如果不满足需求,动作将无法执行。
|
|
1297
|
+
*/
|
|
1298
|
+
requirements?: RequirementDef<S, I, F, X>;
|
|
1299
|
+
/**
|
|
1300
|
+
* 执行效果 - 动作成功执行后产生的效果
|
|
1301
|
+
*
|
|
1302
|
+
* 包括属性变化、物品获得/失去、标记设置等。
|
|
1303
|
+
* 所有效果会在扣除costs后立即应用。
|
|
1304
|
+
*/
|
|
1305
|
+
effects?: EffectDef<S, I, F, X>;
|
|
1306
|
+
/**
|
|
1307
|
+
* 后置效果钩子 - 在所有效果执行完成后调用
|
|
1308
|
+
*
|
|
1309
|
+
* ⚠️ 职责分离:用于执行副作用逻辑,不应返回值
|
|
1310
|
+
*
|
|
1311
|
+
* 这个钩子在以下所有效果执行完成后调用:
|
|
1312
|
+
* 1. 静态效果(statsChange、itemsAdd 等)
|
|
1313
|
+
* 2. conditionalEffects
|
|
1314
|
+
* 3. custom(修改 extra)
|
|
1315
|
+
* 4. customFull(修改完整状态)
|
|
1316
|
+
*
|
|
1317
|
+
* 执行时机:在 resultText 之前执行
|
|
1318
|
+
*
|
|
1319
|
+
* 使用场景:
|
|
1320
|
+
* - 基于最终状态设置额外标记
|
|
1321
|
+
* - 触发连锁反应
|
|
1322
|
+
* - 执行清理逻辑
|
|
1323
|
+
* - 发送自定义事件
|
|
1324
|
+
* - 调用外部 API
|
|
1325
|
+
*
|
|
1326
|
+
* 注意事项:
|
|
1327
|
+
* 1. 这是最后的修改机会,之后只会生成 resultText
|
|
1328
|
+
* 2. 可以访问所有之前效果修改后的最终状态
|
|
1329
|
+
* 3. 不应返回值,所有修改通过 store 方法进行
|
|
1330
|
+
* 4. 避免在这里执行耗时操作,会阻塞 UI
|
|
1331
|
+
*
|
|
1332
|
+
* @param state - 当前游戏状态(所有效果执行后的最终状态)
|
|
1333
|
+
* @param originalState - 动作执行前的原始状态(只读,用于对比)
|
|
1334
|
+
* @param actions - Store 操作方法集合(用于修改状态)
|
|
1335
|
+
*
|
|
1336
|
+
* @example
|
|
1337
|
+
* ```typescript
|
|
1338
|
+
* // 示例1:基于最终状态设置标记
|
|
1339
|
+
* afterEffects: (state, original, actions) => {
|
|
1340
|
+
* // 检查是否达成成就
|
|
1341
|
+
* if (state.stats.knowledge >= 100 && !state.flags.scholar_achievement) {
|
|
1342
|
+
* actions.setFlag('scholar_achievement', true);
|
|
1343
|
+
* actions.showToast('成就解锁:博学者', 'success');
|
|
1344
|
+
* }
|
|
1345
|
+
* }
|
|
1346
|
+
*
|
|
1347
|
+
* // 示例2:连锁反应
|
|
1348
|
+
* afterEffects: (state, original, actions) => {
|
|
1349
|
+
* // 如果理智值过低,触发疯狂事件
|
|
1350
|
+
* if (state.stats.sanity < 10) {
|
|
1351
|
+
* actions.setFlag('going_mad', true);
|
|
1352
|
+
* actions.addLog('你感觉自己正在失去理智...', 'warn');
|
|
1353
|
+
*
|
|
1354
|
+
* // 随机移除一个物品(幻觉中丢失)
|
|
1355
|
+
* if (state.inventory.length > 0) {
|
|
1356
|
+
* const randomIndex = Math.floor(Math.random() * state.inventory.length);
|
|
1357
|
+
* const lostItem = state.inventory[randomIndex];
|
|
1358
|
+
* actions.removeItem(lostItem);
|
|
1359
|
+
* actions.showModal(`你在恍惚中失去了 ${lostItem}`, 'error');
|
|
1360
|
+
* }
|
|
1361
|
+
* }
|
|
1362
|
+
* }
|
|
1363
|
+
*
|
|
1364
|
+
* // 示例3:时间推进和状态同步
|
|
1365
|
+
* afterEffects: (state, original, actions) => {
|
|
1366
|
+
* // 某些动作会推进时间
|
|
1367
|
+
* const timePassed = state.world.day - original.world.day;
|
|
1368
|
+
*
|
|
1369
|
+
* if (timePassed > 0) {
|
|
1370
|
+
* // 每天恢复一些理智
|
|
1371
|
+
* actions.setStats({ sanity: 5 * timePassed });
|
|
1372
|
+
*
|
|
1373
|
+
* // 设置每日标记
|
|
1374
|
+
* for (let i = 0; i < timePassed; i++) {
|
|
1375
|
+
* const day = original.world.day + i + 1;
|
|
1376
|
+
* actions.setFlag(`day_${day}_passed` as F, true);
|
|
1377
|
+
* }
|
|
1378
|
+
* }
|
|
1379
|
+
* }
|
|
1380
|
+
*
|
|
1381
|
+
* // 示例4:清理过期标记
|
|
1382
|
+
* afterEffects: (state, original, actions) => {
|
|
1383
|
+
* const currentDay = state.world.day;
|
|
1384
|
+
*
|
|
1385
|
+
* // 清理7天前的临时标记
|
|
1386
|
+
* for (let i = 1; i <= 7; i++) {
|
|
1387
|
+
* const oldDay = currentDay - 7;
|
|
1388
|
+
* if (oldDay > 0) {
|
|
1389
|
+
* actions.setFlag(`temp_event_day_${oldDay}` as F, false);
|
|
1390
|
+
* }
|
|
1391
|
+
* }
|
|
1392
|
+
* }
|
|
1393
|
+
*
|
|
1394
|
+
* // 示例5:触发自定义事件(需要导入 gameEvents)
|
|
1395
|
+
* afterEffects: (state, original, actions) => {
|
|
1396
|
+
* // 检查特殊组合条件
|
|
1397
|
+
* const hasAllArtifacts =
|
|
1398
|
+
* state.inventory.includes('artifact_1' as I) &&
|
|
1399
|
+
* state.inventory.includes('artifact_2' as I) &&
|
|
1400
|
+
* state.inventory.includes('artifact_3' as I);
|
|
1401
|
+
*
|
|
1402
|
+
* if (hasAllArtifacts && !state.flags.ritual_available) {
|
|
1403
|
+
* actions.setFlag('ritual_available', true);
|
|
1404
|
+
* // 注意:需要在游戏代码中导入 gameEvents 和 EngineEvents
|
|
1405
|
+
* // gameEvents.emit(EngineEvents.CUSTOM, {
|
|
1406
|
+
* // id: 'artifacts_collected',
|
|
1407
|
+
* // data: { day: state.world.day }
|
|
1408
|
+
* // });
|
|
1409
|
+
* }
|
|
1410
|
+
* }
|
|
1411
|
+
*
|
|
1412
|
+
* // 示例6:保存快照
|
|
1413
|
+
* afterEffects: (state, original, actions) => {
|
|
1414
|
+
* // 在重要节点自动保存快照
|
|
1415
|
+
* if (state.flags.boss_defeated) {
|
|
1416
|
+
* actions.saveSnapshot();
|
|
1417
|
+
* actions.showToast('进度已自动保存', 'info');
|
|
1418
|
+
* }
|
|
1419
|
+
* }
|
|
1420
|
+
* ```
|
|
1421
|
+
*/
|
|
1422
|
+
afterEffects?: (state: GameState<S, I, F, X>, originalState: GameState<S, I, F, X>, actions: GameStoreActions<S, I, F, X>) => void;
|
|
1423
|
+
/**
|
|
1424
|
+
* 结果文本 - 动作执行后显示给玩家的反馈信息
|
|
1425
|
+
*
|
|
1426
|
+
* ⚠️ 职责分离:只负责生成文本,不应执行副作用
|
|
1427
|
+
*
|
|
1428
|
+
* 可以是静态字符串或根据游戏状态动态生成的函数。
|
|
1429
|
+
* 如果是函数,它会在所有效果(包括 afterEffects)执行完成后调用,
|
|
1430
|
+
* 因此可以基于最终状态生成准确的反馈文本。
|
|
1431
|
+
*
|
|
1432
|
+
* 注意事项:
|
|
1433
|
+
* 1. 不应在这里修改游戏状态(使用 afterEffects 代替)
|
|
1434
|
+
* 2. 函数接收的是所有效果执行后的最终状态
|
|
1435
|
+
* 3. 返回的文本会作为 'result' 类型的日志添加到游戏日志中
|
|
1436
|
+
*
|
|
1437
|
+
* @example
|
|
1438
|
+
* ```typescript
|
|
1439
|
+
* // 静态文本
|
|
1440
|
+
* resultText: '你完成了探索'
|
|
1441
|
+
*
|
|
1442
|
+
* // 动态文本 - 基于最终状态
|
|
1443
|
+
* resultText: (state) => {
|
|
1444
|
+
* const sanity = state.stats.sanity;
|
|
1445
|
+
* if (sanity < 20) {
|
|
1446
|
+
* return '你勉强完成了仪式,但代价是巨大的...';
|
|
1447
|
+
* } else if (sanity < 50) {
|
|
1448
|
+
* return '仪式完成了,你感到有些不安。';
|
|
1449
|
+
* } else {
|
|
1450
|
+
* return '仪式顺利完成!';
|
|
1451
|
+
* }
|
|
1452
|
+
* }
|
|
1453
|
+
*
|
|
1454
|
+
* // 基于标记生成文本
|
|
1455
|
+
* resultText: (state) => {
|
|
1456
|
+
* if (state.flags.scholar_achievement) {
|
|
1457
|
+
* return '你的知识达到了新的高度!【成就解锁:博学者】';
|
|
1458
|
+
* }
|
|
1459
|
+
* return '你学到了新的知识。';
|
|
1460
|
+
* }
|
|
1461
|
+
*
|
|
1462
|
+
* // 基于多个状态生成复杂文本
|
|
1463
|
+
* resultText: (state) => {
|
|
1464
|
+
* const parts: string[] = ['你凝视着星空'];
|
|
1465
|
+
*
|
|
1466
|
+
* if (state.world.time === 0) {
|
|
1467
|
+
* parts.push('午夜的星空格外诡异');
|
|
1468
|
+
* }
|
|
1469
|
+
*
|
|
1470
|
+
* if (state.stats.sanity < 30) {
|
|
1471
|
+
* parts.push('你看到了不该看到的东西');
|
|
1472
|
+
* }
|
|
1473
|
+
*
|
|
1474
|
+
* if (state.flags.going_mad) {
|
|
1475
|
+
* parts.push('理智正在崩溃...');
|
|
1476
|
+
* }
|
|
1477
|
+
*
|
|
1478
|
+
* return parts.join(',') + '。';
|
|
1479
|
+
* }
|
|
1480
|
+
* ```
|
|
1481
|
+
*/
|
|
1482
|
+
resultText: string | ((state: GameState<S, I, F, X>) => string);
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* 地点定义接口 - 定义游戏中的一个地点/场景
|
|
1486
|
+
*
|
|
1487
|
+
* 地点是游戏世界的基本单位,包含地点信息和可执行的动作列表。
|
|
1488
|
+
* 玩家在不同地点之间移动,每个地点提供不同的动作选项。
|
|
1489
|
+
*
|
|
1490
|
+
* @template S - 数值属性键的联合类型
|
|
1491
|
+
* @template I - 物品ID的联合类型
|
|
1492
|
+
* @template F - 标记键的联合类型
|
|
1493
|
+
* @template X - 扩展数据类型(默认为 Record<string, unknown>)
|
|
1494
|
+
*
|
|
1495
|
+
* @example
|
|
1496
|
+
* ```typescript
|
|
1497
|
+
* // 基础用法
|
|
1498
|
+
* const tavern: LocationDef<Stats, Items, Flags> = {
|
|
1499
|
+
* id: 'tavern',
|
|
1500
|
+
* name: '酒馆',
|
|
1501
|
+
* description: '一个热闹的酒馆,充满了冒险者',
|
|
1502
|
+
* actionIds: ['buy_drink', 'talk_bartender', 'rest'],
|
|
1503
|
+
* extra: {}
|
|
1504
|
+
* };
|
|
1505
|
+
*
|
|
1506
|
+
* // 使用自定义扩展数据
|
|
1507
|
+
* interface LocationEnvironment {
|
|
1508
|
+
* temperature: number;
|
|
1509
|
+
* humidity: number;
|
|
1510
|
+
* lightLevel: number;
|
|
1511
|
+
* }
|
|
1512
|
+
*
|
|
1513
|
+
* const cave: LocationDef<Stats, Items, Flags, LocationEnvironment> = {
|
|
1514
|
+
* id: 'cave',
|
|
1515
|
+
* name: '洞穴',
|
|
1516
|
+
* description: (state) => {
|
|
1517
|
+
* // 可以访问 state.extra
|
|
1518
|
+
* const temp = state.extra.temperature;
|
|
1519
|
+
* return temp < 0 ? '寒冷的洞穴' : '阴暗的洞穴';
|
|
1520
|
+
* },
|
|
1521
|
+
* actionIds: ['explore'],
|
|
1522
|
+
* extra: {
|
|
1523
|
+
* temperature: -5,
|
|
1524
|
+
* humidity: 80,
|
|
1525
|
+
* lightLevel: 2
|
|
1526
|
+
* }
|
|
1527
|
+
* };
|
|
1528
|
+
* ```
|
|
1529
|
+
*/
|
|
1530
|
+
interface LocationDef<S extends string, I extends string, F extends string, X = Record<string, unknown>> {
|
|
1531
|
+
/** 地点的唯一标识符 */
|
|
1532
|
+
id: string;
|
|
1533
|
+
/** 地点名称 - 显示在UI上的地点名称 */
|
|
1534
|
+
name: string;
|
|
1535
|
+
/**
|
|
1536
|
+
* 地点描述 - 描述地点的外观、氛围等
|
|
1537
|
+
* 可以是静态字符串或根据游戏状态动态生成的函数
|
|
1538
|
+
* 例如:根据时间显示不同的描述
|
|
1539
|
+
*/
|
|
1540
|
+
description: string | ((state: GameState<S, I, F, X>) => string);
|
|
1541
|
+
/**
|
|
1542
|
+
* 该地点可执行的动作ID列表
|
|
1543
|
+
*
|
|
1544
|
+
* 存储动作ID而非完整的ActionDef对象,通过动作注册表查找实际定义。
|
|
1545
|
+
* 这种设计提供了更好的解耦和灵活性:
|
|
1546
|
+
* - 同一个动作可以在多个地点复用
|
|
1547
|
+
* - 动作定义可以独立修改而不影响地点定义
|
|
1548
|
+
* - 减少数据冗余和内存占用
|
|
1549
|
+
*
|
|
1550
|
+
* 使用时需要配合ActionRegistry来查找完整的动作定义。
|
|
1551
|
+
*/
|
|
1552
|
+
actionIds: string[];
|
|
1553
|
+
/**
|
|
1554
|
+
* 扩展数据 - 存储地点特定的额外信息
|
|
1555
|
+
*
|
|
1556
|
+
* 这是一个灵活的字段,允许不同游戏为地点存储自定义元数据。
|
|
1557
|
+
* 使用泛型 X 来定义具体的扩展数据类型。
|
|
1558
|
+
*
|
|
1559
|
+
* 常见用途:
|
|
1560
|
+
* - 环境参数(温度、湿度、光照等)
|
|
1561
|
+
* - 地点元数据(类别、发现时间、访问次数等)
|
|
1562
|
+
* - 特殊属性(危险度、资源丰富度等)
|
|
1563
|
+
* - 动态状态(天气、时间相关的变化等)
|
|
1564
|
+
*
|
|
1565
|
+
* @example
|
|
1566
|
+
* ```typescript
|
|
1567
|
+
* // 定义环境参数类型
|
|
1568
|
+
* interface LocationEnvironment {
|
|
1569
|
+
* temperature: number;
|
|
1570
|
+
* humidity: number;
|
|
1571
|
+
* lightLevel: number;
|
|
1572
|
+
* danger: number;
|
|
1573
|
+
* }
|
|
1574
|
+
*
|
|
1575
|
+
* // 使用扩展数据
|
|
1576
|
+
* type MyLocation = LocationDef<Stats, Items, Flags, LocationEnvironment>;
|
|
1577
|
+
*
|
|
1578
|
+
* const location: MyLocation = {
|
|
1579
|
+
* id: 'forest',
|
|
1580
|
+
* name: '森林',
|
|
1581
|
+
* description: (state) => {
|
|
1582
|
+
* // 在 description 函数中访问扩展数据
|
|
1583
|
+
* if (state.extra.danger > 5) {
|
|
1584
|
+
* return '这片森林看起来很危险...';
|
|
1585
|
+
* }
|
|
1586
|
+
* return '一片宁静的森林';
|
|
1587
|
+
* },
|
|
1588
|
+
* actionIds: ['explore'],
|
|
1589
|
+
* extra: {
|
|
1590
|
+
* temperature: 20,
|
|
1591
|
+
* humidity: 60,
|
|
1592
|
+
* lightLevel: 8,
|
|
1593
|
+
* danger: 3
|
|
1594
|
+
* }
|
|
1595
|
+
* };
|
|
1596
|
+
* ```
|
|
1597
|
+
*/
|
|
1598
|
+
extra: X;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
export type { ActionDef as A, EffectDef as E, FlagsBatchOperation as F, GameStoreActions as G, LogEntry as L, NotificationType as N, RequirementCheckResult as R, StatRequirement as S, WorldState as W, GameState as a, NotificationPayload as b, RequirementDef as c, LocationDef as d };
|