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/BattlePeachCoinFlyPresenter.ts

279 lines
9.8 KiB

import { Label, Node, Tween, UIOpacity, Vec3 } from 'cc';
import { GEvent } from 'db://assets/mx/module/event/GEvent';
import { GBundle } from '../../game/ConfigRes';
import { CoinFly } from './CoinFly';
/** 单次最多飘出 coinFly 数量 */
const MAX_FLY_COUNT = 8;
/** 多枚依次起飞间隔(秒) */
const FLY_STAGGER_SEC = 0.05;
/** coinFly prefab 默认 scale=0.6;原放大×2,再缩小 40% */
const FLY_VISUAL_SCALE = 0.6 * 2 * 0.6;
/** +1 相对桃子图标右侧本地偏移(父节点已缩放) */
const PLUS_OFFSET_X = 40 / FLY_VISUAL_SCALE;
/** 挂在桃子上的 +1 节点名(回池前拆掉) */
const PLUS_CHILD = 'dropPlusOne';
/**
* 战斗内金币飘字:使用 battleGame/prefab/coinFly。
* 起点击杀/回收点,终点底部 btnCoin/金币。
*/
export class BattlePeachCoinFlyPresenter {
private _moneyNode: Node | null = null;
private _flyLayer: Node | null = null;
private _flyPending = 0;
private _flyComplete: (() => void) | null = null;
/** 掉落 +1 飘字 */
private readonly _plusQueue: Node[] = [];
/** 绑定飞向目标(btnCoin/金币)与飞行层(ui_fly) */
bind(moneyNode: Node | null | undefined, flyLayer: Node | null | undefined): void {
this._moneyNode = moneyNode?.isValid ? moneyNode : null;
this._flyLayer = flyLayer?.isValid ? flyLayer : null;
}
/** 注销 */
unbind(): void {
this._recycleAllPlus();
this._moneyNode = null;
this._flyLayer = null;
this._flyPending = 0;
this._flyComplete = null;
}
/**
* 从节点世界坐标飘仙桃到 ui_money。
* @param startNode skill1 / skill2 根节点
* @param peachCount 回收蟠桃数量(决定飘出枚数上限)
* @param onAllArrived 全部到达后回调(在此发放蟠桃)
*/
playFromNode(startNode: Node | null | undefined, peachCount: number, onAllArrived: () => void): void {
if (peachCount <= 0) {
onAllArrived();
return;
}
const start = startNode?.isValid ? startNode.worldPosition.clone() : null;
if (!start) {
console.log('[BattlePeachCoinFly] 起点无效,跳过飘字');
onAllArrived();
return;
}
this._playFromWorldStart(start, peachCount, onAllArrived);
}
/**
* 从世界坐标飘仙桃到 ui_money。
* @param startWorld 起点世界坐标
* @param peachCount 蟠桃数量
* @param onAllArrived 全部到达后回调
*/
playFromWorld(startWorld: Readonly<Vec3>, peachCount: number, onAllArrived: () => void): void {
if (peachCount <= 0) {
onAllArrived();
return;
}
this._playFromWorldStart(startWorld.clone(), peachCount, onAllArrived);
}
/**
* 击杀掉落飞桃:先弹跳再飞向 ui_money,不占用回收完成回调。
* @param startWorld 死亡世界坐标
* @param peachCount Monster.peach
*/
playKillDrop(startWorld: Readonly<Vec3>, peachCount: number): void {
if (peachCount <= 0) return;
const n = this._spawnCoinFlies(startWorld.clone(), peachCount, null, true);
if (n <= 0) {
console.log('[BattlePeachCoinFly] 击杀掉桃未生成');
}
}
/**
* AI 击杀掉落:原地弹跳后回池,不飞向本机 ui_money。
* @param startWorld 死亡世界坐标
* @param peachCount Monster.peach
*/
playKillDropLocal(startWorld: Readonly<Vec3>, peachCount: number): void {
if (peachCount <= 0) return;
const n = this._spawnCoinFlies(startWorld.clone(), peachCount, null, false);
if (n <= 0) {
console.log('[BattlePeachCoinFly] AI 击杀掉桃未生成');
}
}
/**
* 乾坤袋回收:与怪物死亡产桃相同(coinFly 弹跳 + battleText「+1」飞向 ui_money)。
* @param startWorld 起点世界坐标
* @param peachCount 回收蟠桃数量
* @param onAllArrived 全部到达 ui_money 后回调
*/
playRecycleFromWorld(startWorld: Readonly<Vec3>, peachCount: number, onAllArrived: () => void): void {
this.playFromWorld(startWorld, peachCount, onAllArrived);
}
/** 从世界坐标飘仙桃到 ui_money(coinFly,多枚) */
private _playFromWorldStart(startWorld: Vec3, peachCount: number, onAllArrived: () => void): void {
this._flyPending = 0;
this._flyComplete = onAllArrived;
const spawned = this._spawnCoinFlies(startWorld, peachCount, this._onOneCoinFlyArrived.bind(this), true);
if (spawned <= 0) {
this._flyComplete = null;
onAllArrived();
}
}
/**
* 生成 coinFly:先原地弹跳;toMoney 时再飞向 ui_money。
* @param startWorld 起点
* @param peachCount 桃子数量
* @param onArrive 单枚到达;击杀掉落可空
* @param toMoney 是否飞向本机 ui_money
*/
private _spawnCoinFlies(
startWorld: Vec3,
peachCount: number,
onArrive: (() => void) | null,
toMoney: boolean,
): number {
const flyLayer = this._flyLayer;
const moneyNode = this._moneyNode;
if (!flyLayer?.isValid) {
console.log('[BattlePeachCoinFly] flyLayer 未绑定,跳过飘字');
return 0;
}
if (toMoney && !moneyNode?.isValid) {
console.log('[BattlePeachCoinFly] ui_money 未绑定,跳过飘字');
return 0;
}
if (!flyLayer.active) flyLayer.active = true;
const endPos = toMoney && moneyNode?.isValid ? moneyNode.worldPosition.clone() : null;
const flyCount = Math.min(Math.max(1, peachCount), MAX_FLY_COUNT);
let spawned = 0;
for (let i = 0; i < flyCount; i++) {
const node = gg.res.getNode('prefab/', 'coinFly', GBundle.BattleGame);
if (!node?.isValid) {
console.log('[BattlePeachCoinFly] coinFly 节点池不足', i);
continue;
}
flyLayer.addChild(node);
node.setScale(FLY_VISUAL_SCALE, FLY_VISUAL_SCALE, 1);
node.setWorldPosition(
startWorld.x + (Math.random() - 0.5) * 40,
startWorld.y + (Math.random() - 0.5) * 30,
startWorld.z,
);
const comp = node.getComponent(CoinFly);
if (!comp) {
console.log('[BattlePeachCoinFly] coinFly 缺少 CoinFly 组件');
gg.res.putNode(node);
continue;
}
spawned++;
if (onArrive) this._flyPending++;
this._attachPlus(node);
comp.flyAni(endPos, this._onPeachDone, i * FLY_STAGGER_SEC);
}
return spawned;
}
/**
* 把 battleText「+1」挂到桃子右侧,跟随弹跳/飞向 ui_money。
* @param peach 桃子节点
*/
private _attachPlus(peach: Node): void {
let node: Node | null = null;
try {
node = gg.res.getNode('prefab/', 'battleText', GBundle.BattleGame);
} catch (err) {
console.log('[BattlePeachCoinFly] battleText 取节点失败', String(err));
return;
}
if (!node?.isValid) {
console.log('[BattlePeachCoinFly] battleText 预制体缺失');
return;
}
const lab = node.getComponent(Label);
if (!lab) {
console.log('[BattlePeachCoinFly] battleText 无 Label');
gg.res.putNode(node);
return;
}
lab.string = '+1';
const arrow = node.getChildByName('上箭头');
if (arrow) arrow.active = false;
let fade = node.getComponent(UIOpacity);
if (!fade) fade = node.addComponent(UIOpacity);
fade.opacity = 255;
node.name = PLUS_CHILD;
node.parent = peach;
node.active = true;
node.setScale(1 / FLY_VISUAL_SCALE, 1 / FLY_VISUAL_SCALE, 1);
node.setPosition(PLUS_OFFSET_X, 0, 0);
this._plusQueue.push(node);
}
/** 桃子动画结束:拆 +1,再走到达计数 */
private readonly _onPeachDone = (peach?: Node): void => {
if (peach?.isValid) this._putPlusChild(peach);
if (this._flyPending > 0) this._onOneCoinFlyArrived();
};
/**
* 从桃子上拆下 +1 并回池。
* @param peach 桃子节点
*/
private _putPlusChild(peach: Node): void {
const plus = peach.getChildByName(PLUS_CHILD);
if (!plus?.isValid) return;
this._removePlusFromQueue(plus);
this._putPlus(plus);
}
/**
* 从跟踪队列移除。
* @param node +1 节点
*/
private _removePlusFromQueue(node: Node): void {
for (let i = 0; i < this._plusQueue.length; i++) {
if (this._plusQueue[i] !== node) continue;
this._plusQueue.splice(i, 1);
return;
}
}
/** 回收全部 +1 */
private _recycleAllPlus(): void {
for (let i = 0; i < this._plusQueue.length; i++) {
this._putPlus(this._plusQueue[i]);
}
this._plusQueue.length = 0;
}
/**
* 回池单条 +1。
* @param node battleText 节点
*/
private _putPlus(node: Node): void {
if (!node?.isValid) return;
Tween.stopAllByTarget(node);
const fade = node.getComponent(UIOpacity);
if (fade) {
Tween.stopAllByTarget(fade);
fade.opacity = 255;
}
node.removeFromParent();
gg.res.putNode(node);
}
/** 单枚 coinFly 到达 ui_money */
private _onOneCoinFlyArrived(): void {
this._flyPending = Math.max(0, this._flyPending - 1);
if (this._flyPending > 0) return;
const done = this._flyComplete;
this._flyComplete = null;
done?.();
GEvent.Ins.emit(GEvent.UpdateGameCoinNum);
}
}