HumanBreak/src/game/enemy/damage.ts

673 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}