refactor: 删除旧 Range 接口

This commit is contained in:
unanmed 2026-05-17 21:50:49 +08:00
parent b779e62eb2
commit 0f5b1f4d80
10 changed files with 1 additions and 1400 deletions

2
dev.md
View File

@ -45,7 +45,7 @@
### 模块原则
- **无副作用**:所有模块只包含函数、类、常量的声明,不允许出现导出的变量声明或顶层代码执行,允许但不建议编写类的静态块。
- **模块初始化**:如需初始化,编写一个 `createXxx` 函数,在 `index.ts` 中整合后逐级向上传递,直至顶层模块统一执行。
- **模块初始化**:如需初始化,编写一个 `createXxx` 函数,在 `index.ts` 中整合后逐级向上传递,直至顶层模块统一执行。注意,当前设计理念下,**不应该**有场景会需要这种函数。
- **不转发导出**:不允许一个文件导出不属于当前 monorepo 或当前文件夹的内容。
- **无循环引用**:不允许出现循环引用。若遇到不得不循环引用的情况,应首先反思接口设计是否存在问题。

View File

@ -1,270 +0,0 @@
import {
DamageEnemy,
ensureFloorDamage,
getEnemy,
state
} from '@user/data-state';
import { hook } from '@user/data-base';
import { Patch, PatchClass } from '@motajs/legacy-common';
import { isNil } from 'lodash-es';
export interface CurrentEnemy {
enemy: DamageEnemy;
// 这个是干啥的?
onMapEnemy: DamageEnemy[];
}
export function patchBattle() {
const patch = new Patch(PatchClass.Enemys);
const patch2 = new Patch(PatchClass.Events);
patch.add('canBattle', function (x, y, floorId) {
const enemy = typeof x === 'number' ? getEnemy(x, y!, floorId) : x;
if (!enemy) {
throw new Error(
`Cannot get enemy on x:${x}, y:${y}, floor: ${floorId}`
);
}
const { damage } = enemy.calDamage();
return damage < core.status.hero.hp;
});
function battle(
x: number | DamageEnemy,
y: number,
force: boolean = false,
callback?: () => void
) {
core.saveAndStopAutomaticRoute();
const isLoc = typeof x === 'number';
const enemy = isLoc ? getEnemy(x, y) : x;
if (!enemy) {
throw new Error(
`Cannot battle with enemy since no enemy on ${x},${y}`
);
}
// 非强制战斗
// @ts-expect-error 2.c 重构
if (!core.canBattle(x, y) && !force && !core.status.event.id) {
core.stopSound();
core.playSound('操作失败');
core.drawTip('你打不过此怪物!', enemy!.id);
return core.clearContinueAutomaticRoute(callback);
}
// 自动存档
if (!core.status.event.id) core.autosave(true);
// 战前事件
// 战后事件
core.afterBattle(enemy, isLoc ? x : enemy.x, y);
callback?.();
}
const getFacedId = (enemy: DamageEnemy) => {
const e = enemy.enemy;
if (e.displayIdInBook) return e.displayIdInBook;
if (e.faceIds) return e.faceIds.down;
return e.id;
};
patch.add('getCurrentEnemys', function (floorId = core.status.floorId) {
floorId = floorId || core.status.floorId;
const enemys: CurrentEnemy[] = [];
const used: Record<string, DamageEnemy[]> = {};
ensureFloorDamage(floorId);
const floor = core.status.maps[floorId];
floor.enemy.list.forEach(v => {
const id = getFacedId(v);
if (!(id in used)) {
const e = new DamageEnemy(v.enemy);
e.calAttribute();
e.getRealInfo();
e.calDamage();
const curr: CurrentEnemy = {
enemy: e,
onMapEnemy: [v]
};
enemys.push(curr);
used[id] = curr.onMapEnemy;
} else {
used[id].push(v);
}
});
return enemys.sort((a, b) => {
const ad = a.enemy.calDamage().damage;
const bd = b.enemy.calDamage().damage;
return ad - bd;
});
});
patch2.add('battle', battle);
patch2.add('_sys_battle', function (data: Block, callback?: () => void) {
// 检查战前事件
const floor = core.floors[core.status.floorId];
const beforeBattle: MotaEvent = [];
const loc = `${data.x},${data.y}` as LocString;
const enemy = getEnemy(data.x, data.y);
beforeBattle.push(...(floor.beforeBattle[loc] ?? []));
beforeBattle.push(...(enemy!.enemy.beforeBattle ?? []));
if (beforeBattle.length > 0) {
beforeBattle.push({ type: 'battle', x: data.x, y: data.y });
core.clearContinueAutomaticRoute();
// 自动存档
const inAction = core.status.event.id === 'action';
if (inAction) {
core.insertAction(beforeBattle, data.x, data.y);
core.doAction();
} else {
core.autosave(true);
core.insertAction(beforeBattle, data.x, data.y, callback);
}
} else {
battle(data.x, data.y, false, callback);
}
});
patch2.add('_action_battle', function (data, x, y, prefix) {
if (data.id) {
// const enemy = getSingleEnemy(data.id as EnemyIds);
// todo: 与不在地图上的怪物战斗
} else {
if (data.floorId !== core.status.floorId) {
core.doAction();
return;
}
const [ex, ey] = core.events.__action_getLoc(
data.loc,
x,
y,
prefix
) as LocArr;
battle(ex, ey, true, core.doAction);
}
});
patch2.add(
'afterBattle',
function (enemy: DamageEnemy, x?: number, y?: number) {
// 播放战斗动画
let animate: AnimationIds = 'hand';
// 检查当前装备是否存在攻击动画
const equipId = core.getEquip(0);
if (equipId && (core.material.items[equipId].equip || {}).animate)
animate = core.material.items[equipId].equip.animate;
// 检查该动画是否存在SE如果不存在则使用默认音效
if (!core.material.animates[animate]?.se)
core.playSound('attack.opus');
// 战斗伤害
const info = enemy.getRealInfo();
const damageInfo = enemy.calDamage(core.status.hero);
const damage = damageInfo.damage;
// 判定是否致死
if (damage >= core.status.hero.hp) {
core.status.hero.hp = 0;
core.updateStatusBar(false, true);
core.events.lose('战斗失败');
return;
}
// 扣减体力值并记录统计数据
core.status.hero.hp -= damage;
core.status.hero.statistics.battleDamage += damage;
core.status.hero.statistics.battle++;
// 获得金币经验
const money = core.hasFlag('curse') ? 0 : enemy.info.money!;
const exp = core.hasFlag('curse') ? 0 : enemy.info.exp!;
core.status.hero.money += money;
core.status.hero.statistics.money += money;
core.status.hero.exp += exp;
core.status.hero.statistics.exp += exp;
const hint = `打败 ${enemy.enemy.name},金币+${money},经验+${exp}`;
core.drawTip(hint, enemy.id);
// 毒衰咒
if (info.special.has(12)) core.setFlag('poison', true);
if (info.special.has(13)) core.setFlag('weak', true);
if (info.special.has(14)) core.setFlag('curse', true);
// 仇恨
if (info.special.has(17)) {
const hatred = state.flags.getFieldValueDefaults('hatred', 0);
core.setFlag('hatred', hatred / 2);
} else {
core.addFlag('hatred', core.values.hatred);
}
// 自爆
if (info.special.has(19)) {
core.status.hero.hp = 1;
}
// 退化
if (info.special.has(21)) {
core.status.hero.atk -= info.atkValue ?? 0;
core.status.hero.def -= info.defValue ?? 0;
}
// 事件的处理
const todo: MotaEvent = [];
// 战后事件
if (!isNil(core.status.floorId)) {
const loc = `${x},${y}` as LocString;
todo.push(
...(core.floors[core.status.floorId].afterBattle[loc] ?? [])
);
}
todo.push(...(enemy.enemy.afterBattle ?? []));
// 如果事件不为空,将其插入
if (todo.length > 0) core.insertAction(todo, x, y);
if (!isNil(x) && !isNil(y)) {
core.drawAnimate(animate, x, y);
core.removeBlock(x, y);
} else core.drawHeroAnimate(animate);
// 如果已有事件正在处理中
if (isNil(core.status.event.id)) core.continueAutomaticRoute();
else core.clearContinueAutomaticRoute();
core.checkAutoEvents();
hook.emit('afterBattle', enemy, x, y);
}
);
}
declare global {
interface Enemys {
getCurrentEnemys(floorId: FloorIds): CurrentEnemy[];
canBattle(enemy: DamageEnemy, _?: number, floorId?: FloorIds): boolean;
canBattle(x: number, y: number, floorId?: FloorIds): boolean;
}
interface Events {
battle(
enemy: DamageEnemy,
y?: number,
force?: boolean,
callback?: () => void
): void;
battle(
x: number,
y?: number,
force?: boolean,
callback?: () => void
): void;
}
}

