mirror of
https://github.com/motajs/template.git
synced 2026-10-17 03:10:17 +08:00
refactor: 数据端用户结构
This commit is contained in:
parent
0a133058ca
commit
a0e402728e
@ -1,14 +1,15 @@
|
||||
# Phase 10: 文档重构 - Context
|
||||
|
||||
**Gathered:** 2026-10-14
|
||||
**Status:** Discussion only — 待引擎整体完工后再规划/执行
|
||||
**Updated:** 2026-10-14(改为增量派发)
|
||||
**Status:** Incremental dispatch — 可先行部分先规划执行,其余待引擎完工
|
||||
|
||||
<domain>
|
||||
## 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 不擅自扩大范围。发现的越界内容只记录、不实施。
|
||||
|
||||
</decisions>
|
||||
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import { UIController } from '@motajs/system';
|
||||
import { IGameUIPropsBase } from '@user/client-base';
|
||||
import { MainBackgroundUI } from './background';
|
||||
|
||||
export const mainUIController = new UIController<IGameUIPropsBase>();
|
||||
|
||||
export function createMainController() {
|
||||
mainUIController.setBackground(MainBackgroundUI, {});
|
||||
}
|
||||
@ -78,7 +78,7 @@ export interface IFlagSystem extends ISaveableContent<IFlagSystemSave> {
|
||||
* @param field 字段名称
|
||||
* @param defaultValue 字段默认值函数
|
||||
*/
|
||||
getOrInsertComputed<K extends PropertyKey, T>(
|
||||
getOrInsertComputed<T, K extends PropertyKey = PropertyKey>(
|
||||
field: K,
|
||||
defaultValue: (field: K) => T
|
||||
): IFlagCommonField<T>;
|
||||
|
||||
@ -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<ITileLocator>;
|
||||
guard: Set<IEnemyView<IEnemyAttr>>;
|
||||
}
|
||||
|
||||
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<IDataCommonHooks> {
|
||||
/** 图块定义存储 */
|
||||
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 {
|
||||
|
||||
@ -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<IDataCommonHooks>
|
||||
implements ICoreState
|
||||
{
|
||||
// Layer 0 公共层,最底层的接口,不会依赖任何其他内容,一般是工具性接口及不需要存档的数据
|
||||
readonly roleFace: IRoleFaceBinder;
|
||||
readonly faceManager: IFaceManager;
|
||||
@ -67,6 +78,7 @@ export class CoreState implements ICoreState {
|
||||
|
||||
// Layer 2 执行层,游戏逻辑对象都在这,包括一些需要操作数据层的逻辑系统等
|
||||
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
|
||||
readonly combat: ICombatFlow<IEnemyAttr, IHeroAttr>;
|
||||
readonly eventSystem: IGameEventSystem;
|
||||
readonly pathfinding: IPathfindingSystem;
|
||||
|
||||
@ -92,6 +104,7 @@ export class CoreState implements ICoreState {
|
||||
>;
|
||||
|
||||
constructor(config: Readonly<ICoreStateConfig>) {
|
||||
super();
|
||||
// L0 定义
|
||||
this.roleFace = new RoleFaceBinder();
|
||||
this.faceManager = new FaceManager();
|
||||
@ -99,6 +112,7 @@ export class CoreState implements ICoreState {
|
||||
this.itemStore = new ItemStore<IHeroAttr>(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<IEnemyAttr, IHeroAttr>(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<IDataCommonHooks>
|
||||
): IHookController<IDataCommonHooks> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
emitTip(tip: string, icon: number): void {
|
||||
this.forEachHook(hook => hook.onTip?.(tip, icon));
|
||||
}
|
||||
|
||||
//#region 插件方法
|
||||
|
||||
/**
|
||||
|
||||
@ -84,10 +84,12 @@ export class CombatFlow<TEnemy, THero>
|
||||
*/
|
||||
private createHandler(
|
||||
enemy: IReadonlyEnemy<TEnemy>,
|
||||
locator: ITileLocator | null
|
||||
locator: ITileLocator | null,
|
||||
force: boolean
|
||||
): ICombatFlowHandler<TEnemy, THero> {
|
||||
return {
|
||||
onMap: locator !== null,
|
||||
force,
|
||||
hero: this.hero!,
|
||||
enemy,
|
||||
context: this.context!,
|
||||
@ -205,7 +207,8 @@ export class CombatFlow<TEnemy, THero>
|
||||
|
||||
@shouldReplay('Battle with enemy should be replayed.')
|
||||
battle(
|
||||
enemy: IEnemyView<TEnemy>
|
||||
enemy: IEnemyView<TEnemy>,
|
||||
force: boolean = false
|
||||
): Promise<IEnemyDamageInfo<TEnemy, THero> | null> {
|
||||
if (!this.context) {
|
||||
logger.warn(139, 'an enemy context object');
|
||||
@ -219,7 +222,7 @@ export class CombatFlow<TEnemy, THero>
|
||||
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<TEnemy, THero>
|
||||
|
||||
@shouldReplay('Battle with enemy should be replayed.')
|
||||
battleComputed(
|
||||
enemy: IReadonlyEnemy<TEnemy>
|
||||
enemy: IReadonlyEnemy<TEnemy>,
|
||||
force: boolean = false
|
||||
): Promise<IEnemyDamageInfo<TEnemy, THero> | null> {
|
||||
if (!this.context) {
|
||||
logger.warn(139, 'an enemy context object');
|
||||
@ -243,7 +247,7 @@ export class CombatFlow<TEnemy, THero>
|
||||
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);
|
||||
}
|
||||
|
||||
@ -102,9 +102,14 @@ export class EnemyContext<TEnemy, THero> 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;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './combat';
|
||||
export * from './context';
|
||||
export * from './damage';
|
||||
export * from './enemy';
|
||||
|
||||
@ -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<TEnemy, THero> 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<TEnemy, THero> extends IReadonlyEnemyContext<
|
||||
export interface ICombatFlowHandler<TEnemy, THero> extends IDataBaseExtended {
|
||||
/** 战斗的怪物是否在地图上 */
|
||||
readonly onMap: boolean;
|
||||
/** 是否为强制战斗 */
|
||||
readonly force: boolean;
|
||||
/** 可修改勇士对象 */
|
||||
readonly hero: IHeroAttribute<THero>;
|
||||
/** 可修改怪物对象 */
|
||||
@ -863,9 +874,11 @@ export interface ICombatFlow<TEnemy, THero>
|
||||
/**
|
||||
* 与指定怪物战斗
|
||||
* @param enemy 怪物视图对象
|
||||
* @param force 是否为强制战斗,默认为 `false`。强制战斗下,不再有战败保护
|
||||
*/
|
||||
battle(
|
||||
enemy: IEnemyView<TEnemy>
|
||||
enemy: IEnemyView<TEnemy>,
|
||||
force?: boolean
|
||||
): Promise<IEnemyDamageInfo<TEnemy, THero> | null>;
|
||||
|
||||
/**
|
||||
@ -879,8 +892,12 @@ export interface ICombatFlow<TEnemy, THero>
|
||||
/**
|
||||
* 添加战前战后脚本
|
||||
* @param script 战前战后脚本
|
||||
* @param force 是否为强制战斗,默认为 `false`。强制战斗下,不再有战败保护
|
||||
*/
|
||||
addCombatScript(script: ICombatScript<TEnemy, THero>): void;
|
||||
addCombatScript(
|
||||
script: ICombatScript<TEnemy, THero>,
|
||||
force?: boolean
|
||||
): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -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<IGameEventExecutorHooks>
|
||||
implements IGameEventExecutor
|
||||
{
|
||||
mode: EventExecuteMode = EventExecuteMode.Normal;
|
||||
reduce: EventReduceMode = EventReduceMode.NoReduce;
|
||||
|
||||
constructor(
|
||||
readonly interpreter: AnonTokyoInterpreter,
|
||||
private readonly storeRef: () => IGameEventStore | null
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IHookBase>
|
||||
): IHookController<IHookBase> {
|
||||
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<R> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<IGameEventExecutorHooks> {
|
||||
/** 当前的执行器执行模式 */
|
||||
readonly mode: EventExecuteMode;
|
||||
/** 当前的执行器返回值折叠方式 */
|
||||
|
||||
@ -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<IPathfindingSystemHooks>
|
||||
implements IPathfindingSystem
|
||||
{
|
||||
/** 寻路求解器 */
|
||||
readonly finder: IPathFinder;
|
||||
|
||||
/** 当前绑定的移动器 */
|
||||
private mover: IObjectMover<IObjectMovable> | null = null;
|
||||
mover: IObjectMover<IObjectMovable> | 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<IPathfindingSystemHooks>
|
||||
): IHookController<IPathfindingSystemHooks> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
useMover(mover: IObjectMover<IObjectMovable> | 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;
|
||||
}
|
||||
|
||||
|
||||
@ -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<IPathfindingSystemHooks> {
|
||||
/** 寻路求解器 */
|
||||
readonly finder: IPathFinder;
|
||||
/** 当前正在使用的移动寻路器 */
|
||||
readonly mover: IObjectMover<IObjectMovable> | null;
|
||||
|
||||
/**
|
||||
* 绑定寻路移动器,可绑定勇士或任意 `IObjectMover`,如动态图块移动器
|
||||
|
||||
@ -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<IEnemyAttr, IHeroAttr>;
|
||||
/** 怪物战斗流程对象 */
|
||||
readonly combat: ICombatFlow<IEnemyAttr, IHeroAttr>;
|
||||
/** 游戏事件系统 */
|
||||
readonly eventSystem: IGameEventSystem;
|
||||
/** 已绑定勇士移动器的寻路系统 */
|
||||
|
||||
@ -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, {});
|
||||
|
||||
@ -41,9 +41,7 @@ export function createClientCoreConfig(
|
||||
* 创建渲染端主对象,返回未经过异步初始化的主对象实例,需要手动调用 `state.load` 初始化
|
||||
* @param starter 加载启动器
|
||||
*/
|
||||
export async function createClientCore(
|
||||
starter: ILoadTaskStarter
|
||||
): Promise<IClientCore> {
|
||||
export function createClientCore(starter: ILoadTaskStarter): IClientCore {
|
||||
const config = createClientCoreConfig(starter);
|
||||
const state = new ClientCore(config);
|
||||
return state;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { IClientPluginDefine } from '@user/client-modules';
|
||||
import { ClientShopPlugin } from './shop';
|
||||
|
||||
/** 渲染端插件列表 */
|
||||
export const clientPlugins: IClientPluginDefine[] = [];
|
||||
export const clientPlugins: IClientPluginDefine[] = [new ClientShopPlugin()];
|
||||
|
||||
2
src/client/plugin/shop/index.ts
Normal file
2
src/client/plugin/shop/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './plugin';
|
||||
export * from './shop';
|
||||
23
src/client/plugin/shop/plugin.ts
Normal file
23
src/client/plugin/shop/plugin.ts
Normal file
@ -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<void> {}
|
||||
|
||||
async afterLoad(): Promise<void> {}
|
||||
}
|
||||
@ -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<ShopProps>;
|
||||
} satisfies SetupComponentOptions<IShopProps>;
|
||||
|
||||
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<ShopProps>(props => {
|
||||
export const Shop = defineComponent<IShopProps>(props => {
|
||||
/** 商店状态对象 */
|
||||
const shop = props.shop;
|
||||
/** 勇士状态对象 */
|
||||
@ -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<IEnemyAttr> | null {
|
||||
|
||||
@ -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,
|
||||
|
||||
222
src/data/enemy/hook.ts
Normal file
222
src/data/enemy/hook.ts
Normal file
@ -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<IHeroLocation> {
|
||||
constructor(readonly state: ICoreState) {}
|
||||
|
||||
/**
|
||||
* 处理中毒效果
|
||||
* @param step 移动步对象
|
||||
*/
|
||||
private handlePoison(
|
||||
step: Readonly<ObjectMoveStep>,
|
||||
mover: IHeroMover<IHeroLocation>
|
||||
) {
|
||||
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<ObjectMoveStep>,
|
||||
tile: IHeroLocation,
|
||||
mover: IObjectMover<IHeroLocation>
|
||||
): Promise<void> {
|
||||
// 检查状态对象
|
||||
if (tile.state !== this.state) return;
|
||||
// 检查移动器是否为勇士移动器
|
||||
if (mover !== this.state.hero.location.mover) return;
|
||||
|
||||
// 处理中毒
|
||||
this.handlePoison(step, mover as IHeroMover<IHeroLocation>);
|
||||
}
|
||||
}
|
||||
|
||||
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<IEnemyAttr, IHeroAttr>, ICombatWeakHandler
|
||||
{
|
||||
/** 当前的虚弱值 */
|
||||
private weakValue: number = 0;
|
||||
/** 攻击虚弱修饰器 */
|
||||
private readonly weakAtkModifier: IHeroModifier<number, number, number>;
|
||||
/** 防御虚弱修饰器 */
|
||||
private readonly weakDefModifier: IHeroModifier<number, number, number>;
|
||||
|
||||
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<IEnemyAttr, IHeroAttr>
|
||||
): Promise<void> {
|
||||
// 判断状态对象
|
||||
if (info.handler.state !== this.state) return;
|
||||
// 判断勇士对象
|
||||
if (info.handler.hero !== this.state.hero.getAttribute()) return;
|
||||
|
||||
// 处理中毒属性
|
||||
const poison = info.handler.enemy.getSpecial<number>(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<number>(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<string, [number, number, number][]>
|
||||
>('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<string, [number, number, number][]>
|
||||
>('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);
|
||||
}
|
||||
}
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
60
src/data/enemy/script.ts
Normal file
60
src/data/enemy/script.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
|
||||
import {
|
||||
ICombatFlowHandler,
|
||||
ICombatScript,
|
||||
IEnemyDamageInfo
|
||||
} from '@user/data-system';
|
||||
|
||||
export class MainCombatScript implements ICombatScript<IEnemyAttr, IHeroAttr> {
|
||||
priority: number = 0;
|
||||
|
||||
async before(
|
||||
info: IEnemyDamageInfo<IEnemyAttr, IHeroAttr>,
|
||||
handler: ICombatFlowHandler<IEnemyAttr, IHeroAttr>
|
||||
): Promise<boolean> {
|
||||
const hero = handler.hero;
|
||||
|
||||
// 如果伤害大于勇士血量,直接不允许战斗
|
||||
if (!handler.force && hero.getFinalAttribute('hp') < info.damage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async after(
|
||||
info: IEnemyDamageInfo<IEnemyAttr, IHeroAttr>,
|
||||
handler: ICombatFlowHandler<IEnemyAttr, IHeroAttr>
|
||||
): Promise<void> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
})
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
24
src/data/hook/combat.ts
Normal file
24
src/data/hook/combat.ts
Normal file
@ -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<IEnemyAttr, IHeroAttr>
|
||||
): Promise<void> {
|
||||
// 删除下面这一行开始编写你自己的战前钩子
|
||||
void info;
|
||||
}
|
||||
|
||||
async onAfterCombat(
|
||||
info: IEnemyDamageInfo<IEnemyAttr, IHeroAttr>
|
||||
): Promise<void> {
|
||||
// 删除下面这一行开始编写你自己的战后钩子
|
||||
void info;
|
||||
}
|
||||
}
|
||||
30
src/data/hook/event.ts
Normal file
30
src/data/hook/event.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
141
src/data/hook/hero.ts
Normal file
141
src/data/hook/hero.ts
Normal file
@ -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<IHeroAttr> {
|
||||
constructor(readonly state: ICoreState) {}
|
||||
|
||||
onUpdateAttribute<K extends keyof IHeroAttr>(
|
||||
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<IHeroLocation> {
|
||||
constructor(readonly state: ICoreState) {}
|
||||
|
||||
async onMoveStart(
|
||||
tile: IHeroLocation,
|
||||
mover: IObjectMover<IHeroLocation>
|
||||
): Promise<void> {
|
||||
// 删除下面这两行注释开始编写你自己的勇士移动前钩子
|
||||
void tile;
|
||||
void mover;
|
||||
}
|
||||
|
||||
async onStepStart(
|
||||
code: number,
|
||||
step: Readonly<ObjectMoveStep>,
|
||||
tile: IHeroLocation,
|
||||
mover: IObjectMover<IHeroLocation>
|
||||
): Promise<void> {
|
||||
// 删除下面这四行注释开始编写你自己的勇士每步开始时钩子
|
||||
void code;
|
||||
void step;
|
||||
void tile;
|
||||
void mover;
|
||||
}
|
||||
|
||||
async onStepEnd(
|
||||
code: number,
|
||||
step: Readonly<ObjectMoveStep>,
|
||||
tile: IHeroLocation,
|
||||
mover: IObjectMover<IHeroLocation>
|
||||
): Promise<void> {
|
||||
// 删除下面这四行注释开始编写你自己的勇士每步结束后钩子
|
||||
void code;
|
||||
void step;
|
||||
void tile;
|
||||
void mover;
|
||||
}
|
||||
|
||||
async onMoveEnd(
|
||||
tile: IHeroLocation,
|
||||
mover: IObjectMover<IHeroLocation>
|
||||
): Promise<void> {
|
||||
// 删除下面这两行注释开始编写你自己的勇士移动前钩子
|
||||
void tile;
|
||||
void mover;
|
||||
}
|
||||
|
||||
onSetFaceDir(
|
||||
dir: FaceDirection,
|
||||
tile: IHeroLocation,
|
||||
mover: IObjectMover<IHeroLocation>
|
||||
): void {
|
||||
// 删除下面这三行注释开始编写你自己的设置面朝朝向的钩子
|
||||
void dir;
|
||||
void tile;
|
||||
void mover;
|
||||
}
|
||||
|
||||
onSetMoveDir(
|
||||
dir: FaceDirection,
|
||||
tile: IHeroLocation,
|
||||
mover: IObjectMover<IHeroLocation>
|
||||
): void {
|
||||
// 删除下面这三行注释开始编写你自己的设置移动朝向的钩子
|
||||
void dir;
|
||||
void tile;
|
||||
void mover;
|
||||
}
|
||||
|
||||
onSetPos(
|
||||
x: number,
|
||||
y: number,
|
||||
tile: IHeroLocation,
|
||||
mover: IObjectMover<IHeroLocation>
|
||||
): void {
|
||||
// 删除下面这三行注释开始编写你自己的设置位置的钩子
|
||||
void x;
|
||||
void y;
|
||||
void tile;
|
||||
void mover;
|
||||
}
|
||||
}
|
||||
|
||||
export class MainHeroStateHooks implements IHeroStateHooks {
|
||||
constructor(readonly state: ICoreState) {}
|
||||
|
||||
async onBeforeChangeFloor(info: IHeroChangeFloorInfo): Promise<void> {
|
||||
// 删除下面这一行注释开始编写你自己的楼层转换前钩子
|
||||
void info;
|
||||
}
|
||||
|
||||
async onAfterChangeFloor(info: IHeroChangeFloorInfo): Promise<void> {
|
||||
// 删除下面这一行注释开始编写你自己的楼层转换后钩子
|
||||
void info;
|
||||
}
|
||||
}
|
||||
6
src/data/hook/index.ts
Normal file
6
src/data/hook/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './combat';
|
||||
export * from './event';
|
||||
export * from './hero';
|
||||
export * from './initialize';
|
||||
export * from './loader';
|
||||
export * from './path';
|
||||
26
src/data/hook/initialize.ts
Normal file
26
src/data/hook/initialize.ts
Normal file
@ -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));
|
||||
}
|
||||
19
src/data/hook/loader.ts
Normal file
19
src/data/hook/loader.ts
Normal file
@ -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> {
|
||||
// 删除下面这两行注释开始编写你自己的核心配置加载后钩子
|
||||
void loader;
|
||||
}
|
||||
|
||||
async onExtraConfigLoaded(loader: IMotaDataLoader): Promise<void> {
|
||||
// 删除下面这两行注释开始编写你自己的额外配置加载后钩子
|
||||
void loader;
|
||||
}
|
||||
}
|
||||
28
src/data/hook/path.ts
Normal file
28
src/data/hook/path.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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()];
|
||||
|
||||
@ -1 +1,5 @@
|
||||
export * from './shop';
|
||||
export * from './manager';
|
||||
export * from './plugin';
|
||||
export * from './state';
|
||||
export * from './system';
|
||||
export * from './types';
|
||||
|
||||
@ -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;
|
||||
7
src/data/types.ts
Normal file
7
src/data/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { IDataPluginDefine } from '@user/data-state';
|
||||
import { ICombatWeakHandler } from './enemy';
|
||||
|
||||
export interface IDataMainPlugin extends IDataPluginDefine {
|
||||
/** 虚弱处理对象 */
|
||||
readonly weak: ICombatWeakHandler;
|
||||
}
|
||||
@ -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" }]
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user