import { logger } from '@motajs/common'; import { ILoadManager, LoadManager } from '@motajs/loader'; import { IRoleFaceBinder, IFaceManager, ITileStore, ISaveableContent, TileStore, SaveCompression, RoleFaceBinder, FaceManager, Dir4FaceHandler, Dir8FaceHandler, FaceGroup, FaceDirection, IHeroAttr, IEnemyAttr, GameEventStore, IGameEventStore, IItemStore, ItemStore, IMapStore, MapStore, IReplaySystem, ReplaySystem, ReplayCode } from '@user/data-common'; import { EnemyManager, IEnemyManager, HeroAttribute, HeroState, IHeroState, IFlagSystem, FlagSystem, IMapState, MapState } from '@user/data-base'; import { DamageSystem, EnemyContext, GameEventSystem, IEnemyContext, IGameEventSystem, MapDamage, IPathfindingSystem, PathfindingSystem } from '@user/data-system'; import { ICoreState, ICoreStateConfig, ISaveableExecutor } from './types'; import { CommonAuraConverter, EnemyLegacyBridge, GuardAuraConverter, MainDamageCalculator, MainEnemyFinalEffect, MainMapDamageConverter, MainMapDamageReducer, registerSpecials, MainEnemyComparer } from './enemy'; import { HERO_DEFAULT_ATTRIBUTE, TILE_HEIGHT, TILE_WIDTH } from './shared'; import { DefaultHeroMoveTopImpl, DefaultPassPredicateImpl } from './hero'; import { createEventRegistrations } from './event'; import { ReplayEquip, ReplayMove, ReplayTeleport, ReplayUnequip, ReplayUseItem } from './replay'; import { IMotaDataLoader, MotaDataLoader, DefaultDataLoaderHook } from './loader'; export class CoreState implements ICoreState { // Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据 readonly roleFace: IRoleFaceBinder; readonly faceManager: IFaceManager; readonly tileStore: ITileStore; readonly itemStore: IItemStore; readonly mapStore: IMapStore; readonly eventStore: IGameEventStore; readonly replaySystem: IReplaySystem; // Layer 1 数据层,所有可存档内容都在这,一般用于数据存储 readonly maps: IMapState; readonly hero: IHeroState; readonly enemyManager: IEnemyManager; readonly flags: IFlagSystem; // Layer 2 执行层,游戏逻辑对象都在这,包括一些需要操作数据层的逻辑系统等 readonly enemyContext: IEnemyContext; readonly eventSystem: IGameEventSystem; readonly pathfinding: IPathfindingSystem; // Layer 3 用户层,也就是最顶层的内容,一般仅用于初始化以及仅供渲染端调用的顶层模块 readonly loader: IMotaDataLoader; readonly loadManager: ILoadManager; /** 可存档对象映射 */ private readonly saveables: Map> = new Map(); /** 所有已添加的可存档对象 */ private readonly addedSaveables: Set> = new Set(); /** 已绑定的存档执行器 */ private readonly executors: Map< ISaveableContent, ISaveableExecutor > = new Map(); constructor(config: Readonly) { //#region L0 初始化 // 朝向 this.roleFace = new RoleFaceBinder(); this.faceManager = new FaceManager(); const dir4 = new Dir4FaceHandler(); const dir8 = new Dir8FaceHandler(); this.faceManager.register(FaceGroup.Dir4, dir4); this.faceManager.registerById('dir4', dir4); this.faceManager.register(FaceGroup.Dir8, dir8); this.faceManager.registerById('dir8', dir8); // 图块 this.tileStore = new TileStore(); // 道具 this.itemStore = new ItemStore(this.tileStore); // 地图 this.mapStore = new MapStore(); // 游戏事件 this.eventStore = new GameEventStore(); //#endregion //#region L1 初始化 // Flag 系统 this.flags = new FlagSystem(); // 地图 this.maps = new MapState(this.tileStore, this); // 勇士 const heroAttribute = new HeroAttribute(HERO_DEFAULT_ATTRIBUTE); const heroState = new HeroState(this, dir8, heroAttribute); this.hero = heroState; // 怪物管理器 const comparer = new MainEnemyComparer(); const enemyManager = new EnemyManager(new EnemyLegacyBridge()); enemyManager.attachEnemyComparer(comparer); enemyManager.setAttributeDefaults('hp', 0); enemyManager.setAttributeDefaults('atk', 0); enemyManager.setAttributeDefaults('def', 0); enemyManager.setAttributeDefaults('exp', 0); enemyManager.setAttributeDefaults('money', 0); enemyManager.setAttributeDefaults('point', 0); registerSpecials(enemyManager); this.enemyManager = enemyManager; //#endregion //#region L2 初始化 // 怪物上下文 const enemyContext = new EnemyContext(this); const damageSystem = new DamageSystem(enemyContext); const mapDamage = new MapDamage(enemyContext); damageSystem.useCalculator(new MainDamageCalculator()); mapDamage.useReducer(new MainMapDamageReducer()); mapDamage.useConverter(new MainMapDamageConverter()); enemyContext.attachDamageSystem(damageSystem); enemyContext.attachMapDamage(mapDamage); enemyContext.registerAuraConverter(new CommonAuraConverter()); enemyContext.registerAuraConverter(new GuardAuraConverter()); enemyContext.registerFinalEffect(new MainEnemyFinalEffect()); enemyContext.resize(TILE_WIDTH, TILE_HEIGHT); enemyContext.bindHero(heroAttribute); this.enemyContext = enemyContext; // 游戏事件系统 const eventSystem = new GameEventSystem( this, createEventRegistrations() ); this.eventSystem = eventSystem; // 录像系统 this.replaySystem = new ReplaySystem(); // 寻路系统 this.pathfinding = new PathfindingSystem(this); this.pathfinding.useMover(this.hero.location.mover); // 初始状态下勇士不在任何楼层,切换楼层后再具体设置 this.pathfinding.finder.useMapLayer(null); const predicate = new DefaultPassPredicateImpl(this); this.pathfinding.finder.usePassPredicate(predicate); //#endregion //#region L3 初始化 // 加载 this.loadManager = new LoadManager(); this.loader = new MotaDataLoader(this.loadManager, config.loadStarter); this.loader.addHook(new DefaultDataLoaderHook()); this.loader.addCoreConfig('core', config.coreURL); // 存档内容 this.addSaveableContent('@system/hero', this.hero); this.addSaveableContent('@system/flags', this.flags); this.addSaveableContent('@system/maps', this.maps); this.addSaveableContent('@system/enemy', this.enemyManager); this.addSaveableContent('@system/replay', this.replaySystem); // 勇士顶层初始化 const heroMoveTopImpl = new DefaultHeroMoveTopImpl(this); this.hero.location.mover.useTopImplementation(heroMoveTopImpl); // 录像系统初始化注册 this.registerReplayCommands(); //#endregion } //#region 私有方法 /** * 注册全部录像指令 */ private registerReplayCommands() { const replay = this.replaySystem; const up = new ReplayMove(this, FaceDirection.Up); const right = new ReplayMove(this, FaceDirection.Right); const down = new ReplayMove(this, FaceDirection.Down); const left = new ReplayMove(this, FaceDirection.Left); const teleport = new ReplayTeleport(this); const useItem = new ReplayUseItem(this); const equip = new ReplayEquip(this); const unequip = new ReplayUnequip(this); replay.registerCommand(ReplayCode.Up, up); replay.registerCommand(ReplayCode.Right, right); replay.registerCommand(ReplayCode.Down, down); replay.registerCommand(ReplayCode.Left, left); replay.registerCommand(ReplayCode.Teleport, teleport); replay.registerCommand(ReplayCode.UseItem, useItem); replay.registerCommand(ReplayCode.Equip, equip); replay.registerCommand(ReplayCode.Unequip, unequip); } //#endregion //#region 存档方法 addSaveableContent(id: string, content: ISaveableContent): void { if (this.saveables.has(id)) { logger.warn(112, id); return; } this.saveables.set(id, content); this.addedSaveables.add(content); } getSaveableContent(id: string): ISaveableContent | null { const content = this.saveables.get(id); return (content as ISaveableContent) ?? null; } bindSaveableExecuter( content: ISaveableContent | string, executor: ISaveableExecutor ): 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 { const result = new Map(); for (const [key, value] of this.saveables) { const content = value.saveState(compression); result.set(key, content); } return result; } loadState( state: ReadonlyMap, 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(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 }