mirror of
https://github.com/motajs/template.git
synced 2026-11-01 14:14:30 +08:00
init: 2.B
This commit is contained in:
commit
6bf32174d8
610 files changed
+151444
No files matched your search
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@user/data-state",
|
||||
"dependencies": {
|
||||
"@motajs/types": "workspace:*",
|
||||
"@motajs/common": "workspace:*",
|
||||
"@user/data-base": "workspace:*",
|
||||
"@user/data-utils": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,813 @@
|
||||
import { getHeroStatusOf, getHeroStatusOn } from '../state/hero';
|
||||
import { Range, ensureArray, has, manhattan } from '@user/data-utils';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { hook } from '@user/data-base';
|
||||
import {
|
||||
EnemyInfo,
|
||||
DamageInfo,
|
||||
DamageDelta,
|
||||
HaloData,
|
||||
CriticalDamageDelta,
|
||||
MapDamage,
|
||||
HaloFn,
|
||||
IEnemyCollection,
|
||||
IDamageEnemy,
|
||||
HaloType,
|
||||
IEnemyCollectionEvent
|
||||
} from '@motajs/types';
|
||||
|
||||
export class EnemyCollection
|
||||
extends EventEmitter<IEnemyCollectionEvent>
|
||||
implements IEnemyCollection
|
||||
{
|
||||
floorId: FloorIds;
|
||||
list: Map<number, DamageEnemy> = new Map();
|
||||
|
||||
range: Range = new Range();
|
||||
/** 地图伤害 */
|
||||
mapDamage: Record<string, MapDamage> = {};
|
||||
haloList: HaloData[] = [];
|
||||
|
||||
/** 楼层宽度 */
|
||||
width: number = 0;
|
||||
/** 楼层高度 */
|
||||
height: number = 0;
|
||||
|
||||
constructor(floorId: FloorIds) {
|
||||
super();
|
||||
this.floorId = floorId;
|
||||
this.extract();
|
||||
}
|
||||
|
||||
get(x: number, y: number) {
|
||||
const index = x + y * this.width;
|
||||
return this.list.get(index) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析本地图的怪物信息
|
||||
*/
|
||||
extract() {
|
||||
this.list.clear();
|
||||
core.extractBlocks(this.floorId);
|
||||
const floor = core.status.maps[this.floorId];
|
||||
this.width = floor.width;
|
||||
this.height = floor.height;
|
||||
floor.blocks.forEach(v => {
|
||||
if (v.disable) return;
|
||||
if (v.event.cls !== 'enemy48' && v.event.cls !== 'enemys') return;
|
||||
const { x, y } = v;
|
||||
const index = x + y * this.width;
|
||||
const enemy = core.material.enemys[v.event.id as EnemyIds];
|
||||
this.list.set(
|
||||
index,
|
||||
new DamageEnemy(enemy, v.x, v.y, this.floorId, this)
|
||||
);
|
||||
});
|
||||
this.emit('extract');
|
||||
hook.emit('enemyExtract', this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算怪物真实属性
|
||||
*/
|
||||
calRealAttribute() {
|
||||
this.haloList = [];
|
||||
this.list.forEach(v => {
|
||||
v.reset();
|
||||
});
|
||||
this.list.forEach(v => {
|
||||
v.preProvideHalo();
|
||||
});
|
||||
this.list.forEach(v => {
|
||||
v.calAttribute();
|
||||
v.provideHalo();
|
||||
});
|
||||
this.list.forEach(v => {
|
||||
v.getRealInfo();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算怪物伤害
|
||||
* @param noCache 是否不使用缓存
|
||||
*/
|
||||
calDamage(noCache: boolean = false) {
|
||||
if (noCache) this.calRealAttribute();
|
||||
this.list.forEach(v => {
|
||||
v.calDamage(void 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算地图伤害
|
||||
*/
|
||||
calMapDamage() {
|
||||
this.mapDamage = {};
|
||||
const hero = getHeroStatusOn(realStatus, this.floorId);
|
||||
this.list.forEach(v => {
|
||||
v.calMapDamage(this.mapDamage, hero);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 向怪物施加光环
|
||||
* @param type 光环的范围类型
|
||||
* @param data 光环范围信息
|
||||
* @param halo 光环效果函数
|
||||
* @param recursion 是否递归施加,只有在光环预平衡阶段会使用到
|
||||
*/
|
||||
applyHalo<K extends keyof HaloType>(
|
||||
type: K,
|
||||
data: HaloType[K],
|
||||
enemy: DamageEnemy,
|
||||
halo: HaloFn | HaloFn[],
|
||||
recursion: boolean = false
|
||||
) {
|
||||
const arr = ensureArray(halo);
|
||||
const enemys = this.range.type(type).scan(this.list.values(), data);
|
||||
if (!recursion) {
|
||||
arr.forEach(v => {
|
||||
enemys.forEach(e => {
|
||||
e.injectHalo(v, enemy.info);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
enemys.forEach(e => {
|
||||
arr.forEach(v => {
|
||||
e.injectHalo(v, enemy.info);
|
||||
e.preProvideHalo();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预平衡光环
|
||||
*/
|
||||
preBalanceHalo() {
|
||||
this.list.forEach(v => {
|
||||
v.preProvideHalo();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DamageEnemy implements IDamageEnemy {
|
||||
id: EnemyIds;
|
||||
x?: number;
|
||||
y?: number;
|
||||
floorId?: FloorIds;
|
||||
enemy: Enemy;
|
||||
col?: EnemyCollection;
|
||||
|
||||
/**
|
||||
* 怪物属性。
|
||||
* 属性计算流程:预平衡光环(即计算加光环的光环怪的光环) -> 计算怪物在没有光环下的属性
|
||||
* -> provide inject 光环 -> 计算怪物的光环加成 -> 计算完毕
|
||||
*/
|
||||
info!: EnemyInfo;
|
||||
|
||||
/** 向其他怪提供过的光环 */
|
||||
providedHalo: Set<number> = new Set();
|
||||
|
||||
/**
|
||||
* 伤害计算进度,0 -> 预平衡光环 -> 1 -> 计算没有光环的属性 -> 2 -> provide inject 光环
|
||||
* -> 3 -> 计算光环加成 -> 4 -> 计算完毕
|
||||
*/
|
||||
progress: number = 0;
|
||||
|
||||
constructor(
|
||||
enemy: Enemy,
|
||||
x?: number,
|
||||
y?: number,
|
||||
floorId?: FloorIds,
|
||||
col?: EnemyCollection
|
||||
) {
|
||||
this.id = enemy.id;
|
||||
this.enemy = enemy;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.floorId = floorId;
|
||||
this.col = col;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
const enemy = this.enemy;
|
||||
this.info = {
|
||||
hp: enemy.hp,
|
||||
atk: enemy.atk,
|
||||
def: enemy.def,
|
||||
special: new Set(enemy.special),
|
||||
atkBuff_: 0,
|
||||
defBuff_: 0,
|
||||
hpBuff_: 0,
|
||||
guard: [],
|
||||
enemy: this.enemy,
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
floorId: this.floorId
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(enemy)) {
|
||||
if (!(key in this.info) && has(value)) {
|
||||
// @ts-expect-error 无法推导
|
||||
this.info[key] = value;
|
||||
}
|
||||
}
|
||||
this.progress = 0;
|
||||
this.providedHalo.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算怪物在不计光环下的属性,在inject光环之前,预平衡光环之后执行
|
||||
*/
|
||||
calAttribute() {
|
||||
if (this.progress !== 1 && has(this.x) && has(this.floorId)) return;
|
||||
this.progress = 2;
|
||||
const special = this.info.special;
|
||||
const info = this.info;
|
||||
|
||||
const { atk = 0, def = 0 } = getHeroStatusOn(realStatus);
|
||||
|
||||
// 坚固
|
||||
if (special.has(3)) {
|
||||
info.def = Math.max(info.def, atk - 1);
|
||||
}
|
||||
|
||||
// 模仿
|
||||
if (special.has(10)) {
|
||||
info.atk = atk;
|
||||
info.def = def;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取怪物的真实属性信息,在inject光环后执行
|
||||
*/
|
||||
getRealInfo() {
|
||||
if (this.progress < 3 && has(this.x) && has(this.floorId)) {
|
||||
throw new Error(
|
||||
`Unexpected early real info calculating. Progress: ${this.progress}`
|
||||
);
|
||||
}
|
||||
if (this.progress === 4) return this.info;
|
||||
this.progress = 4;
|
||||
|
||||
// 此时已经inject光环,因此直接计算真实属性
|
||||
const info = this.info;
|
||||
|
||||
info.atk = Math.floor(info.atk * (info.atkBuff_ / 100 + 1));
|
||||
info.def = Math.floor(info.def * (info.defBuff_ / 100 + 1));
|
||||
info.hp = Math.floor(info.hp * (info.hpBuff_ / 100 + 1));
|
||||
|
||||
return this.info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 光环预提供,用于平衡所有怪的光环属性,避免出现不同情况下光环效果不一致的现象
|
||||
*/
|
||||
preProvideHalo() {
|
||||
if (this.progress !== 0) return;
|
||||
this.progress = 1;
|
||||
if (!this.floorId) return;
|
||||
if (!has(this.x) || !has(this.y)) return;
|
||||
|
||||
// 这里可以做优先级更高的光环,比如加光环的光环怪等,写法与 provideHalo 类似
|
||||
|
||||
// e 是被加成怪的属性,enemy 是施加光环的怪
|
||||
}
|
||||
|
||||
/**
|
||||
* 向其他怪提供光环
|
||||
*/
|
||||
provideHalo() {
|
||||
if (this.progress !== 2) return;
|
||||
this.progress = 3;
|
||||
if (!this.floorId) return;
|
||||
if (!has(this.x) || !has(this.y)) return;
|
||||
const col = this.col ?? core.status.maps[this.floorId].enemy;
|
||||
if (!col) return;
|
||||
const special = this.info.special;
|
||||
|
||||
// e 是被加成怪的属性,enemy 是施加光环的怪
|
||||
|
||||
// 普通光环
|
||||
if (special.has(25)) {
|
||||
// 光环效果,这里直接增加 e 的 buff 属性
|
||||
const halo = (e: EnemyInfo, enemy: EnemyInfo) => {
|
||||
if (enemy.haloAdd) {
|
||||
e.hpBuff_ += enemy.hpBuff ?? 0;
|
||||
e.atkBuff_ += enemy.atkBuff ?? 0;
|
||||
e.defBuff_ += enemy.defBuff ?? 0;
|
||||
} else {
|
||||
e.hpBuff_ = Math.max(e.hpBuff_, enemy.hpBuff ?? 0);
|
||||
e.atkBuff_ = Math.max(e.atkBuff_, enemy.atkBuff ?? 0);
|
||||
e.defBuff_ = Math.max(e.defBuff_, enemy.defBuff ?? 0);
|
||||
}
|
||||
};
|
||||
// 根据范围施加光环
|
||||
const range = this.info.haloRange ?? 1;
|
||||
if (this.info.haloSquare) {
|
||||
col.applyHalo(
|
||||
'square',
|
||||
{ x: this.x, y: this.y, d: range * 2 + 1 },
|
||||
this,
|
||||
halo
|
||||
);
|
||||
} else {
|
||||
col.applyHalo(
|
||||
'manhattan',
|
||||
{ x: this.x, y: this.y, d: range },
|
||||
this,
|
||||
halo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 支援也是一类光环
|
||||
if (special.has(26)) {
|
||||
col.applyHalo(
|
||||
'square',
|
||||
{ x: this.x, y: this.y, d: 3 },
|
||||
this,
|
||||
(e, enemy) => {
|
||||
e.guard.push(enemy);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接受其他怪的光环
|
||||
*/
|
||||
injectHalo(halo: HaloFn, enemy: EnemyInfo) {
|
||||
halo(this.info, enemy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算怪物伤害
|
||||
*/
|
||||
calDamage(hero: Partial<HeroStatus> = core.status.hero): DamageInfo {
|
||||
const enemy = this.getRealInfo();
|
||||
return this.calEnemyDamageOf(hero, enemy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算地图伤害
|
||||
* @param damage 存入的对象
|
||||
*/
|
||||
calMapDamage(
|
||||
damage: Record<string, MapDamage> = {},
|
||||
_hero: Partial<HeroStatus> = getHeroStatusOn(realStatus)
|
||||
) {
|
||||
if (!has(this.x) || !has(this.y) || !has(this.floorId)) return damage;
|
||||
const enemy = this.enemy;
|
||||
const floor = core.status.maps[this.floorId];
|
||||
const w = floor.width;
|
||||
const h = floor.height;
|
||||
const objs = core.getMapBlocksObj(this.floorId);
|
||||
|
||||
// 领域
|
||||
if (this.info.special.has(15)) {
|
||||
const range = enemy.range ?? 1;
|
||||
const startX = Math.max(0, this.x - range);
|
||||
const startY = Math.max(0, this.y - range);
|
||||
const endX = Math.min(floor.width - 1, this.x + range);
|
||||
const endY = Math.min(floor.height - 1, this.y + range);
|
||||
const dam = Math.max(enemy.zone ?? 0, 0);
|
||||
|
||||
for (let x = startX; x <= endX; x++) {
|
||||
for (let y = startY; y <= endY; y++) {
|
||||
if (
|
||||
!enemy.zoneSquare &&
|
||||
manhattan(x, y, this.x, this.y) > range
|
||||
) {
|
||||
// 如果是十字范围而且曼哈顿距离大于范围,则跳过此格
|
||||
continue;
|
||||
}
|
||||
const loc = `${x},${y}` as LocString;
|
||||
if (objs[loc]?.event.noPass) continue;
|
||||
this.setMapDamage(damage, loc, dam, '领域');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 激光
|
||||
if (this.info.special.has(24)) {
|
||||
const dirs: Dir[] = ['left', 'down', 'up', 'right'];
|
||||
const dam = Math.max(enemy.laser ?? 0, 0);
|
||||
|
||||
for (const dir of dirs) {
|
||||
let x = this.x;
|
||||
let y = this.y;
|
||||
const { x: dx, y: dy } = core.utils.scan[dir];
|
||||
while (x >= 0 && y >= 0 && x < w && y < h) {
|
||||
x += dx;
|
||||
y += dy;
|
||||
const loc = `${x},${y}` as LocString;
|
||||
if (objs[loc]?.event.noPass) continue;
|
||||
this.setMapDamage(damage, loc, dam, '激光');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 阻击
|
||||
if (this.info.special.has(18)) {
|
||||
const dirs: Dir[] = ['left', 'down', 'up', 'right'];
|
||||
for (const dir of dirs) {
|
||||
const { x: dx, y: dy } = core.utils.scan[dir];
|
||||
const x = this.x + dx;
|
||||
const y = this.y + dy;
|
||||
const loc = `${x},${y}` as LocString;
|
||||
if (objs[loc]?.event.noPass) continue;
|
||||
this.setMapDamage(damage, loc, this.info.repulse ?? 0, '阻击');
|
||||
damage[loc].repulse ??= [];
|
||||
damage[loc].repulse.push([this.x, this.y]);
|
||||
}
|
||||
}
|
||||
|
||||
// 捕捉
|
||||
if (this.info.special.has(27)) {
|
||||
const dirs: Dir[] = ['left', 'down', 'up', 'right'];
|
||||
for (const dir of dirs) {
|
||||
const { x: dx, y: dy } = core.utils.scan[dir];
|
||||
const x = this.x + dx;
|
||||
const y = this.y + dy;
|
||||
const loc = `${x},${y}` as LocString;
|
||||
if (objs[loc]?.event.noPass) continue;
|
||||
damage[loc] ??= { damage: 0, type: new Set() };
|
||||
damage[loc].ambush ??= [];
|
||||
damage[loc].ambush.push([this.x, this.y]);
|
||||
}
|
||||
}
|
||||
|
||||
// 夹击
|
||||
if (this.info.special.has(16)) {
|
||||
// 只计算右方和下方的怪物,这样就可以避免一个点被重复计算两次
|
||||
const dirs: Dir[] = ['down', 'right'];
|
||||
for (const dir of dirs) {
|
||||
const { x: dx, y: dy } = core.utils.scan[dir];
|
||||
const x = this.x + dx * 2;
|
||||
const y = this.y + dy * 2;
|
||||
const e = this.col?.get(x, y);
|
||||
if (!e) continue;
|
||||
const info = e.getRealInfo();
|
||||
if (!info.special.has(16)) continue;
|
||||
const cx = this.x + dx;
|
||||
const cy = this.y + dy;
|
||||
const loc = `${cx},${cy}` as LocString;
|
||||
if (objs[loc]?.event.noPass) continue;
|
||||
const half = getHeroStatusOn('hp') / 2;
|
||||
let bt = half;
|
||||
// 夹击不超伤害值
|
||||
if (core.flags.betweenAttackMax) {
|
||||
const aDamage = this.calDamage().damage;
|
||||
const bDamage = e.calDamage().damage;
|
||||
bt = Math.min(aDamage, bDamage, half);
|
||||
}
|
||||
this.setMapDamage(damage, loc, bt, '夹击');
|
||||
}
|
||||
}
|
||||
|
||||
return damage;
|
||||
}
|
||||
|
||||
private setMapDamage(
|
||||
damage: Record<string, MapDamage>,
|
||||
loc: string,
|
||||
dam: number,
|
||||
type: string
|
||||
) {
|
||||
damage[loc] ??= { damage: 0, type: new Set() };
|
||||
damage[loc].damage += dam;
|
||||
if (type) damage[loc].type.add(type);
|
||||
}
|
||||
|
||||
private calEnemyDamageOf(
|
||||
hero: Partial<HeroStatus>,
|
||||
enemy: EnemyInfo
|
||||
): DamageInfo {
|
||||
const status = getHeroStatusOf(hero, realStatus, this.floorId);
|
||||
const damage = calDamageWith(enemy, status) ?? Infinity;
|
||||
|
||||
return { damage };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算怪物临界,计算临界时,根据当前方向计算临界,但也会输出与当前最少伤害的伤害差值
|
||||
* @param num 要计算多少个临界
|
||||
* @param dir 从怪物位置指向勇士的方向
|
||||
* @param hero 勇士属性,最终结果将会与由此属性计算出的伤害相减计算减伤
|
||||
*/
|
||||
calCritical(
|
||||
num: number = 1,
|
||||
hero: Partial<HeroStatus> = core.status.hero
|
||||
): CriticalDamageDelta[] {
|
||||
const origin = this.calDamage(hero);
|
||||
const seckill = this.getSeckillAtk();
|
||||
return this.calCriticalWith(num, seckill, origin, hero);
|
||||
}
|
||||
|
||||
/**
|
||||
* 二分计算怪物临界
|
||||
* @param num 计算的临界数量
|
||||
* @param min 当前怪物伤害最小值
|
||||
* @param seckill 秒杀怪物时的攻击
|
||||
* @param hero 勇士真实属性
|
||||
*/
|
||||
private calCriticalWith(
|
||||
num: number,
|
||||
seckill: number,
|
||||
origin: DamageInfo,
|
||||
hero: Partial<HeroStatus>
|
||||
): CriticalDamageDelta[] {
|
||||
if (!isFinite(seckill)) return [];
|
||||
|
||||
const res: CriticalDamageDelta[] = [];
|
||||
const def = hero.def!;
|
||||
const precision =
|
||||
(seckill < Number.MAX_SAFE_INTEGER ? 1 : seckill / 1e15) * 2;
|
||||
const enemy = this.getRealInfo();
|
||||
|
||||
let curr = hero.atk!;
|
||||
let start = curr;
|
||||
let end = seckill;
|
||||
let ori = origin.damage;
|
||||
|
||||
const status = { atk: curr, def };
|
||||
|
||||
const calDam = () => {
|
||||
status.atk = curr;
|
||||
return this.calEnemyDamageOf(status, enemy).damage;
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
while (res.length < num) {
|
||||
if (end - start <= precision) {
|
||||
// 到达二分所需精度,计算临界准确值
|
||||
let cal = false;
|
||||
for (const v of [(start + end) / 2, end]) {
|
||||
curr = v;
|
||||
const dam = calDam();
|
||||
if (dam < ori) {
|
||||
res.push({
|
||||
damage: dam,
|
||||
atkDelta: Math.ceil(v - hero.atk!),
|
||||
delta: -(dam - origin.damage)
|
||||
});
|
||||
|
||||
start = v;
|
||||
end = seckill;
|
||||
cal = true;
|
||||
ori = dam;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!cal) break;
|
||||
}
|
||||
curr = Math.floor((start + end) / 2);
|
||||
|
||||
const damage = calDam();
|
||||
|
||||
if (damage < ori) {
|
||||
end = curr;
|
||||
} else {
|
||||
start = curr;
|
||||
}
|
||||
if (i++ >= 10000) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Unexpected endless loop in calculating critical.` +
|
||||
`Enemy Id: ${this.id}. Loc: ${this.x},${this.y}. Floor: ${this.floorId}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (res.length === 0) {
|
||||
curr = hero.atk!;
|
||||
const dam = calDam();
|
||||
res.push({
|
||||
damage: dam,
|
||||
atkDelta: 0,
|
||||
delta: 0
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算n防减伤
|
||||
* @param num 要加多少防御
|
||||
* @param dir 从怪物位置指向勇士的方向
|
||||
* @param hero 勇士属性,最终结果将会与由此属性计算出的伤害相减计算减伤
|
||||
*/
|
||||
calDefDamage(
|
||||
num: number = 1,
|
||||
hero: Partial<HeroStatus> = core.status.hero
|
||||
): DamageDelta {
|
||||
const damage = this.calDamage({
|
||||
def: (hero.def ?? core.status.hero.def) + num
|
||||
});
|
||||
const origin = this.calDamage(hero);
|
||||
const finite = isFinite(damage.damage);
|
||||
|
||||
return {
|
||||
damage: damage.damage,
|
||||
info: damage,
|
||||
delta: -(finite ? damage.damage - origin.damage : Infinity)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取怪物秒杀时所需的攻击
|
||||
*/
|
||||
getSeckillAtk(): number {
|
||||
const info = this.getRealInfo();
|
||||
|
||||
// 坚固,不可能通过攻击秒杀
|
||||
if (info.special.has(3)) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
// 常规怪物秒杀攻击是怪物防御+怪物生命
|
||||
return info.def + info.hp;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DamageWithTurn {
|
||||
damage: number;
|
||||
turn: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算伤害时会用到的勇士属性,攻击防御,其余的不会有buff加成,直接从core.status.hero取
|
||||
* 如果有属性不会被 buff 加成请在这里去除,有助于提高性能表现
|
||||
*/
|
||||
const realStatus: (keyof HeroStatus)[] = ['atk', 'def', 'mdef', 'hpmax'];
|
||||
|
||||
/** 当前是否正在计算支援怪的伤害 */
|
||||
let inGuard = false;
|
||||
|
||||
/**
|
||||
* 计算伤害,返回值包含伤害与回合数
|
||||
* @param info 怪物信息
|
||||
* @param hero 勇士真实属性
|
||||
*/
|
||||
export function calDamageWithTurn(
|
||||
info: EnemyInfo,
|
||||
hero: Partial<HeroStatus>
|
||||
): DamageWithTurn {
|
||||
const { hp } = core.status.hero;
|
||||
const { atk, def, mdef } = hero as HeroStatus;
|
||||
const { atk: monAtk, def: monDef, special } = info;
|
||||
let { hp: monHp } = info;
|
||||
|
||||
// 无敌
|
||||
if (special.has(20) && core.itemCount('cross') < 1) {
|
||||
return { damage: Infinity, turn: 0 };
|
||||
}
|
||||
|
||||
/** 怪物会对勇士造成的总伤害 */
|
||||
let damage = 0;
|
||||
|
||||
/** 勇士每轮造成的伤害 */
|
||||
let heroPerDamage: number = 0;
|
||||
/** 怪物每轮造成的伤害 */
|
||||
let enemyPerDamage: number = 0;
|
||||
|
||||
// 勇士每轮伤害为勇士攻击减去怪物防御
|
||||
heroPerDamage += atk - monDef;
|
||||
|
||||
// 吸血
|
||||
if (special.has(11)) {
|
||||
const vampire = info.vampire ?? 0;
|
||||
const value = (vampire / 100) * hp;
|
||||
damage += value;
|
||||
// 如果吸血加到自身
|
||||
if (info.add) {
|
||||
monHp += value;
|
||||
}
|
||||
}
|
||||
|
||||
// 魔攻
|
||||
if (special.has(2)) {
|
||||
enemyPerDamage = monAtk;
|
||||
} else {
|
||||
enemyPerDamage = monAtk - def;
|
||||
}
|
||||
|
||||
// 连击
|
||||
if (special.has(4)) enemyPerDamage *= 2;
|
||||
if (special.has(5)) enemyPerDamage *= 3;
|
||||
if (special.has(6)) enemyPerDamage *= info.n!;
|
||||
|
||||
if (enemyPerDamage < 0) enemyPerDamage = 0;
|
||||
|
||||
let turn = Math.ceil(monHp / heroPerDamage);
|
||||
|
||||
// 支援,当怪物被支援且不包含支援标记时执行,因为支援怪不能再被支援了
|
||||
if (info.guard.length > 0 && !inGuard) {
|
||||
inGuard = true;
|
||||
// 支援中魔防只会被计算一次,因此除了当前怪物,计算其他怪物伤害时魔防为 0
|
||||
const status = { ...hero, mdef: 0 };
|
||||
// 计算支援怪的伤害,同时把打支援怪花费的回合数加到当前怪物上,因为打支援怪的时候当前怪物也会打你
|
||||
// 因此回合数需要加上打支援怪的回合数
|
||||
for (const enemy of info.guard) {
|
||||
// 直接把 enemy 传过去,因此支援的 enemy 会吃到其原本所在位置的光环加成
|
||||
const extraInfo = calDamageWithTurn(enemy, status);
|
||||
turn += extraInfo.turn;
|
||||
damage += extraInfo.damage;
|
||||
}
|
||||
inGuard = false;
|
||||
}
|
||||
|
||||
// 先攻
|
||||
if (special.has(1)) {
|
||||
damage += enemyPerDamage;
|
||||
}
|
||||
|
||||
// 破甲
|
||||
if (special.has(7)) {
|
||||
const value = info.breakArmor ?? core.values.breakArmor;
|
||||
damage += (value / 100) * def;
|
||||
}
|
||||
|
||||
// 反击
|
||||
if (special.has(8)) {
|
||||
const value = info.counterAttack ?? core.values.counterAttack;
|
||||
// 反击是每回合生效,因此加到 enemyPerDamage 上
|
||||
enemyPerDamage += (value / 100) * atk;
|
||||
}
|
||||
|
||||
// 净化
|
||||
if (special.has(9)) {
|
||||
const value = info.purify ?? core.values.purify;
|
||||
damage += mdef * value;
|
||||
}
|
||||
|
||||
damage += (turn - 1) * enemyPerDamage;
|
||||
|
||||
// 魔防
|
||||
damage -= mdef;
|
||||
|
||||
// 未开启负伤时,如果伤害为负,则设为 0
|
||||
if (!core.flags.enableNegativeDamage && damage < 0) {
|
||||
damage = 0;
|
||||
}
|
||||
|
||||
// 固伤,无法被魔防减伤
|
||||
if (special.has(22)) {
|
||||
damage += info.damage ?? 0;
|
||||
}
|
||||
|
||||
// 仇恨,无法被魔防减伤
|
||||
if (special.has(17)) {
|
||||
damage += core.getFlag('hatred', 0);
|
||||
}
|
||||
|
||||
return { damage: Math.floor(damage), turn };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算怪物伤害
|
||||
* @param info 怪物信息
|
||||
* @param hero 勇士信息
|
||||
*/
|
||||
export function calDamageWith(
|
||||
info: EnemyInfo,
|
||||
hero: Partial<HeroStatus>
|
||||
): number {
|
||||
return calDamageWithTurn(info, hero).damage;
|
||||
}
|
||||
|
||||
export function ensureFloorDamage(floorId: FloorIds) {
|
||||
const floor = core.status.maps[floorId];
|
||||
floor.enemy ??= new EnemyCollection(floorId);
|
||||
}
|
||||
|
||||
export function getSingleEnemy(id: EnemyIds) {
|
||||
const e = core.material.enemys[id];
|
||||
const enemy = new DamageEnemy(e);
|
||||
enemy.calAttribute();
|
||||
enemy.getRealInfo();
|
||||
enemy.calDamage(core.status.hero);
|
||||
return enemy;
|
||||
}
|
||||
|
||||
export function getEnemy(
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds = core.status.floorId
|
||||
) {
|
||||
const enemy = core.status.maps[floorId].enemy.get(x, y);
|
||||
return enemy;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Floor {
|
||||
enemy: EnemyCollection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './damage';
|
||||
export * from './special';
|
||||
@@ -0,0 +1,243 @@
|
||||
import { EnemyInfo } from '@motajs/types';
|
||||
import { getHeroStatusOn } from '../state/hero';
|
||||
|
||||
export interface SpecialDeclaration {
|
||||
code: number;
|
||||
name: string | ((enemy: EnemyInfo) => string);
|
||||
desc: string | ((enemy: EnemyInfo) => string);
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 怪物特殊属性列表,当前版本中 code 最好与索引保持一致,不然可能会出现问题
|
||||
* 属性实现位置一览('./'表示当前文件夹 '../'表示上一级文件夹):
|
||||
* 1. 调参类属性 / 仅影响战斗过程的属性:./damage.ts calDamageWithTurn 函数
|
||||
* 2. 地图伤害:./damage.ts DamageEnemy.calMapDamage 方法,搜索 calMapDamage 即可搜到
|
||||
* 3. 光环属性:./damage.ts DamageEnemy.provideHalo 方法,搜索 provideHalo 即可搜到
|
||||
* 4. 仇恨 / 退化 等战后效果:packages-user/data-fallback/src/battle.ts 中的 afterBattle
|
||||
* 5. 中毒的每步效果:../state/move.ts HeroMover.onStepEnd 方法,在约 590 行
|
||||
* 6. 中毒的瞬移效果:还在脚本编辑的 moveDirectly
|
||||
* 7. 衰弱效果:../state/hero.ts getHeroStatusOf 方法
|
||||
* 8. 重生属性:还在脚本编辑的 changingFloor
|
||||
* 9. 阻击 / 捕捉 的每步效果:packages-user/legacy-plugin-data/src/enemy/checkblock.ts
|
||||
*/
|
||||
export const specials: SpecialDeclaration[] = [
|
||||
{
|
||||
code: 0,
|
||||
name: '空',
|
||||
desc: '空',
|
||||
color: '#fff'
|
||||
},
|
||||
{
|
||||
code: 1,
|
||||
name: '先攻',
|
||||
desc: `怪物首先攻击。`,
|
||||
color: '#fc3'
|
||||
},
|
||||
{
|
||||
code: 2,
|
||||
name: '魔攻',
|
||||
desc: '怪物攻击无视勇士的防御。',
|
||||
color: '#bbb0ff'
|
||||
},
|
||||
{
|
||||
code: 3,
|
||||
name: '坚固',
|
||||
desc: '怪物防御不小于勇士攻击-1。',
|
||||
color: '#c0b088'
|
||||
},
|
||||
{
|
||||
code: 4,
|
||||
name: '2连击',
|
||||
desc: '怪物每回合攻击2次。',
|
||||
color: '#fe7'
|
||||
},
|
||||
{
|
||||
code: 5,
|
||||
name: '3连击',
|
||||
desc: '怪物每回合攻击3次。',
|
||||
color: '#fe7'
|
||||
},
|
||||
{
|
||||
code: 6,
|
||||
name: enemy => `${enemy.n ?? 4}连击`,
|
||||
desc: enemy => `怪物每回合攻击${enemy.n ?? 4}次。`,
|
||||
color: '#fe7'
|
||||
},
|
||||
{
|
||||
code: 7,
|
||||
name: '破甲',
|
||||
desc: enemy =>
|
||||
`战斗前,附加角色防御的${enemy.breakArmor ?? core.values.breakArmor}%作为伤害。`,
|
||||
color: '#fe7'
|
||||
},
|
||||
{
|
||||
code: 8,
|
||||
name: '反击',
|
||||
desc: enemy =>
|
||||
`战斗时,怪物每回合附加角色攻击的${enemy.counterAttack ?? core.values.counterAttack}%作为伤害,无视角色防御。`,
|
||||
color: '#fa4'
|
||||
},
|
||||
{
|
||||
code: 9,
|
||||
name: '净化',
|
||||
desc: enemy =>
|
||||
`战斗前,怪物附加角色护盾的${enemy.purify ?? core.values.purify}倍作为伤害。`,
|
||||
color: '#80eed6'
|
||||
},
|
||||
{
|
||||
code: 10,
|
||||
name: '模仿',
|
||||
desc: `怪物的攻防与勇士相同。`,
|
||||
color: '#b0c0dd'
|
||||
},
|
||||
{
|
||||
code: 11,
|
||||
name: '吸血',
|
||||
desc: enemy => {
|
||||
const vampire = enemy.vampire ?? 0;
|
||||
return (
|
||||
`战斗前,怪物首先吸取角色的${vampire}%生命` +
|
||||
`(约${Math.floor((vampire / 100) * getHeroStatusOn('hp'))}点)作为伤害` +
|
||||
(enemy.add ? `,并把伤害数值加到自身生命上。` : `。`)
|
||||
);
|
||||
},
|
||||
color: '#ff00d2'
|
||||
},
|
||||
{
|
||||
code: 12,
|
||||
name: '中毒',
|
||||
desc: () =>
|
||||
`战斗后,角色陷入中毒状态,每一步损失生命${core.values.poisonDamage}点。`,
|
||||
color: '#9e8'
|
||||
},
|
||||
{
|
||||
code: 13,
|
||||
name: '衰弱',
|
||||
desc: () => {
|
||||
const weak = core.values.weakValue;
|
||||
if (weak < 1) {
|
||||
return `战斗后,角色陷入衰弱状态,攻防暂时下降${Math.floor(weak * 100)}%`;
|
||||
} else {
|
||||
return `战斗后,角色陷入衰弱状态,攻防暂时下降${weak}点`;
|
||||
}
|
||||
},
|
||||
color: '#f0bbcc'
|
||||
},
|
||||
{
|
||||
code: 14,
|
||||
name: '诅咒',
|
||||
desc: '战斗后,角色陷入诅咒状态,战斗无法获得金币和经验。',
|
||||
color: '#bbeef0'
|
||||
},
|
||||
{
|
||||
code: 15,
|
||||
name: '领域',
|
||||
desc: enemy =>
|
||||
`经过怪物周围${enemy.zoneSquare ? '九宫格' : '十字'}范围内${enemy.range ?? 1}格时自动减生命${enemy.zone ?? 0}点。`,
|
||||
color: '#c677dd'
|
||||
},
|
||||
{
|
||||
code: 16,
|
||||
name: '夹击',
|
||||
desc: '经过两只相同的怪物中间,角色生命值变成一半。',
|
||||
color: '#b9e'
|
||||
},
|
||||
{
|
||||
code: 17,
|
||||
name: '仇恨',
|
||||
desc: () =>
|
||||
`战斗前,怪物附加之前积累的仇恨值作为伤害;战斗后,释放一半的仇恨值。(每杀死一个怪物获得${core.values.hatred}点仇恨值)。`,
|
||||
color: '#b0b666'
|
||||
},
|
||||
{
|
||||
code: 18,
|
||||
name: '阻击',
|
||||
desc: enemy =>
|
||||
`经过怪物十字范围内时怪物后退一格,同时对勇士造成${enemy.repulse ?? 0}点伤害。`,
|
||||
color: '#8888e6'
|
||||
},
|
||||
{
|
||||
code: 19,
|
||||
name: '自爆',
|
||||
desc: '战斗后角色的生命值变成1。',
|
||||
color: '#ff6666'
|
||||
},
|
||||
{
|
||||
code: 20,
|
||||
name: '无敌',
|
||||
desc: `角色无法打败怪物,除非拥有十字架。`,
|
||||
color: '#aaa'
|
||||
},
|
||||
{
|
||||
code: 21,
|
||||
name: '退化',
|
||||
desc: enemy =>
|
||||
`战斗后角色永久下降${enemy.atkValue ?? 0}点攻击和${enemy.defValue ?? 0}点防御。`,
|
||||
color: 'cyan'
|
||||
},
|
||||
{
|
||||
code: 22,
|
||||
name: '固伤',
|
||||
desc: enemy =>
|
||||
`战斗前,怪物对角色造成${enemy.damage ?? 0}点固定伤害,未开启负伤时无视角色护盾。`,
|
||||
color: '#f97'
|
||||
},
|
||||
{
|
||||
code: 23,
|
||||
name: '重生',
|
||||
desc: `怪物被击败后,角色转换楼层则怪物将再次出现。`,
|
||||
color: '#dda0dd'
|
||||
},
|
||||
{
|
||||
code: 24,
|
||||
name: '激光',
|
||||
desc: enemy => `经过怪物同行或同列时自动减生命${enemy.laser ?? 0}点。`,
|
||||
color: '#dda0dd'
|
||||
},
|
||||
{
|
||||
code: 25,
|
||||
name: '光环',
|
||||
desc: enemy => {
|
||||
let str = '';
|
||||
if (enemy.haloRange) {
|
||||
if (enemy.haloSquare) {
|
||||
str += '对于该怪物九宫格';
|
||||
} else {
|
||||
str += '对于该怪物十字';
|
||||
}
|
||||
str += `${enemy.haloRange ?? 1}格范围内所有怪物`;
|
||||
} else {
|
||||
str += `同楼层所有怪物`;
|
||||
}
|
||||
if (enemy.hpBuff) {
|
||||
str += `,生命提升${enemy.hpBuff}%`;
|
||||
}
|
||||
if (enemy.atkBuff) {
|
||||
str += `,攻击提升${enemy.atkBuff}%`;
|
||||
}
|
||||
if (enemy.defBuff) {
|
||||
str += `,防御提升${enemy.defBuff}%`;
|
||||
}
|
||||
if (enemy.haloAdd) {
|
||||
str += `,线性叠加。`;
|
||||
} else {
|
||||
str += `,不可叠加。`;
|
||||
}
|
||||
return str;
|
||||
},
|
||||
color: '#e6e099'
|
||||
},
|
||||
{
|
||||
code: 26,
|
||||
name: '支援',
|
||||
desc: `当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。`,
|
||||
color: '#77c0b6'
|
||||
},
|
||||
{
|
||||
code: 27,
|
||||
name: '捕捉',
|
||||
desc: `当走到怪物十字范围内时会进行强制战斗。`,
|
||||
color: '#ff6f0a'
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './enemy';
|
||||
export * from './state';
|
||||
@@ -0,0 +1,332 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* 获取勇士在某一点的属性
|
||||
* @param name 要获取的勇士属性
|
||||
* @param floorId 勇士所在楼层
|
||||
*/
|
||||
export function getHeroStatusOn(name: 'all', floorId?: FloorIds): HeroStatus;
|
||||
export function getHeroStatusOn(
|
||||
name: (keyof HeroStatus)[],
|
||||
floorId?: FloorIds
|
||||
): Partial<HeroStatus>;
|
||||
export function getHeroStatusOn<K extends keyof HeroStatus>(
|
||||
name: K,
|
||||
floorId?: FloorIds
|
||||
): HeroStatus[K];
|
||||
export function getHeroStatusOn(
|
||||
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
|
||||
floorId?: FloorIds
|
||||
) {
|
||||
// @ts-expect-error 暂时无法推导
|
||||
return getHeroStatusOf(core.status.hero, name, floorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一定状态下的勇士在某一点的属性
|
||||
* @param status 勇士的状态
|
||||
* @param name 要获取的勇士属性
|
||||
* @param floorId 勇士所在楼层
|
||||
*/
|
||||
export function getHeroStatusOf(
|
||||
status: Partial<HeroStatus>,
|
||||
name: 'all',
|
||||
floorId?: FloorIds
|
||||
): HeroStatus;
|
||||
export function getHeroStatusOf(
|
||||
status: Partial<HeroStatus>,
|
||||
name: (keyof HeroStatus)[],
|
||||
floorId?: FloorIds
|
||||
): Partial<HeroStatus>;
|
||||
export function getHeroStatusOf<K extends keyof HeroStatus>(
|
||||
status: Partial<HeroStatus>,
|
||||
name: K,
|
||||
floorId?: FloorIds
|
||||
): HeroStatus[K];
|
||||
export function getHeroStatusOf(
|
||||
status: DeepPartial<HeroStatus>,
|
||||
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
|
||||
floorId?: FloorIds
|
||||
) {
|
||||
return getRealStatus(status, name, floorId);
|
||||
}
|
||||
|
||||
function getRealStatus(
|
||||
status: DeepPartial<HeroStatus>,
|
||||
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
|
||||
floorId: FloorIds = core.status.floorId
|
||||
): any {
|
||||
if (name instanceof Array) {
|
||||
const res: any = {};
|
||||
name.forEach(v => {
|
||||
res[v] = getRealStatus(status, v, floorId);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
if (name === 'all') {
|
||||
const res: any = {};
|
||||
for (const [key, value] of Object.entries(core.status.hero)) {
|
||||
if (typeof value === 'number') {
|
||||
res[key] = getRealStatus(
|
||||
status,
|
||||
key as keyof HeroStatus,
|
||||
floorId
|
||||
);
|
||||
} else {
|
||||
res[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
let s = (status[name] ?? core.status.hero[name]) as number;
|
||||
if (s === null || s === void 0) {
|
||||
throw new ReferenceError(
|
||||
`Wrong hero status property name is delivered: ${name}`
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof s !== 'number') return s;
|
||||
|
||||
// buff
|
||||
s *= core.status.hero.buff[name] ?? 1;
|
||||
s = Math.floor(s);
|
||||
|
||||
// 衰弱效果
|
||||
if ((name === 'atk' || name === 'def') && flags.weak) {
|
||||
const weak = core.values.weakValue;
|
||||
if (weak < 1) {
|
||||
// 百分比衰弱
|
||||
s *= 1 - weak;
|
||||
} else {
|
||||
s -= weak;
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// 下面的内容暂时无用
|
||||
|
||||
export interface IHeroStatusDefault {
|
||||
atk: number;
|
||||
def: number;
|
||||
hp: number;
|
||||
}
|
||||
|
||||
interface HeroStateEvent {
|
||||
set: [key: string | number | symbol, value: any];
|
||||
}
|
||||
|
||||
type HeroStatusCalculate = (
|
||||
hero: HeroState<any>,
|
||||
key: string | number | symbol,
|
||||
value: any
|
||||
) => any;
|
||||
|
||||
export class HeroState<
|
||||
T extends object = IHeroStatusDefault
|
||||
> extends EventEmitter<HeroStateEvent> {
|
||||
readonly status: T;
|
||||
readonly computedStatus: T;
|
||||
|
||||
readonly buffable: Set<keyof T> = new Set();
|
||||
readonly buffMap: Map<keyof T, number> = new Map();
|
||||
|
||||
private static cal: HeroStatusCalculate = (_0, _1, value) => value;
|
||||
|
||||
constructor(init: T) {
|
||||
super();
|
||||
this.status = init;
|
||||
this.computedStatus = cloneDeep(init);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个属性的值
|
||||
* @param key 要设置的属性
|
||||
* @param value 属性值
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
setStatus<K extends keyof T>(key: K, value: T[K]): boolean {
|
||||
this.status[key] = value;
|
||||
this.emit('set', key, value);
|
||||
return this.refreshStatus(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加或减少一个属性的值,只对数字型的属性有效
|
||||
* @param key 要修改的属性
|
||||
* @param value 属性的增量
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
addStatus<K extends SelectKey<T, number>>(key: K, value: number): boolean {
|
||||
if (typeof this.status[key] !== 'number') {
|
||||
logger.warn(14, String(key));
|
||||
return false;
|
||||
}
|
||||
return this.setStatus<K>(key, (this.status[key] + value) as T[K]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个属性的原始值
|
||||
* @param key 要获取的属性
|
||||
* @returns 属性的值
|
||||
*/
|
||||
getStatus<K extends keyof T>(key: K): T[K] {
|
||||
return this.status[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个属性计算后的值,也就是2.x所说的勇士真实属性
|
||||
* @param key 要获取的属性值
|
||||
*/
|
||||
getComputedStatus<K extends keyof T>(key: K): T[K] {
|
||||
return this.computedStatus[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记某个属性为可以被buff加成
|
||||
*/
|
||||
markBuffable(key: SelectKey<T, number>): void {
|
||||
if (typeof this.status[key] !== 'number') {
|
||||
logger.warn(12, String(key));
|
||||
return;
|
||||
}
|
||||
this.buffable.add(key);
|
||||
this.buffMap.set(key, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个属性的buff值
|
||||
* @param key 要设置buff的属性
|
||||
* @param value buff值
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
setBuff(key: SelectKey<T, number>, value: number): boolean {
|
||||
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
|
||||
logger.warn(13, String(key));
|
||||
return false;
|
||||
}
|
||||
this.buffMap.set(key, value);
|
||||
return this.refreshStatus(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加或减少一个buff值
|
||||
* @param key 要修改的buff属性
|
||||
* @param value buff增量
|
||||
* @returns 是否修改成功
|
||||
*/
|
||||
addBuff(key: SelectKey<T, number>, value: number): boolean {
|
||||
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
|
||||
logger.warn(13, String(key));
|
||||
return false;
|
||||
}
|
||||
return this.setBuff(key, this.buffMap.get(key)! + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新某个或所有属性,重新进行计算
|
||||
* @param key 要刷新的属性名,不填表示刷新所有属性
|
||||
* @returns 是否计算成功
|
||||
*/
|
||||
refreshStatus(key?: keyof T): boolean {
|
||||
if (key === void 0) {
|
||||
for (const [key, value] of Object.entries(this.status)) {
|
||||
// @ts-expect-error 暂时无法推导
|
||||
this.computedStatus[key] = HeroState.cal(this, key, value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
this.computedStatus[key] = HeroState.cal(this, key, this.status[key]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算所有可以buff加成的属性
|
||||
* @returns 是否计算成功
|
||||
*/
|
||||
refreshBuffable(): boolean {
|
||||
for (const key of this.buffable) {
|
||||
this.computedStatus[key] = HeroState.cal(
|
||||
this,
|
||||
key,
|
||||
this.status[key]
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复写属性计算函数,默认函数不进行计算,直接将原属性返回
|
||||
* @param fn 计算函数,传入两个参数,key表示属性名,value表示属性值,返回值表示计算结果
|
||||
*/
|
||||
static overrideCalculate(fn: HeroStatusCalculate) {
|
||||
this.cal = fn;
|
||||
}
|
||||
}
|
||||
|
||||
interface _IHeroItem {
|
||||
items: Map<AllIdsOf<'items'>, number>;
|
||||
|
||||
/**
|
||||
* 设置勇士拥有的物品数量
|
||||
* @param item 物品id
|
||||
* @param value 物品数量
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
setItem(item: AllIdsOf<'items'>, value: number): boolean;
|
||||
|
||||
/**
|
||||
* 增加或减少勇士拥有的物品数量
|
||||
* @param item 物品id
|
||||
* @param value 物品数量增量
|
||||
* @returns 是否设置成功
|
||||
*/
|
||||
addItem(item: AllIdsOf<'items'>, value: number): boolean;
|
||||
|
||||
/**
|
||||
* 使用一个物品
|
||||
* @param item 物品id
|
||||
* @returns 是否使用成功
|
||||
*/
|
||||
useItem(item: AllIdsOf<'items'>, x?: number, y?: number): boolean;
|
||||
|
||||
/**
|
||||
* 获得一个物品
|
||||
* @param item 物品id
|
||||
* @param num 获得的数量
|
||||
*/
|
||||
getItem(item: AllIdsOf<'items'>, num: number): void;
|
||||
/**
|
||||
* 获得一个物品
|
||||
* @param item 物品id
|
||||
* @param x 物品所在x坐标
|
||||
* @param y 物品所在y坐标
|
||||
* @param floorId 物品所在楼层
|
||||
* @param num 获得的数量
|
||||
*/
|
||||
getItem(
|
||||
item: AllIdsOf<'items'>,
|
||||
x: number,
|
||||
y: number,
|
||||
floorId?: FloorIds,
|
||||
num?: number
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 获取某个物品的数量
|
||||
* @param item 物品id
|
||||
*/
|
||||
itemCount(item: AllIdsOf<'items'>): number;
|
||||
|
||||
/**
|
||||
* 判断勇士是否拥有这个物品
|
||||
* @param item 物品id
|
||||
*/
|
||||
hasItem(item: AllIdsOf<'items'>): boolean;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function create() {}
|
||||
|
||||
export * from './hero';
|
||||
export * from './interface';
|
||||
export * from './item';
|
||||
export * from './move';
|
||||
export * from './utils';
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IStateDamageable {
|
||||
/** 生命值 */
|
||||
hp: number;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { loading } from '@user/data-base';
|
||||
|
||||
type EffectFn = () => void;
|
||||
type CanUseEffectFn = () => boolean;
|
||||
|
||||
interface ItemStateEvent {
|
||||
use: [];
|
||||
}
|
||||
|
||||
export class ItemState<
|
||||
I extends AllIdsOf<'items'> = AllIdsOf<'items'>
|
||||
> extends EventEmitter<ItemStateEvent> {
|
||||
static items: Map<AllIdsOf<'items'>, ItemState> = new Map();
|
||||
|
||||
id: I;
|
||||
cls: ItemClsOf<I>;
|
||||
name: string;
|
||||
text?: string;
|
||||
hideInToolBox: boolean;
|
||||
hideInReplay: boolean;
|
||||
|
||||
/** 即捡即用效果 */
|
||||
itemEffect?: string;
|
||||
/** 即捡即用道具捡过之后的提示 */
|
||||
itemEffectTip?: string;
|
||||
/** 使用道具时执行的事件 */
|
||||
useItemEvent?: MotaEvent;
|
||||
/** 使用道具时执行的代码 */
|
||||
useItemEffect?: string;
|
||||
/** 能否使用道具 */
|
||||
canUseItemEffect?: string | boolean;
|
||||
|
||||
private noRoute: boolean = false;
|
||||
|
||||
itemEffectFn?: EffectFn;
|
||||
useItemEffectFn?: EffectFn;
|
||||
canUseItemEffectFn?: CanUseEffectFn;
|
||||
|
||||
constructor(id: I) {
|
||||
super();
|
||||
const items = items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a;
|
||||
this.id = id;
|
||||
const item = items[id];
|
||||
this.cls = item.cls;
|
||||
this.name = item.name;
|
||||
this.text = item.text;
|
||||
this.hideInToolBox = item.hideInToolBox;
|
||||
this.hideInReplay = item.hideInReplay;
|
||||
this.itemEffect = item.itemEffect;
|
||||
this.itemEffectTip = item.itemEffectTip;
|
||||
this.useItemEvent = item.useItemEvent;
|
||||
this.useItemEffect = item.useItemEffect;
|
||||
this.canUseItemEffect = item.canUseItemEffect;
|
||||
|
||||
this.compileFunction();
|
||||
}
|
||||
|
||||
private compileFunction() {
|
||||
if (this.itemEffect) {
|
||||
this.itemEffectFn = new Function(
|
||||
`state`,
|
||||
this.itemEffect
|
||||
) as EffectFn;
|
||||
}
|
||||
if (this.useItemEffect) {
|
||||
this.useItemEffectFn = new Function(
|
||||
`state`,
|
||||
this.useItemEffect
|
||||
) as EffectFn;
|
||||
}
|
||||
if (this.canUseItemEffect) {
|
||||
if (typeof this.canUseItemEffect === 'boolean') {
|
||||
this.canUseItemEffectFn = () =>
|
||||
this.canUseItemEffect as boolean;
|
||||
} else {
|
||||
this.useItemEffectFn = new Function(
|
||||
`state`,
|
||||
this.canUseItemEffect
|
||||
) as CanUseEffectFn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个道具的信息
|
||||
* @param id 要获取的道具id
|
||||
*/
|
||||
static item<I extends AllIdsOf<'items'>>(id: I): ItemState<I> | undefined {
|
||||
return this.items.get(id) as ItemState<I>;
|
||||
}
|
||||
}
|
||||
|
||||
loading.once('coreInit', () => {
|
||||
for (const key of Object.keys(items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a)) {
|
||||
ItemState.items.set(
|
||||
key as AllIdsOf<'items'>,
|
||||
new ItemState(key as AllIdsOf<'items'>)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,720 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { backDir, toDir } from './utils';
|
||||
import { loading } from '@user/data-base';
|
||||
import type { RenderAdapter } from '@motajs/render';
|
||||
import type {
|
||||
FloorLayer,
|
||||
FloorViewport,
|
||||
HeroKeyMover,
|
||||
HeroRenderer,
|
||||
Layer,
|
||||
LayerFloorBinder,
|
||||
LayerMovingRenderable
|
||||
} from '@user/client-modules';
|
||||
import { sleep } from '@motajs/common';
|
||||
|
||||
// todo: 转身功能
|
||||
|
||||
interface MoveStepDir {
|
||||
type: 'dir';
|
||||
value: Move2;
|
||||
}
|
||||
|
||||
interface MoveStepSpeed {
|
||||
type: 'speed';
|
||||
value: number;
|
||||
}
|
||||
|
||||
export type MoveStep = MoveStepDir | MoveStepSpeed;
|
||||
|
||||
export interface IMoveController {
|
||||
/** 本次移动是否完成 */
|
||||
done: boolean;
|
||||
/** 当前移动队列 */
|
||||
queue: MoveStep[];
|
||||
/** 当本次移动完成时兑现 */
|
||||
onEnd: Promise<void>;
|
||||
|
||||
/**
|
||||
* 停止本次移动,停止后将不再能继续移动
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 向队列末尾添加移动信息
|
||||
* @param step 移动信息
|
||||
*/
|
||||
push(...step: MoveStep[]): void;
|
||||
}
|
||||
|
||||
interface EObjectMovingEvent {
|
||||
stepEnd: [step: MoveStep];
|
||||
moveEnd: [];
|
||||
moveStart: [queue: MoveStep[]];
|
||||
}
|
||||
|
||||
export abstract class ObjectMoverBase extends EventEmitter<EObjectMovingEvent> {
|
||||
/** 移动队列 */
|
||||
private moveQueue: MoveStep[] = [];
|
||||
|
||||
/** 当前的移动速度 */
|
||||
moveSpeed: number = 100;
|
||||
/** 当前的移动方向 */
|
||||
moveDir: Dir2 = 'down';
|
||||
/** 当前是否正在移动 */
|
||||
moving: boolean = false;
|
||||
/** 面朝方向 */
|
||||
faceDir: Dir2 = 'down';
|
||||
|
||||
/** 当前的控制对象 */
|
||||
controller?: IMoveController;
|
||||
|
||||
/**
|
||||
* 当本次移动开始时执行的函数
|
||||
* @param controller 本次移动的控制对象
|
||||
*/
|
||||
protected abstract onMoveStart(controller: IMoveController): Promise<void>;
|
||||
|
||||
/**
|
||||
* 当本次移动结束时执行的函数
|
||||
* @param controller 本次移动的控制对象
|
||||
*/
|
||||
protected abstract onMoveEnd(controller: IMoveController): Promise<void>;
|
||||
|
||||
/**
|
||||
* 当这一步开始移动时执行的函数,可以用来播放移动动画等
|
||||
* @param step 这一步的移动信息
|
||||
* @param controller 本次移动的控制对象
|
||||
* @returns 一个Promise,其值表示本次移动的代码,并将其值传入这一步移动后的操作,即{@link onStepEnd}
|
||||
*/
|
||||
protected abstract onStepStart(
|
||||
step: MoveStepDir,
|
||||
controller: IMoveController
|
||||
): Promise<number>;
|
||||
|
||||
/**
|
||||
* 当这一步移动结束后执行的函数(即{@link onStepStart}成功兑现后),可以用于设置物体坐标等
|
||||
* @param step 这一步的移动信息
|
||||
* @param code 本步移动的代码,即onMoveStart的返回值
|
||||
* @param controller 本次移动的控制对象
|
||||
* @returns 一个Promise,当其兑现时,本步移动全部完成,将会开始下一步的移动
|
||||
*/
|
||||
protected abstract onStepEnd(
|
||||
step: MoveStepDir,
|
||||
code: number,
|
||||
controller: IMoveController
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* 当移动速度被设置时执行的函数
|
||||
* @param speed 设置为的移动速度
|
||||
* @param controller 本次移动的控制对象
|
||||
*/
|
||||
protected abstract onSetMoveSpeed(
|
||||
speed: number,
|
||||
controller: IMoveController
|
||||
): void;
|
||||
|
||||
private getMoveDir(move: Move2): Dir2 {
|
||||
if (move === 'forward') return this.moveDir;
|
||||
if (move === 'backward') return backDir(this.faceDir);
|
||||
return move;
|
||||
}
|
||||
|
||||
private getFaceDir(move: Move2): Dir2 {
|
||||
if (move === 'forward' || move === 'backward') return this.faceDir;
|
||||
return move;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前面朝方向,移动中设置无效
|
||||
* @param dir 面朝方向
|
||||
*/
|
||||
setFaceDir(dir: Dir2) {
|
||||
if (!this.moving) this.faceDir = dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前移动方向,移动中设置无效
|
||||
* @param dir 移动方向
|
||||
*/
|
||||
setMoveDir(dir: Dir2) {
|
||||
if (!this.moving) this.moveDir = dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始移动,这个操作后,本次移动将不能通过{@link insertMove}添加移动步骤,
|
||||
* 只能通过{@link IMoveController.push}添加,而通过前者添加的移动步骤会在下次移动生效
|
||||
*/
|
||||
startMove(): IMoveController | null {
|
||||
if (this.moving) return null;
|
||||
const queue = this.moveQueue.slice();
|
||||
this.clearMoveQueue();
|
||||
|
||||
this.moving = true;
|
||||
let stopMove = false;
|
||||
|
||||
const start = async () => {
|
||||
// 等待宏任务执行完成,不然controller会在首次调用中未定义
|
||||
await new Promise<void>(res => res());
|
||||
await this.onMoveStart(controller);
|
||||
this.emit('moveStart', queue);
|
||||
while (queue.length > 0) {
|
||||
if (stopMove || !this.moving) break;
|
||||
const step = queue.shift();
|
||||
if (!step) break;
|
||||
if (step.type === 'dir') {
|
||||
this.moveDir = this.getMoveDir(step.value);
|
||||
this.faceDir = this.getFaceDir(step.value);
|
||||
const code = await this.onStepStart(step, controller);
|
||||
await this.onStepEnd(step, code, controller);
|
||||
} else {
|
||||
const speed = step.value;
|
||||
this.moveSpeed = speed;
|
||||
this.onSetMoveSpeed(speed, controller);
|
||||
}
|
||||
this.emit('stepEnd', step);
|
||||
}
|
||||
this.moving = false;
|
||||
this.emit('moveEnd');
|
||||
|
||||
await this.onMoveEnd(controller);
|
||||
};
|
||||
|
||||
const onEnd = start().then(() => {
|
||||
this.controller = void 0;
|
||||
controller.done = true;
|
||||
});
|
||||
const controller: IMoveController = {
|
||||
done: false,
|
||||
onEnd,
|
||||
queue,
|
||||
push(...step) {
|
||||
queue.push(...step);
|
||||
},
|
||||
stop() {
|
||||
stopMove = true;
|
||||
return onEnd;
|
||||
}
|
||||
};
|
||||
this.controller = controller;
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向移动队列末尾插入移动实例
|
||||
* @param move 移动实例
|
||||
*/
|
||||
insertMove(...move: MoveStep[]) {
|
||||
this.moveQueue.push(...move);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空移动队列
|
||||
*/
|
||||
clearMoveQueue() {
|
||||
this.moveQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 向移动队列末尾插入一个移动操作
|
||||
* @param step 移动方向
|
||||
*/
|
||||
oneStep(step: Move2) {
|
||||
this.moveQueue.push({ type: 'dir', value: step });
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照传入的数组移动物体,插入至移动队列末尾
|
||||
* @param steps 移动步骤
|
||||
*/
|
||||
moveAs(steps: MoveStep[]) {
|
||||
this.moveQueue.push(...steps);
|
||||
}
|
||||
}
|
||||
|
||||
const enum BlockMoveCode {
|
||||
Step
|
||||
}
|
||||
|
||||
export class BlockMover extends ObjectMoverBase {
|
||||
/** 楼层渲染适配器,用于显示动画 */
|
||||
static adapter?: RenderAdapter<Layer>;
|
||||
|
||||
x: number;
|
||||
y: number;
|
||||
floorId: FloorIds;
|
||||
layer: FloorLayer;
|
||||
|
||||
/** 本次移动中需要进行动画移动的楼层渲染组件 */
|
||||
private layerItems: Layer[] = [];
|
||||
/** 本次移动过程中的移动renderable实例 */
|
||||
private renderable?: LayerMovingRenderable;
|
||||
/** 本次移动的图块id */
|
||||
private blockNum: number = 0;
|
||||
|
||||
constructor(
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds,
|
||||
layer: FloorLayer,
|
||||
dir: Dir = 'down'
|
||||
) {
|
||||
super();
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.floorId = floorId;
|
||||
this.moveDir = dir;
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定移动点
|
||||
* @param x 绑定点横坐标
|
||||
* @param y 绑定点纵坐标
|
||||
* @param floorId 绑定点楼层
|
||||
* @returns 是否绑定成功,例如如果当前绑定点正在移动,那么就会绑定失败
|
||||
*/
|
||||
bind(
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds,
|
||||
layer: FloorLayer,
|
||||
dir: Dir = 'down'
|
||||
) {
|
||||
if (this.moving) return false;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.floorId = floorId;
|
||||
this.moveDir = dir;
|
||||
this.layer = layer;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async onMoveStart(_controller: IMoveController): Promise<void> {
|
||||
const adapter = BlockMover.adapter;
|
||||
if (adapter) {
|
||||
const list = adapter.items;
|
||||
const items = [...list].filter(v => {
|
||||
if (v.layer !== this.layer) return false;
|
||||
const ex = v.getExtends('floor-binder') as LayerFloorBinder;
|
||||
if (!ex) return false;
|
||||
return ex.getFloor() === core.status.floorId;
|
||||
});
|
||||
this.layerItems = items;
|
||||
}
|
||||
|
||||
let blockNum: number = 0;
|
||||
if (this.layer === 'event') {
|
||||
blockNum = core.status.maps[this.floorId].map[this.y][this.x];
|
||||
} else {
|
||||
const array = core.maps._getBgFgMapArray(this.layer, this.floorId);
|
||||
blockNum = array[this.y][this.x];
|
||||
}
|
||||
this.blockNum = blockNum;
|
||||
|
||||
Mota.r(() => {
|
||||
const { Layer } = Mota.require('@user/client-modules');
|
||||
const r = Layer.getMovingRenderable(blockNum, this.x, this.y);
|
||||
|
||||
if (r) {
|
||||
this.renderable = r;
|
||||
this.layerItems.forEach(v => {
|
||||
v.moving.add(r);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.layer === 'event') {
|
||||
core.removeBlock(this.x, this.y, this.floorId);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onMoveEnd(_controller: IMoveController): Promise<void> {
|
||||
if (this.renderable) {
|
||||
this.layerItems.forEach(v => {
|
||||
v.moving.delete(this.renderable!);
|
||||
});
|
||||
}
|
||||
|
||||
this.layerItems = [];
|
||||
this.renderable = void 0;
|
||||
|
||||
if (this.layer === 'event') {
|
||||
core.setBlock(this.blockNum as AllNumbers, this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onStepStart(
|
||||
step: MoveStepDir,
|
||||
_controller: IMoveController
|
||||
): Promise<BlockMoveCode> {
|
||||
await this.moveAnimate(step);
|
||||
const { x: dx, y: dy } = core.utils.scan2[this.moveDir];
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
|
||||
return BlockMoveCode.Step;
|
||||
}
|
||||
|
||||
protected async onStepEnd(
|
||||
_step: MoveStepDir,
|
||||
_code: BlockMoveCode,
|
||||
_controller: IMoveController
|
||||
): Promise<void> {}
|
||||
|
||||
protected onSetMoveSpeed(
|
||||
_speed: number,
|
||||
_controller: IMoveController
|
||||
): void {}
|
||||
|
||||
private moveAnimate(_step: MoveStepDir) {
|
||||
const layer = this.layerItems[0];
|
||||
if (!layer) return;
|
||||
if (!this.renderable) return;
|
||||
const data = this.renderable;
|
||||
const fx = this.x;
|
||||
const fy = this.y;
|
||||
const { x: dx, y: dy } = core.utils.scan2[this.moveDir];
|
||||
const start = Date.now();
|
||||
const replay = core.status.replay.speed ?? 1;
|
||||
const time = replay === 24 ? 1 : this.moveSpeed / replay;
|
||||
|
||||
return new Promise<void>(res => {
|
||||
layer.delegateTicker(
|
||||
() => {
|
||||
const now = Date.now() - start;
|
||||
const progress = now / time;
|
||||
data.x = fx + dx * progress;
|
||||
data.y = fy + dy * progress;
|
||||
this.layerItems.forEach(v => {
|
||||
v.update(v);
|
||||
});
|
||||
},
|
||||
this.moveSpeed,
|
||||
() => {
|
||||
data.x = fx + dx;
|
||||
data.y = fy + dy;
|
||||
data.zIndex = fy + dy;
|
||||
res();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface CanMoveStatus {
|
||||
/** 由CannotIn和CannotOut计算出的信息,不可移动时不会触发触发器 */
|
||||
canMove: boolean;
|
||||
/** 由图块的noPass计算出的信息,不可移动时会触发触发器 */
|
||||
noPass: boolean;
|
||||
}
|
||||
|
||||
const enum HeroMoveCode {
|
||||
Step,
|
||||
Stop,
|
||||
/** 不能移动,并撞击前面一格的图块,触发其触发器 */
|
||||
Hit,
|
||||
/** 不能移动,同时当前格有CannotOut,或目标格有CannotIn,不会触发前面一格的触发器 */
|
||||
CannotMove
|
||||
}
|
||||
|
||||
export class HeroMover extends ObjectMoverBase {
|
||||
/** 勇士渲染适配器,用于等待动画等操作 */
|
||||
static adapter?: RenderAdapter<HeroRenderer>;
|
||||
/** 视角适配器 */
|
||||
static viewport?: RenderAdapter<FloorViewport>;
|
||||
|
||||
/** 当前移动是否忽略地形 */
|
||||
private ignoreTerrain: boolean = false;
|
||||
/** 当前移动是否不计入录像 */
|
||||
private noRoute: boolean = false;
|
||||
/** 当前移动是否是在lockControl条件下开始的 */
|
||||
private inLockControl: boolean = false;
|
||||
/** 是否会在特殊时刻进行自动存档 */
|
||||
private autoSave: boolean = false;
|
||||
|
||||
/** 本次移动开始时的移动速度 */
|
||||
private beforeMoveSpeed: number = 100;
|
||||
|
||||
override startMove(
|
||||
ignoreTerrain: boolean = false,
|
||||
noRoute: boolean = false,
|
||||
inLockControl: boolean = false,
|
||||
autoSave: boolean = false
|
||||
): IMoveController | null {
|
||||
if (this.moving) return null;
|
||||
this.ignoreTerrain = ignoreTerrain;
|
||||
this.noRoute = noRoute;
|
||||
this.inLockControl = inLockControl;
|
||||
this.autoSave = autoSave;
|
||||
return super.startMove();
|
||||
}
|
||||
|
||||
private checkAutoSave(x: number, y: number, nx: number, ny: number) {
|
||||
const index = `${x},${y}`;
|
||||
const nIndex = `${nx},${ny}`;
|
||||
const map = core.status.thisMap.enemy.mapDamage;
|
||||
const dam = map[index];
|
||||
const nextDam = map[nIndex];
|
||||
if (!dam || !nextDam) return;
|
||||
// 可以在这里判断地图伤害,并进行自动存档,例如在进入或离开地图伤害时存档
|
||||
// if (dam.damage > 0 || nextDam.damage > 0) {
|
||||
// core.autosave()
|
||||
// }
|
||||
}
|
||||
|
||||
protected async onMoveStart(controller: IMoveController): Promise<void> {
|
||||
this.beforeMoveSpeed = this.moveSpeed;
|
||||
const adapter = HeroMover.adapter;
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!adapter || !viewport) return;
|
||||
if (!core.isReplaying() || core.status.replay.speed <= 3) {
|
||||
adapter.sync('startAnimate');
|
||||
await adapter.all('readyMove');
|
||||
}
|
||||
// 这里要检查前面那一格能不能走,不能走则不触发平滑视角,以避免撞墙上视角卡住
|
||||
if (!this.ignoreTerrain) {
|
||||
const { x, y } = core.status.hero.loc;
|
||||
const firstDir = controller.queue.find(
|
||||
v => v.type === 'dir'
|
||||
)?.value;
|
||||
if (firstDir && firstDir !== 'backward' && firstDir !== 'forward') {
|
||||
const data = this.checkCanMove(x, y, toDir(firstDir as Dir));
|
||||
if (data.canMove && !data.noPass) {
|
||||
viewport.sync('startMove');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewport.sync('startMove');
|
||||
}
|
||||
}
|
||||
|
||||
protected async onMoveEnd(controller: IMoveController): Promise<void> {
|
||||
this.moveSpeed = this.beforeMoveSpeed;
|
||||
this.onSetMoveSpeed(this.moveSpeed, controller);
|
||||
const adapter = HeroMover.adapter;
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!adapter || !viewport) return;
|
||||
adapter.sync('endAnimate');
|
||||
await adapter.all('endMove');
|
||||
viewport.sync('endMove');
|
||||
core.clearContinueAutomaticRoute();
|
||||
core.stopAutomaticRoute();
|
||||
}
|
||||
|
||||
protected async onStepStart(
|
||||
_step: MoveStepDir,
|
||||
controller: IMoveController
|
||||
): Promise<HeroMoveCode> {
|
||||
const showDir = toDir(this.faceDir);
|
||||
const dir4Move = toDir(this.moveDir);
|
||||
core.setHeroLoc('direction', showDir);
|
||||
|
||||
const { x, y } = core.status.hero.loc;
|
||||
const { x: nx, y: ny } = this.nextLoc(x, y, this.moveDir);
|
||||
|
||||
if (this.autoSave && !this.ignoreTerrain) {
|
||||
this.checkAutoSave(x, y, nx, ny);
|
||||
}
|
||||
|
||||
if (!this.inLockControl && core.status.lockControl) {
|
||||
controller.stop();
|
||||
return HeroMoveCode.Stop;
|
||||
}
|
||||
|
||||
if (!this.ignoreTerrain || !this.noRoute) {
|
||||
this.moveDir = dir4Move;
|
||||
}
|
||||
|
||||
const dir = this.moveDir;
|
||||
if (!this.ignoreTerrain) {
|
||||
const { noPass, canMove } = this.checkCanMove(x, y, dir4Move);
|
||||
|
||||
if (!canMove) {
|
||||
return HeroMoveCode.CannotMove;
|
||||
}
|
||||
// 不能移动
|
||||
if (noPass) {
|
||||
return HeroMoveCode.Hit;
|
||||
}
|
||||
}
|
||||
|
||||
// 可以移动,显示移动动画
|
||||
|
||||
await this.moveAnimate(nx, ny, showDir, dir);
|
||||
|
||||
return HeroMoveCode.Step;
|
||||
}
|
||||
|
||||
protected async onStepEnd(
|
||||
_step: MoveStepDir,
|
||||
code: HeroMoveCode,
|
||||
controller: IMoveController
|
||||
): Promise<void> {
|
||||
const { x, y } = core.status.hero.loc;
|
||||
const { x: nx, y: ny } = this.nextLoc(x, y, this.moveDir);
|
||||
const showDir = toDir(this.faceDir);
|
||||
|
||||
// 前方不能移动
|
||||
if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Hit) {
|
||||
controller.stop();
|
||||
this.onCannotMove(showDir);
|
||||
if (code === HeroMoveCode.Hit) {
|
||||
core.trigger(nx, ny);
|
||||
}
|
||||
core.checkRouteFolding();
|
||||
return;
|
||||
}
|
||||
|
||||
// 本次移动停止
|
||||
if (code === HeroMoveCode.Stop) {
|
||||
core.clearContinueAutomaticRoute();
|
||||
core.stopAutomaticRoute();
|
||||
controller.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// 本次移动正常完成
|
||||
if (code === HeroMoveCode.Step) {
|
||||
core.setHeroLoc('x', nx, true);
|
||||
core.setHeroLoc('y', ny, true);
|
||||
|
||||
if (!this.ignoreTerrain) {
|
||||
const direction = core.getHeroLoc('direction');
|
||||
core.control._moveAction_popAutomaticRoute();
|
||||
if (!this.noRoute) core.status.route.push(direction);
|
||||
|
||||
// 中毒处理
|
||||
if (core.hasFlag('poison')) {
|
||||
core.status.hero.hp -= core.values.poisonDamage;
|
||||
if (core.status.hero.hp <= 0) {
|
||||
core.status.hero.hp = 0;
|
||||
core.events.lose();
|
||||
}
|
||||
core.updateStatusBar();
|
||||
}
|
||||
|
||||
core.moveOneStep();
|
||||
core.checkRouteFolding();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected onSetMoveSpeed(
|
||||
speed: number,
|
||||
_controller: IMoveController
|
||||
): void {
|
||||
const adapter = HeroMover.adapter;
|
||||
if (!adapter) return;
|
||||
adapter.sync('setMoveSpeed', speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动动画
|
||||
* @param x 目标横坐标
|
||||
* @param y 目标纵坐标
|
||||
* @param showDir 显示方向
|
||||
* @param moveDir 移动方向
|
||||
*/
|
||||
private async moveAnimate(
|
||||
x: number,
|
||||
y: number,
|
||||
showDir: Dir,
|
||||
moveDir: Dir2
|
||||
) {
|
||||
const adapter = HeroMover.adapter;
|
||||
const viewport = HeroMover.viewport;
|
||||
if (!adapter || !viewport) return;
|
||||
const replay = core.status.replay.speed;
|
||||
const speed = replay === 24 ? 1 : this.moveSpeed / replay;
|
||||
viewport.all('moveTo', x, y, speed * 1.6);
|
||||
adapter.sync('setAnimateDir', showDir);
|
||||
if (core.isReplaying() && core.status.replay.speed > 3) {
|
||||
adapter.sync('endAnimate');
|
||||
await sleep(speed);
|
||||
await adapter.all('setHeroLoc', x, y);
|
||||
} else {
|
||||
if (core.isReplaying()) {
|
||||
adapter.sync('startAnimate');
|
||||
}
|
||||
await adapter.all('move', moveDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前方一格不能走时,执行的函数
|
||||
* @param dir 移动方向
|
||||
*/
|
||||
private onCannotMove(dir: Dir) {
|
||||
if (!this.noRoute) core.status.route.push(dir);
|
||||
core.status.automaticRoute.moveStepBeforeStop = [];
|
||||
core.status.automaticRoute.lastDirection = dir;
|
||||
|
||||
if (core.status.automaticRoute.moveStepBeforeStop.length === 0) {
|
||||
core.clearContinueAutomaticRoute();
|
||||
core.stopAutomaticRoute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查前方是否可以移动
|
||||
* @param x 当前横坐标
|
||||
* @param y 当前纵坐标
|
||||
* @param dir 移动方向
|
||||
*/
|
||||
private checkCanMove(x: number, y: number, dir: Dir): CanMoveStatus {
|
||||
// 如果是循环式地图
|
||||
const { x: nx, y: ny } = this.nextLoc(x, y, dir);
|
||||
const noPass = core.noPass(nx, ny);
|
||||
const canMove = core.canMoveHero(x, y, dir);
|
||||
return { noPass, canMove };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前方一格的坐标
|
||||
* @param x 当前横坐标
|
||||
* @param y 当前纵坐标
|
||||
* @param dir 移动方向
|
||||
*/
|
||||
private nextLoc(x: number, y: number, dir: Dir2): Loc {
|
||||
const { x: dx, y: dy } = core.utils.scan2[dir];
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
return { x: nx, y: ny };
|
||||
}
|
||||
}
|
||||
|
||||
interface HeroMoveCollection {
|
||||
mover: HeroMover;
|
||||
keyMover?: HeroKeyMover;
|
||||
}
|
||||
|
||||
// 初始化基础勇士移动实例
|
||||
const heroMover = new HeroMover();
|
||||
export const heroMoveCollection: HeroMoveCollection = {
|
||||
mover: heroMover
|
||||
};
|
||||
loading.once('coreInit', () => {
|
||||
// 注册按键操作
|
||||
Mota.r(() => {
|
||||
const { HeroKeyMover } = Mota.require('@user/client-modules');
|
||||
const { gameKey } = Mota.require('@motajs/system-action');
|
||||
const keyMover = new HeroKeyMover(gameKey, heroMover);
|
||||
heroMoveCollection.keyMover = keyMover;
|
||||
});
|
||||
});
|
||||
|
||||
// Adapter初始化
|
||||
loading.once('coreInit', () => {
|
||||
if (main.replayChecking || main.mode === 'editor') return;
|
||||
const Adapter = Mota.require('@motajs/render').RenderAdapter;
|
||||
const adapter = Adapter.get<HeroRenderer>('hero-adapter');
|
||||
const viewport = Adapter.get<FloorViewport>('viewport');
|
||||
const layerAdapter = Adapter.get<Layer>('layer');
|
||||
HeroMover.adapter = adapter;
|
||||
HeroMover.viewport = viewport;
|
||||
BlockMover.adapter = layerAdapter;
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
const dirMap: Record<Dir2, Dir> = {
|
||||
down: 'down',
|
||||
left: 'left',
|
||||
leftdown: 'left',
|
||||
leftup: 'left',
|
||||
right: 'right',
|
||||
rightdown: 'right',
|
||||
rightup: 'right',
|
||||
up: 'up'
|
||||
};
|
||||
|
||||
/**
|
||||
* 将八方向转换为四方向,一般用于斜角移动时的方向显示
|
||||
* @param dir 八方向
|
||||
*/
|
||||
export function toDir(dir: Dir2): Dir {
|
||||
return dirMap[dir];
|
||||
}
|
||||
|
||||
const backDirMap: Record<Dir2, Dir2> = {
|
||||
up: 'down',
|
||||
down: 'up',
|
||||
left: 'right',
|
||||
right: 'left',
|
||||
leftup: 'rightdown',
|
||||
rightup: 'leftdown',
|
||||
leftdown: 'rightup',
|
||||
rightdown: 'leftup'
|
||||
};
|
||||
|
||||
export function backDir(dir: Dir): Dir;
|
||||
export function backDir(dir: Dir2): Dir2;
|
||||
export function backDir(dir: Dir2): Dir2 {
|
||||
return backDirMap[dir];
|
||||
}
|
||||
|
||||
export function locInMap(x: number, y: number, floorId: FloorIds) {
|
||||
const { width, height } = core.status.maps[floorId];
|
||||
return x >= 0 && y >= 0 && x < width && y < height;
|
||||
}
|
||||
|
||||
/**
|
||||
* 广义检查能否移动,指定两个点以及行走方向,返回能否执行上述移动。
|
||||
* 可以传入地图之外的点,视为可以随意移动。仅检查 cannotIn 和 cannotOut,不检查 noPass
|
||||
* @param fx 出点横坐标
|
||||
* @param fy 出点纵坐标
|
||||
* @param tx 入点横坐标
|
||||
* @param ty 入点纵坐标
|
||||
* @param dir 行走方向
|
||||
* @param fromFloorId 出点楼层id
|
||||
* @param toFloorId 入点楼层id
|
||||
*/
|
||||
export function checkCanMoveExtended(
|
||||
fx: number,
|
||||
fy: number,
|
||||
tx: number,
|
||||
ty: number,
|
||||
dir: Dir,
|
||||
fromFloorId: FloorIds = core.status.floorId,
|
||||
toFloorId: FloorIds = core.status.floorId
|
||||
) {
|
||||
const fromInMap = locInMap(fx, fy, fromFloorId);
|
||||
const toInMap = locInMap(tx, ty, toFloorId);
|
||||
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
|
||||
|
||||
if (fromInMap) {
|
||||
const cannotMove = core.floors[fromFloorId].cannotMove;
|
||||
const fromIndex: LocString = `${fx},${fy}`;
|
||||
// 检查当前点是否有不可出
|
||||
if (cannotMove[fromIndex]?.includes(dir)) return false;
|
||||
const blocks = getBlockForLoc(fx, fy, fromFloorId);
|
||||
const can = blocks.some(v => {
|
||||
if (v === 0) return false;
|
||||
const out = map[v as Exclude<AllNumbers, 0>]?.cannotOut;
|
||||
return out?.includes(dir);
|
||||
});
|
||||
if (can) return false;
|
||||
}
|
||||
if (toInMap) {
|
||||
const cannotMoveIn = core.floors[toFloorId].cannotMoveIn;
|
||||
const toIndex: LocString = `${tx},${ty}`;
|
||||
const back = backDir(dir);
|
||||
// 检查目标点是否有不可入
|
||||
if (cannotMoveIn[toIndex]?.includes(back)) return false;
|
||||
const blocks = getBlockForLoc(tx, ty, toFloorId);
|
||||
const can = blocks.some(v => {
|
||||
if (v === 0) return false;
|
||||
const out = map[v as Exclude<AllNumbers, 0>]?.cannotIn;
|
||||
return out?.includes(back);
|
||||
});
|
||||
if (can) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getBlockForLoc(x: number, y: number, floorId: FloorIds) {
|
||||
const map = core.status.maps[floorId];
|
||||
const floor = core.floors[floorId];
|
||||
return [
|
||||
floor.bgmap[y][x],
|
||||
floor.bg2map[y][x],
|
||||
map.map[y][x],
|
||||
floor.bgmap[y][x],
|
||||
floor.bg2map[y][x]
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user