refactor: 加载系统重构

This commit is contained in:
AncTe committed 2026-09-20 20:53:46 +08:00
1 parent d12790280a
commit cc434a6c66
27 files changed
+409 -266

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
import { IHeroAttr } from '@user/data-common';
//#region 数据端主配置
export interface ICoreStateContentDefaultConfig {
/** 怪物默认对象路径 */
readonly enemyDefault: string;
/** 道具默认对象路径 */
readonly itemDefault: string;
/** 地图默认对象路径 */
readonly mapDefault: string;
/** 图块默认对象路径 */
readonly tileDefault: string;
}
export interface ICoreStateContentConfig {
/** 配置的默认对象配置路径 */
readonly defaults: ICoreStateContentDefaultConfig;
/** 地图文件夹路径 */
readonly mapsDir: string;
/** 需要加载的地图列表文件的文件路径 */
readonly mapListDir: string;
/** 怪物数据文件路径 */
readonly enemyDir: string;
/** 道具数据文件路径 */
readonly itemDir: string;
/** 道具效果文件路径 */
readonly itemEffectDir: string;
/** 图库数据文件路径 */
readonly tileDir: string;
}
export interface ICoreStateHeroInitConfig<T> {
/** 勇士的初始横坐标 */
readonly x: number;
/** 勇士的初始纵坐标 */
readonly y: number;
/** 勇士初始所在楼层 */
readonly floorId: string;
/** 勇士的初始装备栏 */
readonly equipSlots: string[];
/** 勇士的初始属性 */
readonly attribute: Readonly<T>;
}
export interface ICoreStateInitConfig {
/** 初始事件 id,游戏开始时将在执行完毕此事件后正式开始 */
readonly startEvent: string;
/** 勇士初始状态,包含起始位置、初始属性等 */
readonly hero: ICoreStateHeroInitConfig<IHeroAttr>;
}
export interface ICoreStateFlagConfig {
/** 楼层传送是否要求在楼梯边才能使用 */
readonly flyNearStair: boolean;
/** 楼层传送是否为平面塔模式,即传送时会传送至上一次离开此位置的位置而非楼梯口 */
readonly flyRecordLocation: boolean;
/** 道具在首次拾取时是否显示其文字说明 */
readonly itemHint: boolean;
/** 是否启用负伤机制,开启后怪物总伤害可以变为负值 */
readonly enableNegativeDamage: boolean;
/** 是否限制夹击最高伤害为两夹击怪物中伤害最高者 */
readonly betweenDamageMax: boolean;
/** 是否启用轻按机制,即可以按下空格来拾取玩家四周的物品 */
readonly enablePick: boolean;
/** 若在寻路时需要穿过楼梯,是否允许穿越 */
readonly ignoreChangeFLoor: boolean;
/** 若继续前进会导致勇士受到地图伤害而死亡,是否允许前进 */
readonly allowDeadZone: boolean;
/** 是否允许点击瞬移 */
readonly enableTeleport: boolean;
/** 是否开启录像折叠,这可以使走路的录像大幅减少,但可能导致某些机制出错,可查看文档详细了解 */
readonly enableRouteFolding: boolean;
/** 是否开启瞬移合并机制,开启后,连续瞬移会在录像中合并为单次瞬移,但可能导致某些机制出错 */
readonly enableTeleportMerge: boolean;
}
export interface ICoreStateCoreConfig {
/** 游戏的唯一标识符 */
readonly name: string;
/** 呈现给玩家的游戏名称 */
readonly title: string;
/** 游戏版本号,仅呈现作用,对游戏本身没有任何影响 */
readonly version: string;
/** 游戏数据配置 */
readonly content: ICoreStateContentConfig;
/** 游戏的初始状态数据 */
readonly init: ICoreStateInitConfig;
/** 游戏的全局设置 */
readonly flag: ICoreStateFlagConfig;
}
//#endregion
+15 -8
View File
@@ -1,5 +1,4 @@
import { logger } from '@motajs/common';
import { ILoadManager, LoadManager } from '@motajs/loader';
import {
IRoleFaceBinder,
IFaceManager,
@@ -33,8 +32,6 @@ import {
IHeroState,
IFlagSystem,
FlagSystem,
IMotaDataLoader,
MotaDataLoader,
IMapState,
MapState
} from '@user/data-base';
@@ -70,6 +67,10 @@ import {
ReplayUnequip,
ReplayUseItem
} from './replay';
import { IMotaDataLoader } from './loader/types';
import { MotaDataLoader } from './loader/loader';
import { ILoadManager, LoadManager } from '@motajs/loader';
import { DefaultDataLoaderHook } from './loader/hook';
export class CoreState implements ICoreState {
// Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据
@@ -93,8 +94,8 @@ export class CoreState implements ICoreState {
readonly pathfinding: IPathfindingSystem;
// Layer 3 用户层,也就是最顶层的内容,一般仅用于初始化以及仅供渲染端调用的顶层模块
readonly loadProgress: ILoadManager;
readonly dataLoader: IMotaDataLoader;
readonly loader: IMotaDataLoader;
readonly loadManager: ILoadManager;
/** 可存档对象映射 */
private readonly saveables: Map<string, ISaveableContent<any>> = new Map();
@@ -143,9 +144,6 @@ export class CoreState implements ICoreState {
const heroState = new HeroState(this, dir8, heroAttribute);
this.hero = heroState;
this.loadProgress = new LoadManager();
this.dataLoader = new MotaDataLoader(this.loadProgress);
// 怪物管理器
const comparer = new MainEnemyComparer();
const enemyManager = new EnemyManager(new EnemyLegacyBridge());
@@ -201,6 +199,15 @@ export class CoreState implements ICoreState {
//#region L3 初始化
// 加载
this.loadManager = new LoadManager();
this.loader = new MotaDataLoader(
this.loadManager,
config.coreURL,
config.loadStarter
);
this.loader.addHook(new DefaultDataLoaderHook());
// 存档内容
this.addSaveableContent('@system/hero', this.hero);
this.addSaveableContent('@system/flags', this.flags);
@@ -0,0 +1,26 @@
import { ICoreStateCoreConfig } from '../common';
import { IMotaDataLoader, IMotaDataLoaderHooks } from './types';
export class DefaultDataLoaderHook implements IMotaDataLoaderHooks {
awake(): void {}
destroy(): void {}
onCoreConfigLoaded(
coreConfig: ICoreStateCoreConfig,
loader: IMotaDataLoader
): Promise<void> {
const content = coreConfig.content;
loader.addExtraConfig('enemy', content.enemyDir);
loader.addExtraConfig('item', content.itemDir);
loader.addExtraConfig('map-list', content.mapListDir);
loader.addExtraConfig('tile', content.tileDir);
const defaults = content.defaults;
loader.addExtraConfig('enemy-default', defaults.enemyDefault);
loader.addExtraConfig('item-default', defaults.itemDefault);
loader.addExtraConfig('map-default', defaults.mapDefault);
loader.addExtraConfig('tile-default', defaults.tileDefault);
return Promise.resolve();
}
}
Whitespace-only changes.
@@ -0,0 +1,29 @@
import { logger } from '@motajs/common';
import { ILoadTask, ILoadTaskProcessor, LoadDataType } from '@motajs/loader';
export class PrefixedJSONCProcessor<T> implements ILoadTaskProcessor<
LoadDataType.Text,
T
> {
constructor(readonly prefixSplit: string) {}
process(
response: string,
task: ILoadTask<LoadDataType.Text, T>
): Promise<T> {
const splitted = response.split(this.prefixSplit);
if (splitted.length !== 2) {
logger.error(66, task.url, this.prefixSplit);
return Promise.resolve({} as T);
}
const raw = splitted[1];
try {
const obj = JSON.parse(raw);
return Promise.resolve(obj as T);
} catch (e) {
logger.error(67, task.url, String(e));
return Promise.resolve({} as T);
}
}
}
@@ -0,0 +1,145 @@
import {
ILoadManager,
ILoadTask,
ILoadTaskStarter,
LoadDataType,
LoadTask
} from '@motajs/loader';
import { IMotaDataLoader, IMotaDataLoaderHooks } from './types';
import { PrefixedJSONCProcessor } from './jsoncProcessor';
import { ICoreStateCoreConfig } from '../common';
import {
Hookable,
HookController,
IHookController,
logger
} from '@motajs/common';
export class MotaDataLoader
extends Hookable<IMotaDataLoaderHooks>
implements IMotaDataLoader
{
/** 加载完毕时兑现的 `Promise` */
private loadedPromise: Promise<void>;
/** 调用以使 `loadedPromise` 兑现 */
private loadResolve: () => void;
/** 当前是否正在加载 */
private loading: boolean = false;
/** 当前是否已经加载完毕 */
private dataLoaded: boolean = false;
/** 额外配置对象的路径 */
private extraConfigs: Map<string, string> = new Map();
/** 配置文件列表 */
private configMap: Map<string, any> = new Map();
/** 自定义加载任务 */
private customTask: Set<ILoadTask<LoadDataType, unknown>> = new Set();
constructor(
readonly manager: ILoadManager,
readonly coreDir: string,
readonly starter: ILoadTaskStarter
) {
super();
const { promise, resolve } = Promise.withResolvers<void>();
this.loadedPromise = promise;
this.loadResolve = resolve;
}
protected createController(
hook: Partial<IMotaDataLoaderHooks>
): IHookController<IMotaDataLoaderHooks> {
return new HookController(this, hook);
}
addExtraConfig(identifier: string, url: string): void {
this.extraConfigs.set(identifier, url);
}
addCustomTask<T>(task: LoadTask<LoadDataType, T>): Promise<T> {
this.customTask.add(task);
return task.loaded();
}
/**
* 解析加载路径,根据运行环境确认应当使用什么路径格式
* @param url 加载路径
*/
private resolveURL(url: string) {
if (import.meta.env.DEV) {
// 开发时 content 文件都在 src 文件夹下,因此前置 src
return `${import.meta.env.BASE_URL}src/${url}`;
} else {
// 而构建后就会把文件移动至游戏根目录下,因此不需要前置 src
return `${import.meta.env.BASE_URL}${url}`;
}
}
async *start(): AsyncIterable<number> {
if (this.loading || this.dataLoaded) return;
// 首先加载核心配置
const processor = new PrefixedJSONCProcessor<ICoreStateCoreConfig>(
'// --- SYSTEM PREFIX END --- //'
);
const coreTask = new LoadTask<LoadDataType.Text, ICoreStateCoreConfig>({
identifier: 'data-core-config',
url: this.resolveURL(this.coreDir),
dataType: LoadDataType.Text
});
coreTask.setProcessor(processor);
coreTask.setStarter(this.starter);
this.manager.addTask(coreTask);
yield* this.manager.load();
// 然后加载其他配置文件
const coreConfig = await coreTask.loaded();
this.configMap.set('core', coreConfig);
await Promise.all(
this.forEachHook(hook =>
hook.onCoreConfigLoaded?.(coreConfig, this)
)
);
for (const [id, url] of this.extraConfigs) {
const task = new LoadTask({
identifier: id,
url: this.resolveURL(`content/${url}`),
dataType: LoadDataType.Text
});
task.setProcessor(processor);
task.setStarter(this.starter);
this.manager.addTask(task);
task.loaded().then(obj => {
if (this.configMap.has(id)) {
logger.error(68, id);
return;
}
this.configMap.set(id, obj);
});
}
yield* this.manager.load();
await Promise.all(
this.forEachHook(hook => hook.onExtraConfigLoaded?.(this))
);
for (const task of this.customTask) {
this.manager.addTask(task);
}
yield* this.manager.load();
this.loadResolve();
}
getConfig<T>(identifier: string): T | null {
return this.configMap.get(identifier) ?? null;
}
loaded(): Promise<void> {
return this.loadedPromise;
}
}
@@ -0,0 +1,62 @@
import { IHookable, IHookBase } from '@motajs/common';
import { ILoadManager, LoadDataType, LoadTask } from '@motajs/loader';
import { ICoreStateCoreConfig } from '../common';
export interface IMotaDataLoaderHooks extends IHookBase {
/**
* 当核心配置对象加载完毕时触发,往往用于添加自定义配置文件
* @param coreConfig 核心配置对象
* @param loader 数据端加载对象
*/
onCoreConfigLoaded?(
coreConfig: ICoreStateCoreConfig,
loader: IMotaDataLoader
): Promise<void>;
/**
* 当所有的额外配置对象加载完毕后执行,往往用于根据配置文件添加额外加载任务
* @param loader 数据端加载对象
*/
onExtraConfigLoaded?(loader: IMotaDataLoader): Promise<void>;
}
export interface IMotaDataLoader extends IHookable<IMotaDataLoaderHooks> {
/** 当前使用的加载管理器 */
readonly manager: ILoadManager;
/**
* 开始执行加载
*/
start(): AsyncIterable<number>;
/**
* 获取当加载执行完毕时兑现的 `Promise`
*/
loaded(): Promise<void>;
/**
* 添加额外配置文件,仅允许在 `onCoreConfigLoaded` 中调用或在加载前调用,加载中及加载后后调用无效。
* 文件必须放在 `src/content` 下,并填写相对于 `src/content` 文件夹的路径,文件包含如下要求:
*
* - 文件必须为 `jsonc` 格式
* - 文件的注释必须全部集中在开头
* - 文件中的最后一行注释必须是 `// --- SYSTEM PREFIX END --- //`
* @param identifier 加载任务与配置文件的标识符
* @param url 额外配置文件路径
*/
addExtraConfig(identifier: string, url: string): void;
/**
* 添加自定义加载任务,仅允许在 `onCoreConfigLoaded` 和 `onExtraConfigLoaded` 中调用或在加载前调用,
* 加载中及加载后后调用无效。所有的自定义加载任务都会在配置文件加载完毕后开始加载。
* @param task 自定义加载任务实例
* @returns 任务加载完毕后兑现,兑现值为经过加载处理器处理后的值
*/
addCustomTask<T>(task: LoadTask<LoadDataType, T>): Promise<T>;
/**
* 获取指定配置文件,例如核心配置文件是 `core`,怪物配置文件是 `enemy` 等等
* @param identifier 配置文件标识符
*/
getConfig<T>(identifier: string): T | null;
}
+6 -82
View File
@@ -1,7 +1,8 @@
import { IMotaDataLoader, IStateBase } from '@user/data-base';
import { IStateBase } from '@user/data-base';
import { ILoadManager, ILoadTaskStarter } from '@motajs/loader';
import { IStateSystem } from '@user/data-system';
import { IHeroAttr, ISaveableContent } from '@user/data-common';
import { ISaveableContent } from '@user/data-common';
import { IMotaDataLoader } from './loader/types';
//#region 数据端主对象
@@ -26,10 +27,10 @@ export interface ISaveableExecutor<T> {
export interface ICoreState
extends IStateSystem, ISaveableContent<ReadonlyMap<string, unknown>> {
/** 加载进度对象 */
readonly loadProgress: ILoadManager;
/** 数据端加载对象 */
readonly dataLoader: IMotaDataLoader;
readonly loader: IMotaDataLoader;
/** 加载管理器 */
readonly loadManager: ILoadManager;
/**
* 将某个存档执行器绑定至指定的可存档对象,一个可存档对象只能绑定一个执行器,
@@ -44,80 +45,3 @@ export interface ICoreState
}
//#endregion
//#region 数据端主配置
export interface ICoreStateContentConfig {
/** 地图文件夹路径 */
readonly mapsDir: string;
/** 需要加载的地图列表文件的文件路径 */
readonly mapListDir: string;
/** 怪物数据文件路径 */
readonly enemyDir: string;
/** 道具数据文件路径 */
readonly itemDir: string;
/** 图库数据文件路径 */
readonly tileDir: string;
}
export interface ICoreStateHeroInitConfig<T> {
/** 勇士的初始横坐标 */
readonly x: number;
/** 勇士的初始纵坐标 */
readonly y: number;
/** 勇士初始所在楼层 */
readonly floorId: string;
/** 勇士的初始装备栏 */
readonly equipSlots: string[];
/** 勇士的初始属性 */
readonly attribute: Readonly<T>;
}
export interface ICoreStateInitConfig {
/** 初始事件 id,游戏开始时将在执行完毕此事件后正式开始 */
readonly startEvent: string;
/** 勇士初始状态,包含起始位置、初始属性等 */
readonly hero: ICoreStateHeroInitConfig<IHeroAttr>;
}
export interface ICoreStateFlagConfig {
/** 楼层传送是否要求在楼梯边才能使用 */
readonly flyNearStair: boolean;
/** 楼层传送是否为平面塔模式,即传送时会传送至上一次离开此位置的位置而非楼梯口 */
readonly flyRecordLocation: boolean;
/** 道具在首次拾取时是否显示其文字说明 */
readonly itemHint: boolean;
/** 是否启用负伤机制,开启后怪物总伤害可以变为负值 */
readonly enableNegativeDamage: boolean;
/** 是否限制夹击最高伤害为两夹击怪物中伤害最高者 */
readonly betweenDamageMax: boolean;
/** 是否启用轻按机制,即可以按下空格来拾取玩家四周的物品 */
readonly enablePick: boolean;
/** 若在寻路时需要穿过楼梯,是否允许穿越 */
readonly ignoreChangeFLoor: boolean;
/** 若继续前进会导致勇士受到地图伤害而死亡,是否允许前进 */
readonly allowDeadZone: boolean;
/** 是否允许点击瞬移 */
readonly enableTeleport: boolean;
/** 是否开启录像折叠,这可以使走路的录像大幅减少,但可能导致某些机制出错,可查看文档详细了解 */
readonly enableRouteFolding: boolean;
/** 是否开启瞬移合并机制,开启后,连续瞬移会在录像中合并为单次瞬移,但可能导致某些机制出错 */
readonly enableTeleportMerge: boolean;
}
export interface ICoreStateCoreConfig {
/** 游戏的唯一标识符 */
readonly name: string;
/** 呈现给玩家的游戏名称 */
readonly title: string;
/** 游戏版本号,仅呈现作用,对游戏本身没有任何影响 */
readonly version: string;
/** 游戏数据配置 */
readonly content: ICoreStateContentConfig;
/** 游戏的初始状态数据 */
readonly init: ICoreStateInitConfig;
/** 游戏的全局设置 */
readonly flag: ICoreStateFlagConfig;
}
//#endregion