import { Node } from 'cc'; import type { BattleEvent, EntityId } from 'db://assets/mx/module/battle'; import { BattleEventType } from 'db://assets/mx/module/battle'; import { AiHeadlessBagHost } from '../../battle/ai/runtime/AiHeadlessBagHost'; import { getBattleBuffEnemySlowHost } from '../../battle/combat/BattleBuffEnemySlowHost'; import { collectSpawnPortsForCampArea, resolveEnemyCampAreaIdForDefender, resolveGridAheadOfSpawnPort, } from '../../battle/map/MapPathQueries'; import type { MapGrid } from '../../battle/map/legacy/MapGrid'; import type { MapManager } from '../../battle/map/legacy/MapManager'; import { PassiveIconArcFlight } from '../../battle/view/PassiveIconArcFlight'; import { BATTLE_HERO_BUFF_PATH } from '../../battle/res/BattleHeroResCollector'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; import { schedulePresentation } from '../../battle/skill/BattlePresentationClock'; import { SPAWN_PRESENT_SEC, type CocosBattleMonsterPresenter } from '../../battle/view/CocosBattleMonsterPresenter'; import { getMonsterStatusText } from '../../battle/view/MonsterStatusText'; import { GBundle } from '../../game/ConfigRes'; import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore'; import { resolveCarriedEnemySlowWan } from '../../manager/battleBuff/BattleBuffPassiveSkillUtil'; import { BattleBuffFunctionType } from '../../manager/battleBuff/BattleBuffTypes'; import type { BattleGameOtherSkillView } from './BattleGameOtherSkillView'; import type { BattleGamePassiveSkillView } from './BattleGamePassiveSkillView'; /** 弱水预制体(battleHero/prefab/buff/弱水) */ const RUOSHUI_PREFAB_NAME = '弱水'; /** 开局弱水 icon 飞行动画延迟(逻辑秒) */ const RUOSHUI_FLIGHT_START_DELAY_SEC = 2; /** 已放置的弱水表现 */ type RuoshuiVisualEntry = { /** 携带被动防守方 */ defenderPlayerId: number; /** 出怪口 */ port: number; /** 地图格 index */ gridIndex: number; /** 节点 */ node: Node; }; /** 弱水开局飞行动画 UI 上下文 */ export type BattleGameRuoshuiBuffUiContext = { /** 飞行层(ui_fly) */ flyLayer: Node | null; /** 本机被动槽 */ passiveSkillView: BattleGamePassiveSkillView | null; /** 敌方被动槽 */ otherSkillView: BattleGameOtherSkillView | null; }; /** * 被动 functiontype=23 弱水表现: * - 开局复制被动 icon 影子,沿进路弧线飞到进攻方出怪口下一格后消失,再挂弱水预制体; * - 受弱水影响的出怪口怪物出现后,头顶飘「减速」。 */ export class BattleGameRuoshuiBuffPresenter { private _battle: ProjectBattleHandle | null = null; private _map: MapManager | null = null; private _monsterPresenter: CocosBattleMonsterPresenter | null = null; private _flyLayer: Node | null = null; private _passiveSkillView: BattleGamePassiveSkillView | null = null; private _otherSkillView: BattleGameOtherSkillView | null = null; private readonly _visuals: RuoshuiVisualEntry[] = []; private readonly _inflightFlightHosts: Node[] = []; private readonly _inflightGridIndices: number[] = []; private _spawnFxGen = 0; private _spawnListenerBound = false; private _onUnitSpawned: ((event: BattleEvent) => void) | null = null; /** * @param battle 战斗句柄 * @param map 地图 * @param monsterPresenter 怪物表现(头顶减速) * @param ui 被动 icon / 飞行层 */ bind( battle: ProjectBattleHandle | null, map: MapManager | null, monsterPresenter?: CocosBattleMonsterPresenter | null, ui?: BattleGameRuoshuiBuffUiContext | null, ): void { this.unbind(); this._battle = battle; this._map = map?.gridsInitialized ? map : null; this._monsterPresenter = monsterPresenter ?? null; this._flyLayer = ui?.flyLayer?.isValid ? ui.flyLayer : null; this._passiveSkillView = ui?.passiveSkillView ?? null; this._otherSkillView = ui?.otherSkillView ?? null; } /** 释放全部弱水节点并注销出怪监听 */ unbind(): void { this._spawnFxGen++; this._abortInflightFlights(); this._unbindSpawnHeadFx(); this._clearVisuals(); this._battle = null; this._map = null; this._monsterPresenter = null; this._flyLayer = null; this._passiveSkillView = null; this._otherSkillView = null; } /** 按 CarryStore 补齐弱水表现(已放置格不重复飞行) */ refresh(): void { const battle = this._battle; const map = this._map; if (!battle || !map?.gridsInitialized) { this._unbindSpawnHeadFx(); this._clearVisuals(); return; } this._pruneStaleVisuals(); this._tryPlaceForDefender(battle.localPlayerId); this._tryPlaceForDefender(AiHeadlessBagHost.AI_PLAYER_ID); this._syncSpawnHeadFx(battle); } /** 若防守方携带弱水,在其敌方出怪口下一格播放飞行并布置预制体 */ private _tryPlaceForDefender(defenderPlayerId: number): void { const battle = this._battle; const map = this._map; if (!battle || !map) return; const adjust = resolveCarriedEnemySlowWan(BattleBuffCarryStore.getPassives(defenderPlayerId)); if (!adjust.active || adjust.slowWan <= 0) return; const enemyCamp = resolveEnemyCampAreaIdForDefender(battle.session, defenderPlayerId); if (enemyCamp == null) { console.log('[RuoshuiBuff] 进攻方阵营区域缺失', defenderPlayerId); return; } const ports = collectSpawnPortsForCampArea(battle.session, enemyCamp); for (let i = 0; i < ports.length; i++) { const port = ports[i]; const grid = resolveGridAheadOfSpawnPort(map, battle.session, port, defenderPlayerId, 1); if (!grid?.node?.isValid) { console.log('[RuoshuiBuff] 出怪口下一格缺失', defenderPlayerId, port); continue; } this._playFlightThenSpawn(grid, defenderPlayerId, port); } } /** 开局延迟后复制被动 icon 影子飞到目标格,消失后挂弱水预制体 */ private _playFlightThenSpawn(grid: MapGrid, defenderPlayerId: number, port: number): void { const gridIndex = grid.Index; for (let i = 0; i < this._visuals.length; i++) { if (this._visuals[i].gridIndex === gridIndex) return; } if (this._inflightGridIndices.indexOf(gridIndex) >= 0) return; this._inflightGridIndices.push(gridIndex); const gen = this._spawnFxGen; schedulePresentation(RUOSHUI_FLIGHT_START_DELAY_SEC, () => { if (gen !== this._spawnFxGen) { this._removeInflightGridIndex(gridIndex); return; } if (!grid?.node?.isValid) { this._removeInflightGridIndex(gridIndex); return; } for (let i = 0; i < this._visuals.length; i++) { if (this._visuals[i].gridIndex === gridIndex) { this._removeInflightGridIndex(gridIndex); return; } } const iconNode = this._resolvePassiveIconNode(defenderPlayerId); const flyLayer = this._flyLayer; const targetWorld = grid.node.worldPosition; if (!iconNode?.isValid || !flyLayer?.isValid) { console.log('[RuoshuiBuff] 飞行动画跳过:icon 或 flyLayer 缺失', defenderPlayerId, port); this._removeInflightGridIndex(gridIndex); this._spawnOnGrid(grid, defenderPlayerId, port); return; } const startWorld = iconNode.worldPosition.clone(); const flightRef: { host: Node | null } = { host: null }; flightRef.host = PassiveIconArcFlight.play(iconNode, targetWorld, flyLayer, () => { this._removeInflightGridIndex(gridIndex); const host = flightRef.host; if (host?.isValid) { const idx = this._inflightFlightHosts.indexOf(host); if (idx >= 0) this._inflightFlightHosts.splice(idx, 1); } if (gen !== this._spawnFxGen) return; this._spawnOnGrid(grid, defenderPlayerId, port); }, { startWorld, showTrail: true, }); if (flightRef.host?.isValid) { this._inflightFlightHosts.push(flightRef.host); } }); } /** * 按携带方取弱水被动 icon。 * @param defenderPlayerId 携带弱水的防守方 */ private _resolvePassiveIconNode(defenderPlayerId: number): Node | null { const ft = BattleBuffFunctionType.EnemySlow; const battle = this._battle; if (!battle) return null; if (defenderPlayerId === battle.localPlayerId) { return this._passiveSkillView?.getPassiveIconNode(ft) ?? null; } if (defenderPlayerId === AiHeadlessBagHost.AI_PLAYER_ID) { return this._otherSkillView?.getPassiveIconNode(ft) ?? null; } console.log('[RuoshuiBuff] 未知携带方', defenderPlayerId); return null; } /** 同步出怪口头顶减速监听 */ private _syncSpawnHeadFx(battle: ProjectBattleHandle): void { if (!this._monsterPresenter || !this._hasEnemySlowPassive(battle)) { this._unbindSpawnHeadFx(); return; } if (this._spawnListenerBound) return; this._onUnitSpawned = this._handleUnitSpawned; battle.session.events.on(BattleEventType.UnitSpawned, this._onUnitSpawned); this._spawnListenerBound = true; } /** 任一方是否携带弱水 */ private _hasEnemySlowPassive(battle: ProjectBattleHandle): boolean { const local = resolveCarriedEnemySlowWan(BattleBuffCarryStore.getPassives(battle.localPlayerId)); if (local.active && local.slowWan > 0) return true; const ai = resolveCarriedEnemySlowWan(BattleBuffCarryStore.getPassives(AiHeadlessBagHost.AI_PLAYER_ID)); return ai.active && ai.slowWan > 0; } /** 出怪口怪物出现后播头顶减速 */ private _handleUnitSpawned = (event: BattleEvent): void => { if (event.type !== BattleEventType.UnitSpawned) return; if (!(event.spawnPort > 0)) return; const entityId = event.entity; if (getBattleBuffEnemySlowHost().resolveSlowWanForEntity(entityId) == null) return; const gen = this._spawnFxGen; schedulePresentation(SPAWN_PRESENT_SEC, () => { if (gen !== this._spawnFxGen) return; this._playSpawnHeadSlowFx(entityId); }); }; /** 出怪口怪物头顶飘「减速」 */ private _playSpawnHeadSlowFx(entityId: EntityId): void { const battle = this._battle; const presenter = this._monsterPresenter; if (!battle || !presenter) return; if (!battle.session.world.isAlive(entityId)) return; if (!presenter.getEntityNodeMap().get(entityId)?.isValid) return; getMonsterStatusText().play(entityId, '减速'); } /** 在地图格挂载弱水预制体 */ private _spawnOnGrid(grid: MapGrid, defenderPlayerId: number, port: number): void { const gridIndex = grid.Index; for (let i = 0; i < this._visuals.length; i++) { if (this._visuals[i].gridIndex === gridIndex) return; } let node: Node | null = null; try { node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, RUOSHUI_PREFAB_NAME, GBundle.BattleHero); } catch (e) { console.log('[RuoshuiBuff] 弱水预制体加载失败', String(e)); return; } if (!node?.isValid) { console.log('[RuoshuiBuff] 弱水预制体缺失', RUOSHUI_PREFAB_NAME); return; } node.parent = grid.node; node.setPosition(0, 0, 0); node.active = true; this._visuals.push({ defenderPlayerId, port, gridIndex, node }); } /** 注销出怪头顶减速监听 */ private _unbindSpawnHeadFx(): void { const battle = this._battle; if (battle && this._spawnListenerBound && this._onUnitSpawned) { battle.session.events.off(BattleEventType.UnitSpawned, this._onUnitSpawned); } this._spawnListenerBound = false; this._onUnitSpawned = null; } /** 移除不再携带弱水的防守方表现 */ private _pruneStaleVisuals(): void { const battle = this._battle; if (!battle) return; const next: RuoshuiVisualEntry[] = []; for (let i = 0; i < this._visuals.length; i++) { const entry = this._visuals[i]; const adjust = resolveCarriedEnemySlowWan(BattleBuffCarryStore.getPassives(entry.defenderPlayerId)); if (!adjust.active || adjust.slowWan <= 0) { this._releaseNode(entry.node); continue; } next.push(entry); } this._visuals.length = 0; for (let i = 0; i < next.length; i++) { this._visuals.push(next[i]); } } /** 回收全部弱水节点 */ private _clearVisuals(): void { for (let i = 0; i < this._visuals.length; i++) { this._releaseNode(this._visuals[i].node); } this._visuals.length = 0; } /** 中断进行中的飞行动画 */ private _abortInflightFlights(): void { const pending = this._inflightFlightHosts.slice(); this._inflightFlightHosts.length = 0; this._inflightGridIndices.length = 0; for (let i = 0; i < pending.length; i++) { PassiveIconArcFlight.abort(pending[i]); } } /** 移除飞行中的地图格标记 */ private _removeInflightGridIndex(gridIndex: number): void { const idx = this._inflightGridIndices.indexOf(gridIndex); if (idx >= 0) this._inflightGridIndices.splice(idx, 1); } /** 归还节点到对象池 */ private _releaseNode(node: Node | null): void { if (!node?.isValid) return; gg.res.putNode(node); } } let _inst: BattleGameRuoshuiBuffPresenter | null = null; /** 弱水被动表现(单局复用) */ export function getBattleGameRuoshuiBuffPresenter(): BattleGameRuoshuiBuffPresenter { if (!_inst) _inst = new BattleGameRuoshuiBuffPresenter(); return _inst; }