import { Node, sp, Tween, Vec3 } from 'cc'; import type { MapTileRecord } from 'db://assets/mx/module/merge'; import { GBundle } from '../../game/ConfigRes'; import { BATTLE_HERO_BUFF_PATH } from '../../battle/res/BattleHeroResCollector'; import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback'; import { BagToMapPlaceAnimator } from '../../battle/view/BagToMapPlaceAnimator'; import type { StealGlyphReceivePlacement, CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import type { TextGrid } from '../../battle/view/TextGrid'; /** 神偷手预制体(battleHero/prefab/buff/神偷手) */ const STEAL_FX_PREFAB_NAME = '神偷手'; /** 神偷手抓取动画 */ const STEAL_GRAB_ANIM = '2抓取'; /** 神偷手带字移动动画(循环) */ const STEAL_MOVE_ANIM = '3带字移动'; /** 神偷手相对字块本地 Y 偏移 */ const STEAL_FX_OFFSET_Y = 70; /** 神偷带字飞行:比通用拖放略慢,便于看清「3带字移动」 */ const STEAL_FLIGHT_SPEED_PX = 1400; const STEAL_FLIGHT_MIN_SEC = 0.28; const STEAL_FLIGHT_MAX_SEC = 0.58; /** 神偷方向:玩家偷 AI / AI 偷玩家 */ type StealFxDirection = 'playerFromAi' | 'aiFromPlayer'; /** * 妙手神偷(functiontype=16):拖到敌方未合成武将散字或普通单字,播神偷手抓取后带字飞入己方; * AI 施放时反向:锁定玩家字块后同套抓取/带字飞入 AI 侧。 */ export class BattleGameStealGlyphFxPresenter { private _bagView: CocosBagBoardView | null = null; private _flyLayer: Node | null = null; private _playing = false; private _fxNode: Node | null = null; private _presentationGrid: TextGrid | null = null; private _pendingTile: MapTileRecord | null = null; private _pendingPlacement: StealGlyphReceivePlacement | null = null; private _sourceGridIndex = -1; private _direction: StealFxDirection = 'playerFromAi'; private _onExtracted: ((tile: MapTileRecord) => void) | null = null; private _onPlayerStealSuccess: (() => void) | null = null; /** AI 神偷已 beginStealGlyphPresentation,结束时须配对 end */ private _aiStealGuardActive = false; /** 是否正在播放妙手神偷 */ get isPlaying(): boolean { return this._playing; } /** * 绑定背包视图与特效层。 * @param bagView 背包视图 * @param flyLayer 世界坐标特效层 */ bind(bagView: CocosBagBoardView | null, flyLayer: Node | null | undefined): void { this.unbind(); this._bagView = bagView; this._flyLayer = flyLayer?.isValid ? flyLayer : null; } /** 注销并中断进行中的表现 */ unbind(): void { this._abortPresentation(); this._bagView = null; this._flyLayer = null; } /** * 玩家:在敌方字块上播放妙手神偷。 * @param enemyGridIndex 敌方地图格索引 * @param onStealSuccess 偷取成功并进入 CD */ tryPlay(enemyGridIndex: number, onStealSuccess: () => void): boolean { if (this._playing) return false; const bagView = this._bagView; const flyLayer = this._flyLayer; if (!bagView || !flyLayer?.isValid) return false; const reject = bagView.getStealGlyphRejectReason(enemyGridIndex); if (reject === 'composed') { gg.ui.showToast('合成武将不能偷取'); return false; } if (reject != null) return false; const placement = bagView.resolveStealReceivePlacement(); if (!placement) { gg.ui.showToast('没空位放置不能偷取'); return false; } const destWorld = bagView.getStealReceiveWorldPosition(placement); if (!destWorld) { console.log('[StealGlyphFx] 落点世界坐标缺失', placement); return false; } const grid = bagView.detachEnemyMapTextGrid(enemyGridIndex) ?? bagView.getEnemyMapTextGrid(enemyGridIndex); if (!grid?.node?.isValid) { console.log('[StealGlyphFx] 敌方字块视图缺失', enemyGridIndex); return false; } const liftWorld = bagView.liftEnemyMapTextGridForSteal(grid, flyLayer); if (!liftWorld) { console.log('[StealGlyphFx] 敌方字块离槽失败', enemyGridIndex); return false; } return this._beginGrab( 'playerFromAi', enemyGridIndex, placement, grid, flyLayer, destWorld, null, onStealSuccess, ); } /** * AI:锁定玩家字块,播神偷手抓取后带字飞入 AI 落点。 * @param playerGridIndex 玩家地图格 * @param placement AI 接收落点 * @param onExtracted 抓取完成并提取成功 */ tryPlayAiSteal( playerGridIndex: number, placement: StealGlyphReceivePlacement, onExtracted: (tile: MapTileRecord) => void, ): boolean { if (this._playing) { console.log('[StealGlyphFx] AI 神偷时上一场未结束'); return false; } const bagView = this._bagView; const flyLayer = this._flyLayer; if (!bagView || !flyLayer?.isValid) { console.log('[StealGlyphFx] AI 神偷未绑定 bagView/flyLayer'); return false; } const destWorld = bagView.getAiStealReceiveWorldPosition(placement); if (!destWorld) { console.log('[StealGlyphFx] AI 落点世界坐标缺失', placement); return false; } bagView.beginStealGlyphPresentation(); this._aiStealGuardActive = true; const grid = bagView.detachOwnMapTextGridForSteal(playerGridIndex) ?? bagView.getOwnMapTextGrid(playerGridIndex); if (!grid?.node?.isValid) { console.log('[StealGlyphFx] 玩家字块视图缺失', playerGridIndex); this._releaseAiStealGuard(); return false; } const liftWorld = bagView.liftOwnMapTextGridForSteal(grid, flyLayer); if (!liftWorld) { console.log('[StealGlyphFx] 玩家字块离槽失败', playerGridIndex); this._releaseAiStealGuard(); return false; } const started = this._beginGrab( 'aiFromPlayer', playerGridIndex, placement, grid, flyLayer, destWorld, onExtracted, null, ); if (!started) { this._releaseAiStealGuard(); } return started; } /** * 挂载神偷手并播抓取。 * @param direction 方向 * @param sourceGridIndex 源格 * @param placement 落点 * @param grid 字块 * @param flyLayer 飞行层 * @param destWorld 落点世界坐标 * @param onExtracted AI 提取回调 * @param onPlayerStealSuccess 玩家偷取成功回调 */ private _beginGrab( direction: StealFxDirection, sourceGridIndex: number, placement: StealGlyphReceivePlacement, grid: TextGrid, flyLayer: Node, destWorld: Readonly, onExtracted: ((tile: MapTileRecord) => void) | null, onPlayerStealSuccess: (() => void) | null, ): boolean { const bagView = this._bagView; if (!bagView) return false; const fxNode = this._spawnFxOnGrid(grid, flyLayer); if (!fxNode) { console.log('[StealGlyphFx] 神偷手预制体加载失败'); bagView.releaseRecyclePresentationGrid(grid); return false; } const skel = fxNode.getComponent(sp.Skeleton) ?? fxNode.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[StealGlyphFx] 神偷手缺少 sp.Skeleton'); this._releaseFx(); bagView.releaseRecyclePresentationGrid(grid); return false; } this._playing = true; this._direction = direction; this._presentationGrid = grid; this._fxNode = fxNode; this._pendingPlacement = placement; this._sourceGridIndex = sourceGridIndex; this._onExtracted = onExtracted; this._onPlayerStealSuccess = onPlayerStealSuccess; const started = BattleSpinePlayback.play(skel, STEAL_GRAB_ANIM, { loop: false, onComplete: this._onGrabComplete.bind(this, destWorld, skel), }); if (!started) { console.log('[StealGlyphFx] 抓取动画播放失败', STEAL_GRAB_ANIM); this._abortPresentation(); return false; } return true; } /** 抓取动画完成:提取字块并带字飞向落点 */ private _onGrabComplete(destWorld: Readonly, skel: sp.Skeleton): void { if (!this._playing) return; if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); const bagView = this._bagView; const flyLayer = this._flyLayer; const grid = this._presentationGrid; const fxNode = this._fxNode; const placement = this._pendingPlacement; const sourceGridIndex = this._sourceGridIndex; const direction = this._direction; if (!bagView || !flyLayer?.isValid || !grid?.node?.isValid || !fxNode?.isValid || !placement || sourceGridIndex < 0) { this._abortPresentation(); return; } const tile = direction === 'playerFromAi' ? bagView.extractStealGlyphFromEnemy(sourceGridIndex) : bagView.extractStealGlyphFromOwn(sourceGridIndex); if (!tile) { console.log('[StealGlyphFx] 提取字块失败', direction, sourceGridIndex); this._abortPresentation(); return; } if (direction === 'playerFromAi') { bagView.purgeEnemyStealSourceCell(sourceGridIndex, grid); } else { bagView.purgeOwnStealSourceCell(sourceGridIndex, grid); } this._pendingTile = tile; if (direction === 'aiFromPlayer') { this._onExtracted?.(tile); } else { this._onPlayerStealSuccess?.(); } grid.preparePlaceFlightVisual(); if (fxNode.parent !== grid.node) { fxNode.parent = grid.node; fxNode.setPosition(0, STEAL_FX_OFFSET_Y, 0); this._applyFxLayer(fxNode, grid.node, flyLayer); } BattleSpinePlayback.play(skel, STEAL_MOVE_ANIM, { loop: true }); const attachParent = direction === 'playerFromAi' ? bagView.getStealReceiveAttachParent(placement) : bagView.getAiStealReceiveAttachParent(placement); const startWorld = grid.node.worldPosition.clone(); BagToMapPlaceAnimator.play(grid, destWorld, flyLayer, () => { this._onFlightComplete(grid, placement, attachParent, direction); }, { attachParent, restWorld: placement.kind === 'map' ? destWorld : null, startWorld, showTrail: true, moveSpeedPx: STEAL_FLIGHT_SPEED_PX, minTotalSec: STEAL_FLIGHT_MIN_SEC, maxTotalSec: STEAL_FLIGHT_MAX_SEC, }); } /** 飞行结束:写入盘面并提交视图 */ private _onFlightComplete( grid: TextGrid, placement: StealGlyphReceivePlacement, attachParent: Node | null, direction: StealFxDirection, ): void { const bagView = this._bagView; const tile = this._pendingTile; if (!bagView || !tile) { this._abortPresentation(); return; } const ok = direction === 'playerFromAi' ? bagView.applyStealGlyphGrant(tile, placement) : bagView.applyAiStealGlyphGrant(tile, placement); if (!ok) { console.log('[StealGlyphFx] 写入盘面失败', direction, placement); this._abortPresentation(); return; } if (placement.kind !== 'map' && attachParent?.isValid && grid.node.parent !== attachParent) { grid.node.parent = attachParent; grid.node.setPosition(0, 0, 0); } if (direction === 'playerFromAi') { bagView.commitStolenTextGridView(grid, placement); } else { bagView.commitAiStolenTextGridView(grid, placement); this._releaseAiStealGuard(); } this._releaseFx(); this._presentationGrid = null; this._pendingTile = null; this._pendingPlacement = null; this._sourceGridIndex = -1; this._onExtracted = null; this._onPlayerStealSuccess = null; this._playing = false; } /** 配对释放 AI 神偷 rebuild 守卫 */ private _releaseAiStealGuard(): void { if (!this._aiStealGuardActive) return; this._aiStealGuardActive = false; this._bagView?.endStealGlyphPresentation(); } /** 在字块上挂载神偷手(置顶、与飞行层同层) */ private _spawnFxOnGrid(grid: TextGrid, flyLayer: Node): Node | null { let node: Node | null = null; try { node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, STEAL_FX_PREFAB_NAME, GBundle.BattleHero); } catch (e) { console.log('[StealGlyphFx] 神偷手预制体加载异常', String(e)); return null; } if (!node?.isValid) { console.log('[StealGlyphFx] 神偷手预制体缺失', STEAL_FX_PREFAB_NAME); return null; } node.parent = grid.node; node.setPosition(0, STEAL_FX_OFFSET_Y, 0); node.active = true; this._applyFxLayer(node, grid.node, flyLayer); return node; } /** 神偷手与字块同层并置于最前 */ private _applyFxLayer(fxNode: Node, gridNode: Node, flyLayer: Node): void { fxNode.layer = flyLayer.layer; gridNode.layer = flyLayer.layer; fxNode.setSiblingIndex(gridNode.children.length - 1); } /** 回收神偷手特效 */ 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 { const grid = this._presentationGrid; if (grid?.node?.isValid) { BagToMapPlaceAnimator.stop(grid); Tween.stopAllByTarget(grid.node); } this._releaseFx(); if (grid?.node?.isValid) { this._bagView?.releaseRecyclePresentationGrid(grid); } this._presentationGrid = null; this._pendingTile = null; this._pendingPlacement = null; this._sourceGridIndex = -1; this._onExtracted = null; this._onPlayerStealSuccess = null; this._playing = false; this._bagView?.clearBattleBuffDropPreview(); this._releaseAiStealGuard(); } }