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.

215 lines
7.7 KiB

3 weeks ago
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;
}
}