import { _decorator, Component, Node } from 'cc'; import { BATTLE_EVENT_ALL, BattleEventType, TeamComp, type BattleEvent, type EntityId, } from 'db://assets/mx/module/battle'; import type { Player } from '../../game/Player'; import type { IBattleRuntimeHandle } from '../types/ProjectBattleTypes'; import type { MapManager } from '../map/legacy/MapManager'; import { TextGrid } from '../view/TextGrid'; import { Skill } from './Skill'; import { Gong } from '../heros/Gong'; import { GONG_WEAPON1_ID } from '../heros/HeroActorRes'; import { getTowerLoadoutService } from '../combat/TowerLoadoutService'; import { buildSkillPresentationContext } from './SkillPresentationBridge'; import { isActiveAbilityId, isMonsterAbilityId, isWeaponAbilityId } from '../config/weapon1/Weapon1BattleIds'; import { findCompositionForEntity } from '../runtime/DeployEntityQuery'; import { SkillPresentationEnv } from './SkillPresentationEnv'; import { BattleAudio } from '../audio/BattleAudio'; import { SkillNameFloater, ULT_SKILL_RELEASE_DELAY_SEC } from './SkillNameFloater'; import { HeroActorHost } from '../heros/HeroActorHost'; import { isHeroActorWeapon1 } from '../heros/HeroActorRes'; import { isElementCombatHeroId } from '../config/BattleHeroTable'; import { ensureSpecialSkillPresenters } from '../presentation/SpecialSkillPresenters'; import { getNezhaChargeHost } from '../presentation/NezhaChargePresenter'; import { getEarthChargeHost } from '../presentation/EarthChargePresenter'; import { getPagodaSmashHost } from '../presentation/PagodaSmashPresenter'; import { getStaffSmashHost } from '../presentation/StaffSmashPresenter'; const { ccclass } = _decorator; /** 皮肤未就绪时 PresentationCue 最多延迟帧数(约 2s@30fps) */ const PRESENTATION_SKILL_BIND_MAX_RETRIES = 60; const CONFUSION_BUFF_ID = 'debuff_8'; /** 眩晕(土 Boss 51101 等) */ const STUN_BUFF_ID = 'debuff_4'; /** 单个玩家的技能表现控制器(监听战斗事件 → 驱动 HeroActor Skill) */ @ccclass('SkillController') export class SkillController extends Component { private _player: Player | null = null; private _match: IBattleRuntimeHandle | null = null; private _map: MapManager | null = null; private _mapGrids: ReadonlyMap | undefined; private _skills = new Map(); /** 上次 bind 时的地图格 index,用于检测搬家后 stale Skill 缓存 */ private _entityGridIndex = new Map(); private _listener: ((e: BattleEvent) => void) | null = null; /** 是否已 bind 到 Player/Match;unbind 幂等,避免 UIBattleGame 与 onDestroy 重复解绑 */ private _bound = false; get player(): Player | null { return this._player; } bind(player: Player, match: IBattleRuntimeHandle): void { this.unbind(); ensureSpecialSkillPresenters(); this._player = player; this._match = match; player.skillController = this; this._listener = (e) => this._onBattleEvent(e); match.on(BATTLE_EVENT_ALL, this._listener); this._bound = true; } unbind(): void { if (!this._bound) return; this._bound = false; if (this._player) { this._player.skillController = null; this._player = null; } if (this._match && this._listener) { this._match.off(BATTLE_EVENT_ALL, this._listener); } this._listener = null; this._match = null; this._map = null; this._mapGrids = undefined; this._skills.clear(); this._entityGridIndex.clear(); getNezhaChargeHost().abortAll(); getEarthChargeHost().abortAll(); getPagodaSmashHost().abortAll(); getStaffSmashHost().abortAll(); HeroActorHost.instance.destroyAll(); } /** 地图布局变更前打断指定实体的表现(含 ctx.cancel) */ abortEntityPresentation(entityId: EntityId): void { if (!this._ownsEntity(entityId)) return; const skill = this._skills.get(entityId); if (skill?.isValid) skill.abortPresentation(); } registerEntitySkill(entityId: EntityId, skillRoot: Node): void { const skill = skillRoot.getComponent(Skill); if (!skill?.isValid) return; const prev = this._skills.get(entityId); this._skills.set(entityId, skill); if (prev === skill) return; if (!skill.isPresentationBusy() && !skill.visualChannel.isCastOrRestHold) skill.onIdle(); } unregisterEntity(entityId: EntityId): void { this._skills.delete(entityId); HeroActorHost.instance.destroy(entityId); } /** 按 Player.deployedUnits + 地图字块刷新 entity → Skill 映射(不触发词条物化) */ refreshEntitySkillBindings(map: MapManager, mapGrids?: ReadonlyMap): void { this._map = map; this._mapGrids = mapGrids; const prev = this._skills; const prevGridByEntity = this._entityGridIndex; this._skills = new Map(); this._entityGridIndex = new Map(); const player = this._player; if (!player) return; for (const entityId of player.deployedUnits) { const gridIndex = this._resolveGridIndex(entityId); const prevGrid = prevGridByEntity.get(entityId); const gridMoved = prevGrid != null && gridIndex != null && prevGrid !== gridIndex; const old = prev.get(entityId); if (gridMoved && old?.isValid) { old.abortPresentation(); } else if (old) { this._skills.set(entityId, old); } this._tryRegisterSkill(entityId); if (gridIndex != null) this._entityGridIndex.set(entityId, gridIndex); } } /** 词条物化后刷新表现(遗留钩子;特殊 Presenter 自读 loadout) */ refreshEntityLoadoutPresentation(entityId: EntityId): void { if (!this._ownsEntity(entityId)) return; void entityId; } /** 局内 UI 暂停:冻结各塔 Skill 的 Spine / schedule(不 cancel 逻辑) */ freezeAllPresentations(): void { for (const skill of this._skills.values()) { if (skill?.isValid) skill.freezePresentation(); } } /** 恢复 UI 暂停冻结的表现 */ unfreezeAllPresentations(): void { for (const skill of this._skills.values()) { if (skill?.isValid) skill.unfreezePresentation(); } } /** 移动/拆塔等打断未 finish 的表现 */ abortAllPresentations(): void { for (const skill of this._skills.values()) { if (skill?.isValid) skill.abortPresentation(); } } /** @deprecated 使用 refreshEntitySkillBindings */ syncFromDeploy( _deploySync: unknown, map: MapManager, mapGrids?: ReadonlyMap, ): void { this.refreshEntitySkillBindings(map, mapGrids); } protected onDestroy(): void { this.unbind(); } private _ownsEntity(entityId: EntityId): boolean { const player = this._player; if (!player) return false; if (player.deployedUnits.includes(entityId)) return true; const team = this._match?.session.world.getComponent(entityId, TeamComp); return team?.ownerId === player.playerId; } private _onBattleEvent(event: BattleEvent): void { switch (event.type) { case BattleEventType.PresentationCue: if (isMonsterAbilityId(event.abilityId) || event.params?.skillKind === 'monster') { return; } if (!this._ownsEntity(event.caster)) return; this._playPresentationCue(event); break; case BattleEventType.UnitPlaced: if (!this._ownsEntity(event.entity)) return; break; case BattleEventType.UnitSold: if (!this._ownsEntity(event.entity)) return; this.unregisterEntity(event.entity); break; case BattleEventType.BuffApplied: if (event.buffId === CONFUSION_BUFF_ID) { this._setTowerConfusionFx(event.target, true); } else if (event.buffId === STUN_BUFF_ID) { this._setTowerStunFx(event.target, true); } break; case BattleEventType.BuffRemoved: if (event.buffId === CONFUSION_BUFF_ID) { this._setTowerConfusionFx(event.target, false); } else if (event.buffId === STUN_BUFF_ID) { this._setTowerStunFx(event.target, false); } break; case BattleEventType.EntityKilled: case BattleEventType.UnitDespawned: if (event.type === BattleEventType.EntityKilled) { this._setTowerConfusionFx(event.victim, false); this._setTowerStunFx(event.victim, false); } else { this._setTowerConfusionFx(event.entity, false); this._setTowerStunFx(event.entity, false); } break; } } /** 混乱:塔格挂「混乱效果」,禁攻由 AbilitySystem debuff_8 判定 */ private _setTowerConfusionFx(entityId: EntityId, on: boolean): void { if (!this._ownsEntity(entityId)) return; const gridIndex = this._resolveGridIndex(entityId); if (gridIndex == null) return; this._textGridAt(gridIndex)?.setConfusionFx(on); } /** 眩晕:塔格挂「怪_晕眩」,禁攻由 AbilitySystem debuff_4 判定 */ private _setTowerStunFx(entityId: EntityId, on: boolean): void { if (!this._ownsEntity(entityId)) return; const gridIndex = this._resolveGridIndex(entityId); if (gridIndex == null) return; this._textGridAt(gridIndex)?.setStunFx(on); } private _playPresentationCue(event: Extract): void { const session = this._match?.session; if (!session) return; const ctx = buildSkillPresentationContext(session, { caster: event.caster, abilityId: event.abilityId, cueId: event.cueId, castInstanceId: event.castInstanceId, targetEntityIds: event.targetEntityIds, params: event.params, }); this._dispatchPresentationCue(event, ctx, 0); } /** * HeroActor 未就绪时延迟重试,避免首刀 finish() 丢伤害。 */ private _dispatchPresentationCue( event: Extract, ctx: ReturnType, retried: number, ): void { if (!ctx.isAlive) { ctx.cancel('caster_dead'); return; } const skill = this._tryRegisterSkill(event.caster); if (!skill) { if (retried < PRESENTATION_SKILL_BIND_MAX_RETRIES && this._shouldRetrySkillBind(event.caster)) { SkillPresentationEnv.scheduleBattle(0, () => { this._dispatchPresentationCue(event, ctx, retried + 1); }); return; } // 无 HeroActor(桃等)或重试耗尽:先 hit 再 finish,避免丢 deferred 产桃/伤害 this._drainThenFinish(ctx); return; } const cue = event.cueId.toLowerCase(); const isWeaponSk = isWeaponAbilityId(event.abilityId) || cue === 'weaponskill'; const isUlt = !isWeaponSk && ( isActiveAbilityId(event.abilityId) || cue === 'skill' || cue.includes('ult') ); const isComboBurst = Number(event.params?.comboAttack) > 0 || (ctx.flow.vars.get('__comboAttack') ?? 0) > 0; const extraHead = this._extraSkillHead(event.params); const skipUltPresentationDelay = isComboBurst || extraHead === 1; if (isWeaponSk) { const fireWeapon = () => { if (!ctx.isAlive) { ctx.cancel('caster_dead'); return; } if (!skill.isValid) { this._drainThenFinish(ctx); return; } try { BattleAudio.playWeaponSkill(event.caster, true); skill.onWeaponSkill(ctx); } catch (err) { console.error('[SkillController] onWeaponSkill 异常,释放施法锁', event.abilityId, String(err)); if (ctx.isAlive) ctx.finish(); } }; if (skipUltPresentationDelay) fireWeapon(); else SkillPresentationEnv.scheduleBattle(ULT_SKILL_RELEASE_DELAY_SEC, fireWeapon); } else if (isUlt) { const fireUlt = () => { if (!ctx.isAlive) { ctx.cancel('caster_dead'); return; } if (!skill.isValid) { this._drainThenFinish(ctx); return; } try { BattleAudio.playWeaponSkill(event.caster); skill.onSkill(ctx); } catch (err) { console.error('[SkillController] onSkill 异常,释放施法锁', event.abilityId, String(err)); if (ctx.isAlive) ctx.finish(); } }; if (skipUltPresentationDelay) { SkillNameFloater.play(event.caster, event.abilityId); fireUlt(); } else { SkillNameFloater.play(event.caster, event.abilityId); SkillPresentationEnv.scheduleBattle(ULT_SKILL_RELEASE_DELAY_SEC, fireUlt); } } else { this._fireNormAttack(event, ctx, skill); } } /** * Skill.skill 头类型。 * @param params PlayPresentation 参数 */ private _extraSkillHead(params: Record | undefined): number { const raw = String(params?.extraSkill ?? params?.skillRaw ?? '').trim(); return Math.floor(Number(raw.split(',')[0]) || 0); } /** * 先结算剩余 deferred,再解锁施法。 * @param ctx 表现上下文 */ private _drainThenFinish(ctx: ReturnType): void { while (ctx.isAlive && ctx.hit()) { /* drain deferred slices */ } ctx.finish(); } /** 普攻 cue:异常必须 finish,避免施法锁卡住后续 OnTick */ private _fireNormAttack(event: Extract, ctx: ReturnType, skill: Skill): void { if (!ctx.isAlive) { ctx.cancel('caster_dead'); return; } if (!skill.isValid) { this._drainThenFinish(ctx); return; } try { const heroId = getTowerLoadoutService().getLoadout(event.caster)?.weapon1Id; if (heroId !== GONG_WEAPON1_ID && !(skill instanceof Gong)) { BattleAudio.playWeaponAttack(event.caster); } skill.onAttack(ctx); this._syncPartnerAttackVisual(event.caster, skill); } catch (err) { console.error('[SkillController] onAttack 异常,释放施法锁', event.abilityId, String(err)); if (ctx.isAlive) ctx.finish(); } } /** 组合体伴侣字块同步攻击动画(HeroActor 由子类 playPartnerAttackVisual 处理) */ private _syncPartnerAttackVisual(entityId: EntityId, mainSkill: Skill): void { if (HeroActorHost.instance.getActor(entityId) === mainSkill) { const timeScale = Math.max(0.01, SkillPresentationEnv.getFinalAttackSpeed(entityId)); mainSkill.playPartnerAttackVisual(timeScale); } } /** * 地图格上的字块:先查绑定表,缺则从格节点现找(AI 对账晚于 refresh 时表会空)。 * @param gridIndex 地图格 */ private _textGridAt(gridIndex: number): TextGrid | null { const tracked = this._mapGrids?.get(gridIndex); if (tracked?.isValid) return tracked; const parent = this._map?.getMapGridByIndex(gridIndex)?.node; if (!parent?.isValid) return null; const live = parent.getComponentInChildren(TextGrid); return live?.isValid ? live : null; } private _resolveGridIndex(entityId: EntityId): number | null { const session = this._match?.session; const map = this._map; const player = this._player; if (!session || !map || !player) return null; const comp = findCompositionForEntity(entityId, player, session, map); if (!comp?.gridIndices.length) return null; return SkillPresentationEnv.leftmostGridIndex(comp.gridIndices); } /** * 仅 HeroActor 未就绪时重试绑定;桃等无 Actor 塔立即结算。 * @param entityId 施法实体 */ private _shouldRetrySkillBind(entityId: EntityId): boolean { const loadout = getTowerLoadoutService().getLoadout(entityId); if (loadout && !isHeroActorWeapon1(loadout.weapon1Id) && !isElementCombatHeroId(loadout.weapon1Id)) return false; const session = this._match?.session; const player = this._player; const map = this._map; if (!session || !player || !map) return true; const comp = findCompositionForEntity(entityId, player, session, map); if (comp && !isHeroActorWeapon1(comp.weapon1Id) && !isElementCombatHeroId(comp.weapon1Id)) return false; return true; } /** 绑定 HeroActor Skill;全部 Weapon1 英雄已迁新方案,不再从 grid 皮肤找 Skill* */ private _tryRegisterSkill(entityId: EntityId): Skill | null { const actor = HeroActorHost.instance.getActor(entityId); if (actor?.isValid) { this.registerEntitySkill(entityId, actor.node); return this._skills.get(entityId) ?? actor; } const session = this._match?.session; const player = this._player; const map = this._map; if (!session || !player || !map) return null; const comp = findCompositionForEntity(entityId, player, session, map); if (!comp) return null; if (isElementCombatHeroId(comp.weapon1Id)) { const gridIndex = this._resolveGridIndex(entityId); const grid = gridIndex != null ? this._textGridAt(gridIndex) : null; const skill = grid?.ensureNumberSkill(comp.weapon1Id, entityId) ?? null; if (!skill?.isValid) return null; this.registerEntitySkill(entityId, skill.node); return this._skills.get(entityId) ?? skill; } if (!isHeroActorWeapon1(comp.weapon1Id)) return null; HeroActorHost.instance.syncFromMap(entityId, comp, map, !player.isLocal); const created = HeroActorHost.instance.getActor(entityId); if (!created?.isValid) return null; this.registerEntitySkill(entityId, created.node); return this._skills.get(entityId) ?? created; } }