steamsheep-ts-game-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1087 -0
- package/dist/core/index.d.mts +328 -0
- package/dist/core/index.d.ts +328 -0
- package/dist/core/index.js +220 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +195 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1196 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1160 -0
- package/dist/index.mjs.map +1 -0
- package/dist/state/index.d.mts +110 -0
- package/dist/state/index.d.ts +110 -0
- package/dist/state/index.js +479 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/index.mjs +476 -0
- package/dist/state/index.mjs.map +1 -0
- package/dist/store-BP5bpjRr.d.ts +327 -0
- package/dist/store-BeEHel1o.d.mts +327 -0
- package/dist/systems/index.d.mts +318 -0
- package/dist/systems/index.d.ts +318 -0
- package/dist/systems/index.js +347 -0
- package/dist/systems/index.js.map +1 -0
- package/dist/systems/index.mjs +341 -0
- package/dist/systems/index.mjs.map +1 -0
- package/dist/types-CZueoTHl.d.mts +464 -0
- package/dist/types-CZueoTHl.d.ts +464 -0
- package/dist/ui/index.d.mts +56 -0
- package/dist/ui/index.d.ts +56 -0
- package/dist/ui/index.js +412 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +407 -0
- package/dist/ui/index.mjs.map +1 -0
- package/package.json +76 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
3
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
import { useRef, useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
9
|
+
|
|
10
|
+
// core/messages.ts
|
|
11
|
+
var SystemMessages = {
|
|
12
|
+
/** 资源不足提示 */
|
|
13
|
+
NOT_ENOUGH_RESOURCES: "sys:not_enough_resources",
|
|
14
|
+
/** 条件不满足提示 */
|
|
15
|
+
REQUIREMENT_NOT_MET: "sys:requirement_not_met",
|
|
16
|
+
/** 操作成功提示 */
|
|
17
|
+
ACTION_SUCCESS: "sys:action_success",
|
|
18
|
+
/** 时间回溯成功提示 */
|
|
19
|
+
UNDO_SUCCESS: "sys:undo_success"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// core/constants.ts
|
|
23
|
+
var ENGINE_VERSION = "1.0.0";
|
|
24
|
+
var DEFAULT_CONFIG = {
|
|
25
|
+
/** 默认存档键名 */
|
|
26
|
+
DEFAULT_SAVE_KEY: "game-save",
|
|
27
|
+
/** 最大日志条数 */
|
|
28
|
+
MAX_LOG_ENTRIES: 50,
|
|
29
|
+
/** 最大历史快照数 */
|
|
30
|
+
MAX_HISTORY_SNAPSHOTS: 20,
|
|
31
|
+
/** 飘字显示时长(毫秒) */
|
|
32
|
+
TOAST_DURATION: 3e3
|
|
33
|
+
};
|
|
34
|
+
var LOG_TYPE_COLORS = {
|
|
35
|
+
info: "#3b82f6",
|
|
36
|
+
success: "#10b981",
|
|
37
|
+
warn: "#f59e0b",
|
|
38
|
+
error: "#ef4444",
|
|
39
|
+
result: "#6366f1"
|
|
40
|
+
};
|
|
41
|
+
var DEFAULT_STATS = {
|
|
42
|
+
/** 默认生命值 */
|
|
43
|
+
DEFAULT_HP: 100,
|
|
44
|
+
/** 默认金币 */
|
|
45
|
+
DEFAULT_GOLD: 0
|
|
46
|
+
};
|
|
47
|
+
var TIME_CONSTANTS = {
|
|
48
|
+
/** 一天的小时数 */
|
|
49
|
+
HOURS_PER_DAY: 24,
|
|
50
|
+
/** 早晨开始时间 */
|
|
51
|
+
MORNING_START: 6,
|
|
52
|
+
/** 中午开始时间 */
|
|
53
|
+
NOON_START: 12,
|
|
54
|
+
/** 傍晚开始时间 */
|
|
55
|
+
EVENING_START: 18,
|
|
56
|
+
/** 夜晚开始时间 */
|
|
57
|
+
NIGHT_START: 22
|
|
58
|
+
};
|
|
59
|
+
var UI_CONSTANTS = {
|
|
60
|
+
/** 侧边栏宽度 */
|
|
61
|
+
SIDEBAR_WIDTH: 288,
|
|
62
|
+
// 72 * 4 = 288px (w-72)
|
|
63
|
+
/** 日志栏宽度 */
|
|
64
|
+
LOG_WIDTH: 320,
|
|
65
|
+
// 80 * 4 = 320px (w-80)
|
|
66
|
+
/** Toast 容器 z-index */
|
|
67
|
+
TOAST_Z_INDEX: 1e3,
|
|
68
|
+
/** Modal 容器 z-index */
|
|
69
|
+
MODAL_Z_INDEX: 2e3
|
|
70
|
+
};
|
|
71
|
+
var VALIDATION = {
|
|
72
|
+
/** 最小属性值 */
|
|
73
|
+
MIN_STAT_VALUE: 0,
|
|
74
|
+
/** 最大属性值 */
|
|
75
|
+
MAX_STAT_VALUE: 9999,
|
|
76
|
+
/** 最大背包容量 */
|
|
77
|
+
MAX_INVENTORY_SIZE: 100,
|
|
78
|
+
/** 最小动作 ID 长度 */
|
|
79
|
+
MIN_ACTION_ID_LENGTH: 1,
|
|
80
|
+
/** 最大动作 ID 长度 */
|
|
81
|
+
MAX_ACTION_ID_LENGTH: 50
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// core/utils.ts
|
|
85
|
+
function generateId() {
|
|
86
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
87
|
+
}
|
|
88
|
+
function deepClone(obj) {
|
|
89
|
+
return JSON.parse(JSON.stringify(obj));
|
|
90
|
+
}
|
|
91
|
+
function clamp(value, min, max) {
|
|
92
|
+
return Math.min(Math.max(value, min), max);
|
|
93
|
+
}
|
|
94
|
+
function formatNumber(num) {
|
|
95
|
+
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
96
|
+
}
|
|
97
|
+
function getTimeOfDay(hour) {
|
|
98
|
+
if (hour >= TIME_CONSTANTS.MORNING_START && hour < TIME_CONSTANTS.NOON_START) {
|
|
99
|
+
return "\u65E9\u6668";
|
|
100
|
+
} else if (hour >= TIME_CONSTANTS.NOON_START && hour < TIME_CONSTANTS.EVENING_START) {
|
|
101
|
+
return "\u4E0B\u5348";
|
|
102
|
+
} else if (hour >= TIME_CONSTANTS.EVENING_START && hour < TIME_CONSTANTS.NIGHT_START) {
|
|
103
|
+
return "\u508D\u665A";
|
|
104
|
+
} else {
|
|
105
|
+
return "\u591C\u665A";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function getPercentage(current, max) {
|
|
109
|
+
if (max === 0) return 0;
|
|
110
|
+
return Math.round(current / max * 100);
|
|
111
|
+
}
|
|
112
|
+
function randomInt(min, max) {
|
|
113
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
114
|
+
}
|
|
115
|
+
function randomChoice(array) {
|
|
116
|
+
if (array.length === 0) return void 0;
|
|
117
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
118
|
+
}
|
|
119
|
+
function shuffle(array) {
|
|
120
|
+
const result = [...array];
|
|
121
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
122
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
123
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
function delay(ms) {
|
|
128
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
129
|
+
}
|
|
130
|
+
function isEmpty(obj) {
|
|
131
|
+
return Object.keys(obj).length === 0;
|
|
132
|
+
}
|
|
133
|
+
function get(obj, path, defaultValue) {
|
|
134
|
+
const keys = path.split(".");
|
|
135
|
+
let result = obj;
|
|
136
|
+
for (const key of keys) {
|
|
137
|
+
if (result && typeof result === "object" && key in result) {
|
|
138
|
+
result = result[key];
|
|
139
|
+
} else {
|
|
140
|
+
return defaultValue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
function debounce(fn, wait) {
|
|
146
|
+
let timeout = null;
|
|
147
|
+
return function(...args) {
|
|
148
|
+
if (timeout) clearTimeout(timeout);
|
|
149
|
+
timeout = setTimeout(() => fn.apply(this, args), wait);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function throttle(fn, wait) {
|
|
153
|
+
let lastTime = 0;
|
|
154
|
+
return function(...args) {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
if (now - lastTime >= wait) {
|
|
157
|
+
lastTime = now;
|
|
158
|
+
fn.apply(this, args);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function formatGameTime(day, time) {
|
|
163
|
+
const hour = time % TIME_CONSTANTS.HOURS_PER_DAY;
|
|
164
|
+
const period = getTimeOfDay(hour);
|
|
165
|
+
const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
|
166
|
+
return `\u7B2C ${day} \u5929 ${period} ${displayHour}:00`;
|
|
167
|
+
}
|
|
168
|
+
function getStateDiff(oldState, newState) {
|
|
169
|
+
const diff = {
|
|
170
|
+
stats: {},
|
|
171
|
+
itemsAdded: [],
|
|
172
|
+
itemsRemoved: [],
|
|
173
|
+
flagsChanged: {}
|
|
174
|
+
};
|
|
175
|
+
for (const key in newState.stats) {
|
|
176
|
+
const oldValue = oldState.stats[key] || 0;
|
|
177
|
+
const newValue = newState.stats[key] || 0;
|
|
178
|
+
if (oldValue !== newValue) {
|
|
179
|
+
diff.stats[key] = newValue - oldValue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const oldItems = new Set(oldState.inventory);
|
|
183
|
+
const newItems = new Set(newState.inventory);
|
|
184
|
+
for (const item of newState.inventory) {
|
|
185
|
+
if (!oldItems.has(item)) {
|
|
186
|
+
diff.itemsAdded.push(item);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const item of oldState.inventory) {
|
|
190
|
+
if (!newItems.has(item)) {
|
|
191
|
+
diff.itemsRemoved.push(item);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const key in newState.flags) {
|
|
195
|
+
if (oldState.flags[key] !== newState.flags[key]) {
|
|
196
|
+
diff.flagsChanged[key] = newState.flags[key];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return diff;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// systems/events.ts
|
|
203
|
+
var EventBus = class {
|
|
204
|
+
constructor() {
|
|
205
|
+
/**
|
|
206
|
+
* 事件监听器的内部存储
|
|
207
|
+
* 将事件名映射到监听器回调函数数组
|
|
208
|
+
* 使用 Listener[](默认为 Listener<unknown>[])以允许同一事件名有不同类型的监听器
|
|
209
|
+
* 类型安全在订阅/发射时强制执行
|
|
210
|
+
*/
|
|
211
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 订阅事件
|
|
215
|
+
*
|
|
216
|
+
* @template T - 事件载荷的预期类型
|
|
217
|
+
* @param event - 要订阅的事件名称
|
|
218
|
+
* @param callback - 事件触发时调用的监听器函数
|
|
219
|
+
* @returns 取消订阅的函数
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* const handler = (data: { count: number }) => console.log(data.count);
|
|
224
|
+
* const unsubscribe = eventBus.on('update', handler);
|
|
225
|
+
* // 稍后取消订阅
|
|
226
|
+
* unsubscribe();
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
on(event, callback) {
|
|
230
|
+
if (!this.listeners.has(event)) {
|
|
231
|
+
this.listeners.set(event, []);
|
|
232
|
+
}
|
|
233
|
+
this.listeners.get(event).push(callback);
|
|
234
|
+
return () => this.off(event, callback);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 取消订阅事件
|
|
238
|
+
*
|
|
239
|
+
* @template T - 事件载荷的预期类型
|
|
240
|
+
* @param event - 要取消订阅的事件名称
|
|
241
|
+
* @param callback - 要移除的监听器函数
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* const handler = (data: number) => console.log(data);
|
|
246
|
+
* eventBus.on('count', handler);
|
|
247
|
+
* eventBus.off('count', handler); // 移除监听器
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
off(event, callback) {
|
|
251
|
+
const callbacks = this.listeners.get(event);
|
|
252
|
+
if (callbacks) {
|
|
253
|
+
this.listeners.set(
|
|
254
|
+
event,
|
|
255
|
+
callbacks.filter((cb) => cb !== callback)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 发射事件并携带可选的类型化载荷数据
|
|
261
|
+
* 该事件的所有已注册监听器都将被调用并传入数据
|
|
262
|
+
* 单个监听器中的错误会被捕获并记录,不会影响其他监听器
|
|
263
|
+
*
|
|
264
|
+
* @template T - 事件载荷的类型
|
|
265
|
+
* @param event - 要发射的事件名称
|
|
266
|
+
* @param data - 传递给监听器的可选载荷数据
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```typescript
|
|
270
|
+
* eventBus.emit<{ userId: string }>('login', { userId: '123' });
|
|
271
|
+
* eventBus.emit('logout'); // 无载荷
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
emit(event, data) {
|
|
275
|
+
const callbacks = this.listeners.get(event);
|
|
276
|
+
if (callbacks) {
|
|
277
|
+
callbacks.forEach((cb) => {
|
|
278
|
+
try {
|
|
279
|
+
cb(data);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(`Error in event listener for "${event}":`, error);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 清空所有事件的所有监听器
|
|
288
|
+
* 用于清理或重置事件系统
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```typescript
|
|
292
|
+
* eventBus.clear(); // 所有监听器被移除
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
clear() {
|
|
296
|
+
this.listeners.clear();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
var gameEvents = new EventBus();
|
|
300
|
+
var EngineEvents = {
|
|
301
|
+
/** 当角色属性变化时触发 */
|
|
302
|
+
STAT_CHANGE: "engine:stat_change",
|
|
303
|
+
/** 当物品添加到背包时触发 */
|
|
304
|
+
ITEM_ADD: "engine:item_add",
|
|
305
|
+
/** 当物品从背包移除时触发 */
|
|
306
|
+
ITEM_REMOVE: "engine:item_remove",
|
|
307
|
+
/** 当游戏标志变化时触发 */
|
|
308
|
+
FLAG_CHANGE: "engine:flag_change",
|
|
309
|
+
/** 当动作执行时触发 */
|
|
310
|
+
ACTION_EXECUTED: "engine:action_exec",
|
|
311
|
+
/** 当游戏时间推进时触发 */
|
|
312
|
+
TIME_PASS: "engine:time_pass",
|
|
313
|
+
/** 当需要显示瞬时通知时触发(Toast/Modal,不持久化) */
|
|
314
|
+
NOTIFICATION: "engine:notification",
|
|
315
|
+
/** 用于自定义触发事件 */
|
|
316
|
+
CUSTOM: "engine:custom_trigger"
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// state/history.ts
|
|
320
|
+
var HistoryManager = class {
|
|
321
|
+
/**
|
|
322
|
+
* 创建历史管理器实例
|
|
323
|
+
* @param maxSnapshots - 最大保存的快照数量,默认使用配置中的值
|
|
324
|
+
*/
|
|
325
|
+
constructor(maxSnapshots = DEFAULT_CONFIG.MAX_HISTORY_SNAPSHOTS) {
|
|
326
|
+
/** 存储的状态快照数组 */
|
|
327
|
+
__publicField(this, "snapshots", []);
|
|
328
|
+
/** 最大快照数量限制 */
|
|
329
|
+
__publicField(this, "maxSnapshots");
|
|
330
|
+
this.maxSnapshots = maxSnapshots;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 保存当前状态快照
|
|
334
|
+
*
|
|
335
|
+
* 通过深拷贝保存状态,防止后续修改影响历史记录
|
|
336
|
+
* 当快照数量超过最大限制时,自动移除最旧的快照
|
|
337
|
+
*
|
|
338
|
+
* @param state - 要保存的游戏状态
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```typescript
|
|
342
|
+
* history.push(gameState);
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
push(state) {
|
|
346
|
+
const snapshot = JSON.parse(JSON.stringify(state));
|
|
347
|
+
this.snapshots.push(snapshot);
|
|
348
|
+
if (this.snapshots.length > this.maxSnapshots) {
|
|
349
|
+
this.snapshots.shift();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* 回退到上一个状态(撤销操作)
|
|
354
|
+
*
|
|
355
|
+
* 移除并返回最近的一个快照
|
|
356
|
+
* 如果没有可用的快照,返回 undefined
|
|
357
|
+
*
|
|
358
|
+
* @returns 上一个状态快照,如果历史为空则返回 undefined
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```typescript
|
|
362
|
+
* const previousState = history.pop();
|
|
363
|
+
* if (previousState) {
|
|
364
|
+
* // 恢复到上一个状态
|
|
365
|
+
* restoreState(previousState);
|
|
366
|
+
* }
|
|
367
|
+
* ```
|
|
368
|
+
*/
|
|
369
|
+
pop() {
|
|
370
|
+
return this.snapshots.pop();
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* 查看最近的快照但不移除
|
|
374
|
+
*
|
|
375
|
+
* 返回最新的快照但不从历史记录中删除它
|
|
376
|
+
* 用于预览上一个状态而不实际执行撤销操作
|
|
377
|
+
*
|
|
378
|
+
* @returns 最近的状态快照,如果历史为空则返回 undefined
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```typescript
|
|
382
|
+
* const lastState = history.peek();
|
|
383
|
+
* if (lastState) {
|
|
384
|
+
* console.log('上一个状态:', lastState);
|
|
385
|
+
* }
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
peek() {
|
|
389
|
+
return this.snapshots[this.snapshots.length - 1];
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* 清空所有历史记录
|
|
393
|
+
*
|
|
394
|
+
* 移除所有保存的快照,释放内存
|
|
395
|
+
* 通常在开始新游戏或重置时使用
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```typescript
|
|
399
|
+
* history.clear(); // 清空所有历史
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
clear() {
|
|
403
|
+
this.snapshots = [];
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 获取当前历史记录数量
|
|
407
|
+
*
|
|
408
|
+
* @returns 当前保存的快照数量
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```typescript
|
|
412
|
+
* console.log(`历史记录数量: ${history.size}`);
|
|
413
|
+
* if (history.size > 0) {
|
|
414
|
+
* // 可以执行撤销操作
|
|
415
|
+
* }
|
|
416
|
+
* ```
|
|
417
|
+
*/
|
|
418
|
+
get size() {
|
|
419
|
+
return this.snapshots.length;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// state/store.ts
|
|
424
|
+
var createGameEngineStore = (initialState, persistName = "generic-rpg-save") => {
|
|
425
|
+
const history = new HistoryManager(
|
|
426
|
+
DEFAULT_CONFIG.MAX_HISTORY_SNAPSHOTS
|
|
427
|
+
);
|
|
428
|
+
return create()(
|
|
429
|
+
persist(
|
|
430
|
+
(set, get2) => ({
|
|
431
|
+
// 展开初始状态,包含所有游戏数据
|
|
432
|
+
...initialState,
|
|
433
|
+
// ========== 数值属性操作实现 ==========
|
|
434
|
+
/**
|
|
435
|
+
* 实现:更新单个数值属性
|
|
436
|
+
* 使用增量更新,保持其他属性不变
|
|
437
|
+
* 发射 STAT_CHANGE 事件通知监听器
|
|
438
|
+
*/
|
|
439
|
+
updateStat: (stat, delta) => set((state) => {
|
|
440
|
+
const currentVal = state.stats[stat] || 0;
|
|
441
|
+
const newVal = currentVal + delta;
|
|
442
|
+
gameEvents.emit(EngineEvents.STAT_CHANGE, {
|
|
443
|
+
stat,
|
|
444
|
+
delta,
|
|
445
|
+
current: newVal
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
stats: {
|
|
449
|
+
...state.stats,
|
|
450
|
+
[stat]: newVal
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}),
|
|
454
|
+
/**
|
|
455
|
+
* 实现:批量更新多个数值属性
|
|
456
|
+
* 遍历所有变化并应用到新的stats对象
|
|
457
|
+
*/
|
|
458
|
+
setStats: (changes) => set((state) => {
|
|
459
|
+
const newStats = { ...state.stats };
|
|
460
|
+
Object.entries(changes).forEach(([k, v]) => {
|
|
461
|
+
newStats[k] = (newStats[k] || 0) + v;
|
|
462
|
+
});
|
|
463
|
+
return { stats: newStats };
|
|
464
|
+
}),
|
|
465
|
+
// ========== 扩展数据操作实现 ==========
|
|
466
|
+
/**
|
|
467
|
+
* 实现:更新扩展数据
|
|
468
|
+
*
|
|
469
|
+
* 支持两种更新方式:
|
|
470
|
+
* 1. 直接传入部分更新对象
|
|
471
|
+
* 2. 传入更新函数,接收当前值返回部分更新
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```typescript
|
|
475
|
+
* // 方式1:直接更新
|
|
476
|
+
* setExtra({ reputation: 100 });
|
|
477
|
+
*
|
|
478
|
+
* // 方式2:基于当前值更新
|
|
479
|
+
* setExtra((prev) => ({ reputation: prev.reputation + 10 }));
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
setExtra: (updater) => set((state) => {
|
|
483
|
+
const update = typeof updater === "function" ? updater(state.extra) : updater;
|
|
484
|
+
return {
|
|
485
|
+
extra: { ...state.extra, ...update }
|
|
486
|
+
};
|
|
487
|
+
}),
|
|
488
|
+
// ========== 物品操作实现 ==========
|
|
489
|
+
/**
|
|
490
|
+
* 实现:添加物品到库存
|
|
491
|
+
* 创建新数组,保持不可变性
|
|
492
|
+
* 发射 ITEM_ADD 事件通知监听器
|
|
493
|
+
*/
|
|
494
|
+
addItem: (item) => set((state) => {
|
|
495
|
+
gameEvents.emit(EngineEvents.ITEM_ADD, { item });
|
|
496
|
+
return {
|
|
497
|
+
inventory: [...state.inventory, item]
|
|
498
|
+
};
|
|
499
|
+
}),
|
|
500
|
+
/**
|
|
501
|
+
* 实现:从库存移除物品
|
|
502
|
+
* 使用filter移除第一个匹配的物品
|
|
503
|
+
* 发射 ITEM_REMOVE 事件通知监听器
|
|
504
|
+
*/
|
|
505
|
+
removeItem: (item) => set((state) => {
|
|
506
|
+
gameEvents.emit(EngineEvents.ITEM_REMOVE, { item });
|
|
507
|
+
return {
|
|
508
|
+
inventory: state.inventory.filter((i) => i !== item)
|
|
509
|
+
};
|
|
510
|
+
}),
|
|
511
|
+
// ========== 标记操作实现 ==========
|
|
512
|
+
/**
|
|
513
|
+
* 实现:设置布尔标记
|
|
514
|
+
* 创建新的flags对象,更新指定标记
|
|
515
|
+
* 发射 FLAG_CHANGE 事件通知监听器
|
|
516
|
+
*/
|
|
517
|
+
setFlag: (flag, value) => set((state) => {
|
|
518
|
+
gameEvents.emit(EngineEvents.FLAG_CHANGE, { flag, value });
|
|
519
|
+
return {
|
|
520
|
+
flags: { ...state.flags, [flag]: value }
|
|
521
|
+
};
|
|
522
|
+
}),
|
|
523
|
+
// ========== 系统操作实现 ==========
|
|
524
|
+
/**
|
|
525
|
+
* 实现:添加日志条目
|
|
526
|
+
*
|
|
527
|
+
* 创建新日志并添加到日志数组开头。
|
|
528
|
+
* 使用 MAX_LOG_ENTRIES 限制日志数量,防止内存泄漏。
|
|
529
|
+
* 日志ID使用时间戳+随机数确保唯一性。
|
|
530
|
+
*
|
|
531
|
+
* ⚠️ 注意:日志会被持久化到 localStorage
|
|
532
|
+
*/
|
|
533
|
+
addLog: (text, type = "info") => set((state) => {
|
|
534
|
+
const newLog = {
|
|
535
|
+
id: generateId(),
|
|
536
|
+
text,
|
|
537
|
+
type,
|
|
538
|
+
timestamp: state.world.day
|
|
539
|
+
};
|
|
540
|
+
return {
|
|
541
|
+
logs: [newLog, ...state.logs].slice(0, DEFAULT_CONFIG.MAX_LOG_ENTRIES)
|
|
542
|
+
};
|
|
543
|
+
}),
|
|
544
|
+
/**
|
|
545
|
+
* 实现:显示飘字通知
|
|
546
|
+
*
|
|
547
|
+
* 发射 NOTIFICATION 事件,UI 层监听后显示飘字效果。
|
|
548
|
+
*
|
|
549
|
+
* ⚠️ 注意:通知不会被持久化,仅在当前会话中显示
|
|
550
|
+
*/
|
|
551
|
+
showToast: (text, type = "info") => {
|
|
552
|
+
const notification = {
|
|
553
|
+
text,
|
|
554
|
+
type,
|
|
555
|
+
notificationType: "toast",
|
|
556
|
+
timestamp: Date.now()
|
|
557
|
+
};
|
|
558
|
+
gameEvents.emit(EngineEvents.NOTIFICATION, notification);
|
|
559
|
+
},
|
|
560
|
+
/**
|
|
561
|
+
* 实现:显示弹窗通知
|
|
562
|
+
*
|
|
563
|
+
* 发射 NOTIFICATION 事件,UI 层监听后显示模态弹窗。
|
|
564
|
+
*
|
|
565
|
+
* ⚠️ 注意:通知不会被持久化,仅在当前会话中显示
|
|
566
|
+
*/
|
|
567
|
+
showModal: (text, type = "info") => {
|
|
568
|
+
const notification = {
|
|
569
|
+
text,
|
|
570
|
+
type,
|
|
571
|
+
notificationType: "modal",
|
|
572
|
+
timestamp: Date.now()
|
|
573
|
+
};
|
|
574
|
+
gameEvents.emit(EngineEvents.NOTIFICATION, notification);
|
|
575
|
+
},
|
|
576
|
+
/**
|
|
577
|
+
* 实现:推进游戏时间
|
|
578
|
+
* 增加world.day的值
|
|
579
|
+
*/
|
|
580
|
+
advanceTime: (amount = 1) => set((state) => ({
|
|
581
|
+
world: { ...state.world, day: state.world.day + amount }
|
|
582
|
+
})),
|
|
583
|
+
/**
|
|
584
|
+
* 实现:传送到指定地点
|
|
585
|
+
* 更新world.currentLocationId
|
|
586
|
+
*/
|
|
587
|
+
teleport: (locationId) => set((state) => ({
|
|
588
|
+
world: { ...state.world, currentLocationId: locationId }
|
|
589
|
+
})),
|
|
590
|
+
// ========== 存档操作实现 ==========
|
|
591
|
+
/**
|
|
592
|
+
* 实现:重置游戏状态
|
|
593
|
+
* 恢复到初始状态,清空日志
|
|
594
|
+
*/
|
|
595
|
+
reset: () => set({ ...initialState, logs: [] }),
|
|
596
|
+
// ========== 历史记录操作实现 ==========
|
|
597
|
+
/**
|
|
598
|
+
* 实现:保存当前状态快照
|
|
599
|
+
*
|
|
600
|
+
* 保存除日志外的所有状态数据到历史管理器。
|
|
601
|
+
* 日志通常不需要回退,因此被排除以节省内存。
|
|
602
|
+
*/
|
|
603
|
+
saveSnapshot: () => {
|
|
604
|
+
const currentState = get2();
|
|
605
|
+
history.push(currentState);
|
|
606
|
+
},
|
|
607
|
+
/**
|
|
608
|
+
* 实现:撤销到上一个状态
|
|
609
|
+
*
|
|
610
|
+
* 从历史管理器中恢复上一个快照。
|
|
611
|
+
* 恢复时保留当前的日志,并添加系统提示。
|
|
612
|
+
*
|
|
613
|
+
* @returns 是否成功撤销
|
|
614
|
+
*/
|
|
615
|
+
undo: () => {
|
|
616
|
+
const prev = history.pop();
|
|
617
|
+
if (prev) {
|
|
618
|
+
set({
|
|
619
|
+
stats: prev.stats,
|
|
620
|
+
inventory: prev.inventory,
|
|
621
|
+
flags: prev.flags,
|
|
622
|
+
world: prev.world,
|
|
623
|
+
extra: prev.extra
|
|
624
|
+
// logs 保留当前的,不回退日志
|
|
625
|
+
});
|
|
626
|
+
get2().addLog(SystemMessages.UNDO_SUCCESS, "info");
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}),
|
|
632
|
+
{
|
|
633
|
+
// 持久化配置
|
|
634
|
+
name: persistName,
|
|
635
|
+
// localStorage中的键名
|
|
636
|
+
storage: createJSONStorage(() => localStorage),
|
|
637
|
+
// 使用localStorage存储
|
|
638
|
+
/**
|
|
639
|
+
* 部分持久化 - 只保存数据字段,排除方法
|
|
640
|
+
*
|
|
641
|
+
* 这很重要,因为方法不能被序列化到JSON。
|
|
642
|
+
* 我们只持久化游戏状态数据,方法会在store重新创建时自动添加。
|
|
643
|
+
*/
|
|
644
|
+
partialize: (state) => ({
|
|
645
|
+
stats: state.stats,
|
|
646
|
+
inventory: state.inventory,
|
|
647
|
+
flags: state.flags,
|
|
648
|
+
world: state.world,
|
|
649
|
+
logs: state.logs,
|
|
650
|
+
extra: state.extra
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
)
|
|
654
|
+
);
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// systems/query.ts
|
|
658
|
+
var QuerySystem = class {
|
|
659
|
+
/**
|
|
660
|
+
* 检查是否满足所有需求条件
|
|
661
|
+
*
|
|
662
|
+
* 这是核心的条件检查方法,用于验证玩家是否满足执行某个动作的所有前置条件。
|
|
663
|
+
* 内置字段使用 AND 逻辑组合,复杂逻辑使用 custom 函数实现。
|
|
664
|
+
*
|
|
665
|
+
* @template S - 数值属性键的联合类型
|
|
666
|
+
* @template I - 物品ID的联合类型
|
|
667
|
+
* @template F - 标记键的联合类型
|
|
668
|
+
*
|
|
669
|
+
* @param state - 当前游戏状态
|
|
670
|
+
* @param reqs - 需求定义(可选,未定义时返回true)
|
|
671
|
+
*
|
|
672
|
+
* @returns true表示满足所有条件,false表示至少有一个条件不满足
|
|
673
|
+
*
|
|
674
|
+
* @example
|
|
675
|
+
* ```typescript
|
|
676
|
+
* // 简单需求
|
|
677
|
+
* const simple = QuerySystem.checkRequirements(state, {
|
|
678
|
+
* hasItems: ['key'],
|
|
679
|
+
* stats: { strength: { min: 5 } }
|
|
680
|
+
* });
|
|
681
|
+
*
|
|
682
|
+
* // 复杂逻辑使用 custom
|
|
683
|
+
* const complex = QuerySystem.checkRequirements(state, {
|
|
684
|
+
* custom: (state) => {
|
|
685
|
+
* // (有钥匙 AND 力量>=5) OR 有万能钥匙
|
|
686
|
+
* const hasKeyAndStrength =
|
|
687
|
+
* state.inventory.includes('key') && state.stats.strength >= 5;
|
|
688
|
+
* const hasMasterKey = state.inventory.includes('master_key');
|
|
689
|
+
* return hasKeyAndStrength || hasMasterKey;
|
|
690
|
+
* }
|
|
691
|
+
* });
|
|
692
|
+
* ```
|
|
693
|
+
*/
|
|
694
|
+
static checkRequirements(state, reqs) {
|
|
695
|
+
if (!reqs) return true;
|
|
696
|
+
if (reqs.hasFlags && reqs.hasFlags.length > 0) {
|
|
697
|
+
if (!reqs.hasFlags.every((f) => state.flags[f])) return false;
|
|
698
|
+
}
|
|
699
|
+
if (reqs.noFlags && reqs.noFlags.length > 0) {
|
|
700
|
+
if (reqs.noFlags.some((f) => state.flags[f])) return false;
|
|
701
|
+
}
|
|
702
|
+
if (reqs.hasItems && reqs.hasItems.length > 0) {
|
|
703
|
+
if (!reqs.hasItems.every((i) => state.inventory.includes(i)))
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
if (reqs.noItems && reqs.noItems.length > 0) {
|
|
707
|
+
if (reqs.noItems.some((i) => state.inventory.includes(i))) return false;
|
|
708
|
+
}
|
|
709
|
+
if (reqs.stats) {
|
|
710
|
+
for (const [key, condition] of Object.entries(reqs.stats)) {
|
|
711
|
+
const currentVal = state.stats[key] || 0;
|
|
712
|
+
const cond = condition;
|
|
713
|
+
if (typeof cond === "number") {
|
|
714
|
+
if (currentVal < cond) return false;
|
|
715
|
+
} else {
|
|
716
|
+
if (cond.min !== void 0 && currentVal < cond.min) return false;
|
|
717
|
+
if (cond.max !== void 0 && currentVal > cond.max) return false;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (reqs.custom && !reqs.custom(state)) return false;
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* 检查是否有足够资源支付成本
|
|
726
|
+
*
|
|
727
|
+
* 验证玩家的数值属性是否足够支付指定的成本。
|
|
728
|
+
* 这是一个简化的检查,只验证数值是否足够,不实际扣除。
|
|
729
|
+
*
|
|
730
|
+
* 与checkRequirements的区别:
|
|
731
|
+
* - canAfford只检查数值属性
|
|
732
|
+
* - checkRequirements检查完整的需求(标记、物品、数值、自定义)
|
|
733
|
+
* - canAfford用于成本检查,checkRequirements用于前置条件检查
|
|
734
|
+
*
|
|
735
|
+
* @template S - 数值属性键的联合类型
|
|
736
|
+
* @template I - 物品ID的联合类型
|
|
737
|
+
* @template F - 标记键的联合类型
|
|
738
|
+
*
|
|
739
|
+
* @param state - 当前游戏状态
|
|
740
|
+
* @param costs - 成本定义(可选,未定义时返回true)
|
|
741
|
+
*
|
|
742
|
+
* @returns true表示有足够资源,false表示资源不足
|
|
743
|
+
*
|
|
744
|
+
* @example
|
|
745
|
+
* ```typescript
|
|
746
|
+
* // 检查是否有足够金币购买物品
|
|
747
|
+
* const canBuy = QuerySystem.canAfford(state, { gold: 100 });
|
|
748
|
+
*
|
|
749
|
+
* // 检查是否有足够资源施放技能
|
|
750
|
+
* const canCast = QuerySystem.canAfford(state, {
|
|
751
|
+
* mp: 30,
|
|
752
|
+
* stamina: 10
|
|
753
|
+
* });
|
|
754
|
+
*
|
|
755
|
+
* // 在动作执行前使用
|
|
756
|
+
* if (QuerySystem.canAfford(state, action.costs)) {
|
|
757
|
+
* // 执行动作并扣除成本
|
|
758
|
+
* executeAction(action);
|
|
759
|
+
* } else {
|
|
760
|
+
* addLog('资源不足!', 'error');
|
|
761
|
+
* }
|
|
762
|
+
* ```
|
|
763
|
+
*/
|
|
764
|
+
static canAfford(state, costs) {
|
|
765
|
+
if (!costs) return true;
|
|
766
|
+
for (const [key, cost] of Object.entries(costs)) {
|
|
767
|
+
const currentVal = state.stats[key] || 0;
|
|
768
|
+
if (currentVal < cost) return false;
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// systems/flow.ts
|
|
775
|
+
var FlowSystem = class {
|
|
776
|
+
/**
|
|
777
|
+
* 执行一个游戏动作
|
|
778
|
+
*
|
|
779
|
+
* 这是核心方法,负责完整的动作执行流程。
|
|
780
|
+
* 执行过程是原子性的:要么全部成功,要么全部失败。
|
|
781
|
+
*
|
|
782
|
+
* 执行步骤:
|
|
783
|
+
* 1. 最终验证:检查需求和成本(防止UI滞后)
|
|
784
|
+
* 2. 扣除成本:消耗资源(如体力、金币)
|
|
785
|
+
* 3. 应用效果:修改状态(属性、物品、标记等)
|
|
786
|
+
* 4. 记录日志:生成反馈信息
|
|
787
|
+
*
|
|
788
|
+
* @template S - 数值属性键的联合类型
|
|
789
|
+
* @template I - 物品ID的联合类型
|
|
790
|
+
* @template F - 标记键的联合类型
|
|
791
|
+
*
|
|
792
|
+
* @param store - Zustand Store 实例(包含状态和方法)
|
|
793
|
+
* @param action - 要执行的动作定义
|
|
794
|
+
*
|
|
795
|
+
* @returns true表示执行成功,false表示执行失败
|
|
796
|
+
*
|
|
797
|
+
* @example
|
|
798
|
+
* ```typescript
|
|
799
|
+
* // 定义一个攻击动作
|
|
800
|
+
* const attackAction: ActionDef<Stats, Items, Flags> = {
|
|
801
|
+
* id: 'attack_goblin',
|
|
802
|
+
* label: '攻击哥布林',
|
|
803
|
+
* costs: { stamina: 10 },
|
|
804
|
+
* requirements: { hasItems: ['weapon'] },
|
|
805
|
+
* effects: {
|
|
806
|
+
* statsChange: { exp: 5 },
|
|
807
|
+
* flagsSet: { goblin_defeated: true }
|
|
808
|
+
* },
|
|
809
|
+
* resultText: '你击败了哥布林!'
|
|
810
|
+
* };
|
|
811
|
+
*
|
|
812
|
+
* // 执行动作
|
|
813
|
+
* const store = useGameStore.getState();
|
|
814
|
+
* const success = FlowSystem.executeAction(store, attackAction);
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
817
|
+
static executeAction(store, action) {
|
|
818
|
+
const state = store;
|
|
819
|
+
if (!QuerySystem.checkRequirements(state, action.requirements)) {
|
|
820
|
+
console.warn(`\u6761\u4EF6\u4E0D\u6EE1\u8DB3: ${action.id}`);
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
if (!QuerySystem.canAfford(state, action.costs)) {
|
|
824
|
+
store.addLog(SystemMessages.NOT_ENOUGH_RESOURCES, "warn");
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
if (action.costs) {
|
|
828
|
+
const costsToDeduct = {};
|
|
829
|
+
for (const [key, val] of Object.entries(action.costs)) {
|
|
830
|
+
costsToDeduct[key] = -val;
|
|
831
|
+
}
|
|
832
|
+
store.setStats(costsToDeduct);
|
|
833
|
+
}
|
|
834
|
+
const { effects } = action;
|
|
835
|
+
if (effects) {
|
|
836
|
+
if (effects.statsChange) {
|
|
837
|
+
store.setStats(effects.statsChange);
|
|
838
|
+
}
|
|
839
|
+
effects.itemsAdd?.forEach((i) => store.addItem(i));
|
|
840
|
+
effects.itemsRemove?.forEach((i) => store.removeItem(i));
|
|
841
|
+
if (effects.flagsSet) {
|
|
842
|
+
Object.entries(effects.flagsSet).forEach(([f, v]) => {
|
|
843
|
+
store.setFlag(f, v);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (effects.teleport) {
|
|
847
|
+
store.teleport(effects.teleport);
|
|
848
|
+
}
|
|
849
|
+
if (effects.triggerEvent) {
|
|
850
|
+
gameEvents.emit(EngineEvents.CUSTOM, { id: effects.triggerEvent });
|
|
851
|
+
}
|
|
852
|
+
if (effects.custom) {
|
|
853
|
+
const draft = { ...state.extra };
|
|
854
|
+
const result = effects.custom(draft, state);
|
|
855
|
+
if (result) {
|
|
856
|
+
store.setExtra({ ...draft, ...result });
|
|
857
|
+
} else {
|
|
858
|
+
store.setExtra(draft);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const resultText = typeof action.resultText === "function" ? action.resultText(state) : action.resultText;
|
|
863
|
+
store.addLog(resultText, "result");
|
|
864
|
+
gameEvents.emit(EngineEvents.ACTION_EXECUTED, { actionId: action.id });
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
var Layout = ({
|
|
869
|
+
sidebar,
|
|
870
|
+
main,
|
|
871
|
+
logs,
|
|
872
|
+
className = ""
|
|
873
|
+
}) => {
|
|
874
|
+
return /* @__PURE__ */ jsxs(
|
|
875
|
+
"div",
|
|
876
|
+
{
|
|
877
|
+
className: `min-h-screen bg-[#0c0c0e] text-[#a1a1aa] font-sans flex overflow-hidden ${className}`,
|
|
878
|
+
children: [
|
|
879
|
+
sidebar && /* @__PURE__ */ jsx("aside", { className: "w-72 bg-[#141417] border-r border-[#27272a] flex flex-col shrink-0 z-10", children: sidebar }),
|
|
880
|
+
/* @__PURE__ */ jsx("main", { className: "flex-1 flex flex-col min-w-0 relative", children: main }),
|
|
881
|
+
logs && /* @__PURE__ */ jsx("aside", { className: "w-80 bg-[#0c0c0e] border-l border-[#27272a] flex flex-col shrink-0 z-10", children: logs })
|
|
882
|
+
]
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
};
|
|
886
|
+
var MainContent = ({ header, children }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
887
|
+
header && /* @__PURE__ */ jsx("div", { className: "p-10 border-b border-[#27272a] bg-[#0c0c0e] shrink-0", children: /* @__PURE__ */ jsx("div", { className: "max-w-3xl mx-auto", children: header }) }),
|
|
888
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-10 bg-[#101012]", children: /* @__PURE__ */ jsx("div", { className: "max-w-3xl mx-auto", children }) })
|
|
889
|
+
] });
|
|
890
|
+
var LogStream = ({
|
|
891
|
+
logs,
|
|
892
|
+
className = "",
|
|
893
|
+
title = "Chronicle"
|
|
894
|
+
}) => {
|
|
895
|
+
const bottomRef = useRef(null);
|
|
896
|
+
useEffect(() => {
|
|
897
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
898
|
+
}, [logs]);
|
|
899
|
+
const getLogStyle = (type) => {
|
|
900
|
+
switch (type) {
|
|
901
|
+
case "warn":
|
|
902
|
+
return "text-amber-500 border-l-2 border-amber-500/50 pl-2";
|
|
903
|
+
case "error":
|
|
904
|
+
return "text-red-500 border-l-2 border-red-500/50 pl-2 font-bold";
|
|
905
|
+
case "success":
|
|
906
|
+
return "text-green-500 border-l-2 border-green-500/50 pl-2";
|
|
907
|
+
case "result":
|
|
908
|
+
return "text-stone-300 border-l-2 border-stone-600/50 pl-2";
|
|
909
|
+
// 行动结果
|
|
910
|
+
case "info":
|
|
911
|
+
default:
|
|
912
|
+
return "text-stone-500 border-l-2 border-transparent pl-2";
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
return /* @__PURE__ */ jsxs(
|
|
916
|
+
"div",
|
|
917
|
+
{
|
|
918
|
+
className: `flex flex-col bg-[#0c0c0e] border-l border-[#27272a] h-full ${className}`,
|
|
919
|
+
children: [
|
|
920
|
+
/* @__PURE__ */ jsx("div", { className: "p-4 border-b border-[#27272a] bg-[#141417] shrink-0", children: /* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-stone-500 uppercase tracking-wider", children: title }) }),
|
|
921
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-6 space-y-4 font-serif scroll-smooth", children: [
|
|
922
|
+
logs.length === 0 && /* @__PURE__ */ jsx("div", { className: "text-stone-700 text-sm italic text-center mt-10", children: "\u6682\u65E0\u8BB0\u5F55..." }),
|
|
923
|
+
logs.map((log) => /* @__PURE__ */ jsx(
|
|
924
|
+
"div",
|
|
925
|
+
{
|
|
926
|
+
className: `text-sm leading-relaxed transition-all duration-500 animate-in fade-in slide-in-from-bottom-2 ${getLogStyle(
|
|
927
|
+
log.type
|
|
928
|
+
)}`,
|
|
929
|
+
children: log.text
|
|
930
|
+
},
|
|
931
|
+
log.id
|
|
932
|
+
)),
|
|
933
|
+
/* @__PURE__ */ jsx("div", { ref: bottomRef })
|
|
934
|
+
] })
|
|
935
|
+
]
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
};
|
|
939
|
+
function OverlaySystem() {
|
|
940
|
+
const [toasts, setToasts] = useState([]);
|
|
941
|
+
const [modal, setModal] = useState(null);
|
|
942
|
+
useEffect(() => {
|
|
943
|
+
const unsubscribe = gameEvents.on(
|
|
944
|
+
EngineEvents.NOTIFICATION,
|
|
945
|
+
(payload) => {
|
|
946
|
+
switch (payload.notificationType) {
|
|
947
|
+
case "toast": {
|
|
948
|
+
const toastId = `toast-${payload.timestamp}-${Math.random()}`;
|
|
949
|
+
const toast = { ...payload, id: toastId };
|
|
950
|
+
setToasts((prev) => [...prev, toast]);
|
|
951
|
+
setTimeout(() => {
|
|
952
|
+
setToasts((prev) => prev.filter((t) => t.id !== toastId));
|
|
953
|
+
}, DEFAULT_CONFIG.TOAST_DURATION);
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
case "modal": {
|
|
957
|
+
setModal(payload);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
);
|
|
963
|
+
return unsubscribe;
|
|
964
|
+
}, []);
|
|
965
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
966
|
+
/* @__PURE__ */ jsx("div", { className: "toast-container", children: toasts.map((toast) => /* @__PURE__ */ jsx(
|
|
967
|
+
"div",
|
|
968
|
+
{
|
|
969
|
+
className: `toast toast-${toast.type}`,
|
|
970
|
+
role: "alert",
|
|
971
|
+
children: toast.text
|
|
972
|
+
},
|
|
973
|
+
toast.id
|
|
974
|
+
)) }),
|
|
975
|
+
modal && /* @__PURE__ */ jsx(
|
|
976
|
+
"div",
|
|
977
|
+
{
|
|
978
|
+
className: "modal-overlay",
|
|
979
|
+
onClick: () => setModal(null),
|
|
980
|
+
role: "dialog",
|
|
981
|
+
"aria-modal": "true",
|
|
982
|
+
children: /* @__PURE__ */ jsxs(
|
|
983
|
+
"div",
|
|
984
|
+
{
|
|
985
|
+
className: "modal-content",
|
|
986
|
+
onClick: (e) => e.stopPropagation(),
|
|
987
|
+
children: [
|
|
988
|
+
/* @__PURE__ */ jsx("div", { className: `modal-message modal-${modal.type}`, children: modal.text }),
|
|
989
|
+
/* @__PURE__ */ jsx(
|
|
990
|
+
"button",
|
|
991
|
+
{
|
|
992
|
+
className: "modal-button",
|
|
993
|
+
onClick: () => setModal(null),
|
|
994
|
+
autoFocus: true,
|
|
995
|
+
children: "\u786E\u5B9A"
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
]
|
|
999
|
+
}
|
|
1000
|
+
)
|
|
1001
|
+
}
|
|
1002
|
+
),
|
|
1003
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
1004
|
+
/* \u98D8\u5B57\u5BB9\u5668 */
|
|
1005
|
+
.toast-container {
|
|
1006
|
+
position: fixed;
|
|
1007
|
+
top: 20px;
|
|
1008
|
+
right: 20px;
|
|
1009
|
+
z-index: 1000;
|
|
1010
|
+
display: flex;
|
|
1011
|
+
flex-direction: column;
|
|
1012
|
+
gap: 10px;
|
|
1013
|
+
pointer-events: none;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/* \u98D8\u5B57\u6837\u5F0F */
|
|
1017
|
+
.toast {
|
|
1018
|
+
padding: 12px 20px;
|
|
1019
|
+
border-radius: 8px;
|
|
1020
|
+
background: white;
|
|
1021
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
1022
|
+
animation: slideIn 0.3s ease-out, fadeOut 0.3s ease-in 2.7s;
|
|
1023
|
+
pointer-events: auto;
|
|
1024
|
+
max-width: 300px;
|
|
1025
|
+
word-wrap: break-word;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.toast-info {
|
|
1029
|
+
border-left: 4px solid #3b82f6;
|
|
1030
|
+
color: #1e40af;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.toast-success {
|
|
1034
|
+
border-left: 4px solid #10b981;
|
|
1035
|
+
color: #065f46;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.toast-warn {
|
|
1039
|
+
border-left: 4px solid #f59e0b;
|
|
1040
|
+
color: #92400e;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.toast-error {
|
|
1044
|
+
border-left: 4px solid #ef4444;
|
|
1045
|
+
color: #991b1b;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
.toast-result {
|
|
1049
|
+
border-left: 4px solid #6366f1;
|
|
1050
|
+
color: #3730a3;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
@keyframes slideIn {
|
|
1054
|
+
from {
|
|
1055
|
+
transform: translateX(100%);
|
|
1056
|
+
opacity: 0;
|
|
1057
|
+
}
|
|
1058
|
+
to {
|
|
1059
|
+
transform: translateX(0);
|
|
1060
|
+
opacity: 1;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
@keyframes fadeOut {
|
|
1065
|
+
from {
|
|
1066
|
+
opacity: 1;
|
|
1067
|
+
}
|
|
1068
|
+
to {
|
|
1069
|
+
opacity: 0;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/* \u6A21\u6001\u5F39\u7A97 */
|
|
1074
|
+
.modal-overlay {
|
|
1075
|
+
position: fixed;
|
|
1076
|
+
inset: 0;
|
|
1077
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1078
|
+
display: flex;
|
|
1079
|
+
align-items: center;
|
|
1080
|
+
justify-content: center;
|
|
1081
|
+
z-index: 2000;
|
|
1082
|
+
animation: fadeIn 0.2s ease-out;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.modal-content {
|
|
1086
|
+
background: white;
|
|
1087
|
+
padding: 24px;
|
|
1088
|
+
border-radius: 12px;
|
|
1089
|
+
max-width: 400px;
|
|
1090
|
+
min-width: 300px;
|
|
1091
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
1092
|
+
animation: scaleIn 0.2s ease-out;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.modal-message {
|
|
1096
|
+
margin-bottom: 20px;
|
|
1097
|
+
font-size: 16px;
|
|
1098
|
+
line-height: 1.5;
|
|
1099
|
+
color: #1f2937;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.modal-success {
|
|
1103
|
+
color: #10b981;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.modal-warn {
|
|
1107
|
+
color: #f59e0b;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.modal-error {
|
|
1111
|
+
color: #ef4444;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.modal-button {
|
|
1115
|
+
width: 100%;
|
|
1116
|
+
padding: 10px 20px;
|
|
1117
|
+
background: #3b82f6;
|
|
1118
|
+
color: white;
|
|
1119
|
+
border: none;
|
|
1120
|
+
border-radius: 6px;
|
|
1121
|
+
font-size: 14px;
|
|
1122
|
+
font-weight: 500;
|
|
1123
|
+
cursor: pointer;
|
|
1124
|
+
transition: background 0.2s;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.modal-button:hover {
|
|
1128
|
+
background: #2563eb;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.modal-button:active {
|
|
1132
|
+
background: #1d4ed8;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
@keyframes fadeIn {
|
|
1136
|
+
from {
|
|
1137
|
+
opacity: 0;
|
|
1138
|
+
}
|
|
1139
|
+
to {
|
|
1140
|
+
opacity: 1;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
@keyframes scaleIn {
|
|
1145
|
+
from {
|
|
1146
|
+
transform: scale(0.9);
|
|
1147
|
+
opacity: 0;
|
|
1148
|
+
}
|
|
1149
|
+
to {
|
|
1150
|
+
transform: scale(1);
|
|
1151
|
+
opacity: 1;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
` })
|
|
1155
|
+
] });
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
export { DEFAULT_CONFIG, DEFAULT_STATS, ENGINE_VERSION, EngineEvents, EventBus, FlowSystem, HistoryManager, LOG_TYPE_COLORS, Layout, LogStream, MainContent, OverlaySystem, QuerySystem, SystemMessages, TIME_CONSTANTS, UI_CONSTANTS, VALIDATION, clamp, createGameEngineStore, debounce, deepClone, delay, formatGameTime, formatNumber, gameEvents, generateId, get, getPercentage, getStateDiff, getTimeOfDay, isEmpty, randomChoice, randomInt, shuffle, throttle };
|
|
1159
|
+
//# sourceMappingURL=index.mjs.map
|
|
1160
|
+
//# sourceMappingURL=index.mjs.map
|