mirror of
https://github.com/motajs/template.git
synced 2026-11-02 15:04:30 +08:00
refactor: 插件系统 & 数据端顶层移植
This commit is contained in:
1 parent
b0229c627f
commit
2f3d3387a0
74 files changed
+1322
-3321
No files matched your search
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const enum MapDamageType {
|
||||
/** 未知伤害 */
|
||||
Unknown,
|
||||
/** 领域伤害 */
|
||||
Zone,
|
||||
/** 激光伤害 */
|
||||
Layer,
|
||||
/** 阻击伤害 */
|
||||
Repulse,
|
||||
/** 夹击伤害 */
|
||||
Between
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './event';
|
||||
export * from './hero';
|
||||
export * from './map';
|
||||
export * from './registrations';
|
||||
export * from './types';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
];
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './moverImpl';
|
||||
export * from './predicate';
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './hook';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './list';
|
||||
@@ -0,0 +1,5 @@
|
||||
import { IDataPluginDefine } from '@user/data-state';
|
||||
import { ShopPlugin } from './shop';
|
||||
|
||||
/** 数据端插件列表 */
|
||||
export const dataPlugins: IDataPluginDefine[] = [new ShopPlugin()];
|
||||
@@ -0,0 +1 @@
|
||||
export * from './shop';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './commands';
|
||||
export * from './initialize';
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user