|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|