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.
214 lines
7.7 KiB
214 lines
7.7 KiB
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;
|
|
}
|
|
}
|
|
|