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.
407 lines
16 KiB
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();
|
||
|
|
}
|
||
|
|
}
|