mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-07-25 16:31:47 +08:00
673 lines
19 KiB
TypeScript
673 lines
19 KiB
TypeScript
import { getHeroStatusOf, getHeroStatusOn } from '@/game/hero';
|
||
import { Range, RangeCollection } from '@/plugin/game/range';
|
||
import {
|
||
checkV2,
|
||
ensureArray,
|
||
formatDamage,
|
||
has,
|
||
manhattan
|
||
} from '@/plugin/game/utils';
|
||
|
||
interface HaloType {
|
||
square: {
|
||
x: number;
|
||
y: number;
|
||
d: number;
|
||
};
|
||
manhattan: {
|
||
x: number;
|
||
y: number;
|
||
d: number;
|
||
};
|
||
}
|
||
|
||
interface EnemyInfo extends Partial<Enemy> {
|
||
atk: number;
|
||
def: number;
|
||
hp: number;
|
||
special: number[];
|
||
atkBuff: number;
|
||
defBuff: number;
|
||
hpBuff: number;
|
||
enemy: Enemy;
|
||
guard?: DamageEnemy[];
|
||
x?: number;
|
||
y?: number;
|
||
floorId?: FloorIds;
|
||
}
|
||
|
||
interface DamageInfo {
|
||
damage: number;
|
||
}
|
||
|
||
interface MapDamage {
|
||
damage: number;
|
||
type: Set<string>;
|
||
repulse?: LocArr[];
|
||
ambush?: DamageEnemy[];
|
||
}
|
||
|
||
interface HaloData<T extends keyof HaloType = keyof HaloType> {
|
||
type: T;
|
||
data: HaloType[T];
|
||
special: number;
|
||
from?: DamageEnemy;
|
||
}
|
||
|
||
interface DamageDelta {
|
||
/** 跟最小伤害值的减伤 */
|
||
delta: number;
|
||
damage: number;
|
||
info: DamageInfo;
|
||
}
|
||
|
||
interface CriticalDamageDelta extends Omit<DamageDelta, 'info'> {
|
||
/** 勇士的攻击增量 */
|
||
atkDelta: number;
|
||
}
|
||
|
||
type HaloFn = (info: EnemyInfo, enemy: EnemyInfo) => void;
|
||
|
||
export class EnemyCollection implements RangeCollection<DamageEnemy> {
|
||
floorId: FloorIds;
|
||
list: DamageEnemy[] = [];
|
||
|
||
range: Range<DamageEnemy> = new Range(this);
|
||
mapDamage: Record<string, MapDamage> = {};
|
||
haloList: HaloData[] = [];
|
||
|
||
constructor(floorId: FloorIds) {
|
||
this.floorId = floorId;
|
||
this.extract();
|
||
}
|
||
|
||
get(x: number, y: number) {
|
||
return this.list.find(v => v.x === x && v.y === y);
|
||
}
|
||
|
||
/**
|
||
* 解析本地图的怪物信息
|
||
*/
|
||
extract() {
|
||
this.list = [];
|
||
core.extractBlocks(this.floorId);
|
||
core.status.maps[this.floorId].blocks.forEach(v => {
|
||
if (v.event.cls !== 'enemy48' && v.event.cls !== 'enemys') return;
|
||
if (v.disable) return;
|
||
const enemy = core.material.enemys[v.event.id as EnemyIds];
|
||
this.list.push(
|
||
new DamageEnemy(enemy, v.x, v.y, this.floorId, 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();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @deprecated
|
||
* 计算怪物伤害
|
||
* @param noCache 是否不使用缓存
|
||
*/
|
||
calDamage(noCache: boolean = false) {
|
||
if (noCache) this.calRealAttribute();
|
||
this.list.forEach(v => {
|
||
v.calDamage(void 0);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 计算地图伤害
|
||
*/
|
||
calMapDamage() {
|
||
this.mapDamage = {};
|
||
const hero = getHeroStatusOn(Damage.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.scan(type, 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();
|
||
});
|
||
}
|
||
|
||
render(onMap: boolean = false, cal: boolean = false) {
|
||
if (cal) {
|
||
this.calMapDamage();
|
||
}
|
||
core.status.damage.data = [];
|
||
core.status.damage.extraData = [];
|
||
core.status.damage.dir = [];
|
||
|
||
// 怪物伤害
|
||
this.list.forEach(v => {
|
||
if (onMap && !checkV2(v.x, v.y)) return;
|
||
|
||
const { damage } = v.calDamage();
|
||
|
||
// 伤害全部相等,绘制在怪物本身所在位置
|
||
const { damage: dam, color } = formatDamage(damage);
|
||
const critical = v.calCritical(1)[0];
|
||
core.status.damage.data.push({
|
||
text: dam,
|
||
px: 32 * v.x! + 1,
|
||
py: 32 * (v.y! + 1) - 1,
|
||
color: color
|
||
});
|
||
const setting = Mota.require('var', 'mainSetting');
|
||
const criGem = setting.getValue('screen.criticalGem', false);
|
||
const n = critical?.atkDelta ?? Infinity;
|
||
const ratio = core.status.maps[this.floorId].ratio;
|
||
const cri = criGem ? Math.ceil(n / ratio) : n;
|
||
|
||
core.status.damage.data.push({
|
||
text: isFinite(cri) ? cri.toString() : '?',
|
||
px: 32 * v.x! + 1,
|
||
py: 32 * (v.y! + 1) - 11,
|
||
color: '#fff'
|
||
});
|
||
});
|
||
|
||
// 地图伤害
|
||
const floor = core.status.maps[this.floorId];
|
||
const width = floor.width;
|
||
const height = floor.height;
|
||
const objs = core.getMapBlocksObj(this.floorId);
|
||
|
||
const startX =
|
||
onMap && core.bigmap.v2
|
||
? Math.max(0, core.bigmap.posX - core.bigmap.extend)
|
||
: 0;
|
||
const endX =
|
||
onMap && core.bigmap.v2
|
||
? Math.min(
|
||
width,
|
||
core.bigmap.posX + core._WIDTH_ + core.bigmap.extend + 1
|
||
)
|
||
: width;
|
||
const startY =
|
||
onMap && core.bigmap.v2
|
||
? Math.max(0, core.bigmap.posY - core.bigmap.extend)
|
||
: 0;
|
||
const endY =
|
||
onMap && core.bigmap.v2
|
||
? Math.min(
|
||
height,
|
||
core.bigmap.posY + core._HEIGHT_ + core.bigmap.extend + 1
|
||
)
|
||
: height;
|
||
|
||
for (let x = startX; x < endX; x++) {
|
||
for (let y = startY; y < endY; y++) {
|
||
const id = `${x},${y}` as LocString;
|
||
const dam = this.mapDamage[id];
|
||
if (!dam || objs[id]?.event.noPass) continue;
|
||
|
||
// 地图伤害
|
||
if (dam.damage !== 0 && !dam.ambush) {
|
||
const damage = core.formatBigNumber(dam.damage, true);
|
||
const color = dam.damage < 0 ? '#6eff6a' : '#fa3';
|
||
core.status.damage.extraData.push({
|
||
text: damage,
|
||
px: 32 * x + 16,
|
||
py: 32 * y + 16,
|
||
color,
|
||
alpha: 1
|
||
});
|
||
}
|
||
|
||
if (dam.ambush) {
|
||
core.status.damage.extraData.push({
|
||
text: '!',
|
||
px: 32 * x + 16,
|
||
py: 32 * y + 16,
|
||
color: '#fa3',
|
||
alpha: 1
|
||
});
|
||
}
|
||
|
||
if (dam.repulse && dam.damage <= 0) {
|
||
core.status.damage.extraData.push({
|
||
text: '阻',
|
||
px: 32 * x + 16,
|
||
py: 32 * y + 16,
|
||
color: '#fa3',
|
||
alpha: 1
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
export class DamageEnemy<T extends EnemyIds = EnemyIds> {
|
||
id: T;
|
||
x?: number;
|
||
y?: number;
|
||
floorId?: FloorIds;
|
||
enemy: Enemy<T>;
|
||
col?: EnemyCollection;
|
||
|
||
/**
|
||
* 怪物属性。
|
||
* 属性计算流程:预平衡光环(即计算加光环的光环怪的光环) -> 计算怪物在没有光环下的属性
|
||
* -> provide inject 光环 -> 计算怪物的光环加成 -> 计算完毕
|
||
*/
|
||
info!: EnemyInfo;
|
||
|
||
/** 向其他怪提供过的光环 */
|
||
providedHalo: Set<number> = new Set();
|
||
|
||
/**
|
||
* 伤害计算进度,0 -> 预平衡光环 -> 1 -> 计算没有光环的属性 -> 2 -> provide inject 光环
|
||
* -> 3 -> 计算光环加成 -> 4 -> 计算完毕
|
||
*/
|
||
private progress: number = 0;
|
||
|
||
constructor(
|
||
enemy: Enemy<T>,
|
||
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: enemy.special.slice(),
|
||
atkBuff: 0,
|
||
defBuff: 0,
|
||
hpBuff: 0,
|
||
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-ignore
|
||
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 floorId = this.floorId ?? core.status.floorId;
|
||
|
||
// 智慧之源
|
||
if (flags.hard === 2 && special.includes(14)) {
|
||
info.atk += flags[`inte_${floorId}`] ?? 0;
|
||
}
|
||
|
||
// 极昼永夜
|
||
info.atk -= flags[`night_${floorId}`] ?? 0;
|
||
info.def -= flags[`night_${floorId}`] ?? 0;
|
||
|
||
// 融化,融化不属于怪物光环,因此不能用provide和inject计算,需要在这里计算
|
||
if (has(flags[`melt_${floorId}`]) && has(this.x) && has(this.y)) {
|
||
for (const [loc, per] of Object.entries(flags[`melt_${floorId}`])) {
|
||
const [mx, my] = loc.split(',').map(v => parseInt(v));
|
||
if (Math.abs(mx - this.x) <= 1 && Math.abs(my - this.y) <= 1) {
|
||
info.atkBuff += per as number;
|
||
info.defBuff += per as number;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取怪物的真实属性信息,在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;
|
||
}
|
||
|
||
getHaloSpecials(): number[] {
|
||
if (!this.floorId) return [];
|
||
if (!has(this.x) || !has(this.y)) return [];
|
||
const special = this.info.special ?? this.enemy.special;
|
||
|
||
const filter = special.filter(v => {
|
||
return Damage.haloSpecials.has(v) && !this.providedHalo.has(v);
|
||
});
|
||
if (filter.length === 0) return [];
|
||
const collection = this.col ?? core.status.maps[this.floorId].enemy;
|
||
if (!collection) {
|
||
throw new Error(
|
||
`Unexpected undefined of enemy collection in floor ${this.floorId}.`
|
||
);
|
||
}
|
||
return filter;
|
||
}
|
||
|
||
/**
|
||
* 光环预提供,用于平衡所有怪的光环属性,避免出现不同情况下光环效果不一致的现象
|
||
*/
|
||
preProvideHalo() {}
|
||
|
||
/**
|
||
* 向其他怪提供光环
|
||
*/
|
||
provideHalo() {}
|
||
|
||
/**
|
||
* 接受其他怪的光环
|
||
*/
|
||
injectHalo(halo: HaloFn, enemy: EnemyInfo) {
|
||
halo(this.info, enemy);
|
||
}
|
||
|
||
/**
|
||
* 计算怪物伤害
|
||
*/
|
||
calDamage(hero: Partial<HeroStatus> = core.status.hero) {
|
||
const enemy = this.getRealInfo();
|
||
return this.calEnemyDamageOf(hero, enemy);
|
||
}
|
||
|
||
/**
|
||
* 计算地图伤害
|
||
* @param damage 存入的对象
|
||
*/
|
||
calMapDamage(
|
||
damage: Record<string, MapDamage> = {},
|
||
hero: Partial<HeroStatus> = getHeroStatusOn(Damage.realStatus)
|
||
) {
|
||
return damage;
|
||
}
|
||
|
||
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) {
|
||
const status = getHeroStatusOf(hero, Damage.realStatus, this.floorId);
|
||
let damage = Damage.calDamageWith(enemy, status) ?? Infinity;
|
||
|
||
return { damage };
|
||
}
|
||
|
||
/**
|
||
* 计算怪物临界
|
||
* @param num 要计算多少个临界
|
||
* @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[] {
|
||
// todo: 可以优化,根据之前的计算可以直接确定下一个临界的范围
|
||
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 calDam = () => {
|
||
return this.calEnemyDamageOf({ atk: curr, def }, 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 - ori
|
||
});
|
||
|
||
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) {
|
||
console.warn(
|
||
`Unexpected long 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 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();
|
||
const add = info.def + info.hp - core.status.hero.mana;
|
||
|
||
// 坚固,不可能通过攻击秒杀
|
||
if (info.special.includes(3)) {
|
||
return Infinity;
|
||
}
|
||
|
||
return add;
|
||
}
|
||
}
|
||
|
||
export namespace Damage {
|
||
/** 光环属性 */
|
||
export const haloSpecials: Set<number> = new Set();
|
||
|
||
/** 会被第一类光环修改的怪物特殊属性数值 */
|
||
export const changeableHaloValue: Map<
|
||
number,
|
||
SelectKey<Enemy, number | undefined>[]
|
||
> = new Map();
|
||
|
||
/**
|
||
* 计算伤害时会用到的勇士属性,攻击防御,其余的不会有buff加成,直接从core.status.hero取
|
||
*/
|
||
export let realStatus: (keyof HeroStatus)[] = ['atk', 'def'];
|
||
|
||
/**
|
||
* 计算怪物伤害
|
||
* @param info 怪物信息
|
||
* @param hero 勇士信息
|
||
*/
|
||
export function calDamageWith(
|
||
info: DeepReadonly<EnemyInfo>,
|
||
hero: DeepReadonly<Partial<HeroStatus>>
|
||
): number | null {
|
||
return null;
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
declare global {
|
||
interface Floor {
|
||
enemy: EnemyCollection;
|
||
}
|
||
}
|