View File

@ -1,124 +0,0 @@
import { Patch, PatchClass } from '@motajs/legacy-common';
import { EnemyCollection, ensureFloorDamage } from '@user/data-state';
import { formatDamage } from '@user/data-utils';
import { isNil } from 'lodash-es';
export function patchDamage() {
const patch = new Patch(PatchClass.Control);
patch.add(
'updateDamage',
function (
floorId = core.status.floorId,
ctx,
thumbnail: boolean = false
) {
if (!floorId || core.status.gameOver || main.mode !== 'play')
return;
const onMap = isNil(ctx);
const floor = core.status.maps[floorId];
// 没有怪物手册
// if (!core.hasItem('book')) return;
core.status.damage.posX = core.bigmap.posX;
core.status.damage.posY = core.bigmap.posY;
if (!onMap) {
const width = core.floors[floorId].width,
height = core.floors[floorId].height;
// 地图过大的缩略图不绘制显伤
if (width * height > core.bigmap.threshold) return;
}
// 计算伤害
ensureFloorDamage(floorId);
floor.enemy.extract();
floor.enemy.calRealAttribute();
floor.enemy.calMapDamage();
floor.enemy.emit('calculated');
core.status.damage.data = [];
// floor.enemy.render(true);
// getItemDetail(floorId, onMap); // 宝石血瓶详细信息
if (thumbnail) {
renderThumbnailDamage(floor.enemy);
core.control.drawDamage(ctx, floorId);
}
}
);
}
function renderThumbnailDamage(col: EnemyCollection) {
if (main.replayChecking) return;
core.status.damage.data = [];
core.status.damage.extraData = [];
// 怪物伤害
col.list.forEach(v => {
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('@motajs/legacy-ui').mainSetting;
const criGem = setting.getValue('screen.criticalGem', false);
const n = critical?.atkDelta ?? Infinity;
const ratio = core.status.maps[col.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[col.floorId];
const width = floor.width;
const height = floor.height;
const objs = core.getMapBlocksObj(col.floorId);
const startX = 0;
const endX = width;
const startY = 0;
const endY = height;
for (let x = startX; x < endX; x++) {
for (let y = startY; y < endY; y++) {
const id = `${x},${y}` as LocString;
const dam = col.mapDamage[id];
if (!dam || objs[id]?.event.noPass) continue;
// 地图伤害
if (dam.damage !== 0) {
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 + 1) - 14,
color: '#fd4',
alpha: 1
});
}
}
}
}

