diff --git a/.planning/phases/10-docs-restructure/10-CONTEXT.md b/.planning/phases/10-docs-restructure/10-CONTEXT.md index f67d7cb..e7dd689 100644 --- a/.planning/phases/10-docs-restructure/10-CONTEXT.md +++ b/.planning/phases/10-docs-restructure/10-CONTEXT.md @@ -1,14 +1,15 @@ # Phase 10: 文档重构 - Context **Gathered:** 2026-10-14 -**Status:** Discussion only — 待引擎整体完工后再规划/执行 +**Updated:** 2026-10-14(改为增量派发) +**Status:** Incremental dispatch — 可先行部分先规划执行,其余待引擎完工 ## Phase Boundary 重构过老、结构不佳、新人理解难度过高的项目文档。面向多背景读者(完全新手 / 不懂代码的作者 / 有基础的作者 / AI Agent)提供分层指南,并专门为 AI Agent 编写参考文档。 -**执行时机(本阶段关键):** 本阶段当前**只记录讨论共识,不规划、不执行**。引擎仍在收尾、部分内容(编辑器、部分重构)尚未完工,文档尚不具备编写条件;待引擎整体完工后再进入 Plan → Execute → Verify。 +**执行方式(本阶段关键):** 引擎大部分接口已定稿、短期内不会大改,但编辑器、部分重构与整体收尾尚未完工,故本阶段采用**伞阶段 + 增量派发**:用户每次派发一个具体小任务,AI 据此在 Phase 10 下创建独立计划并执行(Plan → Execute → Verify),做完再由用户派发下一个。**已定稿、可先行的部分先做**;未完工部分(初级文档、高级教程、未定稿接口等)挂起,待条件成熟后由用户再派发。 文档分为六大部分: @@ -47,14 +48,21 @@ ### 范围界定 - **D-05:** API 参考**不含 legacy 包**(`@motajs/legacy-*`、`@user/legacy-*`、`@user/data-fallback`)——这部分尚未重构完。 -- **D-06:** **初级文档**本期不规划,待**编辑器实装后**再规划;具体范围后续另行说明。 -- **D-07:** **高级教程**本期不规划,待**引擎整体完工后**再规划。 +- **D-06:** **初级文档**暂缓——待**编辑器实装后**由用户再派发;具体范围届时另行说明。 +- **D-07:** **高级教程**暂缓——待**引擎整体完工后**由用户再派发。 - **D-08:** **Agent 参考**放 `docs/agent`,具体内容后续再讨论。 ### 验收与流程 - **D-09:** 验收标准 = **用户人工审阅** + **链接全部有效** + **构建通过**。 -- **D-10:** 本阶段当前**仅讨论**;待引擎整体完工后再进入 GSD 的 Plan → Execute → Verify。 +- **D-10:** 本阶段采用**增量派发**:用户每次派发一个具体小任务,AI 只针对该任务在 Phase 10 下创建独立计划;不得预先把整个阶段规划到底。可先行部分先做,未完工部分挂起待派发。 + +### 执行方式(本阶段特有,优先级最高) + +- **D-11:** 阶段计划随任务累积,可反复追加/重规划;新任务到来时可新增计划而不必重排既有计划。 +- **D-12:** 每个计划执行前,第一个 block 点必须**先向用户汇报计划内容**(要做什么、为什么、大致怎么做、影响范围)并等待用户明确回复「可以执行」;未收到「可以执行」前不得开始该计划的任何一步,也不得派发执行子代理。 +- **D-13:** 新增或修改任何公共、受保护、私有方法/字段等命名,以及文档目录/文件名,均**必须先向用户反馈并获批准**后方可使用。 +- **D-14:** 每个任务的范围由用户界定;AI 不擅自扩大范围。发现的越界内容只记录、不实施。 diff --git a/packages-user/client-modules/src/ui/controller.tsx b/packages-user/client-modules/src/ui/controller.tsx deleted file mode 100644 index 1a9df1f..0000000 --- a/packages-user/client-modules/src/ui/controller.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { UIController } from '@motajs/system'; -import { IGameUIPropsBase } from '@user/client-base'; -import { MainBackgroundUI } from './background'; - -export const mainUIController = new UIController(); - -export function createMainController() { - mainUIController.setBackground(MainBackgroundUI, {}); -} diff --git a/packages-user/data-base/src/flag/types.ts b/packages-user/data-base/src/flag/types.ts index 8779c87..5034248 100644 --- a/packages-user/data-base/src/flag/types.ts +++ b/packages-user/data-base/src/flag/types.ts @@ -78,7 +78,7 @@ export interface IFlagSystem extends ISaveableContent { * @param field 字段名称 * @param defaultValue 字段默认值函数 */ - getOrInsertComputed( + getOrInsertComputed( field: K, defaultValue: (field: K) => T ): IFlagCommonField; diff --git a/packages-user/data-common/src/types.ts b/packages-user/data-common/src/types.ts index a6ba781..ffe93d4 100644 --- a/packages-user/data-common/src/types.ts +++ b/packages-user/data-common/src/types.ts @@ -1,7 +1,8 @@ -import { ITileLocator } from '@motajs/common'; import { IFaceManager, IRoleFaceBinder } from './common'; import { IGameEventStore, IItemStore, IMapStore, ITileStore } from './store'; import { IReplaySystem } from './replay'; +import { IEnemyView } from '@user/data-system'; +import { IHookable, IHookBase } from '@motajs/common'; export interface IEnemyAttr { /** 怪物名称 */ @@ -19,7 +20,7 @@ export interface IEnemyAttr { /** 怪物加点量 */ point: number; /** 支援来源怪物坐标索引列表 */ - guard: Set; + guard: Set>; } export interface IHeroAttr { @@ -47,7 +48,16 @@ export interface IHeroAttr { exp: number; } -export interface IDataCommon { +export interface IDataCommonHooks extends IHookBase { + /** + * 进行一次提示的渲染 + * @param tip 提示内容 + * @param icon 提示图标 + */ + onTip?(tip: string, icon: number): void; +} + +export interface IDataCommon extends IHookable { /** 图块定义存储 */ readonly tileStore: ITileStore; /** 道具定义存储 */ @@ -62,6 +72,13 @@ export interface IDataCommon { readonly faceManager: IFaceManager; /** 录像系统 */ readonly replaySystem: IReplaySystem; + + /** + * 通知渲染端进行一次提示渲染 + * @param tip 提示文本 + * @param icon 提示图标 + */ + emitTip(tip: string, icon: number): void; } export interface IDataCommonExtended { diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index b5c2e11..9f9a0ce 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -1,4 +1,9 @@ -import { logger } from '@motajs/common'; +import { + Hookable, + HookController, + IHookController, + logger +} from '@motajs/common'; import { ILoadManager, LoadManager } from '@motajs/loader'; import { IRoleFaceBinder, @@ -18,7 +23,8 @@ import { IMapStore, MapStore, IReplaySystem, - ReplaySystem + ReplaySystem, + IDataCommonHooks } from '@user/data-common'; import { EnemyManager, @@ -31,8 +37,10 @@ import { MapState } from '@user/data-base'; import { + CombatFlow, EnemyContext, GameEventSystem, + ICombatFlow, IEnemyContext, IGameEventSystem, IPathfindingSystem, @@ -49,7 +57,10 @@ import { import { IPluginOrderResult, orderPlugins } from './plugin'; import { IMotaDataLoader, MotaDataLoader } from './loader'; -export class CoreState implements ICoreState { +export class CoreState + extends Hookable + implements ICoreState +{ // Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据 readonly roleFace: IRoleFaceBinder; readonly faceManager: IFaceManager; @@ -67,6 +78,7 @@ export class CoreState implements ICoreState { // Layer 2 执行层,游戏逻辑对象都在这,包括一些需要操作数据层的逻辑系统等 readonly enemyContext: IEnemyContext; + readonly combat: ICombatFlow; readonly eventSystem: IGameEventSystem; readonly pathfinding: IPathfindingSystem; @@ -92,6 +104,7 @@ export class CoreState implements ICoreState { >; constructor(config: Readonly) { + super(); // L0 定义 this.roleFace = new RoleFaceBinder(); this.faceManager = new FaceManager(); @@ -99,6 +112,7 @@ export class CoreState implements ICoreState { this.itemStore = new ItemStore(this.tileStore); this.mapStore = new MapStore(); this.eventStore = new GameEventStore(); + this.replaySystem = new ReplaySystem(); // L1 定义 this.flags = new FlagSystem(); @@ -108,8 +122,8 @@ export class CoreState implements ICoreState { // L2 定义 this.enemyContext = new EnemyContext(this); + this.combat = new CombatFlow(this); this.eventSystem = new GameEventSystem(this, config.events); - this.replaySystem = new ReplaySystem(); this.pathfinding = new PathfindingSystem(this); // L3 定义 @@ -133,6 +147,16 @@ export class CoreState implements ICoreState { this.awake(); } + protected createController( + hook: Partial + ): IHookController { + return new HookController(this, hook); + } + + emitTip(tip: string, icon: number): void { + this.forEachHook(hook => hook.onTip?.(tip, icon)); + } + //#region 插件方法 /** diff --git a/packages-user/data-system/src/combat/combat.ts b/packages-user/data-system/src/combat/combat.ts index aa8ef6d..a602a36 100644 --- a/packages-user/data-system/src/combat/combat.ts +++ b/packages-user/data-system/src/combat/combat.ts @@ -84,10 +84,12 @@ export class CombatFlow */ private createHandler( enemy: IReadonlyEnemy, - locator: ITileLocator | null + locator: ITileLocator | null, + force: boolean ): ICombatFlowHandler { return { onMap: locator !== null, + force, hero: this.hero!, enemy, context: this.context!, @@ -205,7 +207,8 @@ export class CombatFlow @shouldReplay('Battle with enemy should be replayed.') battle( - enemy: IEnemyView + enemy: IEnemyView, + force: boolean = false ): Promise | null> { if (!this.context) { logger.warn(139, 'an enemy context object'); @@ -219,7 +222,7 @@ export class CombatFlow const computed = enemy.getComputedEnemy(); const origin = enemy.getModifiableEnemy(); const locator = this.tryGetEnemyLocator(enemy, computed, origin); - const handler = this.createHandler(origin, locator); + const handler = this.createHandler(origin, locator, force); const eHandler = this.createEnemyHandler(computed, locator); return this.combatFlow(handler, eHandler); @@ -227,7 +230,8 @@ export class CombatFlow @shouldReplay('Battle with enemy should be replayed.') battleComputed( - enemy: IReadonlyEnemy + enemy: IReadonlyEnemy, + force: boolean = false ): Promise | null> { if (!this.context) { logger.warn(139, 'an enemy context object'); @@ -243,7 +247,7 @@ export class CombatFlow if (view) return this.battle(view); else { // 否则走单独的流程 - const handler = this.createHandler(enemy, null); + const handler = this.createHandler(enemy, null, force); const eHandler = this.createEnemyHandler(enemy, null); return this.combatFlow(handler, eHandler); } diff --git a/packages-user/data-system/src/combat/context.ts b/packages-user/data-system/src/combat/context.ts index cc9b693..a72e8ea 100644 --- a/packages-user/data-system/src/combat/context.ts +++ b/packages-user/data-system/src/combat/context.ts @@ -102,9 +102,14 @@ export class EnemyContext implements IEnemyContext< built: boolean = false; width: number = 0; height: number = 0; + floor: string | undefined; constructor(readonly state: IStateBase) {} + setFloor(floor: string | undefined): void { + this.floor = floor; + } + resize(width: number, height: number): void { this.clear(); this.width = width; diff --git a/packages-user/data-system/src/combat/index.ts b/packages-user/data-system/src/combat/index.ts index a893526..21caea4 100644 --- a/packages-user/data-system/src/combat/index.ts +++ b/packages-user/data-system/src/combat/index.ts @@ -1,3 +1,4 @@ +export * from './combat'; export * from './context'; export * from './damage'; export * from './enemy'; diff --git a/packages-user/data-system/src/combat/types.ts b/packages-user/data-system/src/combat/types.ts index c67cc87..44ab211 100644 --- a/packages-user/data-system/src/combat/types.ts +++ b/packages-user/data-system/src/combat/types.ts @@ -537,6 +537,8 @@ export interface IReadonlyEnemyContext< readonly width: number; /** 怪物上下文高度 */ readonly height: number; + /** 怪物上下文对应的楼层 */ + readonly floor: string | undefined; /** 此上下文使用的索引对象 */ readonly indexer: ILocationHelper; @@ -612,9 +614,16 @@ export interface IEnemyContext extends IReadonlyEnemyContext< readonly width: number; /** 怪物上下文高度 */ readonly height: number; + /** 此上下文使用的索引对象 */ readonly indexer: ILocationHelper; + /** + * 设置怪物上下文对应的楼层 + * @param floor 上下文对应的楼层 + */ + setFloor(floor: string | undefined): void; + /** * 调整上下文尺寸,并清空当前上下文中的所有怪物与状态 * @param width 地图宽度 @@ -775,6 +784,8 @@ export interface IEnemyContext extends IReadonlyEnemyContext< export interface ICombatFlowHandler extends IDataBaseExtended { /** 战斗的怪物是否在地图上 */ readonly onMap: boolean; + /** 是否为强制战斗 */ + readonly force: boolean; /** 可修改勇士对象 */ readonly hero: IHeroAttribute; /** 可修改怪物对象 */ @@ -863,9 +874,11 @@ export interface ICombatFlow /** * 与指定怪物战斗 * @param enemy 怪物视图对象 + * @param force 是否为强制战斗,默认为 `false`。强制战斗下,不再有战败保护 */ battle( - enemy: IEnemyView + enemy: IEnemyView, + force?: boolean ): Promise | null>; /** @@ -879,8 +892,12 @@ export interface ICombatFlow /** * 添加战前战后脚本 * @param script 战前战后脚本 + * @param force 是否为强制战斗,默认为 `false`。强制战斗下,不再有战败保护 */ - addCombatScript(script: ICombatScript): void; + addCombatScript( + script: ICombatScript, + force?: boolean + ): void; } //#endregion diff --git a/packages-user/data-system/src/event/executor.ts b/packages-user/data-system/src/event/executor.ts index 906c777..1aaa575 100644 --- a/packages-user/data-system/src/event/executor.ts +++ b/packages-user/data-system/src/event/executor.ts @@ -1,4 +1,10 @@ -import { logger } from '@motajs/common'; +import { + Hookable, + HookController, + IHookBase, + IHookController, + logger +} from '@motajs/common'; import { IGameEventStore, shouldReplay } from '@user/data-common'; import { AnonTokyoInterpreter } from '@motajs/anon-tokyo'; import { @@ -7,17 +13,29 @@ import { IGameEventExecutor, IBlockEventEnv, IBlockEventParam, - IGameEventInvocation + IGameEventInvocation, + IGameEventExecutorHooks } from './types'; -export class EventExecutor implements IGameEventExecutor { +export class EventExecutor + extends Hookable + implements IGameEventExecutor +{ mode: EventExecuteMode = EventExecuteMode.Normal; reduce: EventReduceMode = EventReduceMode.NoReduce; constructor( readonly interpreter: AnonTokyoInterpreter, private readonly storeRef: () => IGameEventStore | null - ) {} + ) { + super(); + } + + protected createController( + hook: Partial + ): IHookController { + return new HookController(this, hook); + } setMode(mode: EventExecuteMode): void { this.mode = mode; @@ -57,6 +75,7 @@ export class EventExecutor implements IGameEventExecutor { events: IGameEventInvocation[], param: IBlockEventParam ): Promise { + this.forEachHook(hook => hook.onBeforeExecuteEvent?.(events, param)); // 事件执行 const results: any[] = []; for (const invocation of events) { @@ -86,6 +105,10 @@ export class EventExecutor implements IGameEventExecutor { } } - return this.reduceResults(this.reduce, results); + const reduced = this.reduceResults(this.reduce, results); + this.forEachHook(hook => + hook.onAfterExecuteEvent?.(events, param, reduced) + ); + return reduced; } } diff --git a/packages-user/data-system/src/event/types.ts b/packages-user/data-system/src/event/types.ts index 1105366..e785c7f 100644 --- a/packages-user/data-system/src/event/types.ts +++ b/packages-user/data-system/src/event/types.ts @@ -1,4 +1,4 @@ -import { ITileLocator } from '@motajs/common'; +import { IHookable, IHookBase, ITileLocator } from '@motajs/common'; import { IDataBaseExtended, IGameMap, @@ -105,7 +105,31 @@ export interface IGameEventInit { addBuiltinFunction(name: string, func: GameEventBuiltinFunction): void; } -export interface IGameEventExecutor { +export interface IGameEventExecutorHooks extends IHookBase { + /** + * 当事件执行前触发 + * @param events 要执行的事件列表,可以直接修改此列表 + * @param param 要传递给事件的参数,可以直接修改此参数 + */ + onBeforeExecuteEvent( + events: IGameEventInvocation[], + param: IBlockEventParam + ): void; + + /** + * 当一个事件执行完毕后触发 + * @param events 执行的事件列表 + * @param param 事件的执行参数 + * @param returns 事件的执行返回值 + */ + onAfterExecuteEvent( + events: IGameEventInvocation[], + param: IBlockEventParam, + returns: unknown + ): void; +} + +export interface IGameEventExecutor extends IHookable { /** 当前的执行器执行模式 */ readonly mode: EventExecuteMode; /** 当前的执行器返回值折叠方式 */ diff --git a/packages-user/data-system/src/path/system.ts b/packages-user/data-system/src/path/system.ts index ad02076..8e19ebf 100644 --- a/packages-user/data-system/src/path/system.ts +++ b/packages-user/data-system/src/path/system.ts @@ -1,4 +1,10 @@ -import { ITileLocator, logger } from '@motajs/common'; +import { + Hookable, + HookController, + IHookController, + ITileLocator, + logger +} from '@motajs/common'; import { IObjectMovable, IObjectMover, shouldReplay } from '@user/data-common'; import { IStateBase } from '@user/data-base'; import { PathFinder } from './finder'; @@ -7,25 +13,36 @@ import { IPathfindingController, IPathfindingResult, IPathfindingSystem, + IPathfindingSystemHooks, PathFallbackPolicy, PathfindingStatus } from './types'; -export class PathfindingSystem implements IPathfindingSystem { +export class PathfindingSystem + extends Hookable + implements IPathfindingSystem +{ /** 寻路求解器 */ readonly finder: IPathFinder; - /** 当前绑定的移动器 */ - private mover: IObjectMover | null = null; + mover: IObjectMover | null = null; + /** 注入的瞬移回退策略,未注入时必定回退为逐步寻路 */ private policy: PathFallbackPolicy | null = null; /** 最近一次寻路移动的控制器包装 */ private current: IPathfindingController | null = null; constructor(readonly state: IStateBase) { + super(); this.finder = new PathFinder(state); } + protected createController( + hook: Partial + ): IHookController { + return new HookController(this, hook); + } + useMover(mover: IObjectMover | null): void { this.mover = mover; } @@ -66,6 +83,7 @@ export class PathfindingSystem implements IPathfindingSystem { if (teleport) { const last = array.at(-1)!; mover.tp(last.to.x, last.to.y); + this.forEachHook(hook => hook.onBeforeTeleportTo?.(path, this)); } else { for (const step of array) { mover.step(step.dir); @@ -76,6 +94,11 @@ export class PathfindingSystem implements IPathfindingSystem { if (!controller) return null; const result: IPathfindingController = { controller, path }; this.current = result; + + controller.onEnd.then(() => { + this.forEachHook(hook => hook.onAfterTeleportTo?.(path, this)); + }); + return result; } diff --git a/packages-user/data-system/src/path/types.ts b/packages-user/data-system/src/path/types.ts index ba4de7f..798d5c9 100644 --- a/packages-user/data-system/src/path/types.ts +++ b/packages-user/data-system/src/path/types.ts @@ -1,4 +1,9 @@ -import { ITileLocator, IFaceHandler } from '@motajs/common'; +import { + ITileLocator, + IFaceHandler, + IHookBase, + IHookable +} from '@motajs/common'; import { IDataBaseExtended, ILayerLocation, @@ -156,9 +161,32 @@ export interface IPathFinder extends IDataBaseExtended { */ export type PathFallbackPolicy = (path: readonly IPathfindingStep[]) => boolean; -export interface IPathfindingSystem extends IDataBaseExtended { +export interface IPathfindingSystemHooks extends IHookBase { + /** + * 在瞬移自动寻路前触发 + * @param path 瞬移的寻路路径 + */ + onBeforeTeleportTo?( + path: IPathfindingResult, + system: IPathfindingSystem + ): void; + + /** + * 在自动寻路之后触发 + * @param path 瞬移的寻路路径 + */ + onAfterTeleportTo?( + path: IPathfindingResult, + system: IPathfindingSystem + ): void; +} + +export interface IPathfindingSystem + extends IDataBaseExtended, IHookable { /** 寻路求解器 */ readonly finder: IPathFinder; + /** 当前正在使用的移动寻路器 */ + readonly mover: IObjectMover | null; /** * 绑定寻路移动器,可绑定勇士或任意 `IObjectMover`,如动态图块移动器 diff --git a/packages-user/data-system/src/types.ts b/packages-user/data-system/src/types.ts index 4e1ab12..2b5b827 100644 --- a/packages-user/data-system/src/types.ts +++ b/packages-user/data-system/src/types.ts @@ -1,5 +1,5 @@ import { IStateBase } from '@user/data-base'; -import { IEnemyContext } from './combat'; +import { ICombatFlow, IEnemyContext } from './combat'; import { IEnemyAttr, IHeroAttr } from '@user/data-common'; import { IGameEventSystem } from './event'; import { IPathfindingSystem } from './path'; @@ -7,6 +7,8 @@ import { IPathfindingSystem } from './path'; export interface IStateSystem extends IStateBase { /** 怪物上下文 */ readonly enemyContext: IEnemyContext; + /** 怪物战斗流程对象 */ + readonly combat: ICombatFlow; /** 游戏事件系统 */ readonly eventSystem: IGameEventSystem; /** 已绑定勇士移动器的寻路系统 */ diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 4fc30e1..bcf50fd 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -1,6 +1,7 @@ import { WebLoadStarter } from '@motajs/loader'; import { createClientCore } from './client/entry'; import { IClientCore } from '@user/client-modules'; +import { LoadSceneUI } from '../packages-user/client-modules/src/ui/load'; declare global { interface Window { @@ -8,5 +9,8 @@ declare global { } } -const client = await createClientCore(new WebLoadStarter()); +const client = createClientCore(new WebLoadStarter()); + +// 游戏副作用启动 window.mainClient = client; +client.sceneController.open(LoadSceneUI, {}); diff --git a/src/client/entry.ts b/src/client/entry.ts index 275d789..f16d83d 100644 --- a/src/client/entry.ts +++ b/src/client/entry.ts @@ -41,9 +41,7 @@ export function createClientCoreConfig( * 创建渲染端主对象,返回未经过异步初始化的主对象实例,需要手动调用 `state.load` 初始化 * @param starter 加载启动器 */ -export async function createClientCore( - starter: ILoadTaskStarter -): Promise { +export function createClientCore(starter: ILoadTaskStarter): IClientCore { const config = createClientCoreConfig(starter); const state = new ClientCore(config); return state; diff --git a/src/client/plugin/list.ts b/src/client/plugin/list.ts index a8291fb..c2d7617 100644 --- a/src/client/plugin/list.ts +++ b/src/client/plugin/list.ts @@ -1,4 +1,5 @@ import { IClientPluginDefine } from '@user/client-modules'; +import { ClientShopPlugin } from './shop'; /** 渲染端插件列表 */ -export const clientPlugins: IClientPluginDefine[] = []; +export const clientPlugins: IClientPluginDefine[] = [new ClientShopPlugin()]; diff --git a/src/client/plugin/shop/index.ts b/src/client/plugin/shop/index.ts new file mode 100644 index 0000000..dfc6512 --- /dev/null +++ b/src/client/plugin/shop/index.ts @@ -0,0 +1,2 @@ +export * from './plugin'; +export * from './shop'; diff --git a/src/client/plugin/shop/plugin.ts b/src/client/plugin/shop/plugin.ts new file mode 100644 index 0000000..2bbe632 --- /dev/null +++ b/src/client/plugin/shop/plugin.ts @@ -0,0 +1,23 @@ +import { IClientPluginDefine } from '@user/client-modules'; +import { Shop, ShopUI } from './shop'; + +export interface IClientShopPlugin extends IClientPluginDefine { + /** 商店组件 */ + readonly Shop: typeof Shop; + /** 商店 UI 对象 */ + readonly ShopUI: typeof ShopUI; +} + +export class ClientShopPlugin implements IClientShopPlugin { + id: string = 'client-shop-plugin'; + dependencies: readonly string[] = []; + + Shop: typeof Shop = Shop; + ShopUI: typeof ShopUI = ShopUI; + + awake(): void {} + + async initialize(): Promise {} + + async afterLoad(): Promise {} +} diff --git a/packages-user/client-modules/src/ui/shop.tsx b/src/client/plugin/shop/shop.tsx similarity index 97% rename from packages-user/client-modules/src/ui/shop.tsx rename to src/client/plugin/shop/shop.tsx index 6388034..b936a2a 100644 --- a/packages-user/client-modules/src/ui/shop.tsx +++ b/src/client/plugin/shop/shop.tsx @@ -2,13 +2,13 @@ import { GameUI, SetupComponentOptions } from '@motajs/system'; import { computed, defineComponent, onMounted, ref } from 'vue'; import { Font } from '@motajs/render'; import { Scroll, TextContent } from '@user/client-base'; -import { IHeroState, IShopState } from '@user/data-base'; -import { IShopSystem } from '@user/data-system'; +import { IHeroState } from '@user/data-base'; import { IHeroAttr } from '@user/data-common'; -import { FULL_LOC, MAIN_HEIGHT, MAIN_WIDTH } from '../shared'; -import { IUIPropsBase } from './types'; +import { FULL_LOC, IUIPropsBase, MAIN_WIDTH } from '@user/client-modules'; +import { IShopState, IShopSystem } from '@dp/shop'; +import { RENDER_WIDTH } from '@/constant'; -export interface ShopProps extends IUIPropsBase { +export interface IShopProps extends IUIPropsBase { /** 商店状态对象 */ shop: IShopState; /** 勇士状态对象 */ @@ -27,7 +27,7 @@ const shopProps = { 'heroState', 'shopSystem' ] -} satisfies SetupComponentOptions; +} satisfies SetupComponentOptions; interface IShopItemEntry { /** 道具图块数字 */ @@ -55,7 +55,7 @@ const LIST_WIDTH = 250; /** 左侧商店列表的顶部纵坐标 */ const LIST_TOP = MODE_BAR_HEIGHT + 4; /** 左侧商店列表的高度,底部为退出按钮留出空间 */ -const LIST_HEIGHT = MAIN_HEIGHT - LIST_TOP - 16; +const LIST_HEIGHT = RENDER_WIDTH - LIST_TOP - 16; /** 商店列表中每一行的高度 */ const ROW_HEIGHT = 40; /** 行内图标的尺寸 */ @@ -63,7 +63,7 @@ const ROW_ICON_SIZE = 28; /** 右侧信息区域的起始横坐标 */ const INFO_LEFT = LIST_WIDTH; /** 右侧信息区域的宽度 */ -const INFO_WIDTH = MAIN_WIDTH - INFO_LEFT; +const INFO_WIDTH = RENDER_WIDTH - INFO_LEFT; /** 道具名称的纵坐标 */ const NAME_TOP = MODE_BAR_HEIGHT + 10; /** 道具描述区域的顶部纵坐标 */ @@ -89,7 +89,7 @@ const titleFont = Font.defaults({ size: 14, weight: 700 }); const textFont = Font.defaults({ size: 12 }); /** 商店界面组件 */ -export const Shop = defineComponent(props => { +export const Shop = defineComponent(props => { /** 商店状态对象 */ const shop = props.shop; /** 勇士状态对象 */ diff --git a/src/data/enemy/aura.ts b/src/data/enemy/aura.ts index df19355..2f87782 100644 --- a/src/data/enemy/aura.ts +++ b/src/data/enemy/aura.ts @@ -175,7 +175,7 @@ export class GuardAura implements IEnemyAuraView< if (locator.x === this.locator.x && locator.y === this.locator.y) { return; } - enemy.getAttribute('guard').add(this.locator); + enemy.getAttribute('guard').add(this.sourceView); } applySpecial(): IEnemySpecialModifier | null { diff --git a/src/data/enemy/calculator.ts b/src/data/enemy/calculator.ts index 02df1e5..6e0d032 100644 --- a/src/data/enemy/calculator.ts +++ b/src/data/enemy/calculator.ts @@ -5,7 +5,6 @@ import { IReadonlyEnemyHandler } from '@user/data-system'; import { IEnemyAttr, IHeroAttr } from '@user/data-common'; -import { logger } from '@motajs/common'; import { ISpecial9Value } from './special'; export class MainDamageCalculator implements IDamageCalculator< @@ -83,13 +82,8 @@ export class MainDamageCalculator implements IDamageCalculator< this.inGuard = true; // 计算支援怪的伤害,同时把打支援怪花费的回合数加到当前怪物上,因为打支援怪的时候当前怪物也会打你 // 因此回合数需要加上打支援怪的回合数 - for (const guard of guards) { + for (const view of guards) { // 直接把 enemy 传过去,因此支援的 enemy 会吃到其原本所在位置的光环加成 - const view = handler.context.getEnemyByLocator(guard); - if (!view) { - logger.warn(137, guard.x.toString(), guard.y.toString()); - continue; - } const extraInfo = this.calculate({ enemy: view.getComputedEnemy(), context: handler.context, diff --git a/src/data/enemy/hook.ts b/src/data/enemy/hook.ts new file mode 100644 index 0000000..d227338 --- /dev/null +++ b/src/data/enemy/hook.ts @@ -0,0 +1,222 @@ +import { + IGameMap, + IHeroLocation, + IHeroLocationHooks, + IHeroModifier, + IHeroMover, + PercentageModifier +} from '@user/data-base'; +import { + IEnemyAttr, + IHeroAttr, + IObjectMover, + IObjectMoverHooks, + ObjectMoveStep, + ObjectMoveType +} from '@user/data-common'; +import { ICoreState } from '@user/data-state'; +import { + ICombatFlowHooks, + IEnemyDamageInfo, + IPathfindingResult, + IPathfindingSystem, + IPathfindingSystemHooks +} from '@user/data-system'; +import { ICombatWeakHandler } from './types'; +import { isNil } from 'lodash-es'; + +export class EnemyMoveHooks implements IObjectMoverHooks { + constructor(readonly state: ICoreState) {} + + /** + * 处理中毒效果 + * @param step 移动步对象 + */ + private handlePoison( + step: Readonly, + mover: IHeroMover + ) { + const poison = this.state.flags.getFieldValueDefaults('poison', 0); + // 中毒伤害小于等于 0,直接返回 + if (poison <= 0) return; + const config = mover.getConfig(); + // 如果忽略地形,那么也不计算中毒伤害 + if (config.ignoreTerrain) return; + + // 获取勇士的可修改属性对象 + const hero = this.state.hero.getModifiableAttribute(); + + // 判断是否为常规移动步 + if ( + step.type === ObjectMoveType.Dir || + step.type === ObjectMoveType.DirFace || + step.type === ObjectMoveType.Special + ) { + // 给勇士加上 -poison 的生命值,就相当于扣 poison 的生命值 + hero.add('hp', -poison); + } + } + + async onStepEnd( + _code: number, + step: Readonly, + tile: IHeroLocation, + mover: IObjectMover + ): Promise { + // 检查状态对象 + if (tile.state !== this.state) return; + // 检查移动器是否为勇士移动器 + if (mover !== this.state.hero.location.mover) return; + + // 处理中毒 + this.handlePoison(step, mover as IHeroMover); + } +} + +export class EnemyPathfindingHooks implements IPathfindingSystemHooks { + constructor(readonly state: ICoreState) {} + + /** + * 处理中毒效果 + * @param path 自动寻路路径 + */ + private handlePoison(path: IPathfindingResult) { + const poison = this.state.flags.getFieldValueDefaults('poison', 0); + // 中毒伤害小于等于 0,直接返回 + if (poison <= 0) return; + const mover = this.state.hero.location.mover; + const config = mover.getConfig(); + // 如果忽略地形,那么也不计算中毒伤害 + if (config.ignoreTerrain) return; + + const hero = this.state.hero.getModifiableAttribute(); + // 勇士减少步数乘以单步中毒的伤害。 + hero.add('hp', -path.path.length); + } + + onAfterTeleportTo( + path: IPathfindingResult, + system: IPathfindingSystem + ): void { + // 判断状态对象 + if (system.state !== this.state) return; + // 判断当前绑定的是不是勇士移动器 + if (system.mover !== this.state.hero.location.mover) return; + + // 处理中毒 + this.handlePoison(path); + } +} + +export class EnemyCombatHooks + implements ICombatFlowHooks, ICombatWeakHandler +{ + /** 当前的虚弱值 */ + private weakValue: number = 0; + /** 攻击虚弱修饰器 */ + private readonly weakAtkModifier: IHeroModifier; + /** 防御虚弱修饰器 */ + private readonly weakDefModifier: IHeroModifier; + + constructor(readonly state: ICoreState) { + const hero = state.hero.getModifiableAttribute(); + // 初始化虚弱属性的修饰器,并添加到勇士修饰器中 + this.weakAtkModifier = new PercentageModifier(0); + this.weakDefModifier = new PercentageModifier(0); + hero.addModifier('atk', this.weakAtkModifier); + hero.addModifier('def', this.weakDefModifier); + } + + setWeakValue(value: number): void { + this.weakValue = value; + // 需要填写负数才能使勇士属性降低 + this.weakAtkModifier.setValue(-value); + this.weakDefModifier.setValue(-value); + } + + getWeakValue(): number { + return this.weakValue; + } + + async onAfterCombat( + info: IEnemyDamageInfo + ): Promise { + // 判断状态对象 + if (info.handler.state !== this.state) return; + // 判断勇士对象 + if (info.handler.hero !== this.state.hero.getAttribute()) return; + + // 处理中毒属性 + const poison = info.handler.enemy.getSpecial(10); + if (poison) { + // 获取 poison 全局 flag,如果 flag 不存在,则自动插入为 0 + const field = this.state.flags.getOrInsert('poison', 0); + const max = Math.max(field.get(), poison.value); + field.set(max); + } + + // 处理虚弱属性 + const weak = info.handler.enemy.getSpecial(11); + if (weak) { + const max = Math.max(this.weakValue, weak.value); + this.setWeakValue(max); + } + + // 处理重生属性 + if (info.handler.enemy.hasSpecial(21)) { + const field = this.state.flags.getFieldValueDefaults< + Map + >('rebirth', new Map()); + + // 如果战斗不发生在任何地图,或者不存在坐标,那么不计入重生 + if (isNil(info.handler.context.floor) || !info.handler.locator) { + return; + } + + // 获取当前楼层的重生存储,并向其中添加重生标记 + const floor = field.getOrInsert(info.handler.context.floor, []); + const { x, y } = info.handler.locator; + floor.push([info.handler.enemy.code, x, y]); + } + } +} + +export class EnemyChangeFloorHooks implements IHeroLocationHooks { + constructor(readonly state: ICoreState) {} + + /** + * 处理重生属性 + * @param floor 目标楼层 + */ + private handleRebirth(map: IGameMap) { + // 先获取指定楼层的重生标记 + const field = this.state.flags.getFieldValueDefaults< + Map + >('rebirth', new Map()); + const list = field.get(map.floorId); + + // 如果不存在重生,直接返回 + if (!list || list.length === 0) return; + + const event = map.eventLayer; + // 如果不存在事件层,直接返回 + if (!event) return; + + // 把每个重生怪都放到事件层上 + for (const [tile, x, y] of list) { + event.setBlock(tile, x, y); + } + + // 清空重生怪列表 + list.length = 0; + } + + onSetFloor(map: IGameMap | null): void { + if (!map) return; + // 判断状态对象 + if (map.state !== this.state) return; + + // 处理重生 + this.handleRebirth(map); + } +} diff --git a/src/data/enemy/initialize.ts b/src/data/enemy/initialize.ts index 6fb5310..7fffd4b 100644 --- a/src/data/enemy/initialize.ts +++ b/src/data/enemy/initialize.ts @@ -10,13 +10,23 @@ import { MainEnemyFinalEffect } from './final'; import { MainDamageCalculator } from './calculator'; import { MainEnemyComparer } from './comparer'; import { MainMapDamageConverter, MainMapDamageReducer } from './mapDamage'; +import { IEnemyInitializationReturns } from './types'; +import { + EnemyChangeFloorHooks, + EnemyCombatHooks, + EnemyMoveHooks, + EnemyPathfindingHooks +} from './hook'; +import { MainCombatScript } from './script'; /** * 初始化怪物相关内容 * @param state 数据端主对象 * @param config 数据端配置 */ -export function initializeEnemy(state: ICoreState) { +export function initializeEnemy( + state: ICoreState +): IEnemyInitializationReturns { // 怪物管理器初始化 const comparer = new MainEnemyComparer(); state.enemyManager.attachEnemyComparer(comparer); @@ -45,4 +55,16 @@ export function initializeEnemy(state: ICoreState) { // 注册怪物最终效果 ctx.registerFinalEffect(new MainEnemyFinalEffect()); + + // 添加默认战前战后脚本 + state.combat.addCombatScript(new MainCombatScript()); + + // 注册相关的钩子 + state.hero.location.mover.addHook(new EnemyMoveHooks(state)); + state.pathfinding.addHook(new EnemyPathfindingHooks(state)); + const combatHook = new EnemyCombatHooks(state); + state.combat.addHook(combatHook); + state.hero.location.addHook(new EnemyChangeFloorHooks(state)); + + return { weak: combatHook }; } diff --git a/src/data/enemy/script.ts b/src/data/enemy/script.ts new file mode 100644 index 0000000..360ef46 --- /dev/null +++ b/src/data/enemy/script.ts @@ -0,0 +1,60 @@ +import { IEnemyAttr, IHeroAttr } from '@user/data-common'; +import { + ICombatFlowHandler, + ICombatScript, + IEnemyDamageInfo +} from '@user/data-system'; + +export class MainCombatScript implements ICombatScript { + priority: number = 0; + + async before( + info: IEnemyDamageInfo, + handler: ICombatFlowHandler + ): Promise { + const hero = handler.hero; + + // 如果伤害大于勇士血量,直接不允许战斗 + if (!handler.force && hero.getFinalAttribute('hp') < info.damage) { + return false; + } + + return true; + } + + async after( + info: IEnemyDamageInfo, + handler: ICombatFlowHandler + ): Promise { + const hero = handler.hero; + const readonlyEnemy = info.handler.enemy; + + // 计算金币和经验,支援怪也算上 + const baseExp = readonlyEnemy.getAttribute('exp'); + const baseMoney = readonlyEnemy.getAttribute('money'); + const [guardExp, guardMoney] = [ + ...readonlyEnemy.getAttribute('guard') + ].reduce( + (prev, curr) => { + const attr = curr.getComputedEnemy(); + const exp = attr.getAttribute('exp'); + const money = attr.getAttribute('money'); + return [prev[0] + exp, prev[1] + money]; + }, + [0, 0] + ); + + const totalExp = baseExp + guardExp; + const totalMoney = baseMoney + guardMoney; + + // 给勇士添加经验和金币 + hero.add('exp', totalExp); + hero.add('money', totalMoney); + + // 进行全局提示 + handler.state.emitTip( + `打败${readonlyEnemy.getAttribute('name')},金币+${totalMoney},经验+${totalExp}。`, + readonlyEnemy.code + ); + } +} diff --git a/src/data/enemy/special.ts b/src/data/enemy/special.ts index 305b110..e954a26 100644 --- a/src/data/enemy/special.ts +++ b/src/data/enemy/special.ts @@ -77,10 +77,10 @@ export interface ISpecial26Value { * 2. 坚固、模仿等仅影响自身基础属性的属性:./final.ts, MainEnemyFinalEffect.apply 方法 * 3. 地图伤害:./mapDamage.ts,具体写法参考文档 * 4. 光环属性:./aura.ts 具体写法参考文档 - * 5. 中毒的每步效果:在插件系统中,目前暂未实装 - * 6. 中毒的瞬移效果:在插件系统中,目前暂未实装 - * 7. 衰弱效果:在插件系统中,目前尚未实装 - * 8. 重生属性:在插件系统中,目前尚未实装 + * 5. 中毒的每步效果:./hook.ts EnemyMoveHooks.handlePoison 方法 + * 6. 中毒的瞬移效果:./hook.ts EnemyPathfindingHooks.handlePoison 方法 + * 7. 衰弱效果:./hook.ts EnemyCombatHooks.onAfterCombat 方法 + * 8. 重生属性:./hook.ts EnemyCombatHooks.onAfterCombat 和 EnemyChangeFloorHooks.onSetFloor 方法 */ export function createSpecials( state: ICoreState @@ -233,18 +233,7 @@ export function createSpecials( name: '衰弱', desc: special => { const weak = special.value; - const suffix = ',多次触发时取最高值。'; - if (weak < 1) { - return ( - `战斗后,角色陷入衰弱状态,攻防暂时下降${Math.floor(weak * 100)}%` + - suffix - ); - } else { - return ( - `战斗后,角色陷入衰弱状态,攻防暂时下降${weak}点` + - suffix - ); - } + return `战斗后,角色陷入衰弱状态,攻防暂时下降${Math.floor(weak * 100)}%,多次触发时取最高值。`; }, color: '#F0BBCC' }) diff --git a/src/data/enemy/types.ts b/src/data/enemy/types.ts index 7a8c5d0..716df47 100644 --- a/src/data/enemy/types.ts +++ b/src/data/enemy/types.ts @@ -10,3 +10,21 @@ export const enum MapDamageType { /** 夹击伤害 */ Between } + +export interface IEnemyInitializationReturns { + /** 虚弱处理对象 */ + readonly weak: ICombatWeakHandler; +} + +export interface ICombatWeakHandler { + /** + * 设置当前的虚弱值 + * @param value 虚弱值 + */ + setWeakValue(value: number): void; + + /** + * 获取当前的虚弱值 + */ + getWeakValue(): number; +} diff --git a/src/data/hook/combat.ts b/src/data/hook/combat.ts new file mode 100644 index 0000000..fda068c --- /dev/null +++ b/src/data/hook/combat.ts @@ -0,0 +1,24 @@ +import { IEnemyAttr, IHeroAttr } from '@user/data-common'; +import { ICoreState } from '@user/data-state'; +import { ICombatFlowHooks, IEnemyDamageInfo } from '@user/data-system'; + +export class MainCombatFlowHooks implements ICombatFlowHooks< + IEnemyAttr, + IHeroAttr +> { + constructor(readonly state: ICoreState) {} + + async onBeforeCombat( + info: IEnemyDamageInfo + ): Promise { + // 删除下面这一行开始编写你自己的战前钩子 + void info; + } + + async onAfterCombat( + info: IEnemyDamageInfo + ): Promise { + // 删除下面这一行开始编写你自己的战后钩子 + void info; + } +} diff --git a/src/data/hook/event.ts b/src/data/hook/event.ts new file mode 100644 index 0000000..cab819f --- /dev/null +++ b/src/data/hook/event.ts @@ -0,0 +1,30 @@ +import { ICoreState } from '@user/data-state'; +import { + IBlockEventParam, + IGameEventExecutorHooks, + IGameEventInvocation +} from '@user/data-system'; + +export class MainGameEventExecutorHooks implements IGameEventExecutorHooks { + constructor(readonly state: ICoreState) {} + + onBeforeExecuteEvent( + events: IGameEventInvocation[], + param: IBlockEventParam + ): void { + // 删除下面这两行注释开始编写你自己的事件执行前钩子 + void events; + void param; + } + + onAfterExecuteEvent( + events: IGameEventInvocation[], + param: IBlockEventParam, + returns: unknown + ): void { + // 删除下面这三行注释开始编写你自己的事件执行后钩子 + void events; + void param; + void returns; + } +} diff --git a/src/data/hook/hero.ts b/src/data/hook/hero.ts new file mode 100644 index 0000000..1bbb9bc --- /dev/null +++ b/src/data/hook/hero.ts @@ -0,0 +1,141 @@ +import { + IGameMap, + IHeroAttributeHooks, + IHeroChangeFloorInfo, + IHeroLocation, + IHeroLocationHooks, + IHeroStateHooks +} from '@user/data-base'; +import { + FaceDirection, + IHeroAttr, + IObjectMover, + IObjectMoverHooks, + ObjectMoveStep +} from '@user/data-common'; +import { ICoreState } from '@user/data-state'; + +export class MainHeroAttributeHooks implements IHeroAttributeHooks { + constructor(readonly state: ICoreState) {} + + onUpdateAttribute( + name: K, + value: IHeroAttr[K] + ): void { + // 删除下面这两行开始编写你自己的勇士属性更新钩子 + void name; + void value; + } +} + +export class MainHeroLocationHooks implements IHeroLocationHooks { + constructor(readonly state: ICoreState) {} + + onSetFloor(map: IGameMap | null): void { + // 删除下面这两行开始编写你自己的切换楼层钩子 + void map; + } + + onSetPos(x: number, y: number): void { + // 删除下面这两行开始编写你自己的勇士位置设置钩子 + void x; + void y; + } +} + +export class MainHeroMoverHooks implements IObjectMoverHooks { + constructor(readonly state: ICoreState) {} + + async onMoveStart( + tile: IHeroLocation, + mover: IObjectMover + ): Promise { + // 删除下面这两行注释开始编写你自己的勇士移动前钩子 + void tile; + void mover; + } + + async onStepStart( + code: number, + step: Readonly, + tile: IHeroLocation, + mover: IObjectMover + ): Promise { + // 删除下面这四行注释开始编写你自己的勇士每步开始时钩子 + void code; + void step; + void tile; + void mover; + } + + async onStepEnd( + code: number, + step: Readonly, + tile: IHeroLocation, + mover: IObjectMover + ): Promise { + // 删除下面这四行注释开始编写你自己的勇士每步结束后钩子 + void code; + void step; + void tile; + void mover; + } + + async onMoveEnd( + tile: IHeroLocation, + mover: IObjectMover + ): Promise { + // 删除下面这两行注释开始编写你自己的勇士移动前钩子 + void tile; + void mover; + } + + onSetFaceDir( + dir: FaceDirection, + tile: IHeroLocation, + mover: IObjectMover + ): void { + // 删除下面这三行注释开始编写你自己的设置面朝朝向的钩子 + void dir; + void tile; + void mover; + } + + onSetMoveDir( + dir: FaceDirection, + tile: IHeroLocation, + mover: IObjectMover + ): void { + // 删除下面这三行注释开始编写你自己的设置移动朝向的钩子 + void dir; + void tile; + void mover; + } + + onSetPos( + x: number, + y: number, + tile: IHeroLocation, + mover: IObjectMover + ): void { + // 删除下面这三行注释开始编写你自己的设置位置的钩子 + void x; + void y; + void tile; + void mover; + } +} + +export class MainHeroStateHooks implements IHeroStateHooks { + constructor(readonly state: ICoreState) {} + + async onBeforeChangeFloor(info: IHeroChangeFloorInfo): Promise { + // 删除下面这一行注释开始编写你自己的楼层转换前钩子 + void info; + } + + async onAfterChangeFloor(info: IHeroChangeFloorInfo): Promise { + // 删除下面这一行注释开始编写你自己的楼层转换后钩子 + void info; + } +} diff --git a/src/data/hook/index.ts b/src/data/hook/index.ts new file mode 100644 index 0000000..1768471 --- /dev/null +++ b/src/data/hook/index.ts @@ -0,0 +1,6 @@ +export * from './combat'; +export * from './event'; +export * from './hero'; +export * from './initialize'; +export * from './loader'; +export * from './path'; diff --git a/src/data/hook/initialize.ts b/src/data/hook/initialize.ts new file mode 100644 index 0000000..b124ccb --- /dev/null +++ b/src/data/hook/initialize.ts @@ -0,0 +1,26 @@ +import { ICoreState } from '@user/data-state'; +import { MainCombatFlowHooks } from './combat'; +import { MainGameEventExecutorHooks } from './event'; +import { + MainHeroAttributeHooks, + MainHeroLocationHooks, + MainHeroMoverHooks, + MainHeroStateHooks +} from './hero'; +import { MainMotaDataLoaderHooks } from './loader'; +import { MainPathfindingSystemHooks } from './path'; + +/** + * 初始化数据端主要钩子 + * @param state 数据端主对象 + */ +export function initializeHooks(state: ICoreState) { + state.combat.addHook(new MainCombatFlowHooks(state)); + state.eventSystem.executor.addHook(new MainGameEventExecutorHooks(state)); + state.hero.addHook(new MainHeroStateHooks(state)); + state.hero.attribute.addHook(new MainHeroAttributeHooks(state)); + state.hero.location.addHook(new MainHeroLocationHooks(state)); + state.hero.location.mover.addHook(new MainHeroMoverHooks(state)); + state.loader.addHook(new MainMotaDataLoaderHooks(state)); + state.pathfinding.addHook(new MainPathfindingSystemHooks(state)); +} diff --git a/src/data/hook/loader.ts b/src/data/hook/loader.ts new file mode 100644 index 0000000..48f2976 --- /dev/null +++ b/src/data/hook/loader.ts @@ -0,0 +1,19 @@ +import { + ICoreState, + IMotaDataLoader, + IMotaDataLoaderHooks +} from '@user/data-state'; + +export class MainMotaDataLoaderHooks implements IMotaDataLoaderHooks { + constructor(readonly state: ICoreState) {} + + async onCoreConfigLoaded(loader: IMotaDataLoader): Promise { + // 删除下面这两行注释开始编写你自己的核心配置加载后钩子 + void loader; + } + + async onExtraConfigLoaded(loader: IMotaDataLoader): Promise { + // 删除下面这两行注释开始编写你自己的额外配置加载后钩子 + void loader; + } +} diff --git a/src/data/hook/path.ts b/src/data/hook/path.ts new file mode 100644 index 0000000..7563337 --- /dev/null +++ b/src/data/hook/path.ts @@ -0,0 +1,28 @@ +import { ICoreState } from '@user/data-state'; +import { + IPathfindingResult, + IPathfindingSystem, + IPathfindingSystemHooks +} from '@user/data-system'; + +export class MainPathfindingSystemHooks implements IPathfindingSystemHooks { + constructor(readonly state: ICoreState) {} + + onBeforeTeleportTo( + path: IPathfindingResult, + system: IPathfindingSystem + ): void { + // 删除下面这两行注释开始编写你自己的自动寻路瞬移前钩子,注意非勇士的瞬移也会触发 + void path; + void system; + } + + onAfterTeleportTo( + path: IPathfindingResult, + system: IPathfindingSystem + ): void { + // 删除下面这两行注释开始编写你自己的自动寻路瞬移后钩子,注意非勇士的瞬移也会触发 + void path; + void system; + } +} diff --git a/src/data/initialize.ts b/src/data/initialize.ts index 8aef795..ef64836 100644 --- a/src/data/initialize.ts +++ b/src/data/initialize.ts @@ -5,21 +5,21 @@ import { FaceGroup, IFacedTileLocator } from '@user/data-common'; -import { - ICoreState, - ICoreStateCoreConfig, - IDataPluginDefine -} from '@user/data-state'; -import { initializeEnemy } from './enemy'; +import { ICoreState, ICoreStateCoreConfig } from '@user/data-state'; +import { ICombatWeakHandler, initializeEnemy } from './enemy'; import { initializeReplay } from './replay'; import { logger } from '@motajs/common'; import { DefaultHeroMoveTopImpl, DefaultPassPredicateImpl } from './hero'; import { DefaultDataLoaderHook } from './loader'; +import { IDataMainPlugin } from './types'; +import { initializeHooks } from './hook'; -export class MainDataPlugin implements IDataPluginDefine { +export class MainDataPlugin implements IDataMainPlugin { readonly id: string = 'main-data-plugin'; readonly dependencies: string[] = []; + weak!: ICombatWeakHandler; + constructor(readonly heroFace: FaceGroup) {} awake(state: ICoreState): void { @@ -46,8 +46,11 @@ export class MainDataPlugin implements IDataPluginDefine { state.loader.addHook(new DefaultDataLoaderHook()); // 其他复杂初始化 - initializeEnemy(state); + const enemyReturns = initializeEnemy(state); initializeReplay(state); + initializeHooks(state); + + this.weak = enemyReturns.weak; // 存档初始化 state.addSaveableContent('@system/hero', state.hero); diff --git a/src/data/plugin/list.ts b/src/data/plugin/list.ts index 569a26f..5a12131 100644 --- a/src/data/plugin/list.ts +++ b/src/data/plugin/list.ts @@ -1,5 +1,5 @@ import { IDataPluginDefine } from '@user/data-state'; -import { ShopPlugin } from './shop'; +import { DataShopPlugin } from './shop'; /** 数据端插件列表 */ -export const dataPlugins: IDataPluginDefine[] = [new ShopPlugin()]; +export const dataPlugins: IDataPluginDefine[] = [new DataShopPlugin()]; diff --git a/src/data/plugin/shop/index.ts b/src/data/plugin/shop/index.ts index d8356d8..35da73d 100644 --- a/src/data/plugin/shop/index.ts +++ b/src/data/plugin/shop/index.ts @@ -1 +1,5 @@ -export * from './shop'; +export * from './manager'; +export * from './plugin'; +export * from './state'; +export * from './system'; +export * from './types'; diff --git a/src/data/plugin/shop/shop.ts b/src/data/plugin/shop/plugin.ts similarity index 89% rename from src/data/plugin/shop/shop.ts rename to src/data/plugin/shop/plugin.ts index 36d3978..1a54d7b 100644 --- a/src/data/plugin/shop/shop.ts +++ b/src/data/plugin/shop/plugin.ts @@ -4,8 +4,8 @@ import { ShopManager } from './manager'; import { ShopState } from './state'; import { ShopSystem } from './system'; -export class ShopPlugin implements IShopPlugin { - id: string = 'shop-plugin'; +export class DataShopPlugin implements IShopPlugin { + id: string = 'data-shop-plugin'; dependencies: string[] = []; shop!: IShopSystem; diff --git a/src/data/types.ts b/src/data/types.ts new file mode 100644 index 0000000..956da86 --- /dev/null +++ b/src/data/types.ts @@ -0,0 +1,7 @@ +import { IDataPluginDefine } from '@user/data-state'; +import { ICombatWeakHandler } from './enemy'; + +export interface IDataMainPlugin extends IDataPluginDefine { + /** 虚弱处理对象 */ + readonly weak: ICombatWeakHandler; +} diff --git a/tsconfig.json b/tsconfig.json index c67a6fe..91128c6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,9 @@ "noEmit": true, "paths": { "@motajs/*": ["./packages/*/src"], - "@user/*": ["./packages-user/*/src"] + "@user/*": ["./packages-user/*/src"], + "@dp/*": ["./src/data/plugin/*"], + "@/*": ["./src/*"] } }, "include": [ @@ -26,11 +28,9 @@ "packages/**/*.ts", "packages/**/*.d.ts", "packages/**/*.tsx", - "packages/**/*.vue", "packages-user/**/*.ts", "packages-user/**/*.d.ts", - "packages-user/**/*.tsx", - "src/types/**/*.d.ts" + "packages-user/**/*.tsx" ], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/vite.config.ts b/vite.config.ts index 92000be..c552ac3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -26,6 +26,14 @@ const aliasesUser = glob.sync('packages-user/*/src').map((srcPath) => { }; }); +const aliasesDP = glob.sync('src/data/plugin/*').map((srcPath) => { + const packageName = path.basename(path.dirname(srcPath)); + return { + find: `@dp/${packageName}`, + replacement: path.resolve(__dirname, srcPath), + }; +}); + // https://vitejs.dev/config/ export default defineConfig({ plugins: [ @@ -40,7 +48,12 @@ export default defineConfig({ resolve: { alias: [ ...aliases, - ...aliasesUser + ...aliasesUser, + ...aliasesDP, + { + find: '@/', + replacement: path.resolve(__dirname, 'src/') + } ] }, test: {