import type { AiMatchBuildInput } from '../types/AiTypes'; import { aiLog, aiOp } from '../runtime/AiLog'; import type { AiMatchProfile } from './AiMatchProfile'; import { AiBattlebuffPicker } from './AiBattlebuffPicker'; import { RankDataManager } from '../../../manager/RankDataManager'; import { DailyShopDataManager } from '../../../manager/DailyShopDataManager'; import { MatchStreakStats } from '../../../manager/MatchStreakStats'; /** Const 11: 匹配星偏移权重 */ const CONST_STAR_WEIGHT_ID = 11; /** Const 12: 主动 buff 数量偏移权重 (-1,0,+1) */ const CONST_ACTIVE_OFFSET_ID = 12; /** Const 14: 被动 buff 数量偏移权重 (-2,-1,0,+1) */ const CONST_PASSIVE_OFFSET_ID = 14; /** Const 15: 连胜 → 主动 +1 档加成 */ const CONST_WIN_ACTIVE_BONUS_ID = 15; /** Const 16: 连胜 → 被动 +1 档加成 */ const CONST_WIN_PASSIVE_BONUS_ID = 16; /** Const 17: 连败 → 主动 -1 档加成 */ const CONST_LOSE_ACTIVE_BONUS_ID = 17; /** Const 18: 连败 → 被动 -2 档加成 */ const CONST_LOSE_PASSIVE_BONUS_ID = 18; /** Const 19: 最大主动技能数量 */ const CONST_MAX_ACTIVE_ID = 19; /** Const 20: 最大被动技能数量 */ const CONST_MAX_PASSIVE_ID = 20; /** Const 12 主动数量偏移 */ const ACTIVE_OFFSETS = [-1, 0, 1]; /** Const 14 被动数量偏移 */ const PASSIVE_OFFSETS = [-2, -1, 0, 1]; /** 开战前匹配:Const 11/12/14/15~20 -> AiMatchProfile */ export class AiMatchService { private readonly _buffPicker = new AiBattlebuffPicker(); /** * 构建本局 AI 对手档案。 * @param input 玩家 buff 数量等入参 * @returns 成功返回档案;配表/权重非法时返回 null */ buildProfile(input: AiMatchBuildInput): AiMatchProfile | null { const playerStars = RankDataManager.getPlayerRankStars(); const playerRankId = RankDataManager.getPlayerRankId(); let starMatch = RankDataManager.resolveOpponentStarMatch(playerStars); if (!starMatch) { console.warn( '[AiMatchService] star match failed,回退玩家段位', `playerStars=${playerStars} playerRankId=${playerRankId}`, ); starMatch = { starOffset: 0, opponentStars: Math.max(1, playerStars), opponentRankId: playerRankId > 0 ? playerRankId : 1, }; } const caps = this._readSlotCaps(); const playerActive = Math.max(0, Math.floor(Number(input.playerActiveBuffCount) || 0)); const playerPassive = Math.max(0, Math.floor(Number(input.playerPassiveBuffCount) || 0)); let activeWeights = this._copyWeights(gg.data.table.getConstArray(CONST_ACTIVE_OFFSET_ID), ACTIVE_OFFSETS.length, 'Const12'); let passiveWeights = this._copyWeights(gg.data.table.getConstArray(CONST_PASSIVE_OFFSET_ID), PASSIVE_OFFSETS.length, 'Const14'); if (!activeWeights) { console.warn('[AiMatchService] Const12 权重无效,用均匀权重'); activeWeights = [1, 1, 1]; } if (!passiveWeights) { console.warn('[AiMatchService] Const14 权重无效,用均匀权重'); passiveWeights = [1, 1, 1, 1]; } this._applyStreakWeightBonus(activeWeights, passiveWeights, playerActive, playerPassive, caps); let activeOff = this._rollOffset(activeWeights, ACTIVE_OFFSETS, 'Const12'); let passiveOff = this._rollOffset(passiveWeights, PASSIVE_OFFSETS, 'Const14'); if (activeOff == null) activeOff = 0; if (passiveOff == null) passiveOff = 0; const activeRaw = playerActive + activeOff; const passiveRaw = playerPassive + passiveOff; const activeBuffCount = Math.max(0, Math.min(caps.active, activeRaw)); const passiveBuffCount = Math.max(0, Math.min(caps.passive, passiveRaw)); aiOp( `AI buff 数量推导` + ` 玩家A=${playerActive} 偏移=${activeOff}(权重[${activeWeights.join(',')}]) raw=${activeRaw} → clamp[0,${caps.active}]=${activeBuffCount}` + ` | 玩家P=${playerPassive} 偏移=${passiveOff}(权重[${passiveWeights.join(',')}]) raw=${passiveRaw} → clamp[0,${caps.passive}]=${passiveBuffCount}` + ` | streakW=${MatchStreakStats.getWinStreak()} streakL=${MatchStreakStats.getLoseStreak()}`, ); const seed = input.seed != null ? Math.floor(Number(input.seed) || 0) : ((Date.now() % 100000) ^ (starMatch.opponentStars * 9973)); let activeBuffIds = this._buffPicker.pick(1, activeBuffCount, seed); let passiveBuffIds = this._buffPicker.pick(2, passiveBuffCount, seed ^ 0x9e3779b9); let resolvedActiveCount = activeBuffIds.length; let resolvedPassiveCount = passiveBuffIds.length; // 候选不足时按实际数量建档,禁止整局 AI 因 buff 失败而不启动 if (resolvedActiveCount !== activeBuffCount || resolvedPassiveCount !== passiveBuffCount) { console.warn( '[AiMatchService] buff 抽取不足,按实际数量建档', `active=${activeBuffCount}/${resolvedActiveCount}`, `passive=${passiveBuffCount}/${resolvedPassiveCount}`, ); } aiOp( `AI buff 抽取 seed=${seed}` + ` active[${resolvedActiveCount}]=[${activeBuffIds.join(',')}]` + ` passive[${resolvedPassiveCount}]=[${passiveBuffIds.join(',')}]`, ); const gmActive = gg.game.gmAiActiveBuffIds; const gmPassive = gg.game.gmAiPassiveBuffIds; // 正常:上列每局重抽;GM 有值则常驻覆盖(直至 GM 清除 / 游戏 reset) if (gmActive?.length) { activeBuffIds = gmActive.slice(); resolvedActiveCount = activeBuffIds.length; aiOp(`GM 常驻覆盖 AI 主动 buff=[${activeBuffIds.join(',')}]`); } if (gmPassive?.length) { passiveBuffIds = gmPassive.slice(); resolvedPassiveCount = passiveBuffIds.length; aiOp(`GM 常驻覆盖 AI 被动 buff=[${passiveBuffIds.join(',')}]`); } const profile: AiMatchProfile = { playerRankId, playerStars, opponentRankId: starMatch.opponentRankId, opponentStars: starMatch.opponentStars, starOffset: starMatch.starOffset, activeBuffCount: resolvedActiveCount, passiveBuffCount: resolvedPassiveCount, activeBuffIds, passiveBuffIds, seed, }; aiOp( `匹配档案 playerRank=${playerRankId} stars=${playerStars}` + ` -> aiRank=${profile.opponentRankId} stars=${profile.opponentStars}` + ` buffA=${resolvedActiveCount}[${activeBuffIds.join(',')}]` + ` buffP=${resolvedPassiveCount}[${passiveBuffIds.join(',')}]` + ` streakW=${MatchStreakStats.getWinStreak()} streakL=${MatchStreakStats.getLoseStreak()}` + ` caps=${caps.active}/${caps.passive}`, ); return profile; } /** buff 抽取器 */ get buffPicker(): AiBattlebuffPicker { return this._buffPicker; } /** Const 19/20 槽位上限 */ private _readSlotCaps(): { active: number; passive: number } { const fromShop = DailyShopDataManager.getSlotCaps(); const active = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_MAX_ACTIVE_ID)) || 0)); const passive = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_MAX_PASSIVE_ID)) || 0)); if (active <= 0 || passive <= 0) { aiLog(`[AiMatchService] Const19/20 invalid, use DailyShop caps active=${active} passive=${passive} shopA=${fromShop.active} shopP=${fromShop.passive}`); return { active: Math.max(1, fromShop.active), passive: Math.max(1, fromShop.passive), }; } return { active, passive }; } /** * 复制权重数组;长度不符仍截断/补 0 并 warn。 * @param raw 常量表数组 * @param expectLen 期望档位数 * @param label 日志标签 */ private _copyWeights(raw: number[], expectLen: number, label: string): number[] | null { const weights: number[] = []; for (let i = 0; i < expectLen; i++) { weights.push(Math.max(0, Math.floor(Number(raw[i]) || 0))); } if (raw.length !== expectLen) { aiLog(`[AiMatchService] ${label} weight length invalid len=${raw.length} expect=${expectLen}`); } let total = 0; for (let i = 0; i < weights.length; i++) total += weights[i]; if (total <= 0) { aiLog(`[AiMatchService] ${label} total weight <= 0`); return null; } return weights; } /** * Const 15~18:连胜/连败修正 12/14 权重(不写回表)。 * @param activeWeights Const12 三档 * @param passiveWeights Const14 四档 * @param playerActive 玩家主动数 * @param playerPassive 玩家被动数 * @param caps 槽位上限 */ private _applyStreakWeightBonus(activeWeights: number[], passiveWeights: number[], playerActive: number, playerPassive: number, caps: { active: number; passive: number }): void { const winShares = MatchStreakStats.effectiveShares(MatchStreakStats.getWinStreak()); const loseShares = MatchStreakStats.effectiveShares(MatchStreakStats.getLoseStreak()); if (winShares > 0) { const addA = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_WIN_ACTIVE_BONUS_ID)) || 0)) * winShares; const addP = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_WIN_PASSIVE_BONUS_ID)) || 0)) * winShares; // 主动:+1 档 index=2;已满上限则加到 0 档 index=1 const aIdx = playerActive >= caps.active ? 1 : 2; activeWeights[aIdx] = (activeWeights[aIdx] || 0) + addA; // 被动:+1 档 index=3;已满上限则加到 0 档 index=2 const pIdx = playerPassive >= caps.passive ? 2 : 3; passiveWeights[pIdx] = (passiveWeights[pIdx] || 0) + addP; aiOp(`连胜修正 shares=${winShares} active[+${addA}@${aIdx}] passive[+${addP}@${pIdx}]`); } if (loseShares > 0) { const addA = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_LOSE_ACTIVE_BONUS_ID)) || 0)) * loseShares; const addP = Math.max(0, Math.floor(Number(gg.data.table.getConst(CONST_LOSE_PASSIVE_BONUS_ID)) || 0)) * loseShares; // 主动:-1 档 index=0;被动:-2 档 index=0 activeWeights[0] = (activeWeights[0] || 0) + addA; passiveWeights[0] = (passiveWeights[0] || 0) + addP; aiOp(`连败修正 shares=${loseShares} active[-1 +${addA}] passive[-2 +${addP}]`); } } /** 加权随机偏移;权重全 0 则返回 null */ private _rollOffset(weights: number[], offsets: number[], label: string): number | null { let total = 0; for (let i = 0; i < weights.length; i++) total += Math.max(0, weights[i] || 0); if (total <= 0) { aiLog(`[AiMatchService] ${label} total weight <= 0`); return null; } let roll = Math.random() * total; for (let i = 0; i < offsets.length; i++) { roll -= Math.max(0, weights[i] || 0); if (roll < 0) return offsets[i]; } return offsets[offsets.length - 1]; } }