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.
AdLike2048/assets/script/ui/BattleGame/BattleGameConfuseHeroFxPres...

407 lines
16 KiB

3 weeks ago
import { Node, sp, Tween } from 'cc';
import { GBundle } from '../../game/ConfigRes';
import { BATTLE_HERO_BUFF_PATH } from '../../battle/res/BattleHeroResCollector';
import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback';
import { schedulePresentation } from '../../battle/skill/BattlePresentationClock';
import { BattlePhase } from 'db://assets/mx/module/battle';
import type { CharmTarget } from '../../battle/mechanism/MechanismServices';
import { composeKey } from '../../battle/runtime/ComposeUnitKey';
import type { CocosBagBoardView } from '../../battle/view/CocosBagBoardView';
import type { HeroSkin } from '../../battle/view/HeroSkin';
import { TextGrid, type TextGridBattleBuffFxLayer } from '../../battle/view/TextGrid';
import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
/** 施法幻魂镜预制体(battleHero/prefab/buff/幻魂镜) */
const CONFUSE_HERO_CAST_PREFAB_NAME = '幻魂镜';
/** 幻魂镜下层预制体 */
const CONFUSE_HERO_LOWER_PREFAB_NAME = '幻魂镜下层';
/** 幻魂镜上层预制体 */
const CONFUSE_HERO_UPPER_PREFAB_NAME = '幻魂镜上层';
/** 施法 Spine 动画名 */
const CONFUSE_HERO_CAST_ANIM_NAME = '施法效果';
/** 下层循环 Spine 动画名 */
const CONFUSE_HERO_LOWER_ANIM_NAME = '技能效果';
/** 上层循环 Spine 动画名 */
const CONFUSE_HERO_UPPER_ANIM_NAME = '技能效果2';
/** 施法后延迟添加循环层(逻辑秒) */
const LOOP_FX_DELAY_SEC = 1;
/**
* 幻魂镜(functiontype=22):施法效果 → 1s 后上下层循环技能效果,同步施加迷惑。
*/
export class BattleGameConfuseHeroFxPresenter {
private _battle: ProjectBattleHandle | null = null;
private _bagView: CocosBagBoardView | null = null;
private _playing = false;
private readonly _activeFxNodes: Node[] = [];
/** 延迟回调世代(unbind 时递增以作废待触发回调) */
private _scheduleGen = 0;
/** 当前施法幻魂镜节点(1s 后回收) */
private _castFxNode: Node | null = null;
/** 本次施法目标 composeKey(合成变阵时用于中断表现) */
private _playTargetKey: string | null = null;
/** 施法鬼手挂点字块 */
private _playGrid: TextGrid | null = null;
/** 循环层挂点皮肤 */
private _playSkin: HeroSkin | null = null;
/** 是否正在播放幻魂镜全流程 */
get isPlaying(): boolean {
return this._playing;
}
/**
* 绑定战斗与背包视图。
* @param battle 战斗句柄
* @param bagView 背包视图
* @param _flyLayer 保留参数(历史兼容,特效改挂字块)
*/
bind(battle: ProjectBattleHandle | null, bagView: CocosBagBoardView | null, _flyLayer: Node | null | undefined): void {
this.unbind();
this._battle = battle;
this._bagView = bagView;
}
/** 注销并中断进行中的表现 */
unbind(): void {
this._scheduleGen++;
this._abortAllFx();
this._battle = null;
this._bagView = null;
}
/**
* 合成变阵或魅惑解除:中断仍指向该 composeKey 的幻魂镜序列,并剥离残留鬼手。
* @param composeKey 合成体键
*/
abortForComposeKey(composeKey: string): void {
if (!composeKey) return;
if (this._playTargetKey === composeKey) {
this._scheduleGen++;
this._abortAllFx();
}
this._stripComposeVisuals(composeKey);
}
/**
* 玩家拖到敌方神将:播施法 → 1s 后循环上下层并施加迷惑。
* @param target 敌方神将
* @param durationSec 迷幻时长(functionpar 第二段)
* @param onApplied 施放成功并进入 CD
*/
tryPlay(target: CharmTarget, durationSec: number, onApplied: () => void): boolean {
const bagView = this._bagView;
if (!bagView || !bagView.canApplyConfuseHeroTarget(target)) return false;
return this._beginPlay(target, durationSec, onApplied);
}
/**
* AI 对玩家神将施放:跳过敌方阵营校验,其余表现与玩家一致。
* @param target 神将目标
* @param durationSec 迷幻时长
*/
tryPlayFromAi(target: CharmTarget, durationSec: number): boolean {
const battle = this._battle;
if (!battle || !this._validateCharmTarget(battle, target)) return false;
return this._beginPlay(target, durationSec, null);
}
/**
* 启动幻魂镜表现序列。
* @param target 神将目标
* @param durationSec 迷幻时长
* @param onApplied 进入 CD 回调;AI 可为 null
*/
private _beginPlay(target: CharmTarget, durationSec: number, onApplied: (() => void) | null): boolean {
if (this._playing) return false;
if (durationSec <= 0) return false;
if (this._activeFxNodes.length > 0 || this._castFxNode) this._abortAllFx();
const battle = this._battle;
const bagView = this._bagView;
if (!battle || !bagView) return false;
const textGrid = this._resolveTargetTextGrid(bagView, target);
if (!textGrid?.node?.isValid) {
console.log('[ConfuseHeroFx] 目标字块视图缺失', target.composeKey);
return false;
}
const castFx = this._spawnFxOnTextGrid(textGrid, CONFUSE_HERO_CAST_PREFAB_NAME, 'over');
if (!castFx) return false;
const castSkel = castFx.getComponent(sp.Skeleton) ?? castFx.getComponentInChildren(sp.Skeleton);
if (!castSkel?.isValid) {
console.log('[ConfuseHeroFx] 幻魂镜缺少 sp.Skeleton');
this._releaseFx(castFx);
return false;
}
onApplied?.();
this._playing = true;
this._playTargetKey = target.composeKey;
this._playGrid = textGrid;
this._castFxNode = castFx;
const scheduleGen = ++this._scheduleGen;
const totalSec = LOOP_FX_DELAY_SEC + durationSec;
const started = BattleSpinePlayback.play(castSkel, CONFUSE_HERO_CAST_ANIM_NAME, {
loop: false,
onComplete: this._onCastAnimComplete.bind(this, castFx, castSkel, scheduleGen),
});
if (!started) {
this._abortAllFx();
return false;
}
schedulePresentation(LOOP_FX_DELAY_SEC, () => {
if (scheduleGen !== this._scheduleGen) return;
this._startLoopPhase(target, durationSec, scheduleGen);
});
schedulePresentation(totalSec, () => {
if (scheduleGen !== this._scheduleGen) return;
this._finishPlaySequence();
});
return true;
}
/** 施法幻魂镜播完:回收施法预制体 */
private _onCastAnimComplete(castFx: Node, castSkel: sp.Skeleton, scheduleGen: number): void {
if (scheduleGen !== this._scheduleGen) return;
if (castSkel?.isValid) BattleSpinePlayback.clearPendingComplete(castSkel);
if (this._castFxNode === castFx) this._castFxNode = null;
this._releaseFx(castFx);
}
/**
* 1s 后:回收施法鬼手,添加上下层循环特效并施加迷惑。
* @param target 神将目标
* @param durationSec 迷幻时长
* @param scheduleGen 回调世代
*/
private _startLoopPhase(target: CharmTarget, durationSec: number, scheduleGen: number): void {
if (scheduleGen !== this._scheduleGen) return;
const battle = this._battle;
const bagView = this._bagView;
if (!battle || !bagView) {
this._finishPlaySequence();
return;
}
if (!this._validateCharmTarget(battle, target)) {
console.log('[ConfuseHeroFx] 循环层启动时目标已不可迷惑', target.composeKey);
this._finishPlaySequence();
return;
}
this._recycleCastFx();
const heroSkin = bagView.resolveConfuseHeroVisualSkin(target);
if (!heroSkin?.displayNode?.isValid) {
console.log('[ConfuseHeroFx] 神将合成预制体未就绪', target.composeKey);
this._finishPlaySequence();
return;
}
this._playSkin = heroSkin;
const lowerFx = this._spawnLoopFxOnHeroSkin(heroSkin, CONFUSE_HERO_LOWER_PREFAB_NAME, 'lower');
const upperFx = this._spawnLoopFxOnHeroSkin(heroSkin, CONFUSE_HERO_UPPER_PREFAB_NAME, 'upper');
if (!lowerFx || !upperFx) {
console.log('[ConfuseHeroFx] 幻魂镜上下层预制体加载失败');
this._releaseFx(lowerFx);
this._releaseFx(upperFx);
this._finishPlaySequence();
return;
}
const lowerSkel = lowerFx.getComponent(sp.Skeleton) ?? lowerFx.getComponentInChildren(sp.Skeleton);
const upperSkel = upperFx.getComponent(sp.Skeleton) ?? upperFx.getComponentInChildren(sp.Skeleton);
if (!lowerSkel?.isValid || !upperSkel?.isValid) {
console.log('[ConfuseHeroFx] 幻魂镜上下层缺少 sp.Skeleton');
this._releaseFx(lowerFx);
this._releaseFx(upperFx);
this._finishPlaySequence();
return;
}
BattleSpinePlayback.play(lowerSkel, CONFUSE_HERO_LOWER_ANIM_NAME, { loop: true });
BattleSpinePlayback.play(upperSkel, CONFUSE_HERO_UPPER_ANIM_NAME, { loop: true });
battle.charmController.applyCharm(target, durationSec, null);
}
/** 迷幻时长结束:回收循环层并结束占用 */
private _finishPlaySequence(): void {
this._recycleCastFx();
const pending = this._activeFxNodes.slice();
this._activeFxNodes.length = 0;
for (let i = 0; i < pending.length; i++) {
this._releaseFx(pending[i]);
}
this._stripPlayVisuals();
if (this._playTargetKey) this._stripComposeVisuals(this._playTargetKey);
this._resetPlayState();
}
/** 校验目标仍可被迷惑(不含敌方阵营校验) */
private _validateCharmTarget(battle: ProjectBattleHandle, target: CharmTarget): boolean {
if (battle.session.ended || battle.session.phase !== BattlePhase.COMBAT) return false;
if (battle.charmController.isComposeKeyCharmed(target.composeKey)) return false;
if (!battle.session.world.isAlive(target.entityId)) return false;
return true;
}
/** 解析组合神将任一格的字块视图(含敌方 AiMapTextHost) */
private _resolveTargetTextGrid(bagView: CocosBagBoardView, target: CharmTarget): TextGrid | null {
for (let i = 0; i < target.gridIndices.length; i++) {
const grid = bagView.getTextGridForConfuseHeroTarget(target.gridIndices[i]);
if (grid?.node?.isValid) return grid;
}
return bagView.getTextGridForConfuseHeroTarget(target.anchorGridIndex);
}
/**
* 循环层:挂到 battleHero 合成预制体(下层在品质框上,上层在根节点顶)。
* @param heroSkin 神将皮肤
* @param prefabName 预制体名
* @param layer 上下层
*/
private _spawnLoopFxOnHeroSkin(heroSkin: HeroSkin, prefabName: string, layer: 'lower' | 'upper'): Node | null {
let node: Node | null = null;
try {
node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, prefabName, GBundle.BattleHero);
} catch (e) {
console.log('[ConfuseHeroFx] 预制体加载异常', prefabName, String(e));
return null;
}
if (!node?.isValid) {
console.log('[ConfuseHeroFx] 预制体缺失', prefabName);
return null;
}
const attached = layer === 'lower'
? heroSkin.attachConfuseMirrorLowerFx(node)
: heroSkin.attachConfuseMirrorUpperFx(node);
if (!attached) {
this._releaseFx(node);
return null;
}
this._activeFxNodes.push(node);
return node;
}
/**
* 施法层:挂到字块根节点(多格锚点对齐占格中心)。
* @param grid 字块
* @param prefabName 预制体名
* @param layer 上下层
*/
private _spawnFxOnTextGrid(grid: TextGrid, prefabName: string, layer: TextGridBattleBuffFxLayer): Node | null {
if (!grid?.node?.isValid) return null;
let node: Node | null = null;
try {
node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, prefabName, GBundle.BattleHero);
} catch (e) {
console.log('[ConfuseHeroFx] 预制体加载异常', prefabName, String(e));
return null;
}
if (!node?.isValid) {
console.log('[ConfuseHeroFx] 预制体缺失', prefabName);
return null;
}
grid.attachBattleBuffFx(node, layer);
this._activeFxNodes.push(node);
return node;
}
/** 回收特效节点 */
private _releaseFx(node: Node | null): void {
if (!node) return;
const idx = this._activeFxNodes.indexOf(node);
if (idx >= 0) this._activeFxNodes.splice(idx, 1);
if (!node.isValid) return;
const skel = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton);
if (skel?.isValid) {
BattleSpinePlayback.clearPendingComplete(skel);
BattleSpinePlayback.resetTracks(skel);
}
const grid = node.parent?.getComponent(TextGrid);
if (grid) {
grid.releaseAttachedBattleBuffFx(node);
return;
}
Tween.stopAllByTarget(node);
if (node['resKey']) gg.res.putNode(node);
else node.destroy();
}
/** 回收施法鬼手预制体 */
private _recycleCastFx(): void {
const castFx = this._castFxNode;
this._castFxNode = null;
this._releaseFx(castFx);
}
/** 剥离本次挂点上跟丢的幻魂镜节点 */
private _stripPlayVisuals(): void {
const grid = this._playGrid;
this._playGrid = null;
grid?.releaseAttachedBattleBuffFxByName(CONFUSE_HERO_CAST_PREFAB_NAME);
const skin = this._playSkin;
this._playSkin = null;
skin?.detachConfuseMirrorFx();
}
/**
* 按合成键剥离场上残留鬼手(Presenter 跟丢时仍能清)。
* @param key 合成体键
*/
private _stripComposeVisuals(key: string): void {
const bagView = this._bagView;
if (!bagView || !key) return;
bagView.resolveConfuseHeroVisualSkin({
composeKey: key,
entityId: 0,
gridIndices: [],
anchorGridIndex: -1,
})?.detachConfuseMirrorFx();
const battle = this._battle;
if (!battle) return;
for (let playerId = 0; playerId <= 1; playerId++) {
const comps = battle.getPlayer(playerId)?.compositions;
if (!comps) continue;
for (let i = 0; i < comps.length; i++) {
const comp = comps[i];
if (!comp || composeKey(comp) !== key) continue;
for (let g = 0; g < comp.gridIndices.length; g++) {
bagView.getTextGridForConfuseHeroTarget(comp.gridIndices[g])
?.releaseAttachedBattleBuffFxByName(CONFUSE_HERO_CAST_PREFAB_NAME);
}
}
}
}
/** 重置单次施法占用态 */
private _resetPlayState(): void {
this._playing = false;
this._castFxNode = null;
this._playTargetKey = null;
this._playGrid = null;
this._playSkin = null;
}
/** 中断并清理全部表现 */
private _abortAllFx(): void {
this._recycleCastFx();
const pending = this._activeFxNodes.slice();
this._activeFxNodes.length = 0;
for (let i = 0; i < pending.length; i++) {
this._releaseFx(pending[i]);
}
this._stripPlayVisuals();
this._resetPlayState();
}
}