import type { BattleSession } from 'db://assets/mx/module/battle'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { AbilitySetComp, AtkHookComp, CombatComp, UnitMetaComp, } from 'db://assets/mx/module/battle/ecs/Components'; import { entityHeroAbilityId, entityNormalAbilityId, entityWeaponAbilityId, normalAbilityId, } from '../config/weapon1/Weapon1BattleIds'; import type { Player } from '../../game/Player'; import { TableNames } from '../../game/ConfigTableData'; import { deriveTowerSetupStats } from './TowerStatResolver'; import type { TowerLoadout } from './TowerQueryTypes'; export type { TowerLoadout }; import { SkillAbilityBuilder } from '../config/baseAtk/SkillAbilityBuilder'; import { heroNormalSkillId, heroUltSkillId, isElementHero } from '../config/BattleHeroTable'; import { dumpKnifeFinalAttack, logKnifeBaseAttack } from '../debug/AtkDebugLogCore'; import { isAtkDebugWeapon } from '../debug/AtkDebugConstants'; import { getPassiveSkillHost } from './PassiveSkillRuntime'; import { SkillAtkBuffExecutor } from './SkillAtkBuffExecutor'; import { TableAnomaly } from '../../game/TableAnomaly'; import { GemBattleEffect } from './GemBattleEffect'; /** AI 侧武将武器解析(由 AiBattleBridge start/stop 注册,避免循环 require) */ let _aiHeroWeaponResolver: ((heroId: number) => number | null) | null = null; /** * 注册/清除 AI 武将武器解析器。 * @param resolver 解析函数;传 null 清除 */ export function setAiHeroWeaponResolver(resolver: ((heroId: number) => number | null) | null): void { _aiHeroWeaponResolver = resolver; } /** * 解析英雄当前战斗武器/技能宝石 id。 * 五行数字:本机读镶嵌技能宝石;非五行:本机装备 / AI 掷骰。 */ export function resolveHeroBattleWeaponId(heroId: number, player?: Player | null): number { const hero = gg.data.table.getTableData(TableNames.Hero, heroId); if (isElementHero(hero)) { if (player?.isLocal) { return GemBattleEffect.resolve(Math.floor(Number(hero!.herotype)), player).skillGemId; } return 0; } if (player && !player.isLocal) { if (_aiHeroWeaponResolver) { const aiId = _aiHeroWeaponResolver(heroId); if (typeof aiId === 'number' && aiId > 0) return aiId; } return Number(hero?.startweapon) || 0; } try { const id = gg.equip?.getHeroDisplayWeaponId?.(heroId); if (typeof id === 'number' && id > 0) return id; } catch { /* equip 未就绪 */ } return Number(hero?.startweapon) || 0; } export class TowerLoadoutService { private readonly _byEntity: { entityId: EntityId; loadout: TowerLoadout }[] = []; private _findIndex(entityId: EntityId): number { for (let i = 0; i < this._byEntity.length; i++) { if (this._byEntity[i].entityId === entityId) return i; } return -1; } initEntity(entityId: EntityId, heroId: number, level: number, player: Player): void { const hero = gg.data.table.getTableData(TableNames.Hero, heroId); if (!hero) return; let weaponId = 0; let weaponSkillId = 0; if (isElementHero(hero)) { const gem = GemBattleEffect.resolve(Math.floor(Number(hero.herotype)), player); weaponId = gem.skillGemId; weaponSkillId = gem.skillId; } else { weaponId = resolveHeroBattleWeaponId(heroId, player); const weaponRow = weaponId > 0 ? gg.data.table.getTableData(TableNames.Weapon, weaponId) : null; weaponSkillId = Number(weaponRow?.act_skill) || 0; } let weaponSkillActive = false; if (weaponSkillId > 0) { const sk = gg.data.table.getTableData(TableNames.Skill, weaponSkillId); if (!sk) { console.log(`[TowerLoadout] 武器技能缺失 Skill.id=${weaponSkillId} weaponId=${weaponId}`); } else { weaponSkillActive = Number(sk.type) === 1; } } const loadout: TowerLoadout = { weapon1Id: heroId, level, weaponId, normalBaseAtkId: heroNormalSkillId(hero, level), overAtkType: '', overAtkPara: '', activeSkillBaseAtkId: heroUltSkillId(hero, level), weaponSkillId, weaponSkillActive, }; const idx = this._findIndex(entityId); if (idx >= 0) this._byEntity[idx].loadout = loadout; else this._byEntity.push({ entityId, loadout }); } removeEntity(entityId: EntityId): void { const idx = this._findIndex(entityId); if (idx >= 0) this._byEntity.splice(idx, 1); getPassiveSkillHost().unbind(entityId); } /** 退局清空全部装载(被动/弓由 BattleRuntime.destroy 已 clear) */ clear(): void { this._byEntity.length = 0; } getLoadout(entityId: EntityId): TowerLoadout | undefined { const idx = this._findIndex(entityId); return idx >= 0 ? this._byEntity[idx].loadout : undefined; } /** @deprecated 词条替换已移除 */ replaceActiveSkillForPlayer( _session: BattleSession, _playerId: number, _weapon1Id: number, _baseAtkId: number, ): void { /* no-op:词条表已删 */ } /** @deprecated 词条 hook 已移除 */ appendEntryAtkHooks(_session: BattleSession, _entityId: EntityId, _entry: ITableSkill): void { /* no-op */ } /** @deprecated */ applySkillSpecial(_session: BattleSession, _entityId: EntityId, _entry: ITableSkill): void { /* no-op */ } rebuildHeroAbility(entityId: EntityId, loadout: TowerLoadout): void { if (loadout.activeSkillBaseAtkId <= 0) return; const row = gg.data.table.getTableData(TableNames.Skill, loadout.activeSkillBaseAtkId); if (!row) { console.log(`[TowerLoadout] 英雄技能缺失 Skill.id=${loadout.activeSkillBaseAtkId}`); return; } const abilityId = entityHeroAbilityId(loadout.activeSkillBaseAtkId, entityId); const def = SkillAbilityBuilder.buildHero(row, entityId); def.id = abilityId; if (def.costs?.[0]?.params) def.costs[0].params.abilityId = abilityId; gg.battle.config.registerAbility(abilityId, def); this._stampLoadoutPresentation(def, loadout, 'hero'); this.patchActiveSkillCooldown(null, entityId, abilityId, loadout.activeSkillBaseAtkId); } rebuildWeaponAbility(entityId: EntityId, loadout: TowerLoadout): void { if (!loadout.weaponSkillActive || loadout.weaponSkillId <= 0) return; const row = gg.data.table.getTableData(TableNames.Skill, loadout.weaponSkillId); if (!row) { console.log(`[TowerLoadout] 武器主动技能缺失 Skill.id=${loadout.weaponSkillId}`); return; } const abilityId = entityWeaponAbilityId(loadout.weaponSkillId, entityId); const def = SkillAbilityBuilder.buildWeapon(row, entityId); def.id = abilityId; if (def.costs?.[0]?.params) def.costs[0].params.abilityId = abilityId; gg.battle.config.registerAbility(abilityId, def); this._stampLoadoutPresentation(def, loadout, 'weapon'); this.patchActiveSkillCooldown(null, entityId, abilityId, loadout.weaponSkillId); } /** * 被动 result 10:覆盖宿主普攻 atk_type / atk_para 并重建 Ability。 * 不改共享 Skill 表,按实体隔离,避免同技能其他塔串台。 * @param session 战斗会话 * @param entityId 触发宿主 * @param atkType 新 atk_type * @param atkPara 新 atk_para * @param sourceSkillId 被动 Skill.id */ applyNormalAtkOverride(session: BattleSession, entityId: EntityId, atkType: string, atkPara: string, sourceSkillId: number): void { const loadout = this.getLoadout(entityId); if (!loadout) { console.log(`[TowerLoadout] result 10 无装载 skill=${sourceSkillId} entity=${entityId}`); return; } if (!(loadout.normalBaseAtkId > 0)) { console.log(`[TowerLoadout] result 10 无普攻 skill=${sourceSkillId} entity=${entityId}`); return; } loadout.overAtkType = atkType; loadout.overAtkPara = atkPara; const nextId = this._registerNormalAbility(entityId, loadout); if (!nextId) return; const set = session.world.getComponent(entityId, AbilitySetComp); if (!set) { console.log(`[TowerLoadout] result 10 无 AbilitySet skill=${sourceSkillId} entity=${entityId}`); return; } this._replaceNormalAbilityId(set, loadout.normalBaseAtkId, nextId); session.abilities.bindEntity(entityId, { preserveRuntime: true }); } /** * 按装载(含覆盖)注册普攻 Ability。 * @param entityId 塔实体 * @param loadout 三槽装载 * @returns Ability id;普攻缺失时为空 */ private _registerNormalAbility(entityId: EntityId, loadout: TowerLoadout): string { const skillId = loadout.normalBaseAtkId; const row = this._normalSkillRow(loadout); if (!row) { console.log(`[TowerLoadout] 普攻缺失 Skill.id=${skillId}`); return ''; } const abilityId = loadout.overAtkType ? entityNormalAbilityId(skillId, entityId) : normalAbilityId(skillId); const def = SkillAbilityBuilder.buildNormal(row, abilityId); this._stampLoadoutPresentation(def, loadout, 'normal'); gg.battle.config.registerAbility(abilityId, def); return abilityId; } /** * 普攻 Skill 行;有覆盖时拷贝并改 atk_type / atk_para。 * @param loadout 三槽装载 */ private _normalSkillRow(loadout: TowerLoadout): ITableSkill | null { const row = gg.data.table.getTableData(TableNames.Skill, loadout.normalBaseAtkId); if (!row) return null; if (!loadout.overAtkType) return row; return Object.assign({}, row, { atk_type: loadout.overAtkType, atk_para: loadout.overAtkPara, }); } /** * 把实体普攻 Ability id 换成覆盖后的实体 id。 * @param set 技能集 * @param skillId 普攻 Skill.id * @param nextId 新 Ability id */ private _replaceNormalAbilityId(set: AbilitySetComp, skillId: number, nextId: string): void { const shared = normalAbilityId(skillId); const prefix = `ability_norm_${skillId}_e`; let found = false; for (let i = 0; i < set.abilityIds.length; i++) { const id = set.abilityIds[i]; if (id === shared || id === nextId || id.indexOf(prefix) === 0) { set.abilityIds[i] = nextId; found = true; } } if (!found) { console.log(`[TowerLoadout] result 10 未找到普攻 Ability skill=${skillId}`); set.abilityIds.unshift(nextId); } const prevCd = set.cooldowns[shared]; if (prevCd != null && set.cooldowns[nextId] == null) { set.cooldowns[nextId] = prevCd; } } /** 写入表现对接字段:heroId / weaponId / composeLevel / target 快照由 cue 时补 */ private _stampLoadoutPresentation( def: import('db://assets/mx/module/battle').AbilityDef, loadout: TowerLoadout, skillKind: 'normal' | 'hero' | 'weapon', ): void { for (const ev of def.timeline) { for (const fx of ev.effects) { if (fx.type !== 'PlayPresentation' || !fx.params) continue; fx.params.heroId = loadout.weapon1Id; fx.params.weaponId = loadout.weaponId; fx.params.composeLevel = loadout.level; fx.params.skillKind = skillKind; } } } /** 按 Skill.cd 写 OnTick / Cooldown(不再叠局外角色冷却) */ patchActiveSkillCooldown( session: BattleSession | null, entityId: EntityId, abilityId: string, skillId: number, ): void { if (skillId <= 0 || !abilityId) return; const skillRow = gg.data.table.getTableData(TableNames.Skill, skillId); const baseCd = skillRow?.cd ?? 0; const def = gg.battle.config.getAbility(abilityId); if (!def) return; if (baseCd <= 0) { TableAnomaly.warn('Skill', skillId, 'cd', '主动技能 cd≤0,不编造连放间隔', baseCd); def.costs = []; return; } let cd = baseCd; if (session) cd = session.attributes.getFinalCooldown(entityId, cd); if (def.triggers[0]) def.triggers[0].interval = cd; def.costs = [{ type: 'Cooldown', params: { cd, abilityId } }]; } /** @deprecated 词条改参已移除 */ rebuildUltAbility(entityId: EntityId, loadout: TowerLoadout): void { this.rebuildHeroAbility(entityId, loadout); } /** @deprecated */ buildEntityUltAbility(session: BattleSession, entityId: EntityId, _entries: readonly ITableSkill[]): void { const loadout = this.getLoadout(entityId); if (!loadout) return; this.rebuildHeroAbility(entityId, loadout); this.rebuildWeaponAbility(entityId, loadout); session.abilities.bindEntity(entityId, { preserveRuntime: true }); } /** @deprecated */ reapplySkillChanges(session: BattleSession, entityId: EntityId, entries: readonly ITableSkill[]): void { this.buildEntityUltAbility(session, entityId, entries); } applyAbilities(session: BattleSession, entityId: EntityId, loadout: TowerLoadout): void { const world = session.world; if (!world.isAlive(entityId)) return; if (loadout.activeSkillBaseAtkId > 0) this.rebuildHeroAbility(entityId, loadout); if (loadout.weaponSkillActive) this.rebuildWeaponAbility(entityId, loadout); const abilityIds: string[] = []; if (loadout.normalBaseAtkId > 0) { const nid = this._registerNormalAbility(entityId, loadout); if (nid) abilityIds.push(nid); } if (loadout.activeSkillBaseAtkId > 0) { abilityIds.push(entityHeroAbilityId(loadout.activeSkillBaseAtkId, entityId)); } if (loadout.weaponSkillActive && loadout.weaponSkillId > 0) { abilityIds.push(entityWeaponAbilityId(loadout.weaponSkillId, entityId)); } let set = world.getComponent(entityId, AbilitySetComp); const isRefresh = !!set; const prevCd = set ? { ...set.cooldowns } : {}; if (!set) { set = new AbilitySetComp(abilityIds, {}); world.addComponent(entityId, set); } else { set.abilityIds = abilityIds; const nextCd: Record = {}; for (const id of abilityIds) { if (prevCd[id] != null) nextCd[id] = prevCd[id]; } set.cooldowns = nextCd; } if (loadout.activeSkillBaseAtkId > 0) { this.patchActiveSkillCooldown( session, entityId, entityHeroAbilityId(loadout.activeSkillBaseAtkId, entityId), loadout.activeSkillBaseAtkId, ); } if (loadout.weaponSkillActive && loadout.weaponSkillId > 0) { this.patchActiveSkillCooldown( session, entityId, entityWeaponAbilityId(loadout.weaponSkillId, entityId), loadout.weaponSkillId, ); } session.abilities.bindEntity(entityId, { preserveRuntime: isRefresh }); // 被动:武器 type=2 const passives = getPassiveSkillHost(); passives.bindAtkOverride(this); passives.bindFromLoadout(session, entityId, loadout); } setupCombat(session: BattleSession, entityId: EntityId, weapon1Id: number, level: number, player: Player): void { const hero = gg.data.table.getTableData(TableNames.Hero, weapon1Id); if (!hero) return; const world = session.world; const isRefresh = !!world.getComponent(entityId, AbilitySetComp); const battleWeaponId = resolveHeroBattleWeaponId(weapon1Id, player); const setup = deriveTowerSetupStats(hero, level, battleWeaponId); const lv = setup.effectiveLevel; if (world.getComponent(entityId, CombatComp)) { world.removeComponent(entityId, CombatComp); } world.addComponent(entityId, new CombatComp( setup.attack, setup.attackSpeed, setup.range, setup.critRate, setup.critDamage, setup.maxAttackSpeed, )); const meta = world.getComponent(entityId, UnitMetaComp); if (meta) meta.level = lv; session.attributes.syncCombatToAttributes(entityId); SkillAtkBuffExecutor.clearRangeModifiers(session, entityId); this.initEntity(entityId, weapon1Id, lv, player); const loadout = this.getLoadout(entityId)!; if (isElementHero(hero)) { const gem = GemBattleEffect.resolve(Math.floor(Number(hero.herotype)), player); GemBattleEffect.apply(session, entityId, gem); } else { GemBattleEffect.clear(session, entityId); } this.applyAbilities(session, entityId, loadout); if (!isRefresh && loadout.weaponSkillId > 0 && !loadout.weaponSkillActive) { const sk = gg.data.table.getTableData(TableNames.Skill, loadout.weaponSkillId); if (sk) { SkillAtkBuffExecutor.applyLoadoutBuffs(session, entityId, sk.atk_buff1 ?? '', sk.atk_buff2 ?? ''); } } this._applyWeaponDistDmg(session, entityId, loadout); if (isAtkDebugWeapon(weapon1Id)) { logKnifeBaseAttack({ entityId, weapon1Id, composeLevel: level, effectiveLevel: setup.effectiveLevel, heroAtk: hero.atk, weaponAtk: Math.max(0, setup.attack - hero.atk * setup.levelCoef), combatAttack: setup.attack, }); dumpKnifeFinalAttack(session, entityId, '部署', weapon1Id); } if (!isRefresh) { const set = world.getComponent(entityId, AbilitySetComp); if (set) { if (loadout.activeSkillBaseAtkId > 0) { set.cooldowns[entityHeroAbilityId(loadout.activeSkillBaseAtkId, entityId)] = 0; } if (loadout.weaponSkillActive && loadout.weaponSkillId > 0) { set.cooldowns[entityWeaponAbilityId(loadout.weaponSkillId, entityId)] = 0; } } } } resetAtkHooks(session: BattleSession, entityId: EntityId): void { const world = session.world; let hook = world.getComponent(entityId, AtkHookComp); if (!hook) { hook = new AtkHookComp(); world.addComponent(entityId, hook); } else { hook.before = []; hook.after = []; hook.ultBefore = []; hook.ultAfter = []; hook.ultCanCrit = false; hook.specials = []; hook.distPerPx = 0; hook.distAddWan = 0; hook.distMaxWan = 0; } } /** * 武器被动 Skill.skill 类型8:写入 AtkHookComp,普攻命中按距离加伤。 * @param session 战斗会话 * @param entityId 塔实体 * @param loadout 三槽装载 */ private _applyWeaponDistDmg(session: BattleSession, entityId: EntityId, loadout: TowerLoadout): void { const world = session.world; let hook = world.getComponent(entityId, AtkHookComp); if (!hook) { hook = new AtkHookComp(); world.addComponent(entityId, hook); } hook.distPerPx = 0; hook.distAddWan = 0; hook.distMaxWan = 0; if (!(loadout.weaponSkillId > 0) || loadout.weaponSkillActive) return; const sk = gg.data.table.getTableData(TableNames.Skill, loadout.weaponSkillId); if (!sk) return; const parts = String(sk.skill ?? '').trim().split(',').map(Number); if ((parts[0] ?? 0) !== 8) return; const perPx = parts[1]; const addWan = parts[2]; const maxWan = parts[3]; if (!(perPx > 0) || !(addWan > 0) || !(maxWan > 0)) { console.log('[TowerLoadout] skill 类型8参数无效', sk.id, sk.skill); return; } hook.distPerPx = perPx; hook.distAddWan = addWan; hook.distMaxWan = maxWan; } } let _instance: TowerLoadoutService | null = null; export function getTowerLoadoutService(): TowerLoadoutService { if (!_instance) _instance = new TowerLoadoutService(); return _instance; } export function resetTowerLoadoutServiceForTests(): void { _instance = null; }