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

282 lines
9.9 KiB

3 weeks ago
import { Node, sp, tween, Tween, Vec3, v3 } 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';
import type { BattlePeachCoinFlyPresenter } from './BattlePeachCoinFlyPresenter';
/** 乾坤袋预制体目录 */
const QIANKUNDAI_PREFAB_DIR = BATTLE_HERO_BUFF_PATH;
/** 乾坤袋预制体名 */
const QIANKUNDAI_PREFAB_NAME = '乾坤袋';
/** 乾坤袋 Spine 动画名 */
const QIANKUNDAI_ANIM_NAME = 'animation';
/** 乾坤袋动画与字块吸入并行:延迟后开始移动(逻辑秒) */
const RECYCLE_TEXT_START_DELAY_SEC = 1;
/** 字块吸入偏移(本地坐标) */
const RECYCLE_TEXT_OFFSET_X = 73;
const RECYCLE_TEXT_OFFSET_Y = 73;
/** 字块缩放消失时长(秒) */
const RECYCLE_TEXT_FLY_SEC = 0.35;
/** 单次乾坤袋回收演出 */
type RecycleToPeachSession = {
/** 会话 id */
id: number;
/** 乾坤袋节点 */
bagFxNode: Node | null;
/** 展示中的字块 */
grid: TextGrid;
/** 倒计时世代 */
scheduleGen: number;
};
/**
* 乾坤袋主动 buff:skill 拖到字块上回收,播乾坤袋 → 字块吸入 → 仙桃飘字。
* CD 结束即可再次使用,不等待表现播完。
*/
export class BattleGameRecycleToPeachPresenter {
private _bagView: CocosBagBoardView | null = null;
private _flyLayer: Node | null = null;
private _peachFly: BattlePeachCoinFlyPresenter | null = null;
/** 并行演出会话 */
private _sessions: RecycleToPeachSession[] = [];
/** 会话 id 自增 */
private _sessionIdSeq = 0;
/** 并行倒计时世代(abort 时递增以作废待触发回调) */
private _scheduleGen = 0;
/** 绑定背包视图、飞行层与仙桃飘字 */
bind(
bagView: CocosBagBoardView | null,
flyLayer: Node | null | undefined,
peachFly: BattlePeachCoinFlyPresenter | null,
): void {
this.unbind();
this._bagView = bagView;
this._flyLayer = flyLayer?.isValid ? flyLayer : null;
this._peachFly = peachFly;
}
/** 注销并中断进行中的演出 */
unbind(): void {
this._abortAllSessions();
this._bagView = null;
this._flyLayer = null;
this._peachFly = null;
}
/** 是否仍有进行中的回收演出(仅诊断,不阻塞施放) */
get isPlaying(): boolean {
return this._sessions.length > 0;
}
/**
* 尝试在目标字块上播放乾坤袋回收。
* @param target 地图格 / 背包槽
* @param peachCount 发放蟠桃数量
* @param onSkillConsumed skill 进入 CD
*/
tryPlay(
target: BattleBuffSkillDropTarget,
peachCount: number,
onSkillConsumed: () => void,
): boolean {
const bagView = this._bagView;
const flyLayer = this._flyLayer;
if (!bagView || !flyLayer?.isValid || peachCount <= 0) return false;
if (!bagView.canApplyBattleBuffRecycleTarget(target)) return false;
const grid = bagView.getTextGridForDropTarget(target);
if (!grid?.node?.isValid) {
console.log(`[RecycleToPeach] 目标字块视图缺失 ${target.kind}`);
return false;
}
const textWorld = bagView.liftTextGridForRecyclePresentation(grid, flyLayer);
if (!textWorld) {
console.log(`[RecycleToPeach] 字块离槽失败 ${target.kind}`);
return false;
}
const bagFx = this._spawnQiankundaiFx(flyLayer, textWorld);
if (!bagFx) {
console.log('[RecycleToPeach] 乾坤袋预制体加载失败');
bagView.releaseRecyclePresentationGrid(grid);
return false;
}
const session: RecycleToPeachSession = {
id: ++this._sessionIdSeq,
bagFxNode: bagFx,
grid,
scheduleGen: ++this._scheduleGen,
};
this._sessions.push(session);
onSkillConsumed();
const skel = bagFx.getComponent(sp.Skeleton) ?? bagFx.getComponentInChildren(sp.Skeleton);
if (!skel?.isValid) {
console.log('[RecycleToPeach] 乾坤袋缺少 sp.Skeleton');
this._abortSession(session.id);
return false;
}
BattleSpinePlayback.play(skel, QIANKUNDAI_ANIM_NAME, {
loop: false,
onComplete: () => this._onBagAnimComplete(session.id),
});
const scheduleGen = session.scheduleGen;
schedulePresentation(RECYCLE_TEXT_START_DELAY_SEC, () => {
if (!this._isSessionActive(session.id, scheduleGen)) return;
this._beginTextFly(session, target, peachCount);
});
return true;
}
/** 字块吸入袋口(与乾坤袋动画并行,1s 后触发) */
private _beginTextFly(
session: RecycleToPeachSession,
target: BattleBuffSkillDropTarget,
peachCount: number,
): void {
const grid = session.grid;
if (!this._isSessionActive(session.id) || !grid.node?.isValid) {
this._abortSession(session.id);
return;
}
const textNode = grid.node;
Tween.stopAllByTarget(textNode);
const startPos = textNode.position.clone();
const endPos = v3(
startPos.x + RECYCLE_TEXT_OFFSET_X,
startPos.y + RECYCLE_TEXT_OFFSET_Y,
startPos.z,
);
tween(textNode)
.to(RECYCLE_TEXT_FLY_SEC, { position: endPos, scale: v3(0, 0, 1) }, { easing: 'sineIn' })
.call(() => this._onTextFlyComplete(session.id, target, peachCount, textNode))
.start();
}
/** 乾坤袋动画播完:回收预制体(与字块吸入并行,互不提前删除) */
private _onBagAnimComplete(sessionId: number): void {
const session = this._findSession(sessionId);
if (!session) return;
this._releaseBagFx(session);
session.bagFxNode = null;
}
/** 字块吸入完成:提交回收并发桃 */
private _onTextFlyComplete(
sessionId: number,
target: BattleBuffSkillDropTarget,
peachCount: number,
textNode: Node,
): void {
const session = this._findSession(sessionId);
const bagView = this._bagView;
const peachFly = this._peachFly;
if (!session || !bagView) {
if (sessionId > 0) this._abortSession(sessionId);
return;
}
const grid = session.grid;
const peachStart = textNode.isValid ? textNode.worldPosition.clone() : null;
bagView.applyBattleBuffRecycleTarget(target);
bagView.releaseRecyclePresentationGrid(grid);
this._removeSession(sessionId);
const grantPeach = (): void => {
bagView.grantBattlePeach(peachCount);
};
if (peachStart && peachFly) {
peachFly.playRecycleFromWorld(peachStart, peachCount, grantPeach);
} else {
grantPeach();
}
}
/** 在字块位置生成乾坤袋表现 */
private _spawnQiankundaiFx(flyLayer: Node, worldPos: Readonly<Vec3>): Node | null {
if (!flyLayer.active) flyLayer.active = true;
let node: Node | null = null;
try {
node = gg.res.getNode(QIANKUNDAI_PREFAB_DIR, QIANKUNDAI_PREFAB_NAME, GBundle.BattleHero);
} catch (e) {
console.log('[RecycleToPeach] 乾坤袋加载异常', 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 _releaseBagFx(session: RecycleToPeachSession): void {
const node = session.bagFxNode;
session.bagFxNode = 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 _abortAllSessions(): void {
this._scheduleGen++;
const ids = this._sessions.map((s) => s.id);
for (let i = 0; i < ids.length; i++) {
this._abortSession(ids[i]);
}
}
/** 中断单次演出 */
private _abortSession(sessionId: number): void {
const session = this._findSession(sessionId);
if (!session) return;
this._releaseBagFx(session);
const grid = session.grid;
if (grid?.node?.isValid) {
Tween.stopAllByTarget(grid.node);
this._bagView?.releaseRecyclePresentationGrid(grid);
}
this._removeSession(sessionId);
}
/** 会话是否仍有效 */
private _isSessionActive(sessionId: number, scheduleGen?: number): boolean {
const session = this._findSession(sessionId);
if (!session) return false;
if (scheduleGen != null && session.scheduleGen !== scheduleGen) return false;
return true;
}
/** 查找会话 */
private _findSession(sessionId: number): RecycleToPeachSession | null {
for (let i = 0; i < this._sessions.length; i++) {
if (this._sessions[i].id === sessionId) return this._sessions[i];
}
return null;
}
/** 移除会话记录 */
private _removeSession(sessionId: number): void {
for (let i = this._sessions.length - 1; i >= 0; i--) {
if (this._sessions[i].id === sessionId) {
this._sessions.splice(i, 1);
return;
}
}
}
}