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.
474 lines
17 KiB
474 lines
17 KiB
import { Label, Node, Tween, 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;
|
|
/** 波次结算每枚金币的冒出间隔(秒) */
|
|
const WAVE_COIN_SPAWN_GAP_SEC = 0.15;
|
|
/** 波次结算金币起点相对屏幕中心的世界 Y 偏移 */
|
|
const WAVE_COIN_START_OFFSET_Y = 150;
|
|
/** 波次金币冒出时的水平散开(世界像素) */
|
|
const WAVE_POP_SPREAD_X = 140;
|
|
/** 波次金币冒出时的垂直散开(世界像素) */
|
|
const WAVE_POP_SPREAD_Y = 70;
|
|
/** 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[] = [];
|
|
/** 波次金币还待生成的枚数 */
|
|
private _waveLeft = 0;
|
|
/** 波次金币单枚显示数量 */
|
|
private _waveShowGold = 0;
|
|
/** 波次金币父节点 */
|
|
private _waveLayer: Node | null = null;
|
|
/** 波次金币起点世界坐标 */
|
|
private readonly _waveStart = new Vec3();
|
|
/** 已冒出、尚未一起飞的波次金币 */
|
|
private readonly _waveCoins: Node[] = [];
|
|
/** 本轮已生成枚数 */
|
|
private _waveSpawned = 0;
|
|
/** 本轮已完成冒出的枚数 */
|
|
private _wavePopped = 0;
|
|
/** 间隔生成已结束,等全部冒出后再一起飞 */
|
|
private _waveSpawnFinished = false;
|
|
/** 正在冒出并等待一起飞 */
|
|
private _waveAwaitFly = false;
|
|
|
|
/** 绑定飞向目标(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._stopWaveSpawn();
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 波次结算:金币在不同位置间隔冒出,全部冒完后一起飞向 btnCoin。
|
|
* @param flyLayer BattleGame.ui_fly
|
|
* @param startWorld 起点世界坐标
|
|
* @param showGold 单枚金币上的显示数量
|
|
* @param count 生成枚数
|
|
*/
|
|
playWaveCoin(flyLayer: Node, startWorld: Readonly<Vec3>, showGold: number, count: number): void {
|
|
if (!flyLayer?.isValid) {
|
|
console.log('[BattlePeachCoinFly] 波次金币缺少 ui_fly');
|
|
return;
|
|
}
|
|
const amount = Math.floor(showGold);
|
|
const flyCount = Math.floor(count);
|
|
if (!(amount > 0) || !(flyCount > 0)) {
|
|
console.log('[BattlePeachCoinFly] 波次金币数量非法', showGold, count);
|
|
return;
|
|
}
|
|
this._stopWaveSpawn();
|
|
this._waveLeft = flyCount;
|
|
this._waveShowGold = amount;
|
|
this._waveLayer = flyLayer;
|
|
this._waveStart.set(startWorld.x, startWorld.y + WAVE_COIN_START_OFFSET_Y, startWorld.z);
|
|
this._waveSpawned = 0;
|
|
this._wavePopped = 0;
|
|
this._waveSpawnFinished = false;
|
|
this._waveAwaitFly = true;
|
|
this._spawnOneWaveCoin();
|
|
}
|
|
|
|
/** 在散开位置冒出一枚波次金币,剩余的等间隔再冒 */
|
|
private _spawnOneWaveCoin(): void {
|
|
if (!this._waveAwaitFly || this._waveLeft <= 0) {
|
|
this._stopWaveSpawn();
|
|
return;
|
|
}
|
|
const layer = this._waveLayer;
|
|
if (!layer?.isValid) {
|
|
console.log('[BattlePeachCoinFly] 波次金币缺少 ui_fly');
|
|
this._stopWaveSpawn();
|
|
return;
|
|
}
|
|
const world = new Vec3(
|
|
this._waveStart.x + (Math.random() - 0.5) * WAVE_POP_SPREAD_X,
|
|
this._waveStart.y + (Math.random() - 0.5) * WAVE_POP_SPREAD_Y,
|
|
this._waveStart.z,
|
|
);
|
|
const node = this._createCoinNode(layer, world, `+${this._waveShowGold}`);
|
|
if (!node) {
|
|
console.log('[BattlePeachCoinFly] 波次金币未生成');
|
|
this._stopWaveSpawn();
|
|
return;
|
|
}
|
|
const comp = node.getComponent(CoinFly);
|
|
if (!comp) {
|
|
console.log('[BattlePeachCoinFly] coinFly 缺少 CoinFly 组件');
|
|
this._recycleWaveCoin(node);
|
|
this._stopWaveSpawn();
|
|
return;
|
|
}
|
|
this._waveCoins.push(node);
|
|
this._waveSpawned++;
|
|
this._waveLeft--;
|
|
comp.popStay(this._onOneWavePopped);
|
|
if (this._waveLeft <= 0) {
|
|
this._waveSpawnFinished = true;
|
|
this._tryFlyWaveTogether();
|
|
return;
|
|
}
|
|
tween(this).delay(WAVE_COIN_SPAWN_GAP_SEC).call(this._onWaveCoinGap).start();
|
|
}
|
|
|
|
/** 间隔结束,再冒出一枚波次金币 */
|
|
private readonly _onWaveCoinGap = (): void => {
|
|
this._spawnOneWaveCoin();
|
|
};
|
|
|
|
/** 一枚波次金币冒出结束 */
|
|
private readonly _onOneWavePopped = (): void => {
|
|
if (!this._waveAwaitFly) return;
|
|
this._wavePopped++;
|
|
this._tryFlyWaveTogether();
|
|
};
|
|
|
|
/** 全部冒出后,一起飞向 btnCoin */
|
|
private _tryFlyWaveTogether(): void {
|
|
if (!this._waveAwaitFly || !this._waveSpawnFinished) return;
|
|
if (this._wavePopped < this._waveSpawned) return;
|
|
this._waveAwaitFly = false;
|
|
const money = this._moneyNode;
|
|
const end = money?.isValid ? money.worldPosition.clone() : null;
|
|
const coins = this._takeWaveCoins();
|
|
this._clearWaveSpawn();
|
|
if (!end) {
|
|
console.log('[BattlePeachCoinFly] 波次金币缺少飞向目标');
|
|
for (let i = 0; i < coins.length; i++) this._recycleWaveCoin(coins[i]);
|
|
return;
|
|
}
|
|
for (let i = 0; i < coins.length; i++) {
|
|
const node = coins[i];
|
|
if (!node?.isValid) continue;
|
|
const comp = node.getComponent(CoinFly);
|
|
if (!comp) {
|
|
this._recycleWaveCoin(node);
|
|
continue;
|
|
}
|
|
comp.flyTo(end, this._onPeachDone);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 取出一枚金币并挂上数字,先不飞。
|
|
* @param layer 父节点
|
|
* @param world 世界坐标
|
|
* @param plusText 金币上的数字
|
|
*/
|
|
private _createCoinNode(layer: Node, world: Readonly<Vec3>, plusText: string): Node | null {
|
|
if (!layer.active) layer.active = true;
|
|
const node = gg.res.getNode('prefab/', 'coinFly', GBundle.BattleGame);
|
|
if (!node?.isValid) {
|
|
console.log('[BattlePeachCoinFly] coinFly 节点池不足');
|
|
return null;
|
|
}
|
|
layer.addChild(node);
|
|
node.setScale(FLY_VISUAL_SCALE, FLY_VISUAL_SCALE, 1);
|
|
node.setWorldPosition(world.x, world.y, world.z);
|
|
this._attachPlus(node, plusText);
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* 回池一枚尚未飞走的波次金币。
|
|
* @param node 金币节点
|
|
*/
|
|
private _recycleWaveCoin(node: Node | null): void {
|
|
if (!node?.isValid) return;
|
|
Tween.stopAllByTarget(node);
|
|
this._putPlusChild(node);
|
|
gg.res.putNode(node);
|
|
}
|
|
|
|
/** 取出并清空等待一起飞的金币,避免清长度时把同一数组引用一起清空 */
|
|
private _takeWaveCoins(): Node[] {
|
|
const coins: Node[] = [];
|
|
for (let i = 0; i < this._waveCoins.length; i++) coins.push(this._waveCoins[i]);
|
|
this._waveCoins.length = 0;
|
|
return coins;
|
|
}
|
|
|
|
/** 停掉尚未一起飞的波次金币 */
|
|
private _stopWaveSpawn(): void {
|
|
Tween.stopAllByTarget(this);
|
|
this._waveAwaitFly = false;
|
|
const coins = this._takeWaveCoins();
|
|
for (let i = 0; i < coins.length; i++) this._recycleWaveCoin(coins[i]);
|
|
this._clearWaveSpawn();
|
|
}
|
|
|
|
/** 清空波次金币生成状态,不回收已经开始飞行的金币 */
|
|
private _clearWaveSpawn(): void {
|
|
this._waveLeft = 0;
|
|
this._waveShowGold = 0;
|
|
this._waveLayer = null;
|
|
this._waveSpawned = 0;
|
|
this._wavePopped = 0;
|
|
this._waveSpawnFinished = false;
|
|
}
|
|
|
|
/**
|
|
* 击杀掉落飞桃:先弹跳再飞向 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
|
|
* @param flySec 飞向终点时长;≤0 为击杀掉落的短飞行
|
|
* @param layer 指定父节点;缺省用已绑定的飞行层
|
|
* @param plusText 金币上的数字;缺省为击杀掉落的 +1
|
|
* @param maxCount 本次最多生成枚数;波次结算按配置次数,击杀掉落沿用上限
|
|
*/
|
|
private _spawnCoinFlies(startWorld: Vec3, peachCount: number, onArrive: (() => void) | null, toMoney: boolean, flySec = 0, layer: Node | null = null, plusText = '+1', maxCount = MAX_FLY_COUNT): number {
|
|
const flyLayer = layer?.isValid ? layer : 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, Math.floor(peachCount)), Math.floor(maxCount));
|
|
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, plusText);
|
|
comp.flyAni(endPos, this._onPeachDone, i * FLY_STAGGER_SEC, flySec);
|
|
}
|
|
return spawned;
|
|
}
|
|
|
|
/**
|
|
* 把数字挂到金币右侧,跟随弹跳并飞向 btnCoin。
|
|
* @param peach 金币节点
|
|
* @param text 显示文案,如 +110
|
|
*/
|
|
private _attachPlus(peach: Node, text: string): 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 = text;
|
|
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);
|
|
}
|
|
}
|
|
|