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.
282 lines
9.9 KiB
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|