|
|
|
|
import type { Node } from 'cc';
|
|
|
|
|
import { aiLog, aiOp, aiWarn } from '../runtime/AiLog';
|
|
|
|
|
import type { ProjectBattleHandle } from '../../runtime/BattleRuntime';
|
|
|
|
|
import type { MapManager } from '../../map/legacy/MapManager';
|
|
|
|
|
import type { AiMatchProfile } from '../match/AiMatchProfile';
|
|
|
|
|
import type { BattleBagSession } from '../../bag/BattleBagSession';
|
|
|
|
|
import type { DeploySync } from '../../runtime/DeploySync';
|
|
|
|
|
import { AiMatchService } from '../match/AiMatchService';
|
|
|
|
|
import {
|
|
|
|
|
AiBattlePersonalityFactory,
|
|
|
|
|
AiTextPolicyType,
|
|
|
|
|
} from '../runtime/AiBattlePersonality';
|
|
|
|
|
import { AiHeadlessBagHost } from '../runtime/AiHeadlessBagHost';
|
|
|
|
|
import { AiOpponentController } from '../runtime/AiOpponentController';
|
|
|
|
|
import { AiHeroWeaponRegistry } from '../runtime/AiHeroWeaponRegistry';
|
|
|
|
|
import { AiHeroWeaponAssigner } from '../runtime/AiHeroWeaponAssigner';
|
|
|
|
|
import { AiSkillHost } from '../runtime/AiSkillHost';
|
|
|
|
|
import { AiMapTextHost } from '../runtime/AiMapTextHost';
|
|
|
|
|
import { AiPassiveCdHost, type AiPassiveCdUiSlot } from '../runtime/AiPassiveCdHost';
|
|
|
|
|
import { AiActiveSkillHost, type AiActiveSkillUiSlot, type AiActiveSkillCastFxHandler } from '../runtime/AiActiveSkillHost';
|
|
|
|
|
import { AiAdHost } from '../runtime/AiAdHost';
|
|
|
|
|
import type { AiMatchBuildInput } from '../types/AiTypes';
|
|
|
|
|
import { setAiHeroWeaponResolver } from '../../combat/TowerLoadoutService';
|
|
|
|
|
import { SkillAtkBuffBoardViewBind } from '../../combat/SkillAtkBuffBoardBridge';
|
|
|
|
|
import { AiMatchPlayerBuffCounts } from '../match/AiMatchPlayerBuffCounts';
|
|
|
|
|
import { BattleBuffCarryStore } from '../../../manager/battleBuff/BattleBuffCarryStore';
|
|
|
|
|
import { getBattleBuffMoveSpeedHost } from '../../combat/BattleBuffMoveSpeedHost';
|
|
|
|
|
import { getBattleBuffUnitAttackSpeedHost } from '../../combat/BattleBuffUnitAttackSpeedHost';
|
|
|
|
|
import { getBattleBuffBaseThunderHost } from '../../combat/BattleBuffBaseThunderHost';
|
|
|
|
|
import { getBattleBuffDigitStatHost } from '../../combat/BattleBuffDigitStatHost';
|
|
|
|
|
|
|
|
|
|
/** AI 模块唯一对外集成层:匹配档案 -> 局内 tick */
|
|
|
|
|
export class AiBattleBridge {
|
|
|
|
|
private static _instance: AiBattleBridge | null = null;
|
|
|
|
|
|
|
|
|
|
private readonly _matchService = new AiMatchService();
|
|
|
|
|
private readonly _personalityFactory = new AiBattlePersonalityFactory();
|
|
|
|
|
private readonly _bagHost = new AiHeadlessBagHost();
|
|
|
|
|
private readonly _controller = new AiOpponentController();
|
|
|
|
|
private readonly _weaponRegistry = new AiHeroWeaponRegistry();
|
|
|
|
|
private readonly _weaponAssigner = new AiHeroWeaponAssigner();
|
|
|
|
|
private readonly _skillHost = new AiSkillHost();
|
|
|
|
|
private readonly _mapText = new AiMapTextHost();
|
|
|
|
|
private readonly _passiveCd = new AiPassiveCdHost();
|
|
|
|
|
private readonly _activeSkill = new AiActiveSkillHost();
|
|
|
|
|
private readonly _adHost = new AiAdHost();
|
|
|
|
|
private _battle: ProjectBattleHandle | null = null;
|
|
|
|
|
private _map: MapManager | null = null;
|
|
|
|
|
private _profile: AiMatchProfile | null = null;
|
|
|
|
|
|
|
|
|
|
/** 单例入口 */
|
|
|
|
|
static get inst(): AiBattleBridge {
|
|
|
|
|
if (!AiBattleBridge._instance) {
|
|
|
|
|
AiBattleBridge._instance = new AiBattleBridge();
|
|
|
|
|
}
|
|
|
|
|
return AiBattleBridge._instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 匹配服务 */
|
|
|
|
|
get matchService(): AiMatchService {
|
|
|
|
|
return this._matchService;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 当前局匹配档案;未开战为 null */
|
|
|
|
|
get profile(): AiMatchProfile | null {
|
|
|
|
|
return this._profile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** AI 无头背包会话;未 bind 时为 null */
|
|
|
|
|
get aiBagSession(): BattleBagSession | null {
|
|
|
|
|
return this._bagHost.bagSession;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** AI 部署同步;未 bind 时为 null */
|
|
|
|
|
get aiDeploySync(): DeploySync | null {
|
|
|
|
|
return this._bagHost.deploySync;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 妙手神偷:摘除 AI 地图字块视图(不销毁)。
|
|
|
|
|
* @param gridIndex 地图格索引
|
|
|
|
|
*/
|
|
|
|
|
detachAiMapTextGrid(gridIndex: number): import('../../view/TextGrid').TextGrid | null {
|
|
|
|
|
return this._mapText.detachGridForSteal(gridIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AI 妙手神偷落地:挂接飞行字块到 AI 地图格。
|
|
|
|
|
* @param gridIndex 落点格
|
|
|
|
|
* @param grid 飞行字块
|
|
|
|
|
*/
|
|
|
|
|
attachAiStolenMapTextGrid(gridIndex: number, grid: import('../../view/TextGrid').TextGrid): void {
|
|
|
|
|
this._mapText.attachStolenGrid(gridIndex, grid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 遍历 AI 地图托管字块。
|
|
|
|
|
* @param visitor 回调
|
|
|
|
|
*/
|
|
|
|
|
forEachAiMapTextGrid(visitor: (grid: import('../../view/TextGrid').TextGrid, gridIndex: number) => void): void {
|
|
|
|
|
this._mapText.forEachGrid(visitor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 敌方地图字块点击回调(详情 / 攻击范围)。
|
|
|
|
|
* @param handler 点击回调;null 清除
|
|
|
|
|
*/
|
|
|
|
|
setAiMapTextClickHandler(handler: ((grid: import('../../view/TextGrid').TextGrid) => void) | null): void {
|
|
|
|
|
this._mapText.setMapClickHandler(handler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绑定 AI 字块跳飞层(与玩家 ui_fly 相同)。
|
|
|
|
|
* @param layer 飞行层
|
|
|
|
|
*/
|
|
|
|
|
setMapTextFlyLayer(layer: Node | null): void {
|
|
|
|
|
this._mapText.setFlyLayer(layer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绑定 AI 地图字专用层(与玩家 ui_textGrids 相同)。
|
|
|
|
|
* @param layer 字块层
|
|
|
|
|
*/
|
|
|
|
|
setTextGridLayer(layer: Node | null): void {
|
|
|
|
|
this._mapText.setTextGridLayer(layer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** AI 盘面变更后刷新地图字与技能绑定 */
|
|
|
|
|
notifyAiBoardChanged(): void {
|
|
|
|
|
this._onBoardChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AI 主动技能 UI 快照(CD + 展示 buff)。
|
|
|
|
|
* @returns 槽列表;未启动为空
|
|
|
|
|
*/
|
|
|
|
|
snapshotActiveSkillUi(): AiActiveSkillUiSlot[] {
|
|
|
|
|
return this._activeSkill.snapshotUiSlots();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 注册 AI 主动施放表现(图标飞向受体)。
|
|
|
|
|
* @param handler 回调;null 清除
|
|
|
|
|
*/
|
|
|
|
|
setActiveSkillCastFxHandler(handler: AiActiveSkillCastFxHandler | null): void {
|
|
|
|
|
this._activeSkill.setCastFxHandler(handler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 注册 AI 金矿飞币表现。
|
|
|
|
|
* @param handler 回调;null 清除
|
|
|
|
|
*/
|
|
|
|
|
setGoldMineCoinFlyHandler(handler: import('../runtime/AiPassiveCdHost').AiGoldMineCoinFlyHandler | null): void {
|
|
|
|
|
this._passiveCd.setGoldMineCoinFlyHandler(handler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 注册 AI 妙手神偷神偷手表现。
|
|
|
|
|
* @param handler 回调;null 清除
|
|
|
|
|
*/
|
|
|
|
|
setStealGlyphFxHandler(handler: import('../runtime/AiActiveSkillHost').AiStealGlyphFxHandler | null): void {
|
|
|
|
|
this._activeSkill.setStealFxHandler(handler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 注册 AI 幻魂镜表现。
|
|
|
|
|
* @param handler 回调;null 清除
|
|
|
|
|
*/
|
|
|
|
|
setConfuseHeroFxHandler(handler: import('../runtime/AiActiveSkillHost').AiConfuseHeroFxHandler | null): void {
|
|
|
|
|
this._activeSkill.setConfuseFxHandler(handler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AI CD 被动 UI 快照。
|
|
|
|
|
* @returns 槽列表;未启动为空
|
|
|
|
|
*/
|
|
|
|
|
snapshotPassiveCdUi(): AiPassiveCdUiSlot[] {
|
|
|
|
|
return this._passiveCd.snapshotUiSlots();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 开战前构建匹配档案。
|
|
|
|
|
* @param input 玩家 buff 数量等;缺省读 DailyShop 携带数
|
|
|
|
|
*/
|
|
|
|
|
buildMatchProfile(input?: AiMatchBuildInput): AiMatchProfile | null {
|
|
|
|
|
const counts = AiMatchPlayerBuffCounts.read('匹配读携带');
|
|
|
|
|
const profile = this._matchService.buildProfile(input ?? {
|
|
|
|
|
playerActiveBuffCount: counts.active,
|
|
|
|
|
playerPassiveBuffCount: counts.passive,
|
|
|
|
|
});
|
|
|
|
|
this._profile = profile;
|
|
|
|
|
return profile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 建波前预挂:局内三选一后赛前不再灌携带(保留空实现供旧调用兼容)。
|
|
|
|
|
*/
|
|
|
|
|
prefetchCarryForWaveBuild(): void {
|
|
|
|
|
// 局内解锁三选一获得 buff,不再赛前写入
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 匹配页已抽出的对手写入档案(开战重抽 buff 后保留展示对手)。
|
|
|
|
|
* @param opponentRankId 对手段位
|
|
|
|
|
* @param opponentStars 对手总星
|
|
|
|
|
*/
|
|
|
|
|
applyPendingOpponent(opponentRankId: number, opponentStars: number): void {
|
|
|
|
|
const p = this._profile;
|
|
|
|
|
if (!p) return;
|
|
|
|
|
const rankId = Math.floor(Number(opponentRankId) || 0);
|
|
|
|
|
if (rankId <= 0) return;
|
|
|
|
|
const stars = Math.max(0, Math.floor(Number(opponentStars) || 0));
|
|
|
|
|
this._profile = {
|
|
|
|
|
playerRankId: p.playerRankId,
|
|
|
|
|
playerStars: p.playerStars,
|
|
|
|
|
opponentRankId: rankId,
|
|
|
|
|
opponentStars: stars,
|
|
|
|
|
starOffset: p.starOffset,
|
|
|
|
|
activeBuffCount: p.activeBuffCount,
|
|
|
|
|
passiveBuffCount: p.passiveBuffCount,
|
|
|
|
|
activeBuffIds: p.activeBuffIds,
|
|
|
|
|
passiveBuffIds: p.passiveBuffIds,
|
|
|
|
|
seed: p.seed,
|
|
|
|
|
};
|
|
|
|
|
aiOp(`锁定匹配页对手 rank=${rankId} stars=${stars}(AI buff 用开战重抽结果)`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 是否已绑定战斗并在跑决策 */
|
|
|
|
|
get running(): boolean {
|
|
|
|
|
return !!this._battle && this._controller.running;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 战斗就绪后启动 AI。
|
|
|
|
|
* @param battle 战斗运行时句柄
|
|
|
|
|
* @param map 地图
|
|
|
|
|
* @param profile 本局档案;缺省使用已缓存 profile
|
|
|
|
|
* @param skillParent 挂载 AI SkillController 的 UI 节点
|
|
|
|
|
* @param bagLayout 玩家 ui_textList(AI 大地块槽位/邻格几何)
|
|
|
|
|
*/
|
|
|
|
|
start(battle: ProjectBattleHandle, map: MapManager, profile?: AiMatchProfile, skillParent?: Node | null, bagLayout?: Node | null): boolean {
|
|
|
|
|
try {
|
|
|
|
|
return this._startInner(battle, map, profile, skillParent, bagLayout);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('[AiBattleBridge] start 异常,对手 AI 未启动', String(e));
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* start 主体(异常由 start 捕获)。
|
|
|
|
|
* @param battle 战斗
|
|
|
|
|
* @param map 地图
|
|
|
|
|
* @param profile 档案
|
|
|
|
|
* @param skillParent 技能父节点
|
|
|
|
|
* @param bagLayout 背包布局
|
|
|
|
|
*/
|
|
|
|
|
private _startInner(battle: ProjectBattleHandle, map: MapManager, profile?: AiMatchProfile, skillParent?: Node | null, bagLayout?: Node | null): boolean {
|
|
|
|
|
let resolved = profile ?? this._profile;
|
|
|
|
|
if (!resolved) {
|
|
|
|
|
resolved = this.buildMatchProfile();
|
|
|
|
|
}
|
|
|
|
|
if (!resolved) {
|
|
|
|
|
console.warn('[AiBattleBridge] profile missing,使用应急档案');
|
|
|
|
|
resolved = this._emergencyProfile(battle);
|
|
|
|
|
this._profile = resolved;
|
|
|
|
|
}
|
|
|
|
|
if (resolved.opponentRankId !== battle.opponentRankId) {
|
|
|
|
|
console.warn(
|
|
|
|
|
'[AiBattleBridge] profile.opponentRankId != battle.opponentRankId',
|
|
|
|
|
resolved.opponentRankId,
|
|
|
|
|
battle.opponentRankId,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
this._mountAiBuffs(resolved, battle);
|
|
|
|
|
let personality = this._personalityFactory.roll(resolved.opponentRankId, resolved.seed);
|
|
|
|
|
if (!personality) {
|
|
|
|
|
console.warn('[AiBattleBridge] personality 失败,使用默认', resolved.opponentRankId);
|
|
|
|
|
personality = {
|
|
|
|
|
textPolicy: AiTextPolicyType.Type1,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (!this._bagHost.bind(resolved, battle, map, bagLayout ?? null)) {
|
|
|
|
|
console.error('[AiBattleBridge] start aborted: headless bag bind failed');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
this._weaponRegistry.clear();
|
|
|
|
|
this._battle = battle;
|
|
|
|
|
this._map = map;
|
|
|
|
|
this._profile = resolved;
|
|
|
|
|
this._mapText.setAfterFlush(this._refreshAiSkills);
|
|
|
|
|
if (skillParent) {
|
|
|
|
|
this._skillHost.bind(battle, map, skillParent, this._mapText.getMapGrids());
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('[AiBattleBridge] skillParent missing: AI towers may not attack');
|
|
|
|
|
}
|
|
|
|
|
this._controller.start(
|
|
|
|
|
resolved,
|
|
|
|
|
personality,
|
|
|
|
|
this._bagHost,
|
|
|
|
|
battle,
|
|
|
|
|
map,
|
|
|
|
|
this._onBoardChanged,
|
|
|
|
|
this._tryRecycleSurplusBag,
|
|
|
|
|
this._mapText,
|
|
|
|
|
);
|
|
|
|
|
this._passiveCd.bind(this._bagHost, battle, this._onBoardChanged);
|
|
|
|
|
this._adHost.bind(this._bagHost, battle, map, resolved.opponentRankId, this._onBoardChanged);
|
|
|
|
|
this._onBoardChanged();
|
|
|
|
|
setAiHeroWeaponResolver(this._resolveAiWeaponForLoadout);
|
|
|
|
|
SkillAtkBuffBoardViewBind.bindEnemyNotify(this._onBoardChanged);
|
|
|
|
|
console.warn(
|
|
|
|
|
'[AiBattleBridge] 就绪',
|
|
|
|
|
'rank=' + resolved.opponentRankId,
|
|
|
|
|
'gold=' + this._bagHost.aiGold,
|
|
|
|
|
'nextCost=' + this._bagHost.nextRefreshCost,
|
|
|
|
|
'text=' + personality.textPolicy,
|
|
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 匹配建档全失败时的最小档案(无 buff,仍可走数字合成/解锁)。
|
|
|
|
|
* @param battle 战斗
|
|
|
|
|
*/
|
|
|
|
|
private _emergencyProfile(battle: ProjectBattleHandle): AiMatchProfile {
|
|
|
|
|
const rankId = Math.max(1, Math.floor(Number(battle.opponentRankId) || Number(battle.playerRankId) || 1));
|
|
|
|
|
return {
|
|
|
|
|
playerRankId: Math.max(1, Math.floor(Number(battle.playerRankId) || 1)),
|
|
|
|
|
playerStars: 1,
|
|
|
|
|
opponentRankId: rankId,
|
|
|
|
|
opponentStars: 1,
|
|
|
|
|
starOffset: 0,
|
|
|
|
|
activeBuffCount: 0,
|
|
|
|
|
passiveBuffCount: 0,
|
|
|
|
|
activeBuffIds: [],
|
|
|
|
|
passiveBuffIds: [],
|
|
|
|
|
seed: (Date.now() % 100000) || 1,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 棋盘被外部改写后刷新 AI 字块与技能绑定 */
|
|
|
|
|
notifyBoardChanged(): void {
|
|
|
|
|
this._onBoardChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 部署/落子后:刷新技能绑定 + 地图字 TextGrid */
|
|
|
|
|
private readonly _onBoardChanged = (): void => {
|
|
|
|
|
const map = this._map;
|
|
|
|
|
const bag = this._bagHost.bagSession;
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!map || !bag || !battle) return;
|
|
|
|
|
const aiPlayer = battle.getPlayer(AiHeadlessBagHost.AI_PLAYER_ID) ?? null;
|
|
|
|
|
this._mapText.requestSync(bag, map, aiPlayer);
|
|
|
|
|
this._refreshAiSkills();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 字块表就绪后刷新 AI Skill 绑定 */
|
|
|
|
|
private readonly _refreshAiSkills = (): void => {
|
|
|
|
|
const map = this._map;
|
|
|
|
|
if (!map) return;
|
|
|
|
|
this._skillHost.refresh(map, this._mapText.getMapGrids());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 刷新拍:乾坤袋回收背包多余字 */
|
|
|
|
|
private readonly _tryRecycleSurplusBag = (): boolean => {
|
|
|
|
|
return this._activeSkill.tryRecycleSurplusBag();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 供 TowerLoadout 注册的 AI 武器解析(避免循环 require)。
|
|
|
|
|
* @param heroId Hero.id
|
|
|
|
|
*/
|
|
|
|
|
private readonly _resolveAiWeaponForLoadout = (heroId: number): number | null => {
|
|
|
|
|
return this.getHeroBattleWeaponId(heroId);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 战斗结束或切场景时停止 AI */
|
|
|
|
|
stop(): void {
|
|
|
|
|
setAiHeroWeaponResolver(null);
|
|
|
|
|
SkillAtkBuffBoardViewBind.bindEnemyNotify(null);
|
|
|
|
|
this._controller.stop();
|
|
|
|
|
this._passiveCd.dispose();
|
|
|
|
|
this._activeSkill.dispose();
|
|
|
|
|
this._adHost.dispose();
|
|
|
|
|
this._mapText.setAfterFlush(null);
|
|
|
|
|
this._mapText.clear();
|
|
|
|
|
this._skillHost.dispose();
|
|
|
|
|
this._bagHost.dispose();
|
|
|
|
|
this._weaponRegistry.clear();
|
|
|
|
|
BattleBuffCarryStore.clear();
|
|
|
|
|
this._battle = null;
|
|
|
|
|
this._map = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 局内三选一:赛前不灌匹配 buff,仅重绑 Host。
|
|
|
|
|
* @param profile 档案
|
|
|
|
|
* @param battle 战斗
|
|
|
|
|
*/
|
|
|
|
|
private _mountAiBuffs(profile: AiMatchProfile, battle: ProjectBattleHandle): void {
|
|
|
|
|
const aiPid = AiHeadlessBagHost.AI_PLAYER_ID;
|
|
|
|
|
BattleBuffCarryStore.setFromBuffIds(aiPid, [], []);
|
|
|
|
|
const gmPassive = gg.game.gmAiPassiveBuffIds;
|
|
|
|
|
const gmActive = gg.game.gmAiActiveBuffIds;
|
|
|
|
|
let seeded = 0;
|
|
|
|
|
if (gmActive?.length) {
|
|
|
|
|
for (let i = 0; i < gmActive.length; i++) {
|
|
|
|
|
if (BattleBuffCarryStore.appendBuffId(aiPid, gmActive[i])) seeded++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (gmPassive?.length) {
|
|
|
|
|
for (let i = 0; i < gmPassive.length; i++) {
|
|
|
|
|
if (BattleBuffCarryStore.appendBuffId(aiPid, gmPassive[i])) seeded++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (seeded > 0) {
|
|
|
|
|
aiOp(`GM 覆盖 AI buff 灌入 count=${seeded} profilePassive=[${profile.passiveBuffIds.join(',')}]`);
|
|
|
|
|
} else {
|
|
|
|
|
aiOp(`AI buff 赛前清空(局内解锁三选一)profilePassive=[${profile.passiveBuffIds.join(',')}]`);
|
|
|
|
|
}
|
|
|
|
|
getBattleBuffMoveSpeedHost().bind(battle);
|
|
|
|
|
getBattleBuffUnitAttackSpeedHost().bind(battle);
|
|
|
|
|
getBattleBuffBaseThunderHost().refreshCarry(battle);
|
|
|
|
|
getBattleBuffDigitStatHost().bind(battle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AI 战斗武器 id(旧 aiweapon 分配已下线,非 null 时亦可能为 0)。
|
|
|
|
|
* @param heroId Hero.id
|
|
|
|
|
*/
|
|
|
|
|
getHeroBattleWeaponId(heroId: number): number | null {
|
|
|
|
|
if (!this._profile) {
|
|
|
|
|
aiWarn(`[AiHeroWeapon] skip: profile missing heroId=${heroId}`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const record = this._weaponAssigner.resolveOrAssign(
|
|
|
|
|
this._weaponRegistry,
|
|
|
|
|
heroId,
|
|
|
|
|
this._profile.opponentRankId,
|
|
|
|
|
this._profile.seed,
|
|
|
|
|
);
|
|
|
|
|
return record != null ? record.weaponId : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 每帧 tick。
|
|
|
|
|
* @param dtSec 帧间隔(秒)
|
|
|
|
|
*/
|
|
|
|
|
tick(dtSec: number): void {
|
|
|
|
|
if (!this._battle) return;
|
|
|
|
|
// 结算/会话已结束:只停决策,盘面留给 UI teardown 再 dispose
|
|
|
|
|
if (this._battle.IsGameOver || this._battle.session.ended) {
|
|
|
|
|
if (this._controller.running) {
|
|
|
|
|
this._controller.stop();
|
|
|
|
|
this._passiveCd.dispose();
|
|
|
|
|
this._activeSkill.dispose();
|
|
|
|
|
this._adHost.dispose();
|
|
|
|
|
aiOp('局结束,停止决策');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 暂停、等复活:保留状态但不决策 / 不走 CD
|
|
|
|
|
if (this._battle.IsGamePause) return;
|
|
|
|
|
this._passiveCd.tick(dtSec);
|
|
|
|
|
this._adHost.tick(dtSec);
|
|
|
|
|
this._controller.tick(dtSec);
|
|
|
|
|
}
|
|
|
|
|
}
|