mirror of
https://github.com/motajs/template.git
synced 2026-10-17 03:10:17 +08:00
315 lines
10 KiB
TypeScript
315 lines
10 KiB
TypeScript
import {
|
|
Hookable,
|
|
HookController,
|
|
IHookController,
|
|
logger
|
|
} from '@motajs/common';
|
|
import { ILoadManager, LoadManager } from '@motajs/loader';
|
|
import {
|
|
IRoleFaceBinder,
|
|
IFaceManager,
|
|
ITileStore,
|
|
ISaveableContent,
|
|
TileStore,
|
|
SaveCompression,
|
|
RoleFaceBinder,
|
|
FaceManager,
|
|
IHeroAttr,
|
|
IEnemyAttr,
|
|
GameEventStore,
|
|
IGameEventStore,
|
|
IItemStore,
|
|
ItemStore,
|
|
IMapStore,
|
|
MapStore,
|
|
IReplaySystem,
|
|
ReplaySystem,
|
|
IDataCommonHooks
|
|
} from '@user/data-common';
|
|
import {
|
|
EnemyManager,
|
|
IEnemyManager,
|
|
HeroState,
|
|
IHeroState,
|
|
IFlagSystem,
|
|
FlagSystem,
|
|
IMapState,
|
|
MapState
|
|
} from '@user/data-base';
|
|
import {
|
|
CombatFlow,
|
|
EnemyContext,
|
|
GameEventSystem,
|
|
ICombatFlow,
|
|
IEnemyContext,
|
|
IGameEventSystem,
|
|
IPathfindingSystem,
|
|
PathfindingSystem
|
|
} from '@user/data-system';
|
|
import {
|
|
ICoreState,
|
|
ICoreStateConfig,
|
|
IDataPlugin,
|
|
IDataPluginDefine,
|
|
ISaveableExecutor,
|
|
PluginStatus
|
|
} from './types';
|
|
import { IPluginOrderResult, orderPlugins } from './plugin';
|
|
import { IMotaDataLoader, MotaDataLoader } from './loader';
|
|
|
|
export class CoreState
|
|
extends Hookable<IDataCommonHooks>
|
|
implements ICoreState
|
|
{
|
|
// Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据
|
|
readonly roleFace: IRoleFaceBinder;
|
|
readonly faceManager: IFaceManager;
|
|
readonly tileStore: ITileStore;
|
|
readonly itemStore: IItemStore<IHeroAttr>;
|
|
readonly mapStore: IMapStore;
|
|
readonly eventStore: IGameEventStore;
|
|
readonly replaySystem: IReplaySystem;
|
|
|
|
// Layer 1 数据层,所有可存档内容都在这,一般用于数据存储
|
|
readonly maps: IMapState;
|
|
readonly hero: IHeroState<IHeroAttr>;
|
|
readonly enemyManager: IEnemyManager<IEnemyAttr>;
|
|
readonly flags: IFlagSystem;
|
|
|
|
// Layer 2 执行层,游戏逻辑对象都在这,包括一些需要操作数据层的逻辑系统等
|
|
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
|
|
readonly combat: ICombatFlow<IEnemyAttr, IHeroAttr>;
|
|
readonly eventSystem: IGameEventSystem;
|
|
readonly pathfinding: IPathfindingSystem;
|
|
|
|
// Layer 3 用户层,也就是最顶层的内容,一般仅用于初始化以及仅供渲染端调用的顶层模块
|
|
readonly loader: IMotaDataLoader;
|
|
readonly loadManager: ILoadManager;
|
|
|
|
/** 可存档对象映射 */
|
|
private readonly saveables: Map<string, ISaveableContent<any>> = new Map();
|
|
/** 所有已添加的可存档对象 */
|
|
private readonly addedSaveables: Set<ISaveableContent<any>> = new Set();
|
|
/** 已绑定的存档执行器 */
|
|
private readonly executors: Map<
|
|
ISaveableContent<any>,
|
|
ISaveableExecutor<any>
|
|
> = new Map();
|
|
|
|
/** 已注册的插件 */
|
|
private readonly plugins: Map<string, IDataPlugin<IDataPluginDefine>> =
|
|
new Map();
|
|
private readonly orderedDataPlugins: IPluginOrderResult<
|
|
IDataPlugin<IDataPluginDefine>
|
|
>;
|
|
|
|
constructor(config: Readonly<ICoreStateConfig>) {
|
|
super();
|
|
// L0 定义
|
|
this.roleFace = new RoleFaceBinder();
|
|
this.faceManager = new FaceManager();
|
|
this.tileStore = new TileStore();
|
|
this.itemStore = new ItemStore<IHeroAttr>(this.tileStore);
|
|
this.mapStore = new MapStore();
|
|
this.eventStore = new GameEventStore();
|
|
this.replaySystem = new ReplaySystem();
|
|
|
|
// L1 定义
|
|
this.flags = new FlagSystem();
|
|
this.maps = new MapState(this);
|
|
this.hero = new HeroState(this);
|
|
this.enemyManager = new EnemyManager<IEnemyAttr>(this.tileStore);
|
|
|
|
// L2 定义
|
|
this.enemyContext = new EnemyContext<IEnemyAttr, IHeroAttr>(this);
|
|
this.combat = new CombatFlow(this);
|
|
this.eventSystem = new GameEventSystem(this, config.events);
|
|
this.pathfinding = new PathfindingSystem(this);
|
|
|
|
// L3 定义
|
|
this.loadManager = new LoadManager();
|
|
this.loader = new MotaDataLoader(this.loadManager, config.loadStarter);
|
|
|
|
this.loader.addCoreConfig('core', config.coreURL);
|
|
|
|
// 数据端插件注册表
|
|
for (const define of config.dataPlugins) {
|
|
if (this.plugins.has(define.id)) {
|
|
logger.error(78, define.id);
|
|
continue;
|
|
}
|
|
this.plugins.set(define.id, {
|
|
status: PluginStatus.NotAwakened,
|
|
plugin: define
|
|
});
|
|
}
|
|
this.orderedDataPlugins = orderPlugins(this.plugins);
|
|
this.awake();
|
|
}
|
|
|
|
protected createController(
|
|
hook: Partial<IDataCommonHooks>
|
|
): IHookController<IDataCommonHooks> {
|
|
return new HookController(this, hook);
|
|
}
|
|
|
|
emitTip(tip: string, icon: number): void {
|
|
this.forEachHook(hook => hook.onTip?.(tip, icon));
|
|
}
|
|
|
|
//#region 插件方法
|
|
|
|
/**
|
|
* 插件同步初始化
|
|
*/
|
|
protected awake(): void {
|
|
const result = this.orderedDataPlugins;
|
|
if (result.cyclic.size > 0) {
|
|
logger.error(76, [...result.cyclic].join(', '));
|
|
}
|
|
for (const [pluginId, lacked] of result.missing) {
|
|
for (const dependency of lacked) {
|
|
logger.error(77, pluginId, dependency);
|
|
}
|
|
}
|
|
for (const holder of result.order) {
|
|
if (holder.status !== PluginStatus.NotAwakened) continue;
|
|
const dependencies: IDataPlugin<IDataPluginDefine>[] = [];
|
|
for (const dependency of holder.plugin.dependencies) {
|
|
const plugin = this.plugins.get(dependency);
|
|
if (plugin) dependencies.push(plugin);
|
|
}
|
|
holder.plugin.awake(this, dependencies);
|
|
holder.status = PluginStatus.Awakened;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 插件异步初始化
|
|
*/
|
|
protected async initialize(): Promise<void> {
|
|
const result = this.orderedDataPlugins;
|
|
for (const holder of result.order) {
|
|
if (holder.status !== PluginStatus.Awakened) continue;
|
|
const dependencies: IDataPlugin<IDataPluginDefine>[] = [];
|
|
for (const dependency of holder.plugin.dependencies) {
|
|
const plugin = this.plugins.get(dependency);
|
|
if (plugin) dependencies.push(plugin);
|
|
}
|
|
await holder.plugin.initialize(this, dependencies);
|
|
holder.status = PluginStatus.Initialized;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 插件加载初始化
|
|
*/
|
|
protected async afterLoad(): Promise<void> {
|
|
const result = this.orderedDataPlugins;
|
|
for (const holder of result.order) {
|
|
if (holder.status !== PluginStatus.Initialized) continue;
|
|
const dependencies: IDataPlugin<IDataPluginDefine>[] = [];
|
|
for (const dependency of holder.plugin.dependencies) {
|
|
const plugin = this.plugins.get(dependency);
|
|
if (plugin) dependencies.push(plugin);
|
|
}
|
|
await holder.plugin.afterLoad(this, dependencies);
|
|
holder.status = PluginStatus.Loaded;
|
|
}
|
|
}
|
|
|
|
getDataPlugin<T extends IDataPluginDefine>(
|
|
id: string
|
|
): Readonly<IDataPlugin<T>> | null {
|
|
return (this.plugins.get(id) as IDataPlugin<T>) ?? null;
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region 初始化方法
|
|
|
|
async *load(): AsyncGenerator<number, void> {
|
|
await this.initialize();
|
|
yield* this.loader.start();
|
|
await this.afterLoad();
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region 存档方法
|
|
|
|
addSaveableContent(id: string, content: ISaveableContent<unknown>): void {
|
|
if (this.saveables.has(id)) {
|
|
logger.warn(112, id);
|
|
return;
|
|
}
|
|
this.saveables.set(id, content);
|
|
this.addedSaveables.add(content);
|
|
}
|
|
|
|
getSaveableContent<T>(id: string): ISaveableContent<T> | null {
|
|
const content = this.saveables.get(id);
|
|
return (content as ISaveableContent<T>) ?? null;
|
|
}
|
|
|
|
bindSaveableExecuter<T>(
|
|
content: ISaveableContent<T> | string,
|
|
executor: ISaveableExecutor<T>
|
|
): void {
|
|
if (typeof content === 'string') {
|
|
const saveable = this.saveables.get(content);
|
|
if (!saveable) return;
|
|
this.executors.set(saveable, executor);
|
|
} else {
|
|
if (!this.addedSaveables.has(content)) {
|
|
logger.warn(113);
|
|
return;
|
|
}
|
|
this.executors.set(content, executor);
|
|
}
|
|
}
|
|
|
|
saveState(compression: SaveCompression): ReadonlyMap<string, unknown> {
|
|
const result = new Map<string, unknown>();
|
|
for (const [key, value] of this.saveables) {
|
|
const content = value.saveState(compression);
|
|
result.set(key, content);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
loadState(
|
|
state: ReadonlyMap<string, unknown>,
|
|
compression: SaveCompression
|
|
): void {
|
|
// 需要先清空录像的禁用标记
|
|
this.replaySystem.array.clearDisableFlag();
|
|
for (const [key, value] of this.saveables) {
|
|
// 使用 has 判断是否在映射中,而非值的非空判断,因为空值也有可能是存档的一部分
|
|
if (!state.has(key)) {
|
|
logger.warn(177, key);
|
|
continue;
|
|
}
|
|
const data = state.get(key);
|
|
// 使用禁用录像包裹所有的读档行为,避免产生意外记录
|
|
this.replaySystem.disable();
|
|
value.loadState(data, compression);
|
|
this.replaySystem.revert();
|
|
// 但 Executor 不包裹,因为它确实可能产生需要记录的行为
|
|
const executor = this.executors.get(value);
|
|
if (executor) {
|
|
executor.afterLoad(value, this);
|
|
}
|
|
}
|
|
const loaded = new Set<string>(state.keys());
|
|
const total = new Set(this.saveables.keys());
|
|
const remain = loaded.difference(total);
|
|
if (remain.size > 0) {
|
|
const ids = [...remain].join(' | ');
|
|
logger.warn(178, ids);
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
}
|