You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/battle/combat/SkillAtkBuffExecutor.ts

499 lines
19 KiB

import type { AbilityContext } from 'db://assets/mx/module/battle/combat/AbilityContext';
import type { BattleServices } from 'db://assets/mx/module/battle/combat/BattleServices';
import type { EntityId, World } from 'db://assets/mx/module/ecs';
import type { ProjectBattleHandle } from '../runtime/BattleRuntime';
import { BATTLE_MONSTER_CCCLASS, type IBattleMonsterView } from '../view/IBattleMonsterView';
import {
AttrId,
AttributeComp,
CombatComp,
HealthComp,
MovementComp,
TeamComp,
} from 'db://assets/mx/module/battle/ecs/Components';
import { VAR_LAST_DAMAGE_AMOUNT } from 'db://assets/mx/module/battle/combat/DamagePipeline';
import { EffectRegistry } from 'db://assets/mx/module/battle/registry/EffectRegistry';
import { BattleMapSpec } from '../map/BattleMapSpec';
import { applySkillTableKnockback } from './AtkHookEffects';
import { applySkillAtkBuffBoardEffect } from './SkillAtkBuffBoardBridge';
import { getMonsterStatusText, STATUS_TEXT_HP_CAP } from '../view/MonsterStatusText';
import { getAllySkillExtraHost } from '../presentation/AllySkillExtraHost';
const SKILL_ATK_BUFF_FLAG = '__mx_battle_skillAtkBuffRegistered__';
const MOD_SPEED = 'skill_atk_buff_speed';
const MOD_ASPD = 'skill_atk_buff_aspd';
const MOD_RANGE = 'skill_atk_buff_range';
/** 单条 Skill.atk_buff 执行上下文 */
export interface SkillAtkBuffHitOpts {
/** 本次命中实际伤害(击退等判定) */
lastDamage?: number;
/** 来自被动 result:DoT 用 passive_ 前缀,不回触发 condition */
fromPassiveResult?: boolean;
}
/** 注册 Skill 表 atk_buff 效果 */
export function registerSkillAtkBuffEffects(): void {
const g = globalThis as unknown as Record<string, boolean | undefined>;
if (g[SKILL_ATK_BUFF_FLAG]) return;
g[SKILL_ATK_BUFF_FLAG] = true;
EffectRegistry.register('ApplySkillAtkBuff', (services, ctx, params) => {
const buff1 = String(params.buff1 ?? '');
const buff2 = String(params.buff2 ?? '');
const opts: SkillAtkBuffHitOpts = { lastDamage: Number(params.lastDamage) || 0 };
const targets = ctx.targets;
for (let i = 0; i < targets.length; i++) {
SkillAtkBuffExecutor.applyPair(services, ctx, targets[i], buff1, buff2, opts);
}
});
}
/** Skill.atk_buff1/2 执行器(配表语义见 AutoTable ITableSkill) */
export class SkillAtkBuffExecutor {
/** 命中后依次执行 buff1、buff2 */
static applyPair(
services: BattleServices,
ctx: AbilityContext,
target: EntityId,
buff1: string,
buff2: string,
opts?: SkillAtkBuffHitOpts,
): void {
if (!services.world.isAlive(target)) return;
this.applyRaw(services, ctx, target, buff1, opts);
this.applyRaw(services, ctx, target, buff2, opts);
}
/** 召唤出生:仅移速/血量(type 3/4) */
static applyOnSpawn(services: BattleServices, entity: EntityId, raw: string): void {
const parts = this._parse(raw);
if (!parts.length) return;
const kind = parts[0];
if (kind === 3 || kind === 4) {
this.applyRaw(services, null, entity, raw, {});
return;
}
if (kind > 0) {
console.log('[SkillAtkBuff] 召唤出生仅支持 type 3/4');
}
}
/**
* 武器被动 atk_buff type 11 的射程像素(≤16 按格)。
* 计入 deriveTowerSetupStats,部署时不再走 modifier,避免 getAttr 加两次。
*/
static loadoutRangeAddPx(buff1: string, buff2: string): number {
return this._rangeAddFromRaw(buff1) + this._rangeAddFromRaw(buff2);
}
/** 清掉 skill_atk_buff_range,避免刷新时残留旧的 +1 格 */
static clearRangeModifiers(services: BattleServices, entity: EntityId): void {
services.attributes.removeModifiersByGroup(entity, MOD_RANGE);
}
/** 部署时静态 buff;type 11 已计入 setup.range,此处跳过 */
static applyLoadoutBuffs(services: BattleServices, entity: EntityId, buff1: string, buff2: string): void {
this._applyLoadoutRaw(services, entity, buff1);
this._applyLoadoutRaw(services, entity, buff2);
}
private static _applyLoadoutRaw(services: BattleServices, entity: EntityId, raw: string): void {
const parts = this._parse(raw);
if (!parts.length) return;
// 11 射程已计入 setup;13 冰冻 / 14 点燃仅命中施加
if (parts[0] === 11 || parts[0] === 13 || parts[0] === 14) return;
this._applyKind(services, null, entity, parts, {});
}
private static _rangeAddFromRaw(raw: string): number {
const parts = this._parse(raw);
if (parts[0] !== 11) return 0;
const addVal = Math.floor(parts[1] || 0);
if (addVal === 0) return 0;
return Math.abs(addVal) <= 16 ? addVal * BattleMapSpec.CELL_SIZE : addVal;
}
private static applyRaw(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
raw: string,
opts?: SkillAtkBuffHitOpts,
): void {
const parts = this._parse(raw);
if (!parts.length) return;
const kind = parts[0] || 0;
if (!this._roll(services, ctx, this._chanceWanForKind(kind, parts))) return;
this._applyKind(services, ctx, target, parts, opts ?? {});
}
/** 无概率字段的类型恒为 100% */
private static _chanceWanForKind(kind: number, parts: number[]): number {
if (kind === 5 || kind === 9 || kind === 10 || kind === 11 || kind === 14) return 10000;
return parts[1] ?? 10000;
}
private static _parse(raw: string): number[] {
const s = String(raw ?? '').trim();
if (!s) return [];
return s.split(',').map((x) => Number(String(x).trim()));
}
private static _roll(services: BattleServices, ctx: AbilityContext | null, chanceWan: number): boolean {
const wan = Math.max(0, Number(chanceWan) || 0);
if (wan >= 10000) return true;
const rng = ctx?.rng ?? services.rng;
return rng.next() * 10000 < wan;
}
private static _applyKind(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
parts: number[],
opts: SkillAtkBuffHitOpts,
): void {
const kind = parts[0] || 0;
switch (kind) {
case 1:
this._stun(services, ctx, target, parts);
break;
case 2:
this._knockback(services, ctx, target, parts, opts);
break;
case 3:
this._moveSpeed(services, target, parts[2] ?? 0, parts[3] ?? 0);
break;
case 4:
this._maxHp(services, target, parts[2] ?? 0);
break;
case 5:
this._peaches(services, ctx, parts[1] ?? 0);
break;
case 6:
this._attackSpeed(services, target, parts[2] ?? 0, parts[3] ?? 0);
break;
case 7:
this._lowerLevel(services, ctx, target, parts[2] ?? 1, parts[3] ?? 0);
break;
case 8:
this._confusion(services, ctx, target, parts[2] ?? 0);
break;
case 9:
this._blockTile(services, ctx);
break;
case 10:
this._heal(services, ctx, target, parts[1] ?? 0);
break;
case 11:
this._attackRange(services, target, parts[1] ?? 0, parts[2] ?? -1);
break;
case 13:
this._freeze(services, ctx, target, parts);
break;
case 14:
this._ignite(services, ctx, target, parts, opts);
break;
default:
console.log('[SkillAtkBuff] 未支持的类型', kind, parts.join(','));
break;
}
}
/**
* 冰冻:定身 + 禁施法(debuff_13)。
* @param services 战斗服务
* @param ctx 施法上下文
* @param target 目标
* @param parts `13,概率,冰冻秒`
*/
private static _freeze(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
parts: number[],
): void {
const duration = Math.max(0, Number(parts[2]) || 0);
if (!(duration > 0)) return;
if (!services.config.getBuff('debuff_13')) {
console.log('[SkillAtkBuff] 缺 buff debuff_13');
return;
}
const caster = ctx?.caster ?? target;
services.buffs.add(target, 'debuff_13', caster, 1, { duration });
}
/**
* 点燃:立即首跳,再按间隔跳至结束。
* @param services 战斗服务
* @param ctx 施法上下文(缺则跳过)
* @param target 目标
* @param parts 14,间隔秒,伤害万分比,持续秒
* @param opts 命中选项
*/
private static _ignite(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
parts: number[],
opts: SkillAtkBuffHitOpts,
): void {
if (!ctx) {
console.log('[SkillAtkBuff] 点燃需命中上下文', parts.join(','));
return;
}
const interval = Number(parts[1]);
const coeffWan = Number(parts[2]);
const duration = Number(parts[3]);
if (!(interval > 0) || !(coeffWan > 0) || !(duration > 0)) {
console.log('[SkillAtkBuff] 点燃参数无效(间隔,系数万分比,持续)', parts.join(','));
return;
}
if (!services.config.getBuff('debuff_1')) {
console.log('[SkillAtkBuff] 缺 buff debuff_1');
return;
}
const scale = coeffWan / 10000;
const caster = ctx.caster;
const fromPassive = opts.fromPassiveResult === true;
const damageType = fromPassive ? 'passive_burn' : 'burn_dot';
services.buffs.add(target, 'debuff_1', caster, 1, { duration });
const endAt = (services.clock.time ?? 0) + duration;
const tick = (): void => {
if (!services.world.isAlive(caster) || !services.world.isAlive(target)) return;
if ((services.clock.time ?? 0) > endAt) return;
services.damage.dealToTarget(caster, target, {
scale,
allowCrit: false,
damageType,
fromPassiveResult: fromPassive,
}, ctx);
if ((services.clock.time ?? 0) + interval <= endAt) {
services.clock.schedule(interval, tick);
}
};
tick();
}
private static _stun(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
parts: number[],
): void {
const duration = Math.max(0, Number(parts[2]) || 0);
if (!(duration > 0)) return;
if (!services.config.getBuff('debuff_4')) return;
const caster = ctx?.caster ?? target;
services.buffs.add(target, 'debuff_4', caster, 1, { duration });
}
private static _knockback(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
parts: number[],
opts: SkillAtkBuffHitOpts,
): void {
if (!ctx) return;
const pixels = Math.max(0, Number(parts[2]) || 0);
if (!(pixels > 0)) return;
const lastDamage = opts.lastDamage ?? ctx.vars.get(VAR_LAST_DAMAGE_AMOUNT) ?? 0;
if (!(lastDamage > 0)) return;
applySkillTableKnockback(services, ctx, target, pixels);
}
private static _moveSpeed(services: BattleServices, target: EntityId, bonusWan: number, durationSec: number): void {
if (bonusWan === 0) return;
this._ensureAttr(target, services);
const mul = 1 + bonusWan / 10000;
services.attributes.addModifier(target, {
attr: 'move_speed_mul',
op: 'Mul',
value: mul,
source: MOD_SPEED,
group: MOD_SPEED,
remain: durationSec > 0 ? durationSec : undefined,
});
const move = services.world.getComponent(target, MovementComp);
if (move) {
const base = move.baseSpeed > 0 ? move.baseSpeed : move.speed;
move.speed = Math.max(0, base * mul);
}
getMonsterStatusText().play(target, bonusWan > 0 ? '加速' : '减速');
if (bonusWan > 0 && durationSec > 0) {
getAllySkillExtraHost().playSpeedFx(target, durationSec);
}
}
/**
* 配表血量上限:maxHp ×(1+万分比/10000),当前血量按原比例同步。
* 允许时播 `血量上限` 并飘「血上限」,不播 `怪物加血`;出生加成可关。
* @param world 实体世界
* @param target 目标
* @param coeffWan 上限系数万分比(2000=+20%)
* @param battle 战斗句柄(用于刷新血条/特效)
* @param playHeal 是否播血上限表现
*/
static applyMaxHpCoeff(world: World, target: EntityId, coeffWan: number, battle?: ProjectBattleHandle | null, playHeal = true): void {
if (coeffWan === 0) return;
const health = world.getComponent(target, HealthComp);
if (!health || !(health.maxHp > 0)) return;
const oldHp = health.hp;
const oldMax = health.maxHp;
const newMax = Math.max(1, Math.floor(oldMax * (1 + coeffWan / 10000)));
const ratio = oldHp / oldMax;
const newHp = Math.max(1, Math.min(newMax, Math.floor(newMax * ratio)));
health.maxHp = newMax;
health.hp = newHp;
this._presentHp(target, newHp, newMax, battle, playHeal, STATUS_TEXT_HP_CAP);
}
/**
* 刷新血条;加血播 `怪物加血`,血上限播 `血量上限`(不叠加加血)。
* @param entity 目标
* @param newHp 变更后当前血量
* @param newMax 变更后上限
* @param battle 战斗句柄;缺省用 CurentBattle
* @param playFx 是否播特效
* @param statusText 飘字文案;缺省「加血」
*/
private static _presentHp(entity: EntityId, newHp: number, newMax: number, battle?: ProjectBattleHandle | null, playFx = true, statusText = '加血'): void {
const handle = battle ?? (gg.game.CurentBattle as ProjectBattleHandle | null | undefined);
const node = handle?.getMonsterPresenter?.()?.getEntityNodeMap().get(entity);
if (!node?.isValid) return;
const view = node.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView | null;
view?.syncMxHp?.(newHp, newMax);
if (!playFx) return;
if (statusText === STATUS_TEXT_HP_CAP) {
if (!view?.playHpCapEffect) {
console.warn('[SkillAtkBuff] 缺 playHpCapEffect');
return;
}
view.playHpCapEffect();
return;
}
view?.playHealEffect?.(statusText);
}
private static _maxHp(services: BattleServices, target: EntityId, coeffWan: number): void {
this.applyMaxHpCoeff(services.world, target, coeffWan);
}
private static _peaches(services: BattleServices, ctx: AbilityContext | null, count: number): void {
const n = Math.max(0, Math.floor(Number(count) || 0));
if (!(n > 0)) return;
const ownerId = ctx ? this._ownerId(services, ctx.caster) : 0;
applySkillAtkBuffBoardEffect(services, {
kind: 5,
ownerPlayerId: ownerId,
peachCount: n,
target: ctx?.caster,
});
}
/**
* 攻速万分比走 aspd_add(与攻速符/被动同一通道);负值降攻速。
* 普攻间隔读 getFinalAttackSpeed,写 AttrId.AttackSpeed Mul 不会生效。
* @param services 战斗服务
* @param target 目标塔
* @param bonusWan 攻速加成万分比(-5000=×0.5)
* @param durationSec 持续秒;≤0 永久
*/
private static _attackSpeed(services: BattleServices, target: EntityId, bonusWan: number, durationSec: number): void {
if (bonusWan === 0) return;
this._ensureAttr(target, services);
services.attributes.addModifier(target, {
attr: 'aspd_add',
op: 'Add',
value: bonusWan,
source: MOD_ASPD,
group: MOD_ASPD,
remain: durationSec > 0 ? durationSec : undefined,
});
}
private static _lowerLevel(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
delta: number,
durationSec: number,
): void {
const d = Math.max(1, Math.floor(Number(delta) || 1));
const ownerId = ctx ? this._ownerId(services, ctx.caster) : 0;
applySkillAtkBuffBoardEffect(services, {
kind: 7,
ownerPlayerId: ownerId,
target,
levelDelta: -d,
durationSec,
});
}
private static _confusion(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
durationSec: number,
): void {
const duration = Math.max(0.05, Number(durationSec) || 0);
if (!services.config.getBuff('debuff_8')) return;
const caster = ctx?.caster ?? target;
services.buffs.add(target, 'debuff_8', caster, 1, { duration });
}
private static _blockTile(services: BattleServices, ctx: AbilityContext | null): void {
const ownerId = ctx ? this._ownerId(services, ctx.caster) : 0;
applySkillAtkBuffBoardEffect(services, { kind: 9, ownerPlayerId: ownerId });
}
private static _heal(
services: BattleServices,
ctx: AbilityContext | null,
target: EntityId,
healWan: number,
): void {
const wan = Math.max(0, Number(healWan) || 0);
if (!(wan > 0)) return;
const health = services.world.getComponent(target, HealthComp);
if (!health || !(health.maxHp > 0)) return;
const add = Math.max(1, Math.floor(health.maxHp * wan / 10000));
health.hp = Math.min(health.maxHp, health.hp + add);
this._presentHp(target, health.hp, health.maxHp);
}
/**
* 攻击距离:只走 modifier。禁止再改 CombatComp.range,否则 sync 进 base 后 getAttr 会加两次。
* @param services 战斗服务
* @param target 受益实体
* @param addRaw ≤16 按格,否则按像素
* @param durationSec -1 本场永久
*/
private static _attackRange(services: BattleServices, target: EntityId, addRaw: number, durationSec: number): void {
const addVal = Math.floor(Number(addRaw) || 0);
if (addVal === 0) return;
const addPx = Math.abs(addVal) <= 16 ? addVal * BattleMapSpec.CELL_SIZE : addVal;
this._ensureAttr(target, services);
services.attributes.addModifier(target, {
attr: AttrId.Range,
op: 'Add',
value: addPx,
source: MOD_RANGE,
group: MOD_RANGE,
remain: durationSec < 0 ? undefined : (durationSec > 0 ? durationSec : undefined),
});
}
private static _ensureAttr(target: EntityId, services: BattleServices): void {
if (!services.world.getComponent(target, AttributeComp)) {
services.world.addComponent(target, new AttributeComp());
}
}
private static _ownerId(services: BattleServices, entity: EntityId): number {
const team = services.world.getComponent(entity, TeamComp);
return team?.ownerId ?? team?.team ?? 0;
}
}