//********************* // create by 流云 // time: Thu Sep 25 2025 // desc: //********************* import { _decorator, Label, Layout, Node, tween, Tween, UITransform, v3, Vec3 } from 'cc'; import { Auto_BattleSuccess } from './Auto_BattleSuccess'; import { BattleSuccessNodes, playBattleSuccessOpen, prepareBattleSuccessOpen, stopBattleSuccessOpen, stopHighlightButtonAnim, } from './BattleSuccessOpenAnim'; import { BattleSuccessCoinFlyPresenter } from './BattleSuccessCoinFlyPresenter'; import { TaskType, ItemNumType } from '../../game/GameData'; import { ItemData } from '../../game/ConfigProjectData'; import MTools from 'db://assets/mx/tools/MTools'; import { GAudioPath } from '../../game/ConfigRes'; import { VivoAd } from '../../sdk/functions/VivoAd'; import { IPopAnimConfig } from 'db://assets/mx/module/ui/popAnim'; import { recordKeepHeroClearFromBattle } from '../../battle/bag/KeepHeroClearRecord'; import { MatchWinStats } from '../../manager/MatchWinStats'; import { RankDataManager } from '../../manager/RankDataManager'; import { WeaponDropView } from '../BattleGame/BattleWeaponDropPresenter'; import { WeaponDropService, type WeaponDropRecord } from '../../battle/combat/WeaponDropService'; const { ccclass } = _decorator; /**游戏结果奖励类型 */ export enum GameResultAwardType { Coin = 1,//金币 Diamond = 2,//钻石 } @ccclass('UIBattleSuccess') export class UIBattleSuccess extends Auto_BattleSuccess { addNum = 1 private _victoryNodes: BattleSuccessNodes = null; /** ui_animStar 初始本地坐标 */ private _animStarHome: Vec3 | null = null; /** ui_rankName 初始本地坐标 */ private _rankNameHome: Vec3 | null = null; /** 升星动画起点(已点亮数) */ private _starAnimFrom = 0; /** 升星动画终点(已点亮数) */ private _starAnimTo = 0; /** 结算后段位 id */ private _rankAfterId = 0; /** 结算后星条点亮数 */ private _afterStarCount = 0; /** 飞星节点 */ private _uiAnimStar: Node | null = null; /** 飞段位节点 */ private _uiRankName: Node | null = null; /** 本局待领金币(不含倍率) */ private _pendingCoin = 0; /** 结算奖励列表(领取时入账) */ private _dataList: ItemData[] = []; /** ui_content 宝石展示节点 */ private readonly _lootWeaponNodes: Node[] = []; /** 金币飞行 */ private _coinFly: BattleSuccessCoinFlyPresenter | null = null; /** 领取流程中 */ private _claiming = false; onLoad(): void { super.onLoad(); this._bindVictoryNodes(); this._bindRankAnimNodes(); this._hideGemTemp(); this._coinFly = new BattleSuccessCoinFlyPresenter(); this._coinFly.bind(this.ui_center); } /** 隐藏结算宝石模板,避免占位节点显示在面板上 */ private _hideGemTemp(): void { if (!this.ui_gemTemp?.isValid) { console.log('[UIBattleSuccess] ui_gemTemp 缺失'); return; } this.ui_gemTemp.active = false; } /** preparePopOpenPose 可能在 onLoad 之前调用,需惰性绑定节点 */ private _bindVictoryNodes(): void { if (this._victoryNodes || !this.ui_center) return; this._victoryNodes = { bg: this._child('背景'), title: this._child('挑战成功标题'), rewardLabel: this._child('获得奖励'), tipBg: this._child('你已赢得该对局'), spine: this._child('结算页'), scroll: this.ui_content, btnLayout: this._child('btnLayout'), }; } /** 绑定升星 / 升段飞入节点 */ private _bindRankAnimNodes(): void { if (!this.ui_center?.isValid) return; if (!this._uiAnimStar) { this._uiAnimStar = this.ui_center.getChildByName('ui_animStar'); if (!this._uiAnimStar) console.log('[UIBattleSuccess] 缺节点 ui_animStar'); } if (!this._uiRankName) { this._uiRankName = this.ui_center.getChildByName('ui_rankName'); if (!this._uiRankName) console.log('[UIBattleSuccess] 缺节点 ui_rankName'); } } /** * @param name ui_center 下子节点名 */ private _child(name: string): Node | null { const n = this.ui_center.getChildByName(name); if (!n) console.log('[UIBattleSuccess] 缺节点', name); return n; } onEnable(): void { super.onEnable(); gg.sdk.pfTriggerGC(); this._coinFly?.bind(this.ui_center); } onDisable(): void { if (this._victoryNodes) { stopBattleSuccessOpen(this._victoryNodes); } this._stopStarUpgradeAnim(); this._stopRankNameFly(); this.unschedule(this._onStarAnimDelay); this.unschedule(this._onRankFlyDelay); this._recycleWeaponLoot(); this._coinFly?.unbind(); VivoAd.hideNative(); super.onDisable(); } preparePopOpenPose(): void { const cfg = this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined; if (!cfg) return; // UIManager 会在 onLoad 前调用;此处仅遮罩,节点隐藏与序列在 onInit 执行 gg.ui.popAnim.prepareOpen(this.node, cfg.mask ?? 'dim', 'none'); } playPopFromConfig(): void { const cfg = this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined; if (!cfg) return; if (cfg.mask !== 'none') { this.popMask(cfg.mask ?? 'dim'); } } onInit(): void { this.addNum = 1; this.btnADGet.active = false; this.btnShareGet.active = false; const levelId = gg.sdk.get30rLevelId(); gg.sdk.report30r('LEVEL_PASS', { level_id: levelId, stage_type: gg.sdk.get30rStageType(), ad_cnt: gg.sdk.get30rAdCount(), action_param: levelId, }); gg.audio.playEffect({ name: '游戏胜利', path: GAudioPath.SfxUi }); MatchWinStats.record(true, gg.game.CurentBattle?.opponentRankId ?? 0); gg.sdk.reportDY('inLevel', `${gg.game.getReportDYRankLabel()}-胜利`); this._bindRankAnimNodes(); this._resetAnimStarHome(); this._resetRankNameFly(); this._refreshRankUi(); // 通关时记录场上完整英雄(刀/斧/悟空等;不同局内等级算不同单位) recordKeepHeroClearFromBattle(); gg.data.saveToServer(); gg.data.addDayTaskProgressData(TaskType.successPass, 1) this.showReward() this._refreshMoneyDisplay(); this._refreshCoinRewardDisplay(); this.btnADGet.active = true this.btnShareGet.active = false if (!gg.game.CurentBattle.IsWatchAd && gg.game.CurentSelectLevelId >= 3) { gg.sdk.showInterstitialAd(() => { }, () => { }, true) } this.offlineRewardTimeCount() this._bindVictoryNodes(); if (this._victoryNodes) { prepareBattleSuccessOpen(this._victoryNodes); } this._playVictoryCelebration(); } private _playVictoryCelebration(): void { if (!this._victoryNodes) return; const buttons = [this.btnGet]; if (this.btnADGet.active) buttons.push(this.btnADGet); playBattleSuccessOpen({ nodes: this._victoryNodes, rewardItems: this._lootWeaponNodes.slice(), buttons, highlightBtn: this.btnADGet.active ? this.btnADGet : null, onComplete: () => { this.notifyOpenAnimEnd(); }, }); } offlineRewardTimeCount() { } /** 结算段位与星条:先展示升前,再播飞星 / 飞段位 */ private _refreshRankUi(): void { void RankDataManager.ensureTable().then(() => { if (!this.isValid) return; const starsAfter = RankDataManager.getPlayerRankStars(); const starsBefore = Math.max(1, starsAfter - 1); const rankAfter = RankDataManager.resolveTierByStars(starsAfter); const rankBefore = RankDataManager.resolveTierByStars(starsBefore); this._rankAfterId = rankAfter?.id ?? 0; this._afterStarCount = RankDataManager.getStarModCount(starsAfter); const beforeStarCount = RankDataManager.getStarModCount(starsBefore); const majorUp = RankDataManager.isMajorTierPromotion(rankBefore, rankAfter); const starUp = !majorUp && this._afterStarCount > beforeStarCount; RankDataManager.applyRankLevelIconToNode( this.ui_rankLevel, majorUp ? (rankBefore?.id ?? this._rankAfterId) : this._rankAfterId, ); this._refreshSubRankStars((majorUp || starUp) ? beforeStarCount : this._afterStarCount); this.unschedule(this._onStarAnimDelay); this.unschedule(this._onRankFlyDelay); if (majorUp) { this.scheduleOnce(this._onRankFlyDelay, 0.45); return; } if (!starUp) return; this._starAnimFrom = beforeStarCount; this._starAnimTo = this._afterStarCount; this.scheduleOnce(this._onStarAnimDelay, 0.45); }); } private _onRankFlyDelay = (): void => { this._playRankNameFly(); }; private _onStarAnimDelay = (): void => { this._playStarUpgradeAnim(this._starAnimFrom, this._starAnimTo); }; /** ui_star:star1~star5,内层 star 控制点亮 */ private _refreshSubRankStars(litCount: number): void { if (!this.ui_star?.isValid) { console.log('[UIBattleSuccess] ui_star 缺失'); return; } const count = Math.max(0, Math.min(5, Math.floor(Number(litCount) || 0))); for (let i = 1; i <= 5; i++) { const slot = this.ui_star.getChildByName(`star${i}`); if (!slot?.isValid) continue; slot.active = true; Tween.stopAllByTarget(slot); slot.setScale(1, 1, 1); const icon = slot.getChildByName('star'); if (!icon?.isValid) continue; Tween.stopAllByTarget(icon); icon.setScale(1, 1, 1); icon.active = i <= count; } } private _resetAnimStarHome(): void { this._stopStarUpgradeAnim(); const anim = this._uiAnimStar; if (!anim?.isValid) return; if (!this._animStarHome) this._animStarHome = anim.position.clone(); anim.setPosition(this._animStarHome); anim.setScale(Vec3.ONE); anim.active = false; } private _stopStarUpgradeAnim(): void { this.unschedule(this._onStarAnimDelay); const anim = this._uiAnimStar; if (anim?.isValid) { Tween.stopAllByTarget(anim); anim.active = false; } if (!this.ui_star?.isValid) return; for (let i = 1; i <= 5; i++) { const slot = this.ui_star.getChildByName(`star${i}`); if (slot?.isValid) { Tween.stopAllByTarget(slot); slot.setScale(1, 1, 1); } const icon = slot?.getChildByName('star'); if (icon?.isValid) { Tween.stopAllByTarget(icon); icon.setScale(1, 1, 1); } } } /** ui_animStar 逐颗飞向 star1~star5 并点亮 */ private _playStarUpgradeAnim(fromCount: number, toCount: number): void { if (!this.isValid) return; const anim = this._uiAnimStar; const starRoot = this.ui_star; if (!anim?.isValid || !starRoot?.isValid) { this._refreshSubRankStars(toCount); return; } const home = this._animStarHome ?? anim.position.clone(); this._animStarHome = home; const parentUt = anim.parent?.getComponent(UITransform); if (!parentUt) { this._refreshSubRankStars(toCount); return; } const flyOne = (litCount: number): void => { if (!this.isValid || !anim.isValid) return; if (litCount >= toCount) { anim.active = false; return; } const slotIndex = litCount + 1; const target = starRoot.getChildByName(`star${slotIndex}`); if (!target?.isValid) { flyOne(litCount + 1); return; } const dest = parentUt.convertToNodeSpaceAR(target.worldPosition); Tween.stopAllByTarget(anim); anim.setPosition(home); anim.setScale(Vec3.ONE); anim.active = true; anim.setSiblingIndex(Math.max(0, (anim.parent?.children.length ?? 1) - 1)); tween(anim) .delay(0.12) .to(0.4, { position: dest }, { easing: 'sineIn' }) .call(() => { if (!this.isValid) return; anim.active = false; this._popStarToSolid(slotIndex); }) .delay(0.28) .call(() => flyOne(litCount + 1)) .start(); }; flyOne(fromCount); } /** 点亮实星并放大回弹 */ private _popStarToSolid(slotIndex: number): void { const slot = this.ui_star?.getChildByName(`star${slotIndex}`); if (!slot?.isValid) return; const icon = slot.getChildByName('star'); if (icon?.isValid) { Tween.stopAllByTarget(icon); icon.setScale(1, 1, 1); icon.active = true; } Tween.stopAllByTarget(slot); slot.setScale(1, 1, 1); tween(slot) .to(0.18, { scale: v3(1.4, 1.4, 1) }, { easing: 'backOut' }) .to(0.14, { scale: Vec3.ONE }, { easing: 'sineIn' }) .start(); } private _resetRankNameFly(): void { this._stopRankNameFly(); const n = this._uiRankName; if (!n?.isValid) return; if (!this._rankNameHome) this._rankNameHome = n.position.clone(); n.setPosition(this._rankNameHome); n.setScale(Vec3.ONE); n.active = false; } private _stopRankNameFly(): void { const n = this._uiRankName; if (n?.isValid) { Tween.stopAllByTarget(n); n.active = false; } } /** ui_rankName 赋新段位图,飞到 ui_rankLevel 后刷新段位 */ private _playRankNameFly(): void { const fly = this._uiRankName; const dest = this.ui_rankLevel; const rankId = this._rankAfterId; if (!fly?.isValid || !dest?.isValid || rankId <= 0) { RankDataManager.applyRankLevelIconToNode(dest, rankId); this._refreshSubRankStars(this._afterStarCount); return; } const home = this._rankNameHome ?? fly.position.clone(); this._rankNameHome = home; const parentUt = fly.parent?.getComponent(UITransform); if (!parentUt) { RankDataManager.applyRankLevelIconToNode(dest, rankId); this._refreshSubRankStars(this._afterStarCount); return; } RankDataManager.applyRankLevelIconToNode(fly, rankId); Tween.stopAllByTarget(fly); fly.setPosition(home); fly.setScale(Vec3.ONE); fly.active = true; fly.setSiblingIndex(Math.max(0, (fly.parent?.children.length ?? 1) - 1)); const to = parentUt.convertToNodeSpaceAR(dest.worldPosition); tween(fly) .delay(0.08) .to(0.45, { position: to }, { easing: 'sineIn' }) .call(() => { if (!this.isValid) return; fly.active = false; fly.setPosition(home); RankDataManager.applyRankLevelIconToNode(dest, rankId); this._refreshSubRankStars(this._afterStarCount); }) .start(); } showReward(): void { this._recycleWeaponLoot(); if (this.ui_content?.isValid) this.ui_content.removeAllChildren(); const levelId = gg.game.CurentBattle.LevelConfigId; const rewardStr = gg.data.project.getLevelRewardStr(levelId, true); const pairs = gg.data.project.parseItemPipeNumList(rewardStr); const itemsArray: ItemData[] = []; let addRatio = 1; addRatio += gg.data.getFightRewardRatio(); for (let i = 0; i < pairs.length; i++) { const goodId = pairs[i].id; const itemNum = Math.floor(pairs[i].num * addRatio); if (goodId == 104 || goodId == 105 || goodId == 106 || goodId == 107) { const boxRewardArray = gg.data.getFragmentNumArray(goodId + ',' + itemNum); const statsArray = MTools.mergeToStatsArray(boxRewardArray); for (let j = 0; j < statsArray.length; j++) { const array = statsArray[j].split('|'); itemsArray.push(new ItemData(Number(array[0]), Number(array[1]))); } } else { itemsArray.push(new ItemData(goodId, itemNum)); } } this._appendLootRewards(itemsArray); this._pendingCoin = this._extractCoinAmount(itemsArray); this._dataList = itemsArray; this._spawnWeaponLoot(); } /** lb_money:当前拥有金币 */ private _refreshMoneyDisplay(): void { if (!this.lb_money?.isValid) return; this.lb_money.string = `${gg.data.getMoney()}`; } /** ui_coin/num:本局待领金币(含倍率) */ private _refreshCoinRewardDisplay(): void { const numLb = this._coinNumLabel(); if (!numLb?.isValid) return; const gain = Math.floor(this._pendingCoin * this.addNum); numLb.string = gain > 0 ? `${gain}` : '0'; if (this.ui_coin?.isValid) this.ui_coin.active = gain > 0; } /** ui_coin 数量 Label */ private _coinNumLabel(): Label | null { return this.ui_coin?.getChildByName('num')?.getComponent(Label) ?? null; } /** ui_coin 金币 icon */ private _coinIconNode(): Node | null { return this.ui_coin?.getChildByName('icon') ?? null; } /** ui_money 金币 icon */ private _moneyCoinNode(): Node | null { return this.ui_money?.getChildByName('金币') ?? null; } /** 从奖励列表汇总金币并移除(仅 ui_coin 展示) */ private _extractCoinAmount(items: ItemData[]): number { let total = 0; for (let i = items.length - 1; i >= 0; i--) { if (items[i].id !== ItemNumType.money) continue; total += Math.floor(Number(items[i].num) || 0); items.splice(i, 1); } return total; } /** ui_content:本局掉落宝石(克隆 ui_gemTemp) */ private _spawnWeaponLoot(): void { const root = this.ui_content; if (!root?.isValid) return; const drops = gg.game.CurentBattle?.weaponDrop?.allDrops ?? []; const aggregated = WeaponDropService.aggregateByWeaponId(drops); for (let i = 0; i < aggregated.length; i++) { this._addWeaponLootNode(root, aggregated[i].record); } root.getComponent(Layout)?.updateLayout(); } /** * 在 ui_content 追加一颗宝石。模板只有 icon,不显示数量。 * @param root ui_content * @param record 掉落记录 */ private _addWeaponLootNode(root: Node, record: WeaponDropRecord): void { const node = WeaponDropView.takeSettlePrefab(this.ui_gemTemp); if (!node) return; root.addChild(node); node.setScale(Vec3.ZERO); node.angle = 0; WeaponDropView.applySettleVisual(node, record); const view = node.getComponent(WeaponDropView) ?? node.addComponent(WeaponDropView); view.playDisplay(); this._lootWeaponNodes.push(node); } /** 回收 ui_content 宝石节点 */ private _recycleWeaponLoot(): void { for (let i = 0; i < this._lootWeaponNodes.length; i++) { WeaponDropView.recycleSettleNode(this._lootWeaponNodes[i]); } this._lootWeaponNodes.length = 0; } /** * 胜利时把双方掉落碎片并入奖励列表(按道具 id 堆叠)。 * @param items 已解析的关卡奖励 */ private _appendLootRewards(items: ItemData[]): void { const drops = gg.game.CurentBattle?.weaponDrop?.collectRewardItems() ?? []; for (let i = 0; i < drops.length; i++) { const itemId = drops[i].itemId; const num = drops[i].num; let found = false; for (let j = 0; j < items.length; j++) { if (items[j].id !== itemId) continue; items[j].num += num; found = true; break; } if (!found) items.push(new ItemData(itemId, num)); } } click_btnGet(): void { if (this._claiming) return; let curWaveNum = gg.game.CurentBattle.CurentWaveNum gg.sdk.reportDY("inLevel", `${gg.game.getReportDYBattleCountLabel(curWaveNum)}-成功领取奖励`) this._claimWithCoinFly(); } /** 金币影子飞行 → 入账 → 刷新 lb_money → 回大厅 */ private _claimWithCoinFly(): void { if (this._claiming) return; this._claiming = true; this.btnGet.active = false; this.btnADGet.active = false; this.btnShareGet.active = false; const coinGain = Math.floor(this._pendingCoin * this.addNum); const icon = this._coinIconNode(); const target = this._moneyCoinNode(); const fly = this._coinFly; if (!fly || coinGain <= 0 || !icon?.isValid || !target?.isValid) { this._applyRewards(); this._pendingCoin = 0; this._refreshMoneyDisplay(); this._refreshCoinRewardDisplay(); this._finishClaimNavigate(); return; } fly.play(icon, target, coinGain, () => { if (!this.isValid) return; this._applyRewards(); this._pendingCoin = 0; this._refreshMoneyDisplay(); this._refreshCoinRewardDisplay(); this._finishClaimNavigate(); }); } /** 发放除金币外的奖励并入账金币 */ private _applyRewards(): void { for (let i = 0; i < this._dataList.length; i++) { const value = this._dataList[i]; gg.data.addItemNum(Number(value.id), Number(value.num) * this.addNum); } const coinGain = Math.floor(this._pendingCoin * this.addNum); if (coinGain > 0) gg.data.addItemNum(ItemNumType.money, coinGain); gg.game.IsPlayedGame = true; } private _finishClaimNavigate(): void { if (!this.isValid) return; this._claiming = false; gg.game.enterMain(); } click_btnADGet(): void { if (this._claiming) return; gg.sdk.showVideoAd((res) => { if (res) { let curWaveNum = gg.game.CurentBattle.CurentWaveNum gg.sdk.reportDY("inLevel", `${gg.game.getReportDYBattleCountLabel(curWaveNum)}-AD-广告成功双倍领取`) this.addNum = 2 stopHighlightButtonAnim(this.btnADGet); this._refreshCoinRewardDisplay(); gg.game.CurentBattle.IsWatchAd = true this._claimWithCoinFly(); } else { gg.ui.showToast('观看视频失败'); } }) } click_btnShareGet() { gg.ui.showToast('该功能未开放'); } }