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.
421 lines
15 KiB
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();
|
|
}
|
|
}
|
|
|