You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
14 KiB
362 lines
14 KiB
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;
|
|
}
|
|
|