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; } /** 注册 Skill 表 atk_buff 效果 */ export function registerSkillAtkBuffEffects(): void { const g = globalThis as unknown as Record; 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; if (parts[0] === 11) 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) 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; default: console.log('[SkillAtkBuff] 未支持的类型', kind, parts.join(',')); break; } } 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; } }