View File

@ -1,12 +1,8 @@
import { ICoreState } from '@user/data-state';
import { patchBattle } from './battle';
import { patchDamage } from './damage';
import { patchFlags } from './flag';
import { patchHero } from './hero';
export function patchAll(state: ICoreState) {
patchBattle();
patchDamage();
patchFlags(state);
patchHero(state);
}

View File

@ -1,813 +0,0 @@
import { getHeroStatusOf, getHeroStatusOn } from '../legacy/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;
}
}

View File

@ -1,2 +1 @@
export * from './range';
export * from './utils';

View File

@ -1,86 +0,0 @@
import { isNil } from 'lodash-es';
interface RangeTypeData {
square: { x: number; y: number; d: number };
rect: { x: number; y: number; w: number; h: number };
manhattan: { x: number; y: number; d: number };
}
type InRangeFn<E extends Partial<Loc>, T> = (item: E, data: T) => boolean;
export class Range {
static rangeType: Record<string, RangeType> = {};
/**
*
* @param type
*/
type<T extends string>(
type: T
): T extends keyof RangeTypeData ? RangeType<RangeTypeData[T]> : RangeType {
return Range.rangeType[type] as T extends keyof RangeTypeData
? RangeType<RangeTypeData[T]>
: RangeType;
}
/**
*
* @param type
* @param fn
*/
static register<K extends keyof RangeTypeData>(
type: K,
fn: InRangeFn<Partial<Loc>, RangeTypeData[K]>
): void;
static register(type: string, fn: InRangeFn<Partial<Loc>, any>): void;
static register(type: string, fn: InRangeFn<Partial<Loc>, any>): void {
const range = new RangeType(type, fn);
this.rangeType[type] = range;
}
}
class RangeType<Type = any> {
readonly type: string;
/**
*
* @param item
* @param data
*/
readonly inRange: InRangeFn<Partial<Loc>, Type>;
constructor(type: string, fn: InRangeFn<Partial<Loc>, Type>) {
this.type = type;
this.inRange = fn;
}
/**
*
* @param items
* @param data
*/
scan<T extends Partial<Loc>>(items: Iterable<T>, data: Type): T[] {
const res: T[] = [];
for (const ele of items) {
if (this.inRange(ele, data)) {
res.push(ele);
}
}
return res;
}
}
Range.register('square', (item, { x, y, d }) => {
if (isNil(item.x) || isNil(item.y)) return false;
const r = Math.floor(d / 2);
return Math.abs(item.x - x) <= r && Math.abs(item.y - y) <= r;
});
Range.register('rect', (item, { x, y, w, h }) => {
if (isNil(item.x) || isNil(item.y)) return false;
const ex = x + w;
const ey = y + h;
return item.x >= x && item.y >= y && item.x < ex && item.y < ey;
});
Range.register('manhattan', (item, { x, y, d }) => {
if (isNil(item.x) || isNil(item.y)) return false;
return Math.abs(item.x - x) + Math.abs(item.y - y) < d;
});

View File

@ -1,91 +0,0 @@
import { DamageEnemy } from '@user/data-state';
import { findDir, ofDir } from '@user/data-utils';
export function createCheckBlock() {
// 地图伤害在这实现。2.C 会修改实现方式
control.prototype.checkBlock = function () {
const heroLoc = core.status.hero.loc;
const { x, y } = heroLoc;
const loc = `${x},${y}`;
const col = core.status.thisMap.enemy;
const info = col.mapDamage[loc];
if (!info) return;
const damage = info.damage;
// 阻击夹域伤害
if (damage) {
core.status.hero.hp -= damage;
const type = [...info.type];
const text = type.join('') || '伤害';
core.drawTip('受到' + text + damage + '点');
core.drawHeroAnimate('zone');
this._checkBlock_disableQuickShop();
core.status.hero.statistics.extraDamage += damage;
if (core.status.hero.hp <= 0) {
core.status.hero.hp = 0;
core.updateStatusBar();
core.events.lose();
return;
} else {
core.updateStatusBar();
}
}
const actions: MotaAction[] = [];
// 阻击效果
if (info.repulse) {
for (const [x, y] of info.repulse) {
const loc2 = { x, y };
const dir = findDir(heroLoc, loc2);
if (dir === 'none') continue;
const [nx, ny] = ofDir(x, y, dir);
if (core.noPass(nx, ny) || !core.canMoveHero(x, y, dir)) {
continue;
}
actions.push({
type: 'move',
time: 250,
keep: true,
loc: [x, y],
steps: [`${dir}:1`],
async: true
});
}
}
/** 存储要和哪些捕捉怪战斗 */
const ambushEnemies: DamageEnemy[] = [];
// 捕捉效果
if (info.ambush) {
for (const [x, y] of info.ambush) {
const loc2 = { x, y };
const dir = findDir(loc2, heroLoc);
if (dir === 'none') continue;
actions.push({
type: 'move',
time: 250,
keep: false,
loc: [x, y],
steps: [`${dir}:1`],
async: true
});
const enemy = col.get(x, y);
if (enemy) {
ambushEnemies.push(enemy);
}
}
}
if (actions.length > 0) {
actions.push({ type: 'waitAsync' });
// 与捕捉怪战斗
core.insertAction(actions, void 0, void 0, () => {
ambushEnemies.forEach(v => {
core.battle(v, v.y, true);
});
});
}
};
}

View File

@ -1,7 +0,0 @@
import { createCheckBlock } from './checkblock';
export function createEnemy() {
createCheckBlock();
}
export * from './checkblock';

View File

@ -4,12 +4,10 @@ import { initFiveLayer } from './fiveLayer';
import { createHook } from './hook';
import { initReplay } from './replay';
import { initUI } from './ui';
import { createEnemy } from './enemy';
export function createLegacy() {
initFallback();
loading.once('coreInit', () => {
createEnemy();
initFiveLayer();
createHook();
initReplay();
@ -17,7 +15,6 @@ export function createLegacy() {
});
}
export * from './enemy';
export * from './fallback';
export * from './fiveLayer';
export * from './removeMap';