import { Node, sp, Tween, Vec3 } 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 type { BattleBuffSkillDropTarget, CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import type { TextGrid } from '../../battle/view/TextGrid'; /** 文昌笔预制体目录 */ const WENCHANG_BI_PREFAB_DIR = BATTLE_HERO_BUFF_PATH; /** 文昌笔预制体名 */ const WENCHANG_BI_PREFAB_NAME = '文昌笔'; /** 文昌笔 Spine 动画名 */ const WENCHANG_BI_ANIM_NAME = 'animation'; /** 属性卡:动画进度到该比例时换皮 */ const ATTR_REWRITE_AT_RATIO = 0.5; /** * 文昌笔改字表现:主动 skill 先播后改字;属性卡播到一半再换皮。 */ export class BattleGameRewriteGlyphPresenter { private _bagView: CocosBagBoardView | null = null; private _flyLayer: Node | null = null; private _playing = false; private _fxNode: Node | null = null; private _attrFxNode: Node | null = null; private _attrGen = 0; private _attrRewriteDone = true; private _attrOnRewrite: (() => void) | null = null; /** 绑定背包视图与飞行层 */ bind(bagView: CocosBagBoardView | null, flyLayer: Node | null | undefined): void { this.unbind(); this._bagView = bagView; this._flyLayer = flyLayer?.isValid ? flyLayer : null; } /** 注销并中断进行中的演出 */ unbind(): void { this._flushAttrRewriteIfNeeded(); this._abortPresentation(); this._abortAttrFx(); this._bagView = null; this._flyLayer = null; } /** 是否正在播放改字前置动画 */ get isPlaying(): boolean { return this._playing; } /** * 尝试在目标字块中心播放文昌笔,动画结束后执行随机改字。 * @param target 地图格 / 背包槽 * @param onSkillConsumed skill 进入 CD */ tryPlay(target: BattleBuffSkillDropTarget, onSkillConsumed: () => void): boolean { if (this._playing) return false; const bagView = this._bagView; const flyLayer = this._flyLayer; if (!bagView || !flyLayer?.isValid) return false; if (!bagView.canApplyBattleBuffRewriteGlyphTarget(target)) return false; const grid = bagView.getTextGridForDropTarget(target); if (!grid?.node?.isValid) { console.log(`[RewriteGlyph] 目标字块视图缺失 ${target.kind}`); return false; } const worldPos = grid.node.worldPosition.clone(); this._playing = true; const fx = this._spawnWenchangBiFx(flyLayer, worldPos); if (!fx) { console.log('[RewriteGlyph] 文昌笔预制体加载失败'); this._abortPresentation(); return false; } this._fxNode = fx; onSkillConsumed(); const skel = fx.getComponent(sp.Skeleton) ?? fx.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[RewriteGlyph] 文昌笔缺少 sp.Skeleton'); this._abortPresentation(); return false; } BattleSpinePlayback.play(skel, WENCHANG_BI_ANIM_NAME, { loop: false, onComplete: this._onAnimComplete.bind(this, target), }); return true; } /** * 属性卡:先播文昌笔,动画一半时回调换皮,播完回收。 * @param target 目标字块 * @param onRewrite 半程换皮(须幂等) */ tryPlayAttrRewrite(target: BattleBuffSkillDropTarget, onRewrite: () => void): boolean { if (this._playing || !this._attrRewriteDone) { this._flushAttrRewriteIfNeeded(); onRewrite(); return false; } const bagView = this._bagView; if (!bagView) { onRewrite(); return false; } const grid = bagView.getTextGridForDropTarget(target); if (!grid?.node?.isValid) { console.log(`[RewriteGlyph] 属性卡目标字块缺失 ${target.kind}`); onRewrite(); return false; } const fx = this._spawnFxOnText(grid); if (!fx) { console.log('[RewriteGlyph] 属性卡文昌笔加载失败'); onRewrite(); return false; } const skel = fx.getComponent(sp.Skeleton) ?? fx.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[RewriteGlyph] 属性卡文昌笔缺少 sp.Skeleton'); this._releaseAttrFx(fx); onRewrite(); return false; } this._attrFxNode = fx; this._attrOnRewrite = onRewrite; this._attrRewriteDone = false; const gen = ++this._attrGen; const duration = BattleSpinePlayback.getAnimDuration(skel, WENCHANG_BI_ANIM_NAME); const midDelay = duration != null && duration > 0 ? (duration * ATTR_REWRITE_AT_RATIO) : 0; schedulePresentation(midDelay, this._onAttrMidRewrite.bind(this, gen)); const played = BattleSpinePlayback.play(skel, WENCHANG_BI_ANIM_NAME, { loop: false, onComplete: this._onAttrFxComplete.bind(this, gen, fx, skel), }); if (!played) { this._onAttrMidRewrite(gen); this._releaseAttrFx(fx); return false; } return true; } /** 动画播完:移除预制体并提交随机改字 */ private _onAnimComplete(target: BattleBuffSkillDropTarget): void { this._releaseFx(); const bagView = this._bagView; if (!bagView) { this._playing = false; return; } if (!bagView.canApplyBattleBuffRewriteGlyphTarget(target)) { console.log(`[RewriteGlyph] 动画结束时目标已不可改字 ${target.kind}`); this._playing = false; return; } if (!bagView.applyBattleBuffRewriteGlyphTarget(target)) { console.log(`[RewriteGlyph] 随机改字失败 ${target.kind}`); } this._playing = false; } /** 属性卡半程:换皮 */ private _onAttrMidRewrite(gen: number): void { if (gen !== this._attrGen || this._attrRewriteDone) return; this._attrRewriteDone = true; const cb = this._attrOnRewrite; this._attrOnRewrite = null; cb?.(); } /** 属性卡播完:确保已换皮并回收 */ private _onAttrFxComplete(gen: number, fx: Node, skel: sp.Skeleton): void { if (gen !== this._attrGen) return; this._onAttrMidRewrite(gen); if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); this._releaseAttrFx(fx); } /** 中断时若尚未换皮则立刻提交 */ private _flushAttrRewriteIfNeeded(): void { if (this._attrRewriteDone) return; this._attrRewriteDone = true; const cb = this._attrOnRewrite; this._attrOnRewrite = null; this._attrGen++; cb?.(); } /** 在字块中心生成文昌笔表现 */ private _spawnWenchangBiFx(flyLayer: Node, worldPos: Readonly): Node | null { if (!flyLayer.active) flyLayer.active = true; let node: Node | null = null; try { node = gg.res.getNode(WENCHANG_BI_PREFAB_DIR, WENCHANG_BI_PREFAB_NAME, GBundle.BattleHero); } catch (e) { console.log('[RewriteGlyph] 文昌笔加载异常', String(e)); return null; } if (!node?.isValid) return null; node.parent = flyLayer; node.setWorldPosition(worldPos); node.setSiblingIndex(flyLayer.children.length - 1); node.active = true; return node; } /** 挂到字块上叠播文昌笔 */ private _spawnFxOnText(grid: TextGrid): Node | null { if (!grid?.node?.isValid) return null; let node: Node | null = null; try { node = gg.res.getNode(WENCHANG_BI_PREFAB_DIR, WENCHANG_BI_PREFAB_NAME, GBundle.BattleHero); } catch (e) { console.log('[RewriteGlyph] 属性卡文昌笔加载异常', String(e)); return null; } if (!node?.isValid) return null; grid.attachBattleBuffFx(node, 'over'); return node; } /** 回收属性卡文昌笔 */ private _releaseAttrFx(node: Node | null): void { if (this._attrFxNode === node) this._attrFxNode = null; if (!node?.isValid) return; const skel = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); const host = node.parent?.getComponent(TextGrid); if (host?.isValid) { host.releaseAttachedBattleBuffFx(node); return; } Tween.stopAllByTarget(node); if (node['resKey']) gg.res.putNode(node); else node.destroy(); } /** 中断属性卡表现 */ private _abortAttrFx(): void { this._attrGen++; const node = this._attrFxNode; this._attrFxNode = null; this._attrOnRewrite = null; this._attrRewriteDone = true; if (node?.isValid) this._releaseAttrFx(node); } /** 回收文昌笔节点 */ private _releaseFx(): void { const node = this._fxNode; this._fxNode = null; if (!node?.isValid) return; const skel = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); Tween.stopAllByTarget(node); if (node['resKey']) gg.res.putNode(node); else node.destroy(); } /** 中断演出并清理 */ private _abortPresentation(): void { this._releaseFx(); this._playing = false; } }