import { instantiate, Node, Tween, tween, UIOpacity, Vec3 } from 'cc'; import type { AiActiveSkillCastFxInfo } from '../../battle/ai/runtime/AiActiveSkillHost'; import type { CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import type { MapManager } from '../../battle/map/legacy/MapManager'; import type { BattleGameOtherSkillView } from './BattleGameOtherSkillView'; /** 拖拽影子透明度(与玩家 skillDrag 一致) */ const ICON_GHOST_OPACITY = 200; /** 飞向受体时长(秒) */ const FLY_DURATION_SEC = 0.35; /** 落地后停留再淡出(秒) */ const LAND_HOLD_SEC = 0.12; /** 淡出时长(秒) */ const FADE_OUT_SEC = 0.18; /** * AI 主动技能施放表现:复制 otherskill 图标飞向单位/基地/受体(对齐玩家拖放落点)。 * 妙手神偷由 BattleGameStealGlyphFxPresenter 播神偷手,不走飞图标。 */ export class BattleGameAiActiveSkillFxPresenter { private _otherSkillView: BattleGameOtherSkillView | null = null; private _bagView: CocosBagBoardView | null = null; private _map: MapManager | null = null; private _flyLayer: Node | null = null; private readonly _activeGhosts: Node[] = []; private readonly _tmpEnd = new Vec3(); /** * 绑定 UI / 地图。 * @param otherSkillView 敌方主动栏 * @param bagView 本机背包视图(出怪口 / 迷惑中心) * @param map 地图 * @param flyLayer 飞行层 */ bind( otherSkillView: BattleGameOtherSkillView | null, bagView: CocosBagBoardView | null, map: MapManager | null, flyLayer: Node | null | undefined, ): void { this.unbind(); this._otherSkillView = otherSkillView; this._bagView = bagView; this._map = map; this._flyLayer = flyLayer?.isValid ? flyLayer : null; } /** 注销并清理进行中的飞图标 */ unbind(): void { this._abortAll(); this._otherSkillView = null; this._bagView = null; this._map = null; this._flyLayer = null; } /** * 播放一次施放飞图标。 * @param info AI 施放载荷 */ play(info: AiActiveSkillCastFxInfo): void { const flyLayer = this._flyLayer; const icon = this._otherSkillView?.getActiveIconNode(info.slotIndex) ?? null; if (!flyLayer?.isValid) return; if (!icon?.isValid) return; const endWorld = this._resolveEndWorld(info); if (!endWorld) return; const ghost = instantiate(icon); ghost.name = 'aiActiveSkillIconGhost'; ghost.parent = flyLayer; ghost.setWorldPosition(icon.worldPosition); ghost.setWorldScale(icon.worldScale); ghost.setSiblingIndex(flyLayer.children.length - 1); const opacity = ghost.getComponent(UIOpacity) ?? ghost.addComponent(UIOpacity); opacity.opacity = ICON_GHOST_OPACITY; this._activeGhosts.push(ghost); Tween.stopAllByTarget(ghost); tween(ghost) .to(FLY_DURATION_SEC, { worldPosition: endWorld }, { easing: 'quadOut' }) .delay(LAND_HOLD_SEC) .call(this._onLandFadeStart.bind(this, ghost)) .start(); } /** 落地后淡出并回收 */ private _onLandFadeStart(ghost: Node): void { if (!ghost?.isValid) { this._releaseGhost(ghost); return; } const opacity = ghost.getComponent(UIOpacity); if (!opacity?.isValid) { this._releaseGhost(ghost); return; } tween(opacity) .to(FADE_OUT_SEC, { opacity: 0 }) .call(this._onFadeDone.bind(this, ghost)) .start(); } /** 淡出结束 */ private _onFadeDone(ghost: Node): void { this._releaseGhost(ghost); } /** * 解析受体世界坐标。 * @param info 载荷 */ private _resolveEndWorld(info: AiActiveSkillCastFxInfo): Vec3 | null { const receptor = info.receptor; if (receptor.kind === 'map' || receptor.kind === 'steal') { const node = this._map?.getMapGridByIndex(receptor.gridIndex)?.node; if (!node?.isValid) return null; node.getWorldPosition(this._tmpEnd); return this._tmpEnd.clone(); } if (receptor.kind === 'spawn') { const node = this._bagView?.getSpawnPointNode(receptor.spawnPort) ?? null; if (!node?.isValid) return null; node.getWorldPosition(this._tmpEnd); return this._tmpEnd.clone(); } if (receptor.kind === 'confuse') { const center = this._bagView?.getConfuseHeroTargetWorldCenter(receptor.target) ?? null; if (!center) return null; return center.clone(); } if (receptor.kind === 'bag') { // AI 无头背包无字块视图;无法对齐玩家 bag 落点 return null; } return null; } /** 回收单个飞图标 */ private _releaseGhost(ghost: Node | null): void { if (!ghost) return; const idx = this._activeGhosts.indexOf(ghost); if (idx >= 0) this._activeGhosts.splice(idx, 1); if (!ghost.isValid) return; Tween.stopAllByTarget(ghost); const opacity = ghost.getComponent(UIOpacity); if (opacity?.isValid) Tween.stopAllByTarget(opacity); ghost.destroy(); } /** 中断全部飞图标 */ private _abortAll(): void { const pending = this._activeGhosts.slice(); this._activeGhosts.length = 0; for (let i = 0; i < pending.length; i++) { const ghost = pending[i]; if (!ghost?.isValid) continue; Tween.stopAllByTarget(ghost); const opacity = ghost.getComponent(UIOpacity); if (opacity?.isValid) Tween.stopAllByTarget(opacity); ghost.destroy(); } } }