import type { BattleSession } from 'db://assets/mx/module/battle'; import { AttrId, AttributeComp, CombatComp, TeamComp } from 'db://assets/mx/module/battle/ecs/Components'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { GEvent } from 'db://assets/mx/module/event/GEvent'; import { TableNames } from '../../game/ConfigTableData'; import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore'; import { BattleBuffFunctionType } from '../../manager/battleBuff/BattleBuffTypes'; import { BattleBuffTargetParse, parseBattleBuffSkillListMul, parseFunctionParNums, } from '../../manager/battleBuff/BattleBuffParamParse'; import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea'; import { isElementHero } from '../config/BattleHeroTable'; import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider'; import { BattleMapSpec } from '../map/BattleMapSpec'; import { getTowerLoadoutService } from './TowerLoadoutService'; import type { TowerCombatStats } from './TowerQueryTypes'; import { getPassiveSkillHost } from './PassiveSkillRuntime'; /** modifier source 前缀 */ const DIGIT_STAT_SOURCE = 'battle_buff_digit_'; /** 单塔战斗侧额外效果(105+) */ export interface DigitCombatFlags { /** 107:技能伤害倍数 */ skillMuls: { skillIds: number[]; mul: number }[]; /** 110:技能必暴(convert=暴击率转爆伤) */ forceCritSkills: { skillIds: number[]; convert: boolean }[]; /** 111:概率多倍伤 */ chanceMul: { wan: number; mul: number } | null; /** 112:条件必暴 */ condCrit: 'poison' | 'slow' | 'always_convert' | null; /** 108:穿透加值 */ pierceAdd: number; /** 109:额外箭 */ extraArrow: number; } /** 绑定所需最小战斗句柄 */ interface DigitStatBattleRef { /** 本机 playerId */ localPlayerId: number; /** 取玩家 */ getPlayer(playerId: number): { deployedUnits: readonly EntityId[] } | null | undefined; /** 会话 */ session: BattleSession; } /** 场上单位匹配快照 */ interface DigitUnitSnap { /** 实体 */ entityId: EntityId; /** 五行 */ heroType: number; /** 匹配等级 */ level: number; /** 同五行排名(1=最高) */ rank: number; } /** * 局内 functiontype 101~112:按 target/targetpar 给数字塔属性/技能/战斗标记。 */ export class BattleBuffDigitStatHost { private _battle: DigitStatBattleRef | null = null; private readonly _flags: { entityId: EntityId; flags: DigitCombatFlags }[] = []; /** 绑定本局并刷新已部署单位 */ bind(battle: DigitStatBattleRef): void { this._battle = battle; this.reapplyPlayer(battle.localPlayerId); this.reapplyPlayer(AI_PLAYER_ID); } /** 解除绑定 */ unbind(): void { this._battle = null; this._flags.length = 0; } /** * 取塔战斗标记(伤害管线 / 弓箭用)。 * @param entityId 塔 */ getFlags(entityId: EntityId): DigitCombatFlags | null { for (let i = 0; i < this._flags.length; i++) { if (this._flags[i].entityId === entityId) return this._flags[i].flags; } return null; } /** * 某玩家携带变更后重刷其场上单位。 * @param playerId 玩家 */ reapplyPlayer(playerId: number): void { const battle = this._battle; if (!battle) { console.warn('[BattleBuffDigitStat] reapply 未 bind', playerId); return; } const pid = Math.max(0, Math.floor(Number(playerId) || 0)); const session = battle.session; const seen: EntityId[] = []; const player = battle.getPlayer(pid); const deployed = player?.deployedUnits ?? []; for (let i = 0; i < deployed.length; i++) { const eid = deployed[i]; if (this._hasSeen(seen, eid)) continue; seen.push(eid); this.refreshEntity(session, eid, pid); } session.world.view2(CombatComp, TeamComp).forEach((entityId, _combat, team) => { if (team.ownerId !== pid) return; if (this._hasSeen(seen, entityId)) return; seen.push(entityId); this.refreshEntity(session, entityId, pid); }); if (this._hasDigitPassives(pid) && seen.length <= 0) { console.warn('[BattleBuffDigitStat] 有数字 buff 但场上无塔可刷', pid); } GEvent.Ins.emit(GEvent.UpdateBestPlace); } /** * 未部署/配表预览:把 101~104 叠到快照数值上(与 ECS 通道一致)。 * @param stats 基础战斗数值 * @param weapon1Id Hero.id * @param level 合成等级 * @param playerId 携带方 */ previewOntoStats(stats: TowerCombatStats, weapon1Id: number, level: number, playerId: number): TowerCombatStats { const hero = gg.data.table.getTableData(TableNames.Hero, weapon1Id); if (!hero) return stats; const heroType = this._resolveElementType(weapon1Id, hero); const lv = this._resolveMatchLevel(hero, level); const pid = Math.max(0, Math.floor(Number(playerId) || 0)); let attackMul = 1; let aspdAdd = 0; let critRateAdd = 0; let critDmgAdd = 0; let rangeAdd = 0; const passives = BattleBuffCarryStore.getPassives(pid); for (let i = 0; i < passives.length; i++) { const row = passives[i].row as ITableBattlebuff | undefined; if (!row) continue; const ft = Math.floor(Number(row.functiontype) || 0); if (!this._isStatFt(ft) && ft !== BattleBuffFunctionType.DigitRange) continue; if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, lv, 1)) continue; const nums = parseFunctionParNums(row.functionpar); const wan = nums.length > 0 ? Math.floor(nums[0]) : 0; if (ft === BattleBuffFunctionType.DigitDamage) { if (wan === 0) continue; attackMul *= 1 + wan / 10000; } else if (ft === BattleBuffFunctionType.DigitCritRate) { if (wan === 0) continue; critRateAdd += wan / 10000; } else if (ft === BattleBuffFunctionType.DigitCritDamage) { if (wan === 0) continue; critDmgAdd += wan / 10000; } else if (ft === BattleBuffFunctionType.DigitAttackSpeed) { if (wan === 0) continue; aspdAdd += wan; } else if (ft === BattleBuffFunctionType.DigitRange) { if (wan === 0) continue; rangeAdd += Math.abs(wan) <= 16 ? wan * BattleMapSpec.CELL_SIZE : wan; } } if (attackMul === 1 && aspdAdd === 0 && critRateAdd === 0 && critDmgAdd === 0 && rangeAdd === 0) return stats; const attackSpeed = stats.attackSpeed * (1 + aspdAdd / 10000); return { ...stats, attack: Math.floor(stats.attack * attackMul), attackSpeed, attackInterval: attackSpeed > 0 ? 1 / attackSpeed : 0, critRate: Math.min(1, stats.critRate + critRateAdd), critDamage: stats.critDamage + critDmgAdd, range: stats.range + rangeAdd, }; } /** * 单塔刷新 101~112。 * @param session 会话 * @param entityId 实体 * @param ownerPlayerId 归属 */ refreshEntity(session: BattleSession, entityId: EntityId, ownerPlayerId: number): void { this._clearDigitMods(session, entityId); this._clearFlags(entityId); const pid = Math.max(0, Math.floor(Number(ownerPlayerId) || 0)); const loadout = getTowerLoadoutService().getLoadout(entityId); if (!loadout) { getPassiveSkillHost().syncBuffSkills(session, entityId, []); return; } const hero = gg.data.table.getTableData(TableNames.Hero, loadout.weapon1Id); if (!hero) { console.warn('[BattleBuffDigitStat] Hero 缺失', loadout.weapon1Id, entityId); getPassiveSkillHost().syncBuffSkills(session, entityId, []); return; } const heroType = this._resolveElementType(loadout.weapon1Id, hero); const level = this._resolveMatchLevel(hero, loadout.level); const world = session.world; if (!world.isAlive(entityId)) return; if (!world.getComponent(entityId, CombatComp)) { console.warn('[BattleBuffDigitStat] 无 CombatComp', entityId, loadout.weapon1Id); getPassiveSkillHost().syncBuffSkills(session, entityId, []); return; } const snaps = this._buildSnaps(session, pid); let rank = 0; for (let i = 0; i < snaps.length; i++) { if (snaps[i].entityId === entityId) { rank = snaps[i].rank; break; } } const grantSkills: number[] = []; const flags = this._emptyFlags(); const passives = BattleBuffCarryStore.getPassives(pid); for (let i = 0; i < passives.length; i++) { const row = passives[i].row as ITableBattlebuff | undefined; if (!row) continue; if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, level, rank)) continue; const ft = Math.floor(Number(row.functiontype) || 0); const ft1 = Math.floor(Number(row.functiontype1) || 0); this._applyPrimary(session, entityId, row, ft, i, grantSkills, flags); this._applySecondary(row, ft1, flags); } this._setFlags(entityId, flags); getPassiveSkillHost().syncBuffSkills(session, entityId, grantSkills); } private _applyPrimary( session: BattleSession, entityId: EntityId, row: ITableBattlebuff, ft: number, index: number, grantSkills: number[], flags: DigitCombatFlags, ): void { const source = `${DIGIT_STAT_SOURCE}${row.id}_${index}`; const nums = parseFunctionParNums(row.functionpar); if (ft === BattleBuffFunctionType.DigitDamage || ft === BattleBuffFunctionType.DigitCritRate || ft === BattleBuffFunctionType.DigitCritDamage || ft === BattleBuffFunctionType.DigitAttackSpeed) { const wan = nums.length > 0 ? Math.floor(nums[0]) : 0; if (wan === 0) { console.warn('[BattleBuffDigitStat] functionpar 无效', row.id, row.functionpar); return; } this._applyStat(session, entityId, ft, wan, source); return; } if (ft === BattleBuffFunctionType.DigitGrantSkill) { const skillId = nums.length > 0 ? Math.floor(nums[0]) : 0; if (!(skillId > 0)) { console.warn('[BattleBuffDigitStat] ft105 缺 Skill.id', row.id, row.functionpar); return; } grantSkills.push(skillId); return; } if (ft === BattleBuffFunctionType.DigitRange) { const raw = nums.length > 0 ? Math.floor(nums[0]) : 0; if (raw === 0) { console.warn('[BattleBuffDigitStat] ft106 缺距离', row.id, row.functionpar); return; } const addPx = Math.abs(raw) <= 16 ? raw * BattleMapSpec.CELL_SIZE : raw; this._ensureAttr(session, entityId); session.attributes.addModifier(entityId, { attr: AttrId.Range, op: 'Add', value: addPx, source, }); return; } if (ft === BattleBuffFunctionType.DigitPierce) { const n = nums.length > 0 ? Math.floor(nums[0]) : 0; if (!(n > 0)) { console.warn('[BattleBuffDigitStat] ft108 缺穿透', row.id, row.functionpar); return; } flags.pierceAdd += n; return; } if (ft === BattleBuffFunctionType.DigitExtraArrow) { const n = nums.length > 0 ? Math.floor(nums[0]) : 0; if (!(n > 0)) { console.warn('[BattleBuffDigitStat] ft109 缺箭数', row.id, row.functionpar); return; } flags.extraArrow += n; return; } if (ft === BattleBuffFunctionType.DigitChanceMulDmg) { if (nums.length < 2) { console.warn('[BattleBuffDigitStat] ft111 需 概率,倍数', row.id, row.functionpar); return; } flags.chanceMul = { wan: Math.floor(nums[0]), mul: Math.floor(nums[1]) }; return; } if (ft === BattleBuffFunctionType.DigitCondForceCrit) { const t = Math.floor(Number(row.target) || 0); if (t === 2) flags.condCrit = 'poison'; else if (t === 3) flags.condCrit = 'slow'; else if (t === 4) flags.condCrit = 'always_convert'; else console.warn('[BattleBuffDigitStat] ft112 无法从 target 推断模式', row.id, t); return; } if (ft === BattleBuffFunctionType.DigitSkillDmgMul) { const parsed = parseBattleBuffSkillListMul(row.functionpar); if (!parsed) { console.warn('[BattleBuffDigitStat] ft107 functionpar 无效', row.id, row.functionpar); return; } flags.skillMuls.push({ skillIds: this._expandWeaponSkillIds(parsed.skillIds), mul: parsed.mul }); } } private _applySecondary(row: ITableBattlebuff, ft1: number, flags: DigitCombatFlags): void { if (!(ft1 > 0)) return; const parsed = parseBattleBuffSkillListMul(row.functionpar1); if (!parsed) { console.warn('[BattleBuffDigitStat] functiontype1 参数无效', row.id, ft1, row.functionpar1); return; } const skillIds = this._expandWeaponSkillIds(parsed.skillIds); if (ft1 === BattleBuffFunctionType.DigitSkillDmgMul) { flags.skillMuls.push({ skillIds, mul: parsed.mul }); return; } if (ft1 === BattleBuffFunctionType.DigitSkillForceCrit) { flags.forceCritSkills.push({ skillIds, convert: true }); return; } console.warn('[BattleBuffDigitStat] 未知 functiontype1', row.id, ft1); } /** * Weapon.id 列表展开为 act_skill(Skill.id),便于伤害管线按 Ability 匹配。 * @param weaponIds Weapon.id */ private _expandWeaponSkillIds(weaponIds: readonly number[]): number[] { const out: number[] = []; for (let i = 0; i < weaponIds.length; i++) { const wid = weaponIds[i]; const w = gg.data.table.getTableData(TableNames.Weapon, wid); if (!w) { console.warn('[BattleBuffDigitStat] 技能列表缺 Weapon', wid); continue; } const sid = Math.floor(Number(w.act_skill) || 0); if (!(sid > 0)) { console.warn('[BattleBuffDigitStat] Weapon 无 act_skill', wid); continue; } if (out.indexOf(sid) < 0) out.push(sid); } return out; } private _buildSnaps(session: BattleSession, playerId: number): DigitUnitSnap[] { const battle = this._battle; const out: DigitUnitSnap[] = []; const seen: EntityId[] = []; const push = (entityId: EntityId): void => { if (this._hasSeen(seen, entityId)) return; const loadout = getTowerLoadoutService().getLoadout(entityId); if (!loadout) return; const hero = gg.data.table.getTableData(TableNames.Hero, loadout.weapon1Id); if (!hero) return; if (!session.world.isAlive(entityId)) return; if (!session.world.getComponent(entityId, CombatComp)) return; seen.push(entityId); out.push({ entityId, heroType: this._resolveElementType(loadout.weapon1Id, hero), level: this._resolveMatchLevel(hero, loadout.level), rank: 0, }); }; const player = battle?.getPlayer(playerId); const deployed = player?.deployedUnits ?? []; for (let i = 0; i < deployed.length; i++) push(deployed[i]); session.world.view2(CombatComp, TeamComp).forEach((entityId, _c, team) => { if (team.ownerId !== playerId) return; push(entityId); }); for (let elem = 1; elem <= 5; elem++) { const group: DigitUnitSnap[] = []; for (let i = 0; i < out.length; i++) { if (out[i].heroType === elem) group.push(out[i]); } group.sort((a, b) => b.level - a.level || a.entityId - b.entityId); for (let i = 0; i < group.length; i++) group[i].rank = i + 1; } return out; } private _resolveMatchLevel(hero: ITableHero, loadoutLevel: number): number { if (isElementHero(hero)) { const hlv = Math.floor(Number(hero.level) || 0); if (hlv > 0) return hlv; } return Math.max(1, Math.floor(Number(loadoutLevel) || 0)); } private _hasSeen(seen: readonly EntityId[], eid: EntityId): boolean { for (let i = 0; i < seen.length; i++) { if (seen[i] === eid) return true; } return false; } private _hasDigitPassives(playerId: number): boolean { const passives = BattleBuffCarryStore.getPassives(playerId); for (let i = 0; i < passives.length; i++) { const row = passives[i].row as ITableBattlebuff | undefined; if (!row) continue; const ft = Math.floor(Number(row.functiontype) || 0); const ft1 = Math.floor(Number(row.functiontype1) || 0); if (this._isDigitFt(ft) || ft1 > 0) return true; } return false; } private _isStatFt(ft: number): boolean { return ft === BattleBuffFunctionType.DigitDamage || ft === BattleBuffFunctionType.DigitCritRate || ft === BattleBuffFunctionType.DigitCritDamage || ft === BattleBuffFunctionType.DigitAttackSpeed; } private _isDigitFt(ft: number): boolean { return this._isStatFt(ft) || ft === BattleBuffFunctionType.DigitGrantSkill || ft === BattleBuffFunctionType.DigitRange || ft === BattleBuffFunctionType.DigitPierce || ft === BattleBuffFunctionType.DigitExtraArrow || ft === BattleBuffFunctionType.DigitChanceMulDmg || ft === BattleBuffFunctionType.DigitCondForceCrit || ft === BattleBuffFunctionType.DigitSkillDmgMul || ft === BattleBuffFunctionType.DigitSkillForceCrit; } private _resolveElementType(weapon1Id: number, hero: ITableHero): number { const provider = WeaponComposeConfigProvider.instance; provider.load(); const w2 = provider.getWeapon2IdForWeapon1(weapon1Id); if (w2 != null) { const t = Math.floor(Number(provider.getWeapon2Type(w2)) || 0); if (t >= 1 && t <= 5) return t; } return Math.floor(Number(hero.herotype) || 0); } private _clearDigitMods(session: BattleSession, entityId: EntityId): void { const world = session.world; if (!world.isAlive(entityId)) return; const attr = world.getComponent(entityId, AttributeComp); if (!attr) return; const next = []; for (let i = 0; i < attr.modifiers.length; i++) { const src = String(attr.modifiers[i].source ?? ''); if (src.indexOf(DIGIT_STAT_SOURCE) === 0) continue; next.push(attr.modifiers[i]); } attr.modifiers = next; } private _ensureAttr(session: BattleSession, entityId: EntityId): void { if (!session.world.getComponent(entityId, AttributeComp)) { session.world.addComponent(entityId, new AttributeComp()); } } private _applyStat(session: BattleSession, entityId: EntityId, ft: number, wan: number, source: string): void { const world = session.world; if (!world.isAlive(entityId)) return; if (!world.getComponent(entityId, CombatComp)) return; this._ensureAttr(session, entityId); const attrSvc = session.attributes; if (ft === BattleBuffFunctionType.DigitDamage) { attrSvc.addModifier(entityId, { attr: AttrId.Attack, op: 'Mul', value: 1 + wan / 10000, source, }); return; } if (ft === BattleBuffFunctionType.DigitCritRate) { attrSvc.addModifier(entityId, { attr: AttrId.CritRate, op: 'Add', value: wan / 10000, source, }); return; } if (ft === BattleBuffFunctionType.DigitCritDamage) { attrSvc.addModifier(entityId, { attr: AttrId.CritDamage, op: 'Add', value: wan / 10000, source, }); return; } if (ft === BattleBuffFunctionType.DigitAttackSpeed) { attrSvc.addModifier(entityId, { attr: 'aspd_add', op: 'Add', value: wan, source, }); } } private _emptyFlags(): DigitCombatFlags { return { skillMuls: [], forceCritSkills: [], chanceMul: null, condCrit: null, pierceAdd: 0, extraArrow: 0, }; } private _clearFlags(entityId: EntityId): void { for (let i = this._flags.length - 1; i >= 0; i--) { if (this._flags[i].entityId === entityId) this._flags.splice(i, 1); } } private _setFlags(entityId: EntityId, flags: DigitCombatFlags): void { this._clearFlags(entityId); const empty = flags.skillMuls.length <= 0 && flags.forceCritSkills.length <= 0 && !flags.chanceMul && !flags.condCrit && flags.pierceAdd <= 0 && flags.extraArrow <= 0; if (empty) return; this._flags.push({ entityId, flags }); } } let _host: BattleBuffDigitStatHost | null = null; /** 局内数字属性 Host */ export function getBattleBuffDigitStatHost(): BattleBuffDigitStatHost { if (!_host) _host = new BattleBuffDigitStatHost(); return _host; }