import { BattlePhase } from 'db://assets/mx/module/battle'; import { getShieldAdMax, getShieldAdTriggerDist, getShovelAdMax } from '../../bag/BagPrepConfig'; import type { BattleBagSession } from '../../bag/BattleBagSession'; import type { MapManager } from '../../map/legacy/MapManager'; import type { ProjectBattleHandle } from '../../runtime/BattleRuntime'; import { RankDataManager } from '../../../manager/RankDataManager'; import { BattleGameMenShenTrapController } from '../../../ui/BattleGame/BattleGameMenShenTrapController'; import { AiHeadlessBagHost } from './AiHeadlessBagHost'; import { aiOp, aiWarn } from './AiLog'; /** 万分比上限 */ const WAN_DENOM = 10000; /** * AI 广告效果模拟(§8.2):不播激励视频。 * 触发条件对齐玩家铲子/门神广告;是否发放读 Rank.aiusead; * Const22/25 限制本局**投掷次数**(成败均消耗,避免条件反复满足时必中)。 */ export class AiAdHost { private _bagHost: AiHeadlessBagHost | null = null; private _battle: ProjectBattleHandle | null = null; private _map: MapManager | null = null; private _onBoardChanged: (() => void) | null = null; private _opponentRankId = 0; private _aiUseAdWan = 0; private _shovelAdMax = 0; /** 铲子已投掷次数(成败均计) */ private _shovelAdUsed = 0; private _shieldAdMax = 0; /** 盾牌已投掷次数(成败均计) */ private _shieldAdUsed = 0; /** 铲子机会已掷过(失败则等条件退出后再掷) */ private _shovelLatched = false; /** 盾牌机会已掷过 */ private _shieldLatched = false; private readonly _menShenTrap = new BattleGameMenShenTrapController(); /** * 绑定本局 AI 广告模拟。 * @param bagHost 无头背包 * @param battle 战斗 * @param map 地图 * @param opponentRankId 对手段位(读 aiusead) * @param onBoardChanged 发铲后刷新盘面 */ bind( bagHost: AiHeadlessBagHost, battle: ProjectBattleHandle, map: MapManager, opponentRankId: number, onBoardChanged?: () => void, ): void { this.dispose(); const rank = RankDataManager.getRankConfig(opponentRankId); if (!rank) { aiWarn(`[AiAd] Rank 缺失 rankId=${opponentRankId},本局不模拟广告`); return; } const wan = Math.floor(Number(rank.aiusead) || 0); if (!(wan >= 0)) { aiWarn(`[AiAd] aiusead 非法 rankId=${opponentRankId} raw=${rank.aiusead}`); return; } this._bagHost = bagHost; this._battle = battle; this._map = map; this._onBoardChanged = onBoardChanged ?? null; this._opponentRankId = opponentRankId; this._aiUseAdWan = wan; this._shovelAdMax = getShovelAdMax(0); this._shovelAdUsed = 0; this._shieldAdMax = getShieldAdMax(); this._shieldAdUsed = 0; this._shovelLatched = false; this._shieldLatched = false; this._menShenTrap.bind(battle, map, AiHeadlessBagHost.AI_PLAYER_ID); aiOp( `AI 广告就绪 rank=${opponentRankId} aiusead=${wan}` + ` shovelMax=${this._shovelAdMax} shieldMax=${this._shieldAdMax}`, ); } /** 释放引用与门神陷阱 */ dispose(): void { this._menShenTrap.unbind(); this._bagHost = null; this._battle = null; this._map = null; this._onBoardChanged = null; this._opponentRankId = 0; this._aiUseAdWan = 0; this._shovelAdMax = 0; this._shovelAdUsed = 0; this._shieldAdMax = 0; this._shieldAdUsed = 0; this._shovelLatched = false; this._shieldLatched = false; } /** * 每帧:门神陷阱推进 + 条件满足时掷骰发效果。 * @param dtSec 帧间隔 */ tick(dtSec: number): void { if (!this._battle || !this._bagHost || !this._map) return; this._menShenTrap.tick(dtSec); this._tickOffers(); } /** 解析当前机会并掷骰(盾牌优先于铲子,对齐玩家 btnItem) */ private _tickOffers(): void { const battle = this._battle; if (!battle || battle.session.ended) return; const canShield = this._canOfferShield(); const canShovel = this._canOfferShovel(); if (!canShield) this._shieldLatched = false; if (!canShovel) this._shovelLatched = false; // 对齐玩家:Const23 有威胁时优先盾牌,否则铲子 if (canShield) { this._tryRollShield(); return; } if (canShovel) { this._tryRollShovel(); } } /** 盾牌:战斗中、次数未满、尚无陷阱 */ private _canOfferShield(): boolean { const battle = this._battle; if (!battle) return false; if (battle.session.phase !== BattlePhase.COMBAT) return false; if (this._shieldAdUsed >= this._shieldAdMax) return false; if (this._menShenTrap.hasTrap()) return false; return this._hasShieldThreat(); } /** Const23 内是否有进攻 AI 城墙的怪 */ private _hasShieldThreat(): boolean { const dist = getShieldAdTriggerDist(); if (!(dist > 0)) return false; return this._menShenTrap.shouldOfferShieldAd(dist); } /** 铲子:无可放 Put + 背包有空位且无铲 + 投掷次数未满 */ private _canOfferShovel(): boolean { const session = this._bagHost?.bagSession; if (!session) return false; if (this._shovelAdUsed >= this._shovelAdMax) return false; if (session.hasLocalPlaceablePutGrid()) return false; if (this._countEmptyBagSlots(session) <= 0) return false; if (session.countLocalShovelTiles() > 0) return false; return true; } /** 本机会掷盾牌广告(无论成败都消耗一次 Const25) */ private _tryRollShield(): void { if (this._shieldLatched || !this._canOfferShield()) return; this._shieldLatched = true; this._shieldAdUsed++; const ok = this._rollAiUseAd(); if (!ok) return; if (!this._menShenTrap.tryPlaceTraps()) return; } /** 本机会掷铲子广告(无论成败都消耗一次 Const22) */ private _tryRollShovel(): void { if (this._shovelLatched || !this._canOfferShovel()) return; this._shovelLatched = true; this._shovelAdUsed++; const ok = this._rollAiUseAd(); if (!ok) return; const session = this._bagHost?.bagSession; if (!session) return; const grantCount = this._resolveShovelGrantCount(session); let granted = 0; for (let i = 0; i < grantCount; i++) { const slot = session.findEmptyBagSlot(); if (slot == null) break; const reason = session.grantShovelInBag(slot); if (reason) break; granted++; } if (granted <= 0) return; this._onBoardChanged?.(); } /** 按 Rank.aiusead 万分比判定是否发放 */ private _rollAiUseAd(): boolean { const battle = this._battle; if (!battle) return false; const wan = this._aiUseAdWan; if (wan <= 0) return false; const roll = battle.session.rng.int(0, WAN_DENOM - 1); return roll < wan; } /** 背包空位 >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; } }