refactor: 插件系统 & 数据端顶层移植

This commit is contained in:
AncTe committed 2026-10-14 20:32:46 +08:00
1 parent b0229c627f
commit 2f3d3387a0
74 files changed
+1322 -3321

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
import { WebLoadStarter } from '@motajs/loader';
import { createClientCore } from './client/entry';
import { IClientCore } from '@user/client-modules';
declare global {
interface Window {
mainClient: IClientCore;
}
}
const client = await createClientCore(new WebLoadStarter());
window.mainClient = client;
+38
View File
@@ -0,0 +1,38 @@
import { ILoadTaskStarter } from '@motajs/loader';
import {
ClientCore,
IClientCore,
IClientCoreConfig
} from '@user/client-modules';
import { CLIENT_CORE_URL, TILESET_RESERVE, TILESET_UNIT } from '../constant';
import { createCoreStateConfig } from '../data/entry';
/**
* 创建渲染端配置对象
* @param starter 加载启动器
*/
export function createClientCoreConfig(
starter: ILoadTaskStarter
): IClientCoreConfig {
const config: IClientCoreConfig = {
clientURL: CLIENT_CORE_URL,
tilesetReserve: TILESET_RESERVE,
tilesetUnit: TILESET_UNIT,
...createCoreStateConfig(starter)
};
return config;
}
/**
* 创建渲染端主对象,返回经过初始化的主对象实例
* @param starter 加载启动器
*/
export async function createClientCore(
starter: ILoadTaskStarter
): Promise<IClientCore> {
const config = createClientCoreConfig(starter);
const state = new ClientCore(config);
await state.load();
return state;
}
+23
View File
@@ -0,0 +1,23 @@
import { FaceGroup } from '@user/data-common';
// 本文件是游戏的底层核心配置,如果不懂的话千万不要动
//#region 数据端配置
/** 数据端核心配置对象,不懂的话就不要动 */
export const CORE_URL = 'content/core.jsonc';
/** 勇士的默认朝向组,用于控制勇士的移动,不懂的话就不要动 */
export const HERO_FACE = FaceGroup.Dir8;
//#endregion
//#region 渲染端配置
/** 渲染端核心配置对象,不懂的话就不要动 */
export const CLIENT_CORE_URL = 'content/client.jsonc';
/** Tileset 预留数量,用于控制最大允许注册多少个普通图块,例如填写 10000 表示最大 10000 个普通图块,不懂的话不要动 */
export const TILESET_RESERVE = 100000;
/** 一个 Tileset 的图块数量单位,例如 10000 时表示一个 Tileset 会占用 10000 或 10000 的倍数个图块,不懂的话不要动 */
export const TILESET_UNIT = 10000;
//#endregion
+301
View File
@@ -0,0 +1,301 @@
import {
FullRange,
IManhattanRangeParam,
IRange,
IRectRangeParam,
ITileLocator,
ManhattanRange,
RectRange
} from '@motajs/common';
import {
IAuraConverter,
IEnemyHandler,
IEnemyAuraView,
IEnemyContext,
IEnemySpecialModifier,
IEnemyView,
IReadonlyEnemyHandler
} from '@user/data-system';
import { IReadonlyEnemy, ISpecial } from '@user/data-base';
import { ISpecial23Value, ISpecial26Value } from './special';
import { IHeroAttr, IEnemyAttr } from '@user/data-common';
const FULL_RANGE = new FullRange();
const RECT_RANGE = new RectRange();
const MANHATTAN_RANGE = new ManhattanRange();
//#region 23-光环
export class CommonAuraConverter implements IAuraConverter<
IEnemyAttr,
IHeroAttr
> {
shouldConvert(special: ISpecial<any>): boolean {
return special.code === 23;
}
convert(
special: ISpecial<ISpecial23Value>,
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): CommonAura {
return new CommonAura(handler.enemy, special, handler.locator);
}
}
export class CommonAura implements IEnemyAuraView<
IEnemyAttr,
IRectRangeParam | IManhattanRangeParam | void,
ISpecial23Value
> {
readonly priority: number = 10;
readonly couldApplyBase: boolean = true;
readonly couldApplySpecial: boolean = false;
readonly range: IRange<IRectRangeParam | IManhattanRangeParam | void>;
constructor(
readonly enemy: IReadonlyEnemy<IEnemyAttr>,
readonly special: ISpecial<ISpecial23Value>,
readonly locator: ITileLocator
) {
this.range = this.createRange();
}
private createRange(): IRange<
IRectRangeParam | IManhattanRangeParam | void
> {
const { range, square } = this.special.value;
if (range <= 0) {
return FULL_RANGE;
}
return square ? RECT_RANGE : MANHATTAN_RANGE;
}
getRangeParam(): IRectRangeParam | IManhattanRangeParam | void {
const { range, square } = this.special.value;
if (range <= 0) {
return undefined;
}
if (square) {
return {
x: this.locator.x - range,
y: this.locator.y - range,
w: range * 2 + 1,
h: range * 2 + 1
};
}
return {
cx: this.locator.x,
cy: this.locator.y,
radius: range
};
}
apply(
handler: IEnemyHandler<IEnemyAttr, any>,
baseEnemy: IReadonlyEnemy<IEnemyAttr>
): void {
const { enemy } = handler;
const { hpBuff, atkBuff, defBuff } = this.special.value;
if (hpBuff !== 0) {
const hpValue = (baseEnemy.getAttribute('hp') * hpBuff) / 100;
enemy.addAttribute('hp', Math.floor(hpValue));
}
if (atkBuff !== 0) {
const atkValue = (baseEnemy.getAttribute('atk') * atkBuff) / 100;
enemy.addAttribute('atk', Math.floor(atkValue));
}
if (defBuff !== 0) {
const defValue = (baseEnemy.getAttribute('def') * defBuff) / 100;
enemy.addAttribute('def', Math.floor(defValue));
}
}
applySpecial(): IEnemySpecialModifier<IEnemyAttr> | null {
return null;
}
}
//#endregion
//#region 24-支援
export class GuardAuraConverter implements IAuraConverter<
IEnemyAttr,
IHeroAttr
> {
shouldConvert(special: ISpecial<any>): boolean {
return special.code === 24;
}
convert(
special: ISpecial<void>,
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
context: IEnemyContext<IEnemyAttr, IHeroAttr>
): GuardAura {
return new GuardAura(context, handler.enemy, special, handler.locator);
}
}
export class GuardAura implements IEnemyAuraView<
IEnemyAttr,
IRectRangeParam,
void
> {
readonly priority: number = 5;
readonly couldApplyBase: boolean = true;
readonly couldApplySpecial: boolean = false;
readonly range: IRange<IRectRangeParam> = RECT_RANGE;
private readonly sourceView: IEnemyView<IEnemyAttr> | null;
constructor(
context: IEnemyContext<IEnemyAttr, IHeroAttr>,
readonly enemy: IReadonlyEnemy<IEnemyAttr>,
readonly special: ISpecial<void>,
readonly locator: ITileLocator
) {
this.sourceView = context.getViewByComputed(enemy);
}
getRangeParam(): IRectRangeParam {
return {
x: this.locator.x - 1,
y: this.locator.y - 1,
w: 3,
h: 3
};
}
apply(handler: IEnemyHandler<IEnemyAttr, IHeroAttr>): void {
if (!this.sourceView) return;
const { enemy, locator } = handler;
if (locator.x === this.locator.x && locator.y === this.locator.y) {
return;
}
enemy.getAttribute('guard').add(this.locator);
}
applySpecial(): IEnemySpecialModifier<IEnemyAttr> | null {
return null;
}
}
//#endregion
//#region 26-特殊光环
export class SpecialAuraConverter implements IAuraConverter<
IEnemyAttr,
IHeroAttr
> {
shouldConvert(special: ISpecial<any>): boolean {
return special.code === 26;
}
convert(
special: ISpecial<ISpecial26Value>,
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): SpecialAura {
return new SpecialAura(handler.enemy, special, handler.locator);
}
}
export class SpecialAuraModifier implements IEnemySpecialModifier<IEnemyAttr> {
constructor(readonly special: ISpecial<ISpecial26Value>) {}
add(handler: IReadonlyEnemyHandler<IEnemyAttr, any>): ISpecial<any>[] {
const manager = handler.state.enemyManager;
const list: ISpecial<any>[] = [];
for (const [code, param] of this.special.value.specials) {
if (handler.enemy.hasSpecial(code)) continue;
const sp = manager.getSpecial(code)?.();
if (!sp) continue;
sp.setValue(param);
list.push(sp);
}
return list;
}
delete(): ISpecial<any>[] {
// 特殊光环不删除特殊属性
return [];
}
modify(
_handler: IEnemyHandler<IEnemyAttr, any>,
special: ISpecial<any>
): boolean {
const sp = this.special.value.specials.find(v => v[0] === special.code);
if (!sp) return false;
const param = sp[1];
if (typeof param !== 'number') return false;
if (this.special.value.mulAdd) {
special.setValue(special.value * param);
} else {
special.setValue(special.value + param);
}
return true;
}
}
export class SpecialAura implements IEnemyAuraView<
IEnemyAttr,
IRectRangeParam | IManhattanRangeParam | void,
ISpecial26Value
> {
readonly priority: number = 20;
readonly couldApplyBase: boolean = true;
readonly couldApplySpecial: boolean = false;
readonly range: IRange<IRectRangeParam | IManhattanRangeParam | void>;
constructor(
readonly enemy: IReadonlyEnemy<IEnemyAttr>,
readonly special: ISpecial<ISpecial26Value>,
readonly locator: ITileLocator
) {
this.range = this.createRange();
}
private createRange(): IRange<
IRectRangeParam | IManhattanRangeParam | void
> {
const { range, square } = this.special.value;
if (range <= 0) {
return FULL_RANGE;
}
return square ? RECT_RANGE : MANHATTAN_RANGE;
}
getRangeParam(): IRectRangeParam | IManhattanRangeParam | void {
const { range, square } = this.special.value;
if (range <= 0) {
return undefined;
}
if (square) {
return {
x: this.locator.x - range,
y: this.locator.y - range,
w: range * 2 + 1,
h: range * 2 + 1
};
}
return {
cx: this.locator.x,
cy: this.locator.y,
radius: range
};
}
apply(): void {
// 特殊光环不修改基本属性
}
applySpecial(): IEnemySpecialModifier<IEnemyAttr> | null {
return new SpecialAuraModifier(this.special);
}
}
//#endregion
+180
View File
@@ -0,0 +1,180 @@
import {
CriticalableHeroStatus,
IDamageCalculator,
IEnemyDamageInfoBase,
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<
IEnemyAttr,
IHeroAttr
> {
/** 当前是否正在计算支援怪的伤害 */
private inGuard: boolean = false;
calculate(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): IEnemyDamageInfoBase {
const { enemy, locator, hero } = handler;
const hp = hero.getBaseAttribute('hp');
const atk = hero.getFinalAttribute('atk');
const def = hero.getFinalAttribute('def');
const mdef = this.inGuard ? 0 : hero.getFinalAttribute('mdef');
// 支援中魔防只会被计算一次,因此除了当前怪物,计算其他怪物伤害时魔防为 0
const monAtk = enemy.getAttribute('atk');
const monDef = enemy.getAttribute('def');
let monHp = enemy.getAttribute('hp');
// 无敌
if (
enemy.hasSpecial(20) &&
handler.state.hero.items.itemCount('cross') < 1
) {
return { damage: Infinity, turn: 0 };
}
/** 怪物会对勇士造成的总伤害 */
let damage = 0;
/** 勇士每轮造成的伤害 */
let heroPerDamage = 0;
/** 怪物每轮造成的伤害 */
let enemyPerDamage = 0;
// 勇士每轮伤害为勇士攻击减去怪物防御
heroPerDamage += atk - monDef;
if (heroPerDamage <= 0) {
return { damage: Infinity, turn: 0 };
}
// 吸血
const vampire = enemy.getSpecial<ISpecial9Value>(9);
if (vampire) {
const value = (vampire.value.vampire / 100) * hp;
damage += value;
// 如果吸血加到自身
if (vampire.value.add) {
monHp += value;
}
}
// 魔攻
if (enemy.hasSpecial(2)) {
enemyPerDamage = monAtk;
} else {
enemyPerDamage = monAtk - def;
}
const multiHit = enemy.getSpecial<number>(4);
if (multiHit) {
enemyPerDamage *= multiHit.value;
}
if (enemyPerDamage < 0) enemyPerDamage = 0;
let turn = Math.ceil(monHp / heroPerDamage);
// 支援,当怪物被支援且不包含支援标记时执行,因为支援怪不能再被支援了
const guards = enemy.getAttribute('guard');
if (guards.size > 0 && !this.inGuard) {
this.inGuard = true;
// 计算支援怪的伤害,同时把打支援怪花费的回合数加到当前怪物上,因为打支援怪的时候当前怪物也会打你
// 因此回合数需要加上打支援怪的回合数
for (const guard 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,
locator,
hero,
state: handler.state
});
turn += extraInfo.turn;
damage += extraInfo.damage;
}
this.inGuard = false;
}
// 先攻
if (enemy.hasSpecial(1)) {
damage += enemyPerDamage;
}
// 破甲
const breakArmor = enemy.getSpecial<number>(5);
if (breakArmor) {
damage += (breakArmor.value / 100) * def;
}
// 反击
const counterAttack = enemy.getSpecial<number>(6);
if (counterAttack) {
// 反击是每回合生效,因此加到 enemyPerDamage 上
enemyPerDamage += (counterAttack.value / 100) * atk;
}
// 净化
const purify = enemy.getSpecial<number>(7);
if (purify) {
damage += purify.value * mdef;
}
damage += (turn - 1) * enemyPerDamage;
// 魔防
damage -= mdef;
// 未开启负伤时,如果伤害为负,则设为 0
if (
!handler.state.flags.getFieldValueDefaults(
'enableNegativeDamage',
false
) &&
damage < 0
) {
damage = 0;
}
// 固伤,无法被魔防减伤
const fixedDamage = enemy.getSpecial<number>(20);
if (fixedDamage) {
damage += fixedDamage.value;
}
// 仇恨,无法被魔防减伤
if (enemy.hasSpecial(15)) {
damage += handler.state.flags.getFieldValueDefaults('hatred', 0);
}
return {
damage: Math.floor(damage),
turn
};
}
getCriticalLimit(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
attribute: CriticalableHeroStatus<IHeroAttr>
): number {
switch (attribute) {
case 'atk': {
if (handler.enemy.hasSpecial(3)) {
return Infinity;
}
return (
handler.enemy.getAttribute('def') +
handler.enemy.getAttribute('hp')
);
}
}
return handler.hero.getFinalAttribute(attribute);
}
}
+31
View File
@@ -0,0 +1,31 @@
import { IEnemyComparer, IReadonlyEnemy } from '@user/data-base';
import { IEnemyAttr } from '@user/data-common';
export class MainEnemyComparer implements IEnemyComparer<IEnemyAttr> {
compare(
enemyA: IReadonlyEnemy<IEnemyAttr>,
enemyB: IReadonlyEnemy<IEnemyAttr>
): boolean {
// 比较基本属性
if (
enemyA.getAttribute('hp') !== enemyB.getAttribute('hp') ||
enemyA.getAttribute('atk') !== enemyB.getAttribute('atk') ||
enemyA.getAttribute('def') !== enemyB.getAttribute('def') ||
enemyA.getAttribute('money') !== enemyB.getAttribute('money') ||
enemyA.getAttribute('exp') !== enemyB.getAttribute('exp') ||
enemyA.getAttribute('point') !== enemyB.getAttribute('point')
) {
return false;
}
// 比较特殊属性
const specialsA = [...enemyA.iterateSpecials()];
const specialsB = [...enemyB.iterateSpecials()];
if (specialsA.length !== specialsB.length) return false;
for (const special of specialsA) {
const other = enemyB.getSpecial(special.code);
if (!other || !special.deepEqualsTo(other)) return false;
}
return true;
}
}
+27
View File
@@ -0,0 +1,27 @@
import { IEnemyFinalEffect, IEnemyHandler } from '@user/data-system';
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
export class MainEnemyFinalEffect implements IEnemyFinalEffect<
IEnemyAttr,
IHeroAttr
> {
readonly priority: number = 0;
apply(handler: IEnemyHandler<IEnemyAttr, IHeroAttr>): void {
const enemy = handler.enemy;
const heroAtk = handler.hero.getFinalAttribute('atk');
const heroDef = handler.hero.getFinalAttribute('def');
// 3 - 坚固
if (enemy.hasSpecial(3)) {
const target = Math.max(enemy.getAttribute('def'), heroAtk - 1);
enemy.setAttribute('def', target);
}
// 8 - 模仿
if (enemy.hasSpecial(8)) {
enemy.setAttribute('atk', heroAtk);
enemy.setAttribute('def', heroDef);
}
}
}
+8
View File
@@ -0,0 +1,8 @@
export * from './aura';
export * from './calculator';
export * from './comparer';
export * from './final';
export * from './initialize';
export * from './mapDamage';
export * from './special';
export * from './types';
+48
View File
@@ -0,0 +1,48 @@
import { ICoreState } from '@user/data-state';
import { createSpecials } from './special';
import { DamageSystem, MapDamage } from '@user/data-system';
import {
SpecialAuraConverter,
CommonAuraConverter,
GuardAuraConverter
} from './aura';
import { MainEnemyFinalEffect } from './final';
import { MainDamageCalculator } from './calculator';
import { MainEnemyComparer } from './comparer';
import { MainMapDamageConverter, MainMapDamageReducer } from './mapDamage';
/**
* 初始化怪物相关内容
* @param state 数据端主对象
* @param config 数据端配置
*/
export function initializeEnemy(state: ICoreState) {
// 怪物管理器初始化
const comparer = new MainEnemyComparer();
state.enemyManager.attachEnemyComparer(comparer);
// 特殊属性定义注册
const specials = createSpecials(state);
for (const [code, cons] of specials) {
state.enemyManager.registerSpecial(code, cons);
}
// 初始化怪物上下文
const ctx = state.enemyContext;
const damageSystem = new DamageSystem(ctx);
const mapDamage = new MapDamage(ctx);
damageSystem.useCalculator(new MainDamageCalculator());
mapDamage.useReducer(new MainMapDamageReducer());
mapDamage.useConverter(new MainMapDamageConverter());
ctx.attachDamageSystem(damageSystem);
ctx.attachMapDamage(mapDamage);
ctx.bindHero(state.hero.getAttribute());
// 注册光环转换器
ctx.registerAuraConverter(new CommonAuraConverter());
ctx.registerAuraConverter(new GuardAuraConverter());
ctx.registerAuraConverter(new SpecialAuraConverter());
// 注册怪物最终效果
ctx.registerFinalEffect(new MainEnemyFinalEffect());
}
+359
View File
@@ -0,0 +1,359 @@
import {
IManhattanRangeParam,
IRange,
IRayRangeParam,
IRectRangeParam,
ManhattanRange,
RayRange,
RectRange,
ITileLocator,
IFaceHandler,
IFaceDescriptor
} from '@motajs/common';
import {
IEnemyContext,
IMapDamageConverter,
IMapDamageInfo,
IMapDamageInfoExtra,
IMapDamageReducer,
IReadonlyEnemyHandler,
IMapDamageView
} from '@user/data-system';
import {
ISpecial,
IReadonlyHeroAttribute,
IReadonlyEnemy
} from '@user/data-base';
import { ISpecial13Value } from './special';
import { MapDamageType } from './types';
import { IHeroAttr, IEnemyAttr } from '@user/data-common';
import { FaceGroup } from '@user/data-common';
const RECT_RANGE = new RectRange();
const MANHATTAN_RANGE = new ManhattanRange();
const RAY_RANGE = new RayRange();
//#region 地图伤害
abstract class BaseMapDamageView<T> implements IMapDamageView<T> {
constructor(
protected readonly context: IEnemyContext<IEnemyAttr, IHeroAttr>
) {}
abstract getRange(): IRange<T>;
abstract getRangeParam(): T;
getDamageAt(locator: ITileLocator): Readonly<IMapDamageInfo> | null {
const range = this.getRange();
const param = this.getRangeParam();
range.bindHost(this.context);
if (!range.inRange(locator.x, locator.y, param)) {
return null;
}
return this.getDamageWithoutCheck(locator);
}
abstract getDamageWithoutCheck(
locator: ITileLocator
): Readonly<IMapDamageInfo> | null;
/**
* 创建伤害信息
* @param damage 伤害值
* @param type 伤害类型
* @param extra 额外信息
*/
protected createInfo(
damage: number,
type: number,
extra?: Partial<IMapDamageInfoExtra>
): IMapDamageInfo {
return {
damage,
type,
extra: {
catch: extra?.catch ?? new Set(),
repulse: extra?.repulse ?? new Set()
}
};
}
}
export class ZoneDamageView extends BaseMapDamageView<
IRectRangeParam | IManhattanRangeParam
> {
constructor(
context: IEnemyContext<IEnemyAttr, IHeroAttr>,
private readonly locator: Readonly<ITileLocator>,
private readonly special: Readonly<ISpecial<ISpecial13Value>>
) {
super(context);
}
getRange(): IRange<IRectRangeParam | IManhattanRangeParam> {
return this.special.value.square ? RECT_RANGE : MANHATTAN_RANGE;
}
getRangeParam(): IRectRangeParam | IManhattanRangeParam {
if (this.special.value.square) {
return {
h: this.special.value.range * 2 + 1,
w: this.special.value.range * 2 + 1,
x: this.locator.x - this.special.value.range,
y: this.locator.y - this.special.value.range
};
}
return {
cx: this.locator.x,
cy: this.locator.y,
radius: this.special.value.range
};
}
getDamageWithoutCheck(): Readonly<IMapDamageInfo> | null {
return this.createInfo(this.special.value.zone, MapDamageType.Zone);
}
}
export class RepulseDamageView extends BaseMapDamageView<IManhattanRangeParam> {
constructor(
context: IEnemyContext<IEnemyAttr, IHeroAttr>,
private readonly locator: Readonly<ITileLocator>,
private readonly special: Readonly<ISpecial<number>>
) {
super(context);
}
getRange(): IRange<IManhattanRangeParam> {
return MANHATTAN_RANGE;
}
getRangeParam(): IManhattanRangeParam {
return {
cx: this.locator.x,
cy: this.locator.y,
radius: 1
};
}
getDamageWithoutCheck(
locator: ITileLocator
): Readonly<IMapDamageInfo> | null {
if (locator.x === this.locator.x && locator.y === this.locator.y) {
return null;
}
return this.createInfo(this.special.value, MapDamageType.Repulse, {
repulse: new Set([this.locator])
});
}
}
export class LaserDamageView extends BaseMapDamageView<IRayRangeParam> {
/** 激光方向列表 */
private readonly dirs: IFaceDescriptor[];
constructor(
context: IEnemyContext<IEnemyAttr, IHeroAttr>,
private readonly locator: Readonly<ITileLocator>,
private readonly special: Readonly<ISpecial<number>>,
dir: IFaceHandler<number>
) {
super(context);
this.dirs = [...dir.mapMovement()].map(v => v[1]);
}
getRange(): IRange<IRayRangeParam> {
return RAY_RANGE;
}
getRangeParam(): IRayRangeParam {
return {
cx: this.locator.x,
cy: this.locator.y,
dir: this.dirs
};
}
getDamageWithoutCheck(): Readonly<IMapDamageInfo> | null {
return this.createInfo(this.special.value, MapDamageType.Layer);
}
}
export class BetweenDamageView extends BaseMapDamageView<IManhattanRangeParam> {
constructor(
context: IEnemyContext<IEnemyAttr, IHeroAttr>,
private readonly enemy: IReadonlyEnemy<IEnemyAttr>,
private readonly locator: Readonly<ITileLocator>,
private readonly hero: IReadonlyHeroAttribute<IHeroAttr>
) {
super(context);
}
getRange(): IRange<IManhattanRangeParam> {
return MANHATTAN_RANGE;
}
getRangeParam(): IManhattanRangeParam {
return {
cx: this.locator.x,
cy: this.locator.y,
radius: 1
};
}
getDamageWithoutCheck(
locator: ITileLocator
): Readonly<IMapDamageInfo> | null {
const deltaX = locator.x - this.locator.x;
const deltaY = locator.y - this.locator.y;
if (Math.abs(deltaX) + Math.abs(deltaY) !== 1) {
return null;
}
if (deltaX <= 0 && deltaY <= 0) {
return null;
}
const otherX = locator.x + deltaX;
const otherY = locator.y + deltaY;
const range = this.getRange();
range.bindHost(this.context);
if (!range.inBound(otherX, otherY)) {
return null;
}
const other = this.context.getEnemyByLoc(otherX, otherY);
if (!other) {
return null;
}
const otherEnemy = other.getComputedEnemy();
if (!otherEnemy.hasSpecial(16)) {
return null;
}
const half = this.hero.getFinalAttribute('hp') / 2;
if (core.flags.betweenAttackMax) {
// 夹击不超伤害值,需要获取两个怪物的伤害
const sys = this.context.getDamageSystem();
if (!sys) {
return this.createInfo(half, MapDamageType.Between);
} else {
const currInfo = sys.getDamageInfoByComputed(this.enemy);
const otherInfo = sys.getDamageInfoByComputed(otherEnemy);
const currDamage = currInfo?.damage ?? Infinity;
const otherDamage = otherInfo?.damage ?? Infinity;
const min = Math.min(half, currDamage, otherDamage);
return this.createInfo(min, MapDamageType.Between);
}
} else {
return this.createInfo(half, MapDamageType.Between);
}
}
}
export class AmbushDamageView extends BaseMapDamageView<IManhattanRangeParam> {
constructor(
context: IEnemyContext<IEnemyAttr, IHeroAttr>,
private readonly locator: Readonly<ITileLocator>
) {
super(context);
}
getRange(): IRange<IManhattanRangeParam> {
return MANHATTAN_RANGE;
}
getRangeParam(): IManhattanRangeParam {
return {
cx: this.locator.x,
cy: this.locator.y,
radius: 1
};
}
getDamageWithoutCheck(): Readonly<IMapDamageInfo> | null {
return this.createInfo(0, MapDamageType.Unknown, {
catch: new Set([this.locator])
});
}
}
//#endregion
//#region 转换器
export class MainMapDamageConverter implements IMapDamageConverter<
IEnemyAttr,
IHeroAttr
> {
convert(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
context: IEnemyContext<IEnemyAttr, IHeroAttr>
): IMapDamageView<any>[] {
const views: IMapDamageView<any>[] = [];
const { enemy, locator, hero } = handler;
const zone = enemy.getSpecial<ISpecial13Value>(15);
if (zone) {
views.push(new ZoneDamageView(context, locator, zone));
}
if (enemy.hasSpecial(16)) {
views.push(new BetweenDamageView(context, enemy, locator, hero));
}
const repulse = enemy.getSpecial<number>(18);
if (repulse) {
views.push(new RepulseDamageView(context, locator, repulse));
}
const laser = enemy.getSpecial<number>(24);
if (laser) {
const face = handler.state.faceManager.get(FaceGroup.Dir4)!;
views.push(new LaserDamageView(context, locator, laser, face));
}
if (enemy.hasSpecial(27)) {
views.push(new AmbushDamageView(context, locator));
}
return views;
}
}
//#endregion
//#region 合并器
export class MainMapDamageReducer implements IMapDamageReducer {
reduce(info: Iterable<Readonly<IMapDamageInfo>>): Readonly<IMapDamageInfo> {
let damage = 0;
let type = MapDamageType.Unknown;
let maxDamage = -Infinity;
const extra = {
catch: new Set<ITileLocator>(),
repulse: new Set<ITileLocator>()
};
for (const item of info) {
damage += item.damage;
if (item.damage > maxDamage) {
maxDamage = item.damage;
type = item.type;
}
item.extra.catch.forEach(v => extra.catch.add(v));
item.extra.repulse.forEach(v => extra.repulse.add(v));
}
return {
damage,
extra,
type
};
}
}
//#endregion
+488
View File
@@ -0,0 +1,488 @@
import {
defineCommonSerializableSpecial,
defineNonePropertySpecial,
SpecialCreation
} from '@user/data-base';
import { ICoreState } from '@user/data-state';
// 该文件中的一些注释会充当编辑器的识别模式,如果删除可能导致编辑器无法识别,谨慎删除!!!
// 重要!!!下面这一行的注释不要删
//#region 复合属性值类型
// 对象参数名称必须命名为 ISpecial{code}Value,接口前必须添加如下格式的注释,否则不会被编辑器识别
// /** {code} - {name} */
/** 9 - 吸血 */
export interface ISpecial9Value {
/** 吸血比例 */
vampire: number;
/** 是否加到自身生命值上 */
add: boolean;
}
/** 13 - 领域 */
export interface ISpecial13Value {
/** 领域伤害值 */
zone: number;
/** 矩形领域(true)还是十字形领域(false),当范围大于 1 时,十字型领域采用曼哈顿距离计算 */
square: boolean;
/** 领域范围值 */
range: number;
}
/** 21 - 退化 */
export interface ISpecial21Value {
/** 攻击下降值 */
atkValue: number;
/** 防御下降值 */
defValue: number;
}
/** 23 - 光环 */
export interface ISpecial23Value {
/** 光环范围值 */
range: number;
/** 光环是矩形范围(true)还是十字型范围(false),当范围大于 1 时,十字型范围采用曼哈顿距离计算 */
square: boolean;
/** 生命值增加比例 */
hpBuff: number;
/** 攻击力增加比例 */
atkBuff: number;
/** 防御力增加比例 */
defBuff: number;
}
/** 26 - 特殊光环 */
export interface ISpecial26Value {
/** 光环范围值 */
range: number;
/** 光环是矩形范围(true)还是十字型范围(false),当范围大于 1 时,十字型范围采用曼哈顿距离计算 */
square: boolean;
/** 当怪物受到多个特殊光环加成时,是乘算叠加(true),还是加算叠加(false) */
mulAdd: boolean;
/** 增加的特殊属性列表 */
specials: [number, any][];
}
// 重要!!!下面这一行的注释不要删
//#endregion
// TODO: 注释需要更新
/**
* 注册所有怪物特殊属性到 enemyManager
*
* 属性实现位置一览('./'表示当前文件夹 '../'表示上一级文件夹):
* 1. 调参类属性 | 仅影响战斗过程的属性:./calculator.ts, MainDamageCalculator.calculate 方法
* 2. 坚固、模仿等仅影响自身基础属性的属性:./final.ts, MainEnemyFinalEffect.apply 方法
* 3. 地图伤害:./mapDamage.ts,具体写法参考文档
* 4. 光环属性:./aura.ts 具体写法参考文档
* 5. 中毒的每步效果:在插件系统中,目前暂未实装
* 6. 中毒的瞬移效果:在插件系统中,目前暂未实装
* 7. 衰弱效果:在插件系统中,目前尚未实装
* 8. 重生属性:在插件系统中,目前尚未实装
*/
export function createSpecials(
state: ICoreState
): ReadonlyMap<number, SpecialCreation<any>> {
const hero = state.hero;
const map = new Map<number, SpecialCreation<any>>();
// 注意,不需要参数的特殊属性,如先攻、魔攻、坚固这些纯机制,
// 不需要一个数值来描述的特殊属性,使用 defineNonePropertySpecial 定义。
// 需要参数的特殊属性,如连击、领域、破甲这些需要一个或多个数值来描述的特殊属性,
// 使用 defineCommonSerializableSpecial 定义。
// 特殊属性的参数可以填写数值、字符串,也可以填写对象,参考吸血、领域、退化、光环的写法。
// 注意,每个属性定义前必须添加注释 // {code} - {name},这会作为编辑器识别的匹配模式。
// 重要!!!下面这一行的注释不要删
//#region 属性定义
// 0 - 空
map.set(
0,
defineNonePropertySpecial(0, {
name: '空',
desc: '空',
color: '#FFF'
})
);
// 1 - 先攻
map.set(
1,
defineNonePropertySpecial(1, {
name: '先攻',
desc: '怪物首先攻击。',
color: '#FC3'
})
);
// 2 - 魔攻
map.set(
2,
defineNonePropertySpecial(2, {
name: '魔攻',
desc: '怪物攻击无视勇士的防御。',
color: '#BBB0FF'
})
);
// 3 - 坚固
map.set(
3,
defineNonePropertySpecial(3, {
name: '坚固',
desc: '怪物防御不小于勇士攻击-1。',
color: '#C0B088'
})
);
// 4 - n连击
map.set(
4,
defineCommonSerializableSpecial(4, 2, {
name: special => `${special.value}连击`,
desc: special => `怪物每回合攻击${special.value}次。`,
color: '#FE7'
})
);
// 5 - 破甲
map.set(
5,
defineCommonSerializableSpecial(5, 0, {
name: '破甲',
desc: special =>
`战斗前,附加角色防御的${special.value}%作为伤害。`,
color: '#88C0FF'
})
);
// 6 - 反击
map.set(
6,
defineCommonSerializableSpecial(6, 0, {
name: '反击',
desc: special =>
`战斗时,怪物每回合附加角色攻击的${special.value}%作为伤害,无视角色防御。`,
color: '#FA4'
})
);
// 7 - 净化
map.set(
7,
defineCommonSerializableSpecial(7, 0, {
name: '净化',
desc: special =>
`战斗前,怪物附加角色护盾的${special.value}倍作为伤害。`,
color: '#80EED6'
})
);
// 8 - 模仿
map.set(
8,
defineNonePropertySpecial(8, {
name: '模仿',
desc: '怪物的攻防与勇士相同。',
color: '#B0C0DD'
})
);
// 9 - 吸血
map.set(
9,
defineCommonSerializableSpecial<ISpecial9Value>(
9,
{ vampire: 0, add: false },
{
name: '吸血',
desc: special => {
const { vampire, add } = special.value;
const attr = hero.getAttribute();
return (
`战斗前,怪物首先吸取角色的${vampire}%生命` +
`(约${Math.floor((vampire / 100) * attr.getFinalAttribute('hp'))}点)作为伤害` +
(add ? `,并把伤害数值加到自身生命上。` : `。`)
);
},
color: '#DD4448'
}
)
);
// 10 - 中毒
map.set(
10,
defineCommonSerializableSpecial(10, 0, {
name: '中毒',
desc: special =>
`战斗后,角色陷入中毒状态,每一步损失生命${special.value}点,多次触发时,${state.flags.getFieldValueDefaults('poisonAdd', false) ? '与已有中毒值叠加' : '取最高值'}。`,
color: '#9E8'
})
);
// 11 - 衰弱
map.set(
11,
defineCommonSerializableSpecial(11, 0, {
name: '衰弱',
desc: special => {
const weak = special.value;
const suffix = ',多次触发时取最高值。';
if (weak < 1) {
return (
`战斗后,角色陷入衰弱状态,攻防暂时下降${Math.floor(weak * 100)}%` +
suffix
);
} else {
return (
`战斗后,角色陷入衰弱状态,攻防暂时下降${weak}点` +
suffix
);
}
},
color: '#F0BBCC'
})
);
// 12 - 诅咒
map.set(
12,
defineNonePropertySpecial(12, {
name: '诅咒',
desc: '战斗后,角色陷入诅咒状态,战斗无法获得金币和经验。',
color: '#BBEEF0'
})
);
// 13 - 领域
map.set(
13,
defineCommonSerializableSpecial<ISpecial13Value>(
13,
{ zone: 0, square: false, range: 1 },
{
name: '领域',
desc: special => {
const { zone, square: zoneSquare, range } = special.value;
return `经过怪物周围${zoneSquare ? '九宫格' : '十字'}范围内${range}格时自动减生命${zone}点。`;
},
color: '#C677DD'
}
)
);
// 14 - 夹击
map.set(
14,
defineNonePropertySpecial(14, {
name: '夹击',
desc: '经过两只相同的怪物中间,角色生命值变成一半。',
color: '#B9E'
})
);
// 15 - 仇恨
map.set(
15,
defineNonePropertySpecial(15, {
name: '仇恨',
desc: `战斗前,怪物附加之前积累的仇恨值作为伤害;战斗后,释放一半的仇恨值。(每杀死一个怪物获得${state.flags.getFieldValueDefaults('hatred', 1)}点仇恨值)。`,
color: '#B0B666'
})
);
// 16 - 阻击
map.set(
16,
defineCommonSerializableSpecial(16, 0, {
name: '阻击',
desc: special =>
`经过怪物十字范围内时怪物后退一格,同时对勇士造成${special.value}点伤害。`,
color: '#8888E6'
})
);
// 17 - 自爆
map.set(
17,
defineNonePropertySpecial(17, {
name: '自爆',
desc: '战斗后角色的生命值变成1。',
color: '#F66'
})
);
// 18 - 无敌
map.set(
18,
defineNonePropertySpecial(18, {
name: '无敌',
desc: '角色无法打败怪物,除非拥有十字架。',
color: '#AAA'
})
);
// 19 - 退化
map.set(
19,
defineCommonSerializableSpecial<ISpecial21Value>(
19,
{ atkValue: 0, defValue: 0 },
{
name: '退化',
desc: special => {
const { atkValue, defValue } = special.value;
return `战斗后角色永久下降${atkValue}点攻击和${defValue}点防御。`;
},
color: '#FFF'
}
)
);
// 20 - 固伤
map.set(
20,
defineCommonSerializableSpecial(20, 0, {
name: '固伤',
desc: special =>
`战斗前,怪物对角色造成${special.value}点固定伤害,未开启负伤时无视角色护盾。`,
color: '#F97'
})
);
// 21 - 重生
map.set(
21,
defineNonePropertySpecial(21, {
name: '重生',
desc: '怪物被击败后,角色转换楼层则怪物将再次出现。',
color: '#A0E0FF'
})
);
// 22 - 激光
map.set(
22,
defineCommonSerializableSpecial(22, 0, {
name: '激光',
desc: special =>
`经过怪物同行或同列时自动减生命${special.value}点。`,
color: '#D0A0DD'
})
);
// 23 - 光环
map.set(
23,
defineCommonSerializableSpecial<ISpecial23Value>(
23,
{
range: 0,
square: false,
hpBuff: 0,
atkBuff: 0,
defBuff: 0
},
{
name: '光环',
desc: special => {
const {
range: haloRange,
square: haloSquare,
hpBuff,
atkBuff,
defBuff
} = special.value;
let str = '';
if (haloRange > 0) {
if (haloSquare) {
str += '对于该怪物九宫格';
} else {
str += '对于该怪物十字';
}
str += `${haloRange}格范围内所有怪物`;
} else {
str += '同楼层所有怪物';
}
if (hpBuff) {
str += `,生命提升${hpBuff}%`;
}
if (atkBuff) {
str += `,攻击提升${atkBuff}%`;
}
if (defBuff) {
str += `,防御提升${defBuff}%`;
}
str += `,线性叠加。`;
return str;
},
color: '#E6E099'
}
)
);
// 24 - 支援
map.set(
24,
defineNonePropertySpecial(24, {
name: '支援',
desc: '当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。',
color: '#77C0B6'
})
);
// 25 - 捕捉
map.set(
25,
defineNonePropertySpecial(25, {
name: '捕捉',
desc: '当走到怪物十字范围内时会进行强制战斗。',
color: '#C0DDBB'
})
);
// 26 - 特殊光环
map.set(
26,
defineCommonSerializableSpecial<ISpecial26Value>(
26,
{
range: 0,
square: false,
mulAdd: false,
specials: []
},
{
name: '特殊光环',
desc: special => {
const { range, square, specials, mulAdd } = special.value;
const prefix = `在怪物周围${square ? '正方形' : '十字'} ${range} 格范围内的怪物获得如下特殊属性,${mulAdd ? '乘算' : '加算'}叠加:\n`;
const list: string[] = [];
for (const [code, param] of specials) {
const special = state.enemyManager.getSpecial(code)?.();
if (!special) continue;
special.setValue(param);
const name = special.getSpecialName();
const color = special.getNameColor();
const desc = special.getDescription();
const item = ` ${list.length + 1}. \\r[${color}]${name}\\r: ${desc}`;
list.push(item);
}
return prefix + list.join('\n');
},
color: '#e945ff'
}
)
);
// 重要!!!下面这一行的注释不要删
//#endregion
return map;
}
+12
View File
@@ -0,0 +1,12 @@
export const enum MapDamageType {
/** 未知伤害 */
Unknown,
/** 领域伤害 */
Zone,
/** 激光伤害 */
Layer,
/** 阻击伤害 */
Repulse,
/** 夹击伤害 */
Between
}
+35
View File
@@ -0,0 +1,35 @@
import { CoreState, ICoreState, ICoreStateConfig } from '@user/data-state';
import { CORE_URL, HERO_FACE } from '../constant';
import { ILoadTaskStarter } from '@motajs/loader';
import { MainDataPlugin } from './initialize';
import { dataPlugins } from './plugin';
import { createEventRegistrations } from './event';
/**
* 创建数据端配置对象
* @param starter 加载启动器
*/
export function createCoreStateConfig(
starter: ILoadTaskStarter
): ICoreStateConfig {
const config: ICoreStateConfig = {
loadStarter: starter,
coreURL: CORE_URL,
dataPlugins: [new MainDataPlugin(HERO_FACE), ...dataPlugins],
events: createEventRegistrations()
};
return config;
}
/**
* 创建数据端主对象,返回经过初始化的主对象实例
* @param starter 加载启动器
*/
export async function createCoreState(
starter: ILoadTaskStarter
): Promise<ICoreState> {
const config = createCoreStateConfig(starter);
const state = new CoreState(config);
await state.load();
return state;
}
+54
View File
@@ -0,0 +1,54 @@
import { BuiltInFunction } from '@motajs/anon-tokyo';
import {
IBlockEventEnv,
IBlockEventParam,
IGameEventInvocation
} from '@user/data-system';
import { IGameEventStore } from '@user/data-common';
import { IInsertEventEventParam, IInsertEventsEventParam } from './types';
export class EventInsertEvents implements BuiltInFunction<
IInsertEventsEventParam,
IBlockEventEnv
> {
name: string = 'insertEvents';
async func(param: IInsertEventsEventParam, env: IBlockEventEnv) {
if (param.ids.length === 0) return;
const store = env.state.eventStore;
const invocations = this.collectInvocations(param.ids, env, store);
if (invocations.length === 0) return;
await env.system.executor.execute<void>(invocations, { custom: {} });
}
/**
* 过滤存在的事件 id 并构造临时事件调用
*/
private collectInvocations(
ids: readonly string[],
env: IBlockEventEnv,
store: IGameEventStore
): IGameEventInvocation[] {
const invocations: IGameEventInvocation[] = [];
for (const id of ids) {
if (store.getEvent<IBlockEventParam, IBlockEventEnv, void>(id)) {
invocations.push({ id, env });
}
}
return invocations;
}
}
export class EventInsertEvent implements BuiltInFunction<
IInsertEventEventParam,
IBlockEventEnv
> {
name: string = 'insertEvent';
async func(param: IInsertEventEventParam, env: IBlockEventEnv) {
if (param.length === 0) return;
await env.system.executor.interpreter.exec(param, { custom: {} }, env);
}
}
+41
View File
@@ -0,0 +1,41 @@
import { BuiltInFunction } from '@motajs/anon-tokyo';
import { IBlockEventEnv } from '@user/data-system';
import { IMoveHeroEventParam, IStepHeroEventParam } from './types';
export class EventMoveHero implements BuiltInFunction<
IMoveHeroEventParam,
IBlockEventEnv
> {
name: string = 'moveHero';
async func(param: IMoveHeroEventParam, env: IBlockEventEnv) {
const mover = env.state.hero.location.mover;
if (mover.moving) return;
env.state.replaySystem.disable();
mover.push([...param.steps]);
const controller = mover.start();
if (!controller) return;
await controller.onEnd;
env.state.replaySystem.revert();
}
}
export class EventStepHero implements BuiltInFunction<
IStepHeroEventParam,
IBlockEventEnv
> {
name: string = 'stepHero';
async func(_param: IStepHeroEventParam, env: IBlockEventEnv) {
const mover = env.state.hero.location.mover;
if (mover.moving) return;
env.state.replaySystem.disable();
mover.forward(1);
const controller = mover.start();
if (!controller) return;
await controller.onEnd;
env.state.replaySystem.revert();
}
}
+5
View File
@@ -0,0 +1,5 @@
export * from './event';
export * from './hero';
export * from './map';
export * from './registrations';
export * from './types';
+78
View File
@@ -0,0 +1,78 @@
import { BuiltInFunction } from '@motajs/anon-tokyo';
import { IBlockEventEnv } from '@user/data-system';
import {
IRemoveBlockEventParam,
IMoveBlockEventParam,
ISetBlockEventParam
} from './types';
import { isNil } from 'lodash-es';
import { logger } from '@motajs/common';
import { getPossibleLayer } from './utils';
export class EventSetBlock implements BuiltInFunction<
ISetBlockEventParam,
IBlockEventEnv
> {
name: string = 'setBlock';
func(param: ISetBlockEventParam, env: IBlockEventEnv) {
const layer = getPossibleLayer(env);
if (!layer) return;
if (!layer.inMap(param.x, param.y)) return;
const num = env.state.tileStore.num(param.tile);
if (isNil(num)) {
logger.warn(1001);
return;
}
layer.setBlock(num, param.x, param.y);
}
}
export class EventMoveBlock implements BuiltInFunction<
IMoveBlockEventParam,
IBlockEventEnv
> {
name: string = 'moveBlock';
async func(param: IMoveBlockEventParam, env: IBlockEventEnv) {
const layer = getPossibleLayer(env);
if (!layer) return;
const { x, y, keepEvent } = param;
const dynamic = layer.transferToDynamic(x, y, keepEvent);
if (!dynamic) return;
dynamic.mover.push(param.steps);
const controller = dynamic.mover.start();
if (!controller) return;
await controller.onEnd;
if (param.safe) {
layer.transferToStaticIfSafe(dynamic, keepEvent);
} else {
layer.transferToStatic(dynamic, keepEvent);
}
}
}
export class EventRemoveBlock implements BuiltInFunction<
IRemoveBlockEventParam,
IBlockEventEnv
> {
name: string = 'removeBlock';
async func(param: IRemoveBlockEventParam, env: IBlockEventEnv) {
const layer = getPossibleLayer(env);
if (!layer || !layer.inMap(param.x, param.y)) return;
if (param.dynamic) {
const dynamics = [...layer.getDynamicTilesAt(param.x, param.y)];
await Promise.all(dynamics.map(tile => layer.deleteDynamic(tile)));
}
layer.removeBlock(param.x, param.y);
}
}
+24
View File
@@ -0,0 +1,24 @@
import { BuiltInFunction } from '@motajs/anon-tokyo';
import { EventRemoveBlock, EventMoveBlock, EventSetBlock } from './map';
import { EventMoveHero, EventStepHero } from './hero';
import { EventInsertEvent, EventInsertEvents } from './event';
/**
* 创建全部内建事件的注册项
*/
export function createEventRegistrations(): BuiltInFunction[] {
return [
// 地图控制
new EventSetBlock(),
new EventMoveBlock(),
new EventRemoveBlock(),
// 玩家控制
new EventMoveHero(),
new EventStepHero(),
// 事件控制
new EventInsertEvents(),
new EventInsertEvent()
];
}
+66
View File
@@ -0,0 +1,66 @@
import { ObjectMoveStep } from '@user/data-common';
import { Statement } from '@motajs/anon-tokyo';
//#region 地图控制
/** 事件:设置图块 */
export interface ISetBlockEventParam {
/** 横坐标 */
readonly x: number;
/** 纵坐标 */
readonly y: number;
/** 要设置为的图块 */
readonly tile: number | string;
}
/** 事件:移动图块 */
export interface IMoveBlockEventParam {
/** 起始横坐标 */
readonly x: number;
/** 起始纵坐标 */
readonly y: number;
/** 移动步骤 */
readonly steps: readonly ObjectMoveStep[];
/** 是否保留图块的图块事件 */
readonly keepEvent?: boolean;
/** 是否仅在目标位置安全时转回静态图块 */
readonly safe?: boolean;
}
/** 事件:删除图块 */
export interface IRemoveBlockEventParam {
/** 横坐标 */
readonly x: number;
/** 纵坐标 */
readonly y: number;
/** 是否一并删除动态图块 */
readonly dynamic?: boolean;
}
//#endregion
//#region 玩家控制
/** 事件:按步骤移动勇士 */
export interface IMoveHeroEventParam {
/** 移动步骤 */
readonly steps: readonly ObjectMoveStep[];
}
/** 事件:向前移动一步 */
export interface IStepHeroEventParam {}
//#endregion
//#region 事件控制
/** 事件:临时插入多个事件 */
export interface IInsertEventsEventParam {
/** 事件 id 顺序 */
readonly ids: readonly string[];
}
/** 事件:临时执行一段事件语句 */
export type IInsertEventEventParam = Statement[];
//#endregion
+30
View File
@@ -0,0 +1,30 @@
import { IGameMap, IMapLayer } from '@user/data-base';
import { IBlockEventEnv } from '@user/data-system';
import { isNil } from 'lodash-es';
/**
* 通过环境参量获取可能的地图对象
*/
export function getPossibleMap(env: IBlockEventEnv): IGameMap | null {
if (env.map) return env.map;
if (env.layer) return env.layer.map;
if (!isNil(env.heroFloor)) {
const map = env.state.maps.getMap(env.heroFloor);
if (map) return map;
}
return null;
}
/**
* 通过环境参量获取可能的事件图层
*/
export function getPossibleLayer(env: IBlockEventEnv): IMapLayer | null {
if (env.layer) return env.layer;
const map = getPossibleMap(env);
if (map?.eventLayer) return map.eventLayer;
return null;
}
+2
View File
@@ -0,0 +1,2 @@
export * from './moverImpl';
export * from './predicate';
+93
View File
@@ -0,0 +1,93 @@
import {
IHeroMoveTopHandler,
IHeroMoveTopImpl,
IMapState,
IPassPredicate
} from '@user/data-base';
import { EventTrigger } from '@user/data-common';
import { IGameEventExecutor, IStateSystem } from '@user/data-system';
import { DefaultPassPredicate, DefaultPassPredicateImpl } from './predicate';
import { isNil } from 'lodash-es';
export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
/** 地图存储对象 */
private readonly maps: IMapState;
/** 游戏事件执行器 */
private readonly executor: IGameEventExecutor;
/** 勇士移动使用的通行性谓词 */
private readonly passPredicate: DefaultPassPredicate;
constructor(private readonly state: IStateSystem) {
this.maps = state.maps;
this.executor = state.eventSystem.executor;
this.passPredicate = new DefaultPassPredicateImpl(this.state);
}
//#region 通行性判断
inBound(x: number, y: number, floorId: string | undefined): boolean {
if (isNil(floorId)) return false;
const layerState = this.maps.getMap(floorId);
if (!layerState) return false;
const { width, height } = layerState;
return x >= 0 && y >= 0 && x < width && y < height;
}
predicate(): IPassPredicate {
return this.passPredicate;
}
//#endregion
//#region 事件触发行为
/**
* 统一收集、排序并执行指定位置的点事件与图块事件。
* @param trigger 事件触发条件
* @param handler 移动信息对象
* @param x 收集横坐标
* @param y 收集纵坐标
*/
private async commonTrigger(
trigger: EventTrigger,
handler: IHeroMoveTopHandler,
x: number,
y: number
): Promise<void> {
if (isNil(handler.floorId)) return Promise.resolve();
const map = this.maps.getMap(handler.floorId);
if (!map) return Promise.resolve();
const event = map.eventLayer;
if (!event) return Promise.resolve();
const system = this.state.eventSystem;
const invocations = system.collectEvent(event, trigger, x, y);
await this.executor.execute<void>(invocations, { custom: {} });
}
async enter(handler: IHeroMoveTopHandler): Promise<void> {
const { x, y } = handler.nextLoc;
return this.commonTrigger(EventTrigger.OnEnter, handler, x, y);
}
async leave(handler: IHeroMoveTopHandler): Promise<void> {
const { x, y } = handler.currLoc;
return this.commonTrigger(EventTrigger.OnLeave, handler, x, y);
}
async hit(handler: IHeroMoveTopHandler): Promise<void> {
const { x, y } = handler.nextLoc;
return this.commonTrigger(EventTrigger.OnTouch, handler, x, y);
}
/**
* 新事件触发器没有无法进入的对应项,保留空实现以满足移动接口
*/
async cannotEnter(): Promise<void> {
return Promise.resolve();
}
//#endregion
}
+81
View File
@@ -0,0 +1,81 @@
import { IPassCheckHandler, IPassPredicate } from '@user/data-base';
import { FaceDirection, FaceGroup, PassBit } from '@user/data-common';
import { IStateSystem } from '@user/data-system';
export interface DefaultPassPredicate extends IPassPredicate {}
export class DefaultPassPredicateImpl implements DefaultPassPredicate {
constructor(readonly state: IStateSystem) {}
private directionToPassBit(dir: FaceDirection): number {
switch (dir) {
case FaceDirection.Up:
return PassBit.Up;
case FaceDirection.Right:
return PassBit.Right;
case FaceDirection.Down:
return PassBit.Down;
case FaceDirection.Left:
return PassBit.Left;
default:
return 0;
}
}
canPass(handler: IPassCheckHandler): boolean {
const { currLoc, nextLoc, direction, map } = handler;
const face = this.state.faceManager.get(FaceGroup.Dir4);
if (!face) return false;
const degraded = face.degrade(direction);
if (degraded === FaceDirection.Unknown) return false;
if (!map) return false;
const event = map.eventLayer;
if (!event) return false;
const { x, y } = currLoc;
const { x: nx, y: ny } = nextLoc;
const leaveMask = this.directionToPassBit(direction);
const enterMask = this.directionToPassBit(face.opposite(direction));
let canLeave = true;
let canEnter = true;
const curr = event.getLocationData(x, y);
const next = event.getLocationData(nx, ny);
const currRaw = curr?.static?.raw();
const nextRaw = next?.static?.raw();
if (currRaw) canLeave = !!(leaveMask & currRaw.pass.outPass);
if (nextRaw) canEnter = !!(enterMask & nextRaw.pass.inPass);
if (!canLeave || !canEnter) return false;
for (const layer of map.layerList) {
if (layer === event) continue;
const curr = layer.getLocationData(x, y);
const next = layer.getLocationData(nx, ny);
let canLeave = true;
let canEnter = true;
const currRaw = curr?.static?.raw();
const nextRaw = next?.static?.raw();
if (currRaw?.pass.onlyEvents) {
canLeave = !!(leaveMask & currRaw.pass.outPass);
}
if (nextRaw?.pass.onlyEvents) {
canEnter = !!(enterMask & nextRaw.pass.inPass);
}
if (!canLeave || !canEnter) return false;
}
return true;
}
shouldHit(handler: IPassCheckHandler): boolean {
const { nextLoc, map } = handler;
if (!map) return false;
const event = map.eventLayer;
if (!event) return false;
const next = event.getLocationData(nextLoc.x, nextLoc.y);
const nextRaw = next?.static?.raw();
return !!nextRaw && !nextRaw.eventPass;
}
}
+82
View File
@@ -0,0 +1,82 @@
import {
Dir4FaceHandler,
Dir8FaceHandler,
FaceDirection,
FaceGroup,
IFacedTileLocator
} from '@user/data-common';
import {
ICoreState,
ICoreStateCoreConfig,
IDataPluginDefine
} from '@user/data-state';
import { initializeEnemy } from './enemy';
import { initializeReplay } from './replay';
import { logger } from '@motajs/common';
import { DefaultHeroMoveTopImpl, DefaultPassPredicateImpl } from './hero';
import { DefaultDataLoaderHook } from './loader';
export class MainDataPlugin implements IDataPluginDefine {
readonly id: string = 'main-data-plugin';
readonly dependencies: string[] = [];
constructor(readonly heroFace: FaceGroup) {}
awake(state: ICoreState): void {
// 朝向组初始化
const dir4 = new Dir4FaceHandler();
const dir8 = new Dir8FaceHandler();
state.faceManager.register(FaceGroup.Dir4, dir4);
state.faceManager.registerById('dir4', dir4);
state.faceManager.register(FaceGroup.Dir8, dir8);
state.faceManager.registerById('dir8', dir8);
// 寻路初始化
const predicate = new DefaultPassPredicateImpl(state);
state.pathfinding.finder.usePassPredicate(predicate);
// 初始状态下勇士不在任何楼层,切换楼层后再具体设置
state.pathfinding.finder.useMapLayer(null);
state.pathfinding.useMover(state.hero.location.mover);
// 勇士同步初始化
const heroMoveTopImpl = new DefaultHeroMoveTopImpl(state);
state.hero.location.mover.useTopImplementation(heroMoveTopImpl);
// 加载
state.loader.addHook(new DefaultDataLoaderHook());
// 其他复杂初始化
initializeEnemy(state);
initializeReplay(state);
// 存档初始化
state.addSaveableContent('@system/hero', state.hero);
state.addSaveableContent('@system/flags', state.flags);
state.addSaveableContent('@system/maps', state.maps);
state.addSaveableContent('@system/enemy', state.enemyManager);
state.addSaveableContent('@system/replay', state.replaySystem);
}
async initialize(): Promise<void> {}
async afterLoad(state: ICoreState): Promise<void> {
// 进行勇士的加载后初始化
const coreConfig = state.loader.getConfig<ICoreStateCoreConfig>('core');
if (!coreConfig) {
logger.error(79);
return;
}
const dir = state.faceManager.get(this.heroFace);
if (!dir) {
logger.error(80);
return;
}
state.hero.createAttribute(coreConfig.init.hero.attribute);
const location: IFacedTileLocator = {
x: coreConfig.init.hero.x,
y: coreConfig.init.hero.y,
direction: FaceDirection.Down
};
state.hero.createLocation(location, dir);
}
}
+28
View File
@@ -0,0 +1,28 @@
import { logger } from '@motajs/common';
import {
ICoreStateCoreConfig,
IMotaDataLoader,
IMotaDataLoaderHooks
} from '@user/data-state';
export class DefaultDataLoaderHook implements IMotaDataLoaderHooks {
onCoreConfigLoaded(loader: IMotaDataLoader): Promise<void> {
const coreConfig = loader.getConfig<ICoreStateCoreConfig>('core');
if (!coreConfig) {
logger.error(69, 'Data');
return Promise.resolve();
}
const content = coreConfig.content;
loader.addExtraConfig('enemy', content.enemyDir);
loader.addExtraConfig('item', content.itemDir);
loader.addExtraConfig('map-list', content.mapListDir);
loader.addExtraConfig('tile', content.tileDir);
const defaults = content.defaults;
loader.addExtraConfig('enemy-default', defaults.enemyDefault);
loader.addExtraConfig('item-default', defaults.itemDefault);
loader.addExtraConfig('map-default', defaults.mapDefault);
loader.addExtraConfig('tile-default', defaults.tileDefault);
return Promise.resolve();
}
}
+1
View File
@@ -0,0 +1 @@
export * from './hook';
+1
View File
@@ -0,0 +1 @@
export * from './list';
+5
View File
@@ -0,0 +1,5 @@
import { IDataPluginDefine } from '@user/data-state';
import { ShopPlugin } from './shop';
/** 数据端插件列表 */
export const dataPlugins: IDataPluginDefine[] = [new ShopPlugin()];
+1
View File
@@ -0,0 +1 @@
export * from './shop';
+103
View File
@@ -0,0 +1,103 @@
import { SaveCompression } from '@user/data-common';
import { IShopManager, IShopManagerSave, IShopState } from './types';
import { logger } from '@motajs/common';
import { ShopState } from './state';
import { isNil } from 'lodash-es';
import { ICoreState } from '@user/data-state';
export class ShopManager implements IShopManager {
/** 当前管理器包含的商店列表 */
private readonly shops: Map<string, IShopState> = new Map();
constructor(readonly state: ICoreState) {}
/**
* 传入商店 id 或实例,输出商店实例
* @param shop 商店的 id 或其实例
*/
private getShopState(shop: string | IShopState): IShopState | null {
if (typeof shop === 'string') {
return this.shops.get(shop) ?? null;
} else {
return shop;
}
}
/**
* 传入商店 id 或实例,输出商店 id
* @param shop 商店的 id 或其实例
*/
private getShopId(shop: string | IShopState): string | undefined {
if (typeof shop === 'string') {
return shop;
} else {
return shop.id;
}
}
addShop(shop: IShopState): void {
if (this.shops.has(shop.id)) {
logger.warn(198, shop.id);
return;
}
this.shops.set(shop.id, shop);
}
removeShop(shop: string | IShopState): void {
const id = this.getShopId(shop);
if (!isNil(id)) this.shops.delete(id);
}
getShop(id: string): IShopState | null {
return this.shops.get(id) ?? null;
}
hasShop(shop: string | IShopState): boolean {
const id = this.getShopId(shop);
return isNil(id) || this.shops.has(id);
}
iterateShops(): Iterable<[string, IShopState]> {
return this.shops;
}
*iterateEnabledShops(): Iterable<[string, IShopState]> {
for (const [key, state] of this.shops) {
if (state.enabled) yield [key, state];
}
}
*iterateDisabledShops(): Iterable<[string, IShopState]> {
for (const [key, state] of this.shops) {
if (!state.enabled) yield [key, state];
}
}
enableShop(shop: string | IShopState): void {
const state = this.getShopState(shop);
if (state) state.enable();
}
disableShop(shop: string | IShopState): void {
const state = this.getShopState(shop);
if (state) state.disable();
}
saveState(compression: SaveCompression): IShopManagerSave {
const list = [...this.shops.values()].map(v =>
v.saveState(compression)
);
return {
list
};
}
loadState(state: IShopManagerSave, compression: SaveCompression): void {
this.shops.clear();
for (const save of state.list) {
const shop: IShopState = new ShopState(save.id, this.state);
shop.loadState(save, compression);
this.shops.set(save.id, shop);
}
}
}
+32
View File
@@ -0,0 +1,32 @@
import { ICoreState } from '@user/data-state';
import { IShopManager, IShopPlugin, IShopState, IShopSystem } from './types';
import { ShopManager } from './manager';
import { ShopState } from './state';
import { ShopSystem } from './system';
export class ShopPlugin implements IShopPlugin {
id: string = 'shop-plugin';
dependencies: string[] = [];
shop!: IShopSystem;
manager!: IShopManager;
/** 数据端主对象 */
private state!: ICoreState;
createShopState(id: string): IShopState {
return new ShopState(id, this.state);
}
awake(state: ICoreState): void {
this.state = state;
this.shop = new ShopSystem(state);
this.manager = new ShopManager(state);
state.addSaveableContent('@plugin/shop', this.manager);
}
async initialize(): Promise<void> {}
async afterLoad(): Promise<void> {}
}
+105
View File
@@ -0,0 +1,105 @@
import { IShopState, IShopStateSave } from './types';
import { isNil } from 'lodash-es';
import { logger } from '@motajs/common';
import { ICoreState } from '@user/data-state';
export class ShopState implements IShopState {
enabled: boolean = false;
/** 此商店拥有的所有道具及其库存 */
private readonly items: Map<number, number> = new Map();
/** 此商店中,每个道具的买入价格 */
private readonly buyPrice: Map<number, number> = new Map();
/** 此商店中,每个道具的售出价格 */
private readonly sellPrice: Map<number, number> = new Map();
constructor(
readonly id: string,
readonly state: ICoreState
) {}
addItem(token: number | string, count: number): void {
const num = this.state.tileStore.num(token);
if (isNil(num)) {
logger.warn(196, token.toString());
return;
}
// 注意,这里不把库存为 0 的置空是因为置空之后迭代不到,商店将不再显示这个道具
// 但是商店理应显示库存为 0 的道具,一是因为玩家可能需要卖出,二是因为玩家可能需要 0 库存这一信息
const before = this.items.getOrInsert(num, 0);
this.items.set(num, before + count);
}
getCount(token: number | string): number {
const num = this.state.tileStore.num(token);
if (isNil(num)) {
logger.warn(196, token.toString());
return -1;
}
return this.items.get(num) ?? -1;
}
iterateItems(): Iterable<[number, number]> {
return this.items;
}
setPrice(item: number | string, buy: number, sell: number): void {
const num = this.state.tileStore.num(item);
if (isNil(num)) {
logger.warn(196, item.toString());
return;
}
this.buyPrice.set(num, buy);
this.sellPrice.set(num, sell);
}
getPrice(item: number | string): [buy: number, sell: number] {
const num = this.state.tileStore.num(item);
if (isNil(num)) {
logger.warn(196, item.toString());
return [-1, -1];
}
const buy = this.buyPrice.get(num);
const sell = this.sellPrice.get(num);
if (isNil(buy) || isNil(sell)) return [-1, -1];
else return [buy, sell];
}
enable(): void {
this.enabled = true;
}
disable(): void {
this.enabled = false;
}
saveState(): IShopStateSave {
return {
id: this.id,
enabled: this.enabled,
items: this.items,
buyPrice: this.buyPrice,
sellPrice: this.sellPrice
};
}
loadState(state: IShopStateSave): void {
if (state.id !== this.id) {
logger.warn(197, this.id, state.id);
return;
}
this.enabled = state.enabled;
this.items.clear();
for (const [key, value] of state.items) {
this.items.set(key, value);
}
this.buyPrice.clear();
for (const [key, value] of state.buyPrice) {
this.buyPrice.set(key, value);
}
this.sellPrice.clear();
for (const [key, value] of state.sellPrice) {
this.sellPrice.set(key, value);
}
}
}
+106
View File
@@ -0,0 +1,106 @@
import {
IShopHeroAttr,
IShopManager,
IShopState,
IShopSystem,
ShopTradeResult
} from './types';
import { logger } from '@motajs/common';
import { isNil } from 'lodash-es';
import { shouldReplay } from '@user/data-common';
import { ICoreState } from '@user/data-state';
import { IHeroState } from '@user/data-base';
export class ShopSystem implements IShopSystem {
manager: IShopManager | null = null;
hero: IHeroState<IShopHeroAttr> | null = null;
private current: IShopState | null = null;
constructor(readonly state: ICoreState) {}
bindManager(manager: IShopManager | null): void {
this.closeShop();
this.manager = manager;
}
bindHero(hero: IHeroState<IShopHeroAttr> | null): void {
this.closeShop();
this.hero = hero;
}
@shouldReplay('Openning shop should be replayed.')
openShop(shop: IShopState): void {
this.current = shop;
}
getCurrentShop(): IShopState | null {
return this.current;
}
@shouldReplay('Closing shop should be replayed.')
closeShop(): void {
this.current = null;
}
@shouldReplay('Buying items should be replayed.')
buyItem(item: number | string, count: number): ShopTradeResult {
if (!this.current || !this.hero) {
logger.warn(199);
return ShopTradeResult.Invalid;
}
const num = this.state.tileStore.num(item);
if (isNil(num)) {
logger.warn(200, item.toString());
return ShopTradeResult.Invalid;
}
const shop = this.current;
const hero = this.hero.getModifiableAttribute();
const currCount = shop.getCount(count);
if (currCount < count) return ShopTradeResult.ShopLack;
const [buyPrice] = shop.getPrice(num);
if (buyPrice < 0) {
return ShopTradeResult.Invalid;
}
const need = buyPrice * count;
const owned = hero.getBaseAttribute('money');
if (owned < need) {
return ShopTradeResult.MoneyLack;
}
hero.add('money', -need);
shop.addItem(num, -count);
return ShopTradeResult.Success;
}
sellItem(item: number | string, count: number): ShopTradeResult {
if (!this.current || !this.hero) {
logger.warn(199);
return ShopTradeResult.Invalid;
}
const num = this.state.tileStore.num(item);
if (isNil(num)) {
logger.warn(200, item.toString());
return ShopTradeResult.Invalid;
}
const shop = this.current;
const hero = this.hero.getModifiableAttribute();
const pack = this.hero.items;
const currCount = pack.itemCount(num);
if (currCount < count) return ShopTradeResult.HeroLack;
const [, sellPrice] = shop.getPrice(num);
if (sellPrice < 0) {
return ShopTradeResult.Invalid;
}
const need = sellPrice * count;
hero.add('money', need);
shop.addItem(num, count);
return ShopTradeResult.Success;
}
}
+205
View File
@@ -0,0 +1,205 @@
import { IHeroState } from '@user/data-base';
import { ISaveableContent } from '@user/data-common';
import { ICoreStateExtended, IDataPluginDefine } from '@user/data-state';
export interface IShopPlugin extends IDataPluginDefine {
/** 商店插件所使用的商店系统对象 */
readonly shop: IShopSystem;
/** 商店插件所使用的商店管理器对象 */
readonly manager: IShopManager;
/**
* 创建商店状态对象
* @param id 商店状态对象的标识符
*/
createShopState(id: string): IShopState;
}
export interface IShopStateSave {
/** 商店 id */
readonly id: string;
/** 当前商店是否已启用 */
readonly enabled: boolean;
/** 商店拥有的库存 */
readonly items: ReadonlyMap<number, number>;
/** 商店道具的买入价 */
readonly buyPrice: ReadonlyMap<number, number>;
/** 商店道具的售出价 */
readonly sellPrice: ReadonlyMap<number, number>;
}
export interface IShopState
extends ICoreStateExtended, ISaveableContent<IShopStateSave> {
/** 当前商店是否已经启用 */
readonly enabled: boolean;
/** 当前商店的标识符 */
readonly id: string;
/**
* 向此商店添加或减少道具库存,会与原本的库存叠加
* @param token 道具数字或 id
* @param count 添加或减少的道具数量,正数表示添加,负数表示减少
*/
addItem(token: number | string, count: number): void;
/**
* 获取指定道具的库存数量,不存在道具则返回 -1
* @param token 道具数字或 id
*/
getCount(token: number | string): number;
/**
* 迭代商店拥有的所有道具,键表示道具的图块数字,值表示数量
*/
iterateItems(): Iterable<[number, number]>;
/**
* 设置道具价格
* @param item 道具数字或 id
* @param buy 道具买入价格
* @param sell 道具卖出价格
*/
setPrice(item: number | string, buy: number, sell: number): void;
/**
* 获取道具价格,如果道具不存在则返回 -1
* @param item 道具数字或 id
*/
getPrice(item: number | string): [buy: number, sell: number];
/**
* 启用该商店
*/
enable(): void;
/**
* 禁用该商店
*/
disable(): void;
}
export interface IShopManagerSave {
/** 商店状态列表 */
readonly list: IShopStateSave[];
}
export interface IShopManager
extends ICoreStateExtended, ISaveableContent<IShopManagerSave> {
/**
* 添加新的商店
* @param shop 商店状态对象
*/
addShop(shop: IShopState): void;
/**
* 移除已有商店
* @param shop 商店 id 或其状态对象
*/
removeShop(shop: string | IShopState): void;
/**
* 根据商店 id 获取其状态对象
* @param id 商店 id
*/
getShop(id: string): IShopState | null;
/**
* 判断指定商店是否属于当前管理器
* @param shop 商店 id 或其状态对象
*/
hasShop(shop: string | IShopState): boolean;
/**
* 迭代当前的所有商店,包括未启用的
*/
iterateShops(): Iterable<[string, IShopState]>;
/**
* 迭代当前所有已启用的商店
*/
iterateEnabledShops(): Iterable<[string, IShopState]>;
/**
* 迭代当前所有未启用的商店
*/
iterateDisabledShops(): Iterable<[string, IShopState]>;
/**
* 启用指定商店
* @param shop 商店 id 或其状态对象
*/
enableShop(shop: string | IShopState): void;
/**
* 禁用指定商店
* @param shop 商店 id 或其状态对象
*/
disableShop(shop: string | IShopState): void;
}
export interface IShopHeroAttr {
/** 勇士拥有的金币 */
money: number;
}
export const enum ShopTradeResult {
/** 成功购入或售出 */
Success,
/** 操作不合法,一般是因为道具不存在或未打开商店 */
Invalid,
/** 商店库存不足,无法购入这么多道具 */
ShopLack,
/** 玩家库存不足,无法卖出这么多道具 */
HeroLack,
/** 玩家货币不足,无法购入这么多道具 */
MoneyLack
}
export interface IShopSystem extends ICoreStateExtended {
/** 当前使用的商店管理器 */
readonly manager: IShopManager | null;
/** 当前绑定的勇士状态对象 */
readonly hero: IHeroState<IShopHeroAttr> | null;
/**
* 绑定商店管理器
* @param manager 商店管理器
*/
bindManager(manager: IShopManager | null): void;
/**
* 绑定系统使用的勇士状态对象
* @param hero 勇士状态对象
*/
bindHero(hero: IHeroState<IShopHeroAttr> | null): void;
/**
* 打开指定商店,仅数据端操作,不会打开商店 UI
* @param shop 商店状态对象
*/
openShop(shop: IShopState): void;
/**
* 获取当前已打开的商店状态对象
*/
getCurrentShop(): IShopState | null;
/**
* 关闭当前打开的商店
*/
closeShop(): void;
/**
* 在当前打开的商店里面购买指定道具
* @param item 道具数字或 id
* @param count 购买的道具数量
*/
buyItem(item: number | string, count: number): ShopTradeResult;
/**
* 在当前打开的商店里面售出指定道具
* @param item 道具数字或 id
* @param count 卖出的道具数量
*/
sellItem(item: number | string, count: number): ShopTradeResult;
}
+225
View File
@@ -0,0 +1,225 @@
import {
FaceDirection,
IReplayStepHandler,
IReplayCommand,
ReplayParamValue,
ReplayCommandResult,
ReplayCommandType
} from '@user/data-common';
import { IStateSystem } from '@user/data-system';
import { logger } from '@motajs/common';
//#region 指令基类
export abstract class BaseReplayCommand implements IReplayCommand {
/** 当前的状态对象 */
protected readonly state: IStateSystem;
/** 此录像步的字符串名称 */
protected abstract readonly name: string;
/** 预期的参数类型列表 */
protected abstract readonly paramTypes: readonly string[];
abstract readonly type: ReplayCommandType;
constructor(state: IStateSystem) {
this.state = state;
}
/**
* 判断指令参数是否符合预期
* @param command 指令的名称
* @param parameter 指令读取到的参数列表
* @param expect 指令的预期参数列表
*/
protected assertParameter(
command: string,
parameter: readonly ReplayParamValue[],
expect: readonly string[]
) {
if (parameter.length !== expect.length) {
const e = expect.length.toString();
const p = parameter.length.toString();
logger.error(2001, command, e, p);
return false;
}
return parameter.every((v, i) => {
const type = typeof v;
if (type === expect[i]) {
return true;
} else {
logger.error(2002, command, i.toString(), expect[i], type);
return false;
}
});
}
/**
* 执行录像步,已进行必要的参数校验,内部仅包含录像逻辑,不必包含参数校验
* @param step 当前录像步信息
*/
abstract wrappedExecute(
step: IReplayStepHandler
): Promise<ReplayCommandResult>;
execute(step: IReplayStepHandler): Promise<ReplayCommandResult> {
if (!this.assertParameter(this.name, step.params, this.paramTypes)) {
return Promise.resolve(ReplayCommandResult.Failed);
} else {
return this.wrappedExecute(step);
}
}
}
//#endregion
//#region 移动指令
export class ReplayMove extends BaseReplayCommand implements IReplayCommand {
readonly type: ReplayCommandType = ReplayCommandType.Active;
protected readonly name: string = 'move';
protected readonly paramTypes: readonly string[] = [];
constructor(
state: IStateSystem,
private readonly direction: FaceDirection
) {
super(state);
}
async wrappedExecute(): Promise<ReplayCommandResult> {
// Parameter: []
const mover = this.state.hero.location.mover;
if (mover.moving) {
logger.error(2003);
return ReplayCommandResult.Failed;
}
mover.step(this.direction);
return ReplayCommandResult.Success;
}
async finalize(): Promise<ReplayCommandResult> {
const mover = this.state.hero.location.mover;
const controller = mover.start();
if (!controller) {
logger.error(2004);
return ReplayCommandResult.Failed;
}
await controller.onEnd;
return ReplayCommandResult.Success;
}
}
//#endregion
//#region 瞬移指令
export class ReplayTeleport
extends BaseReplayCommand
implements IReplayCommand
{
readonly type: ReplayCommandType = ReplayCommandType.Active;
protected readonly name: string = 'teleport';
protected readonly paramTypes: readonly string[] = ['number', 'number'];
async wrappedExecute(
step: IReplayStepHandler
): Promise<ReplayCommandResult> {
// Parameter: [number x, number y]
const [x, y] = step.params as [number, number];
const result = this.state.pathfinding.teleportTo({ x, y });
if (!result) {
logger.error(2005, x.toString(), y.toString());
return ReplayCommandResult.Failed;
}
await result.controller.onEnd;
return ReplayCommandResult.Success;
}
}
//#endregion
//#region 使用物品指令
export class ReplayUseItem extends BaseReplayCommand implements IReplayCommand {
readonly type: ReplayCommandType = ReplayCommandType.Active;
protected readonly name: string = 'use-item';
protected readonly paramTypes: readonly string[] = ['number'];
async wrappedExecute(
step: IReplayStepHandler
): Promise<ReplayCommandResult> {
// Parameter: [number item]
const item = step.params[0] as number;
if (!this.state.hero.items.useItem(item)) {
logger.error(2006, item.toString());
return ReplayCommandResult.Failed;
}
return ReplayCommandResult.Success;
}
}
//#endregion
//#region 装备指令
export class ReplayEquip extends BaseReplayCommand implements IReplayCommand {
readonly type: ReplayCommandType = ReplayCommandType.Active;
protected readonly name: string = 'equip';
protected readonly paramTypes: readonly string[] = [
'number',
'number',
'boolean'
];
async wrappedExecute(
step: IReplayStepHandler
): Promise<ReplayCommandResult> {
// Parameter: [number uid, number slot, boolean autoUnload]
const [uid, slot, autoUnload] = step.params as [
number,
number,
boolean
];
const equipment = this.state.hero.equip;
equipment.equip(uid, slot, autoUnload);
if (equipment.getEquipped(slot) !== uid) {
logger.error(2007, uid.toString(), slot.toString());
return ReplayCommandResult.Failed;
}
return ReplayCommandResult.Success;
}
}
//#endregion
//#region 卸下装备指令
export class ReplayUnequip extends BaseReplayCommand implements IReplayCommand {
readonly type: ReplayCommandType = ReplayCommandType.Active;
protected readonly name: string = 'unequip';
protected readonly paramTypes: readonly string[] = ['number'];
async wrappedExecute(
step: IReplayStepHandler
): Promise<ReplayCommandResult> {
// Parameter: [number slot]
const slot = step.params[0] as number;
const equipment = this.state.hero.equip;
equipment.unequip(slot);
if (equipment.getEquipped(slot) !== void 0) {
logger.error(2008, slot.toString());
return ReplayCommandResult.Failed;
}
return ReplayCommandResult.Success;
}
}
//#endregion
+2
View File
@@ -0,0 +1,2 @@
export * from './commands';
export * from './initialize';
+35
View File
@@ -0,0 +1,35 @@
import { FaceDirection, ReplayCode } from '@user/data-common';
import { ICoreState } from '@user/data-state';
import {
ReplayEquip,
ReplayMove,
ReplayTeleport,
ReplayUnequip,
ReplayUseItem
} from './commands';
/**
* 初始化录像指令
* @param state 数据端主对象
*/
export function initializeReplay(state: ICoreState) {
const replay = state.replaySystem;
const up = new ReplayMove(state, FaceDirection.Up);
const right = new ReplayMove(state, FaceDirection.Right);
const down = new ReplayMove(state, FaceDirection.Down);
const left = new ReplayMove(state, FaceDirection.Left);
const teleport = new ReplayTeleport(state);
const useItem = new ReplayUseItem(state);
const equip = new ReplayEquip(state);
const unequip = new ReplayUnequip(state);
replay.registerCommand(ReplayCode.Up, up);
replay.registerCommand(ReplayCode.Right, right);
replay.registerCommand(ReplayCode.Down, down);
replay.registerCommand(ReplayCode.Left, left);
replay.registerCommand(ReplayCode.Teleport, teleport);
replay.registerCommand(ReplayCode.UseItem, useItem);
replay.registerCommand(ReplayCode.Equip, equip);
replay.registerCommand(ReplayCode.Unequip, unequip);
}