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/BattleGameStealGlyphFxPrese...

421 lines
15 KiB

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<Vec3>,
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<Vec3>, 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,
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 (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();
}
}