import { Label, Node } from 'cc'; import { BattlePhase } from 'db://assets/mx/module/battle'; import { GEvent } from '../../../mx/module/event/GEvent'; import { getShieldAdTriggerDist } from '../../battle/bag/BagPrepConfig'; import type { BattleBagSession } from '../../battle/bag/BattleBagSession'; import { BattleUiPauseGate } from '../../battle/BattleUiPauseGate'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; import type { CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import type { BattleGameMenShenTrapController } from './BattleGameMenShenTrapController'; import { BattleGameItemAdBtnPulse } from './BattleGameItemAdBtnPulse'; /** 广告类型 */ enum ItemAdKind { /** 无 */ None = 0, /** 门神盾牌(优先级高) */ MenShen = 1, /** 铲子 */ Shovel = 2, } /** 观看广告期间暂停 */ const AD_PAUSE_TOKEN = 'ad:battleItem'; /** * BattleGame btnItem:铲子 / 门神盾牌广告入口。 * - 盾牌:怪首次离终点 Const23 格内触发,每局 1 次;放置陷阱后隐藏 * - 铲子:己方无可放置 Put + 背包有空位且无铲(同时满足);每局 Const10 次数上限 * - 门神已触发且铲子亦满足:Const23 内有怪优先门神,无怪则铲子顶掉门神;仅铲子时 Const23 有怪且门神可展示则切回门神 * - 无掘宝铲:固定 shovelIcon,藏 goldShovelIcon */ export class BattleGameItemAdPresenter { private _btnItem: Node | null = null; private _shovelIcon: Node | null = null; private _goldShovelIcon: Node | null = null; private _menIcon: Node | null = null; private _videoIcon: Node | null = null; private _numNode: Node | null = null; private _numLabel: Label | null = null; private _battle: ProjectBattleHandle | null = null; private _bagView: CocosBagBoardView | null = null; private _menShenTrap: BattleGameMenShenTrapController | null = null; private _adPending = false; private _activeKind = ItemAdKind.None; private _shieldScanDone = false; /** 盾牌广告已点击并放置陷阱 */ private _shieldBtnDismissed = false; /** 飞铲动画进行中 */ private _shovelFlying = false; /** 飞铲期间展示的赠送数量(避免首把入包后条件变化导致闪隐) */ private _shovelFlyingGrantCount = 0; private _onBoardChanged: (() => void) | null = null; private readonly _btnPulse = new BattleGameItemAdBtnPulse(); private _btnPulseActive = false; /** 威胁扫描降频 */ private _threatScanAcc = 0; /** 缓存:Const23 内是否有进攻怪 */ private _threatInZone = false; /** * @param btnItem ui_ready/btnItem 根节点 * @param bagView 背包视图(飞铲入包) * @param menShenTrap 门神陷阱控制器 */ bind( battle: ProjectBattleHandle | null, bagView: CocosBagBoardView | null, btnItem: Node | null, menShenTrap: BattleGameMenShenTrapController | null, ): void { this.unbind(); this._battle = battle; this._bagView = bagView; this._menShenTrap = menShenTrap; this._btnItem = btnItem; if (!btnItem?.isValid) { console.log('[BattleItemAd] btnItem 缺失'); return; } this._shovelIcon = btnItem.getChildByName('shovelIcon'); this._goldShovelIcon = btnItem.getChildByName('goldShovelIcon'); this._menIcon = btnItem.getChildByName('menIcon'); this._videoIcon = btnItem.getChildByName('video'); this._numNode = btnItem.getChildByName('num'); this._numLabel = this._numNode?.getComponent(Label) ?? null; this._setNodeActive(this._goldShovelIcon, false); btnItem.on(Node.EventType.TOUCH_END, this._onBtnClick, this); this._onBoardChanged = () => this._refreshVisibility(); GEvent.Ins.on(GEvent.UpdateBestPlace, this._onBoardChanged, this); this._refreshVisibility(); } /** 解除绑定 */ unbind(): void { if (this._btnItem?.isValid) { this._btnItem.off(Node.EventType.TOUCH_END, this._onBtnClick, this); } if (this._onBoardChanged) { GEvent.Ins.off(GEvent.UpdateBestPlace, this._onBoardChanged, this); } this._onBoardChanged = null; if (this._adPending) { this._adPending = false; BattleUiPauseGate.release(AD_PAUSE_TOKEN); } this._btnItem = null; this._shovelIcon = null; this._goldShovelIcon = null; this._menIcon = null; this._videoIcon = null; this._numNode = null; this._numLabel = null; this._battle = null; this._bagView = null; this._menShenTrap = null; this._activeKind = ItemAdKind.None; this._shieldScanDone = false; this._shieldBtnDismissed = false; this._shovelFlying = false; this._shovelFlyingGrantCount = 0; this._btnPulseActive = false; this._threatScanAcc = 0; this._threatInZone = false; this._syncBtnPulse(false); } /** 帧更新:扫描盾牌触发条件并刷新显隐 */ tick(_dt: number): void { this._threatScanAcc++; if (this._threatScanAcc % 12 === 1) { this._threatInZone = this._scanThreatInZone(); } this._tickShieldOffer(); this._refreshVisibility(); } /** * GM 玩家 AI:当前有可点广告则免广告发放(不播激励视频)。 * @returns 是否已发起领取 */ tryClaimForPlayerAi(): boolean { if (this._adPending || this._shovelFlying) return false; const kind = this._activeKind; if (kind === ItemAdKind.MenShen) { return this._grantMenShenWithoutAd(); } if (kind === ItemAdKind.Shovel) { return this._grantShovelWithoutAd(); } return false; } /** 首次满足距离时触发盾牌广告 */ private _tickShieldOffer(): void { const battle = this._battle; const trap = this._menShenTrap; if (!battle || !trap || battle.menShenAdConsumed) return; if (battle.canShowMenShenAd()) { this._shieldScanDone = true; return; } if (this._shieldScanDone) return; if (battle.session.phase !== BattlePhase.COMBAT || battle.session.ended) return; const triggerDist = getShieldAdTriggerDist(); if (!(triggerDist > 0)) return; if (!this._threatInZone) return; battle.triggerMenShenAdOffer(); this._shieldScanDone = true; } /** 刷新 btnItem 显隐与图标 */ private _refreshVisibility(): void { const kind = this._resolveActiveKind(); this._activeKind = kind; const btn = this._btnItem; if (!btn?.isValid) return; const show = kind !== ItemAdKind.None; if (btn.active !== show) btn.active = show; this._syncBtnPulse(show); if (!show) return; const showMen = kind === ItemAdKind.MenShen; const showShovel = kind === ItemAdKind.Shovel; this._setNodeActive(this._menIcon, showMen); this._setNodeActive(this._shovelIcon, showShovel); this._setNodeActive(this._goldShovelIcon, false); this._setNodeActive(this._videoIcon, true); this._setNodeActive(this._numNode, showShovel); if (showShovel) { const grantCount = this._shovelFlying ? this._shovelFlyingGrantCount : this._battle?.bagSession ? this._resolveShovelGrantCount(this._battle.bagSession) : 0; if (grantCount > 0 && this._numLabel) { this._numLabel.string = `X${grantCount}`; } } } /** 解析当前应展示的广告类型(门神/铲子按 Const23 威胁动态切换) */ private _resolveActiveKind(): ItemAdKind { const battle = this._battle; if (!battle || battle.session.ended) return ItemAdKind.None; const canMenShen = !this._shieldBtnDismissed && battle.canShowMenShenAd(); const canShovel = this._shovelFlying || this._canShowShovelAd(); const hasThreatInConst23 = this._hasMonsterInShieldTriggerZone(); if (canMenShen && hasThreatInConst23) return ItemAdKind.MenShen; if (canMenShen && canShovel && !hasThreatInConst23) return ItemAdKind.Shovel; if (canShovel) return ItemAdKind.Shovel; if (canMenShen) return ItemAdKind.MenShen; return ItemAdKind.None; } /** Const23 触发距离内是否存在进攻怪(用降频缓存) */ private _hasMonsterInShieldTriggerZone(): boolean { return this._threatInZone; } /** 扫描 Const23 内进攻怪 */ private _scanThreatInZone(): boolean { const trap = this._menShenTrap; if (!trap || !this._battle) return false; const triggerDist = getShieldAdTriggerDist(); if (!(triggerDist > 0)) return false; return trap.shouldOfferShieldAd(triggerDist); } /** 铲子广告:① 己方无可放置 Put ② 背包有空位且无铲(同时满足) */ private _canShowShovelAd(): boolean { const battle = this._battle; const session = battle?.bagSession; if (!battle || !session) return false; if (battle.getPrepShovelAdRemaining() <= 0) return false; if (session.hasLocalPlaceablePutGrid()) return false; if (this._countEmptyBagSlots(session) <= 0) return false; if (session.countLocalShovelTiles() > 0) return false; return true; } /** 本次广告可获得的铲数量(背包空位 >1 给 2,否则 1) */ private _resolveShovelGrantCount(session: BattleBagSession): number { return this._countEmptyBagSlots(session) > 1 ? 2 : 1; } /** 背包空槽数量 */ private _countEmptyBagSlots(session: BattleBagSession): number { const slots = session.getBagSlots(); let n = 0; for (let i = 0; i < slots.length; i++) { if (!slots[i]) n++; } return n; } private readonly _onBtnClick = (): void => { if (this._adPending || this._shovelFlying) return; const kind = this._activeKind; if (kind === ItemAdKind.MenShen) { this._playMenShenAd(); return; } if (kind === ItemAdKind.Shovel) { this._playShovelAd(); } }; /** 免广告发放门神盾牌 */ private _grantMenShenWithoutAd(): boolean { const battle = this._battle; const trap = this._menShenTrap; if (!battle?.canShowMenShenAd() || !trap) return false; battle.IsWatchAd = true; battle.consumeMenShenAd(); if (!trap.tryPlaceTraps()) { console.log('[BattleItemAd] 玩家AI门神陷阱放置失败'); } this._shieldBtnDismissed = true; this._refreshVisibility(); console.log('[BattleItemAd] 玩家AI免广告领取门神盾牌'); return true; } /** 免广告发放铲子 */ private _grantShovelWithoutAd(): boolean { const battle = this._battle; if (!battle || battle.getPrepShovelAdRemaining() <= 0) return false; if (!this._canShowShovelAd()) return false; const bagView = this._bagView; const btn = this._btnItem; const session = battle.bagSession; if (!session || !bagView || !btn?.isValid) { console.log('[BattleItemAd] 玩家AI铲奖励失败:bag 未就绪'); return false; } battle.IsWatchAd = true; battle.consumePrepShovelAd(); const shovelCount = this._resolveShovelGrantCount(session); const fromWorld = btn.worldPosition.clone(); this._shovelFlying = true; this._shovelFlyingGrantCount = shovelCount; bagView.playItemAdShovelFlights({ fromWorld, count: shovelCount, onAllComplete: (granted) => { this._shovelFlying = false; this._shovelFlyingGrantCount = 0; if (granted <= 0) { console.log('[BattleItemAd] 玩家AI铲飞入背包失败'); } this._refreshVisibility(); }, }); this._refreshVisibility(); console.log('[BattleItemAd] 玩家AI免广告领取铲子', shovelCount); return true; } /** 观看门神广告并布置陷阱 */ private _playMenShenAd(): void { const battle = this._battle; const trap = this._menShenTrap; if (!battle?.canShowMenShenAd() || !trap) return; this._adPending = true; BattleUiPauseGate.acquire(AD_PAUSE_TOKEN); gg.sdk.showVideoAd((res) => { if (!this._adPending) return; this._adPending = false; BattleUiPauseGate.release(AD_PAUSE_TOKEN); if (!res) { gg.ui.showToast('观看视频失败'); return; } const b = this._battle; const t = this._menShenTrap; if (!b?.canShowMenShenAd() || !t) return; b.IsWatchAd = true; b.consumeMenShenAd(); if (!t.tryPlaceTraps()) { console.log('[BattleItemAd] 门神陷阱放置失败'); } const curWaveNum = gg.game.CurentBattle.CurentWaveNum; gg.sdk.reportDY('inLevel', `${gg.game.getReportDYBattleCountLabel(curWaveNum)}-AD-广告门神盾牌`); this._shieldBtnDismissed = true; this._refreshVisibility(); }); } /** 观看铲子广告并按背包空位数量发放 */ private _playShovelAd(): void { const battle = this._battle; if (!battle || battle.getPrepShovelAdRemaining() <= 0) return; if (!this._canShowShovelAd()) return; this._adPending = true; BattleUiPauseGate.acquire(AD_PAUSE_TOKEN); gg.sdk.showVideoAd((res) => { if (!this._adPending) return; this._adPending = false; BattleUiPauseGate.release(AD_PAUSE_TOKEN); if (!res) { gg.ui.showToast('观看视频失败'); return; } const b = this._battle; const bagView = this._bagView; const btn = this._btnItem; if (!b || b.getPrepShovelAdRemaining() <= 0) return; const session = b.bagSession; if (!session || !bagView || !btn?.isValid) { console.log('[BattleItemAd] 铲奖励失败:bag 未就绪'); return; } if (!this._canShowShovelAd()) return; b.IsWatchAd = true; b.consumePrepShovelAd(); const shovelCount = this._resolveShovelGrantCount(session); const fromWorld = btn.worldPosition.clone(); this._shovelFlying = true; this._shovelFlyingGrantCount = shovelCount; bagView.playItemAdShovelFlights({ fromWorld, count: shovelCount, onAllComplete: (granted) => { this._shovelFlying = false; this._shovelFlyingGrantCount = 0; if (granted <= 0) { console.log('[BattleItemAd] 铲飞入背包失败'); } const curWaveNum = gg.game.CurentBattle.CurentWaveNum; gg.sdk.reportDY('inLevel', `${gg.game.getReportDYBattleCountLabel(curWaveNum)}-AD-广告铲子`); this._refreshVisibility(); }, }); this._refreshVisibility(); }); } private _setNodeActive(node: Node | null, active: boolean): void { if (node?.isValid && node.active !== active) node.active = active; } /** btnItem 显示时循环跳动,隐藏时停止 */ private _syncBtnPulse(active: boolean): void { if (active === this._btnPulseActive) return; this._btnPulseActive = active; if (active) { this._btnPulse.bind(this._btnItem); } else { this._btnPulse.unbind(); } } }