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, parseFunctionParNums } from '../../manager/battleBuff/BattleBuffParamParse'; import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea'; import { isElementHero } from '../config/BattleHeroTable'; import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider'; import { getTowerLoadoutService } from './TowerLoadoutService'; import type { TowerCombatStats } from './TowerQueryTypes'; /** modifier source 前缀 */ const DIGIT_STAT_SOURCE = 'battle_buff_digit_'; /** 绑定所需最小战斗句柄(避免 import BattleRuntime 造成循环) */ interface DigitStatBattleRef { /** 本机 playerId */ localPlayerId: number; /** 取玩家 */ getPlayer(playerId: number): { deployedUnits: readonly EntityId[] } | null | undefined; /** 会话 */ session: BattleSession; } /** * 局内 functiontype 101~104:按 target/targetpar 给数字塔加伤/暴击/爆伤/攻速。 */ export class BattleBuffDigitStatHost { private _battle: DigitStatBattleRef | null = null; /** 绑定本局并刷新已部署单位 */ bind(battle: DigitStatBattleRef): void { this._battle = battle; this.reapplyPlayer(battle.localPlayerId); this.reapplyPlayer(AI_PLAYER_ID); } /** 解除绑定 */ unbind(): void { this._battle = 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); } // deployedUnits 与 ECS 偶发不同步时,按 TeamComp+CombatComp 补刷 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] 有 101~104 但场上无塔可刷', 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; 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._isDigitFt(ft)) continue; if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, lv)) continue; const nums = parseFunctionParNums(row.functionpar); const wan = nums.length > 0 ? Math.floor(nums[0]) : 0; if (wan === 0) continue; if (ft === BattleBuffFunctionType.DigitDamage) attackMul *= 1 + wan / 10000; else if (ft === BattleBuffFunctionType.DigitCritRate) critRateAdd += wan / 10000; else if (ft === BattleBuffFunctionType.DigitCritDamage) critDmgAdd += wan / 10000; else if (ft === BattleBuffFunctionType.DigitAttackSpeed) aspdAdd += wan; } if (attackMul === 1 && aspdAdd === 0 && critRateAdd === 0 && critDmgAdd === 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, }; } /** * 单塔刷新 101~104 修正。 * @param session 会话 * @param entityId 实体 * @param ownerPlayerId 归属 */ refreshEntity(session: BattleSession, entityId: EntityId, ownerPlayerId: number): void { this._clearDigitMods(session, entityId); const pid = Math.max(0, Math.floor(Number(ownerPlayerId) || 0)); if (!this._hasDigitPassives(pid)) return; const loadout = getTowerLoadoutService().getLoadout(entityId); if (!loadout) return; const hero = gg.data.table.getTableData(TableNames.Hero, loadout.weapon1Id); if (!hero) { console.warn('[BattleBuffDigitStat] Hero 缺失', loadout.weapon1Id, 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); return; } this._applyPassivesToEntity(session, entityId, pid, heroType, level); } /** * 匹配用等级:五行以 Hero.level 为准(weapon1Id 已定级)。 * @param hero Hero 行 * @param loadoutLevel 装载等级 */ 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)); } /** * @param session 会话 * @param entityId 实体 * @param playerId 携带方 * @param heroType 五行 * @param level 等级 */ private _applyPassivesToEntity(session: BattleSession, entityId: EntityId, playerId: number, heroType: number, level: number): void { 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); if (!this._isDigitFt(ft)) continue; if (!BattleBuffTargetParse.matches(row.target, row.targetpar, heroType, level)) continue; const nums = parseFunctionParNums(row.functionpar); const wan = nums.length > 0 ? Math.floor(nums[0]) : 0; if (wan === 0) { console.warn('[BattleBuffDigitStat] functionpar 无效', row.id, row.functionpar); continue; } if (wan > 0 && wan < 100) { console.warn('[BattleBuffDigitStat] functionpar 过小(疑非万分比)', row.id, wan, row.desc); } this._applyOne(session, entityId, ft, wan, `${DIGIT_STAT_SOURCE}${row.id}_${i}`); } } 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; if (this._isDigitFt(Math.floor(Number(row.functiontype) || 0))) return true; } return false; } private _isDigitFt(ft: number): boolean { return ft === BattleBuffFunctionType.DigitDamage || ft === BattleBuffFunctionType.DigitCritRate || ft === BattleBuffFunctionType.DigitCritDamage || ft === BattleBuffFunctionType.DigitAttackSpeed; } /** * 五行 type:优先 Text.type,否则 Hero.herotype。 * @param weapon1Id Hero.id * @param hero Hero 行 */ 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 _applyOne(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; if (!world.getComponent(entityId, AttributeComp)) { world.addComponent(entityId, new AttributeComp()); } 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, }); } } } let _host: BattleBuffDigitStatHost | null = null; /** 局内数字属性 Host */ export function getBattleBuffDigitStatHost(): BattleBuffDigitStatHost { if (!_host) _host = new BattleBuffDigitStatHost(); return _host; }