import type { DailyShopParsed } from '../../../manager/DailyShopTypes'; import { BattleBuffCarryStore } from '../../../manager/battleBuff/BattleBuffCarryStore'; import { isBattleBuffPassiveGoldMine, passiveBuffUsesBattleGameCd, readBattleBuffPassiveCdSec, } from '../../../manager/battleBuff/BattleBuffPassiveSkillUtil'; import { BattleBuffFunctionType } from '../../../manager/battleBuff/BattleBuffTypes'; import type { ProjectBattleHandle } from '../../runtime/BattleRuntime'; import { aiOp, aiWarn } from './AiLog'; import { AiHeadlessBagHost } from './AiHeadlessBagHost'; /** 单槽 CD 被动状态 */ type AiPassiveCdSlot = { /** 携带解析结果 */ parsed: DailyShopParsed; /** CD 总秒 */ cdTotalSec: number; /** CD 剩余秒 */ cdLeftSec: number; /** 本轮 CD 结束是否已尝试触发 */ triggerConsumed: boolean; }; /** AI CD 被动 UI 快照 */ export type AiPassiveCdUiSlot = { /** buff id */ buffId: number; /** CD 剩余秒 */ cdLeftSec: number; /** CD 总秒 */ cdTotalSec: number; }; /** AI 金矿飞币回调(起点可由 UI 解析敌方图标) */ export type AiGoldMineCoinFlyHandler = () => void; /** * AI 局内 CD 被动:金矿(14) CD 满 +1 金币。 */ export class AiPassiveCdHost { private _battle: ProjectBattleHandle | null = null; private readonly _slots: AiPassiveCdSlot[] = []; private _goldMineFly: AiGoldMineCoinFlyHandler | null = null; /** * 绑定并按 CarryStore 重建 CD 槽。 * @param _bagHost 无头背包(金矿不入 bag) * @param battle 战斗 * @param _onBoardChanged 盘面变更(金矿不用) */ bind(_bagHost: AiHeadlessBagHost, battle: ProjectBattleHandle, _onBoardChanged: () => void): void { this.dispose(); this._battle = battle; this._rebuildSlots(); } /** * 注册金矿飞币表现。 * @param handler 回调;null 清除 */ setGoldMineCoinFlyHandler(handler: AiGoldMineCoinFlyHandler | null): void { this._goldMineFly = handler; } /** 解除绑定 */ dispose(): void { this._slots.length = 0; this._battle = null; this._goldMineFly = null; } /** * 帧更新 CD。 * @param dtSec 帧间隔 */ tick(dtSec: number): void { if (!this._battle || this._slots.length <= 0) return; const dt = Math.max(0, Number(dtSec) || 0); for (let i = 0; i < this._slots.length; i++) { this._tickSlot(this._slots[i], dt); } } /** * CD 被动 UI 快照。 * @returns 槽列表 */ snapshotUiSlots(): AiPassiveCdUiSlot[] { const out: AiPassiveCdUiSlot[] = []; for (let i = 0; i < this._slots.length; i++) { const slot = this._slots[i]; out.push({ buffId: slot.parsed.id, cdLeftSec: slot.cdLeftSec, cdTotalSec: slot.cdTotalSec, }); } return out; } /** 从 CarryStore 重建有 CD 的被动槽 */ private _rebuildSlots(): void { this._slots.length = 0; const passives = BattleBuffCarryStore.getPassives(AiHeadlessBagHost.AI_PLAYER_ID); for (let i = 0; i < passives.length; i++) { const parsed = passives[i]; const row = parsed.row as ITableBattlebuff; const ft = Math.floor(Number(row.functiontype) || 0); if (!passiveBuffUsesBattleGameCd(ft)) continue; if (!isBattleBuffPassiveGoldMine(row)) { aiWarn(`AI CD 被动未接线 functiontype=${ft} id=${parsed.id} name=${parsed.name}`); continue; } const cdSec = Math.max(0, Math.floor(Number(parsed.cd) || 0) || readBattleBuffPassiveCdSec(row)); this._slots.push({ parsed, cdTotalSec: cdSec, cdLeftSec: cdSec, triggerConsumed: false, }); aiOp(`AI CD被动槽#${this._slots.length - 1} id=${parsed.id} ${parsed.name} ft=${ft} cd=${cdSec}s`); } } /** * 单槽倒计时与触发。 * @param slot 槽 * @param dt 帧间隔 */ private _tickSlot(slot: AiPassiveCdSlot, dt: number): void { if (slot.cdLeftSec > 0) { slot.cdLeftSec = Math.max(0, slot.cdLeftSec - dt); return; } if (slot.triggerConsumed) return; slot.triggerConsumed = true; this._onGoldMineReady(slot); } /** * 金矿 CD 结束:AI +1 金币并重开 CD。 * @param slot 槽 */ private _onGoldMineReady(slot: AiPassiveCdSlot): void { const battle = this._battle; if (!battle) { aiWarn('AI 金矿失败:battle 未就绪'); this._restartCd(slot); return; } const row = slot.parsed.row as ITableBattlebuff; const ft = Math.floor(Number(row.functiontype) || 0); if (ft !== BattleBuffFunctionType.GoldMine) { aiWarn(`AI 金矿 ft 异常 id=${slot.parsed.id} ft=${ft}`); this._restartCd(slot); return; } battle.addPlayerGold(AiHeadlessBagHost.AI_PLAYER_ID, 1); this._goldMineFly?.(); aiOp(`AI 金矿 +1 coin player=${AiHeadlessBagHost.AI_PLAYER_ID}`); this._restartCd(slot); } /** * 重启 CD。 * @param slot 槽 */ private _restartCd(slot: AiPassiveCdSlot): void { slot.cdLeftSec = slot.cdTotalSec; slot.triggerConsumed = false; } }