From 61f71f8f0868ec573522c8cd5ac3c0cbb7bf2355 Mon Sep 17 00:00:00 2001 From: humingxing <930621236@qq.com> Date: Mon, 19 Oct 2026 17:18:42 +0800 Subject: [PATCH] =?UTF-8?q?AI=E9=95=B6=E5=B5=8C=E5=AE=9D=E7=9F=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/bundles/first/config/Rank.json | 122 ++++++------- .../script/battle/ai/bridge/AiBattleBridge.ts | 48 +++--- assets/script/battle/ai/index.ts | 7 +- .../battle/ai/runtime/AiGemSocketAssigner.ts | 163 ++++++++++++++++++ .../ai/runtime/AiGemSocketAssigner.ts.meta | 9 + .../battle/ai/runtime/AiGemSocketStore.ts | 66 +++++++ .../ai/runtime/AiGemSocketStore.ts.meta | 9 + .../battle/ai/types/AiHeroWeaponTypes.ts | 6 +- .../script/battle/combat/GemBattleEffect.ts | 31 +++- .../battle/combat/TowerLoadoutService.ts | 5 +- assets/script/interface/AutoTable.ts | 2 +- 11 files changed, 360 insertions(+), 108 deletions(-) create mode 100644 assets/script/battle/ai/runtime/AiGemSocketAssigner.ts create mode 100644 assets/script/battle/ai/runtime/AiGemSocketAssigner.ts.meta create mode 100644 assets/script/battle/ai/runtime/AiGemSocketStore.ts create mode 100644 assets/script/battle/ai/runtime/AiGemSocketStore.ts.meta diff --git a/assets/bundles/first/config/Rank.json b/assets/bundles/first/config/Rank.json index 031e012..2d55c0b 100644 --- a/assets/bundles/first/config/Rank.json +++ b/assets/bundles/first/config/Rank.json @@ -11,7 +11,7 @@ "levelvalue": 0, "diffcult": 0, "weapondrop": 0, - "aiweapon": "100,0,0,0,0", + "aiweapon": "100,0,0,0,0,0", "aiusead": 3300, "aiusechanzi": [ 5, @@ -35,7 +35,7 @@ "levelvalue": 0, "diffcult": 0, "weapondrop": 0, - "aiweapon": "90,10,0,0,0", + "aiweapon": "90,10,0,0,0,0", "aiusead": 3350, "aiusechanzi": [ 5, @@ -59,7 +59,7 @@ "levelvalue": 0, "diffcult": 0, "weapondrop": 0, - "aiweapon": "80,20,0,0,0", + "aiweapon": "80,20,0,0,0,0", "aiusead": 3400, "aiusechanzi": [ 5, @@ -83,7 +83,7 @@ "levelvalue": 0, "diffcult": 0, "weapondrop": 0, - "aiweapon": "70,30,0,0,0", + "aiweapon": "70,30,0,0,0,0", "aiusead": 3450, "aiusechanzi": [ 5, @@ -107,7 +107,7 @@ "levelvalue": 0, "diffcult": 1, "weapondrop": 5, - "aiweapon": "60,40,0,0,0", + "aiweapon": "60,40,0,0,0,0", "aiusead": 3500, "aiusechanzi": [ 5, @@ -131,7 +131,7 @@ "levelvalue": 0, "diffcult": 1, "weapondrop": 6, - "aiweapon": "50,50,0,0,0", + "aiweapon": "50,50,0,0,0,0", "aiusead": 3550, "aiusechanzi": [ 5, @@ -155,7 +155,7 @@ "levelvalue": 0, "diffcult": 1, "weapondrop": 7, - "aiweapon": "40,60,0,0,0", + "aiweapon": "40,60,0,0,0,0", "aiusead": 3600, "aiusechanzi": [ 5, @@ -179,7 +179,7 @@ "levelvalue": 0, "diffcult": 1, "weapondrop": 8, - "aiweapon": "30,70,0,0,0", + "aiweapon": "30,70,0,0,0,0", "aiusead": 3650, "aiusechanzi": [ 5, @@ -203,7 +203,7 @@ "levelvalue": 1, "diffcult": 2, "weapondrop": 9, - "aiweapon": "20,80,0,0,0", + "aiweapon": "20,80,0,0,0,0", "aiusead": 3700, "aiusechanzi": [ 5, @@ -227,7 +227,7 @@ "levelvalue": 1, "diffcult": 2, "weapondrop": 10, - "aiweapon": "10,90,0,0,0", + "aiweapon": "10,90,0,0,0,0", "aiusead": 3750, "aiusechanzi": [ 5, @@ -251,7 +251,7 @@ "levelvalue": 1, "diffcult": 2, "weapondrop": 11, - "aiweapon": "0,90,10,0,0", + "aiweapon": "0,90,10,0,0,0", "aiusead": 3800, "aiusechanzi": [ 5, @@ -275,7 +275,7 @@ "levelvalue": 1, "diffcult": 2, "weapondrop": 12, - "aiweapon": "0,85,15,0,0", + "aiweapon": "0,85,15,0,0,0", "aiusead": 3850, "aiusechanzi": [ 5, @@ -299,7 +299,7 @@ "levelvalue": 1, "diffcult": 3, "weapondrop": 13, - "aiweapon": "0,80,20,0,0", + "aiweapon": "0,80,20,0,0,0", "aiusead": 3900, "aiusechanzi": [ 5, @@ -323,7 +323,7 @@ "levelvalue": 1, "diffcult": 3, "weapondrop": 14, - "aiweapon": "0,75,25,0,0", + "aiweapon": "0,75,25,0,0,0", "aiusead": 3950, "aiusechanzi": [ 5, @@ -347,7 +347,7 @@ "levelvalue": 1, "diffcult": 3, "weapondrop": 15, - "aiweapon": "0,70,30,0,0", + "aiweapon": "0,70,30,0,0,0", "aiusead": 4000, "aiusechanzi": [ 5, @@ -371,7 +371,7 @@ "levelvalue": 1, "diffcult": 3, "weapondrop": 16, - "aiweapon": "0,65,30,5,0", + "aiweapon": "0,65,30,5,0,0", "aiusead": 4050, "aiusechanzi": [ 5, @@ -395,7 +395,7 @@ "levelvalue": 2, "diffcult": 4, "weapondrop": 17, - "aiweapon": "0,60,30,10,0", + "aiweapon": "0,60,30,10,0,0", "aiusead": 4100, "aiusechanzi": [ 5, @@ -419,7 +419,7 @@ "levelvalue": 2, "diffcult": 4, "weapondrop": 18, - "aiweapon": "0,55,35,10,0", + "aiweapon": "0,55,35,10,0,0", "aiusead": 4150, "aiusechanzi": [ 5, @@ -443,7 +443,7 @@ "levelvalue": 2, "diffcult": 4, "weapondrop": 19, - "aiweapon": "0,50,35,15,0", + "aiweapon": "0,50,35,15,0,0", "aiusead": 4200, "aiusechanzi": [ 5, @@ -467,7 +467,7 @@ "levelvalue": 2, "diffcult": 4, "weapondrop": 20, - "aiweapon": "0,45,40,15,0", + "aiweapon": "0,45,40,15,0,0", "aiusead": 4250, "aiusechanzi": [ 5, @@ -491,7 +491,7 @@ "levelvalue": 2, "diffcult": 5, "weapondrop": 21, - "aiweapon": "0,40,40,20,0", + "aiweapon": "0,40,40,20,0,0", "aiusead": 4300, "aiusechanzi": [ 5, @@ -515,7 +515,7 @@ "levelvalue": 2, "diffcult": 5, "weapondrop": 22, - "aiweapon": "0,35,45,20,0", + "aiweapon": "0,35,45,20,0,0", "aiusead": 4350, "aiusechanzi": [ 5, @@ -539,7 +539,7 @@ "levelvalue": 2, "diffcult": 5, "weapondrop": 23, - "aiweapon": "0,30,45,25,0", + "aiweapon": "0,30,45,25,0,0", "aiusead": 4400, "aiusechanzi": [ 5, @@ -563,7 +563,7 @@ "levelvalue": 2, "diffcult": 5, "weapondrop": 24, - "aiweapon": "0,25,50,25,0", + "aiweapon": "0,25,50,25,0,0", "aiusead": 4450, "aiusechanzi": [ 5, @@ -587,7 +587,7 @@ "levelvalue": 3, "diffcult": 6, "weapondrop": 25, - "aiweapon": "0,20,50,25,0", + "aiweapon": "0,20,50,25,0,0", "aiusead": 4500, "aiusechanzi": [ 5, @@ -611,7 +611,7 @@ "levelvalue": 3, "diffcult": 6, "weapondrop": 26, - "aiweapon": "0,15,50,30,5", + "aiweapon": "0,15,50,30,5,0", "aiusead": 4550, "aiusechanzi": [ 5, @@ -635,7 +635,7 @@ "levelvalue": 3, "diffcult": 6, "weapondrop": 27, - "aiweapon": "0,10,50,35,5", + "aiweapon": "0,10,50,35,5,0", "aiusead": 4600, "aiusechanzi": [ 5, @@ -659,7 +659,7 @@ "levelvalue": 3, "diffcult": 6, "weapondrop": 28, - "aiweapon": "0,5,50,35,10", + "aiweapon": "0,5,50,35,10,0", "aiusead": 4650, "aiusechanzi": [ 5, @@ -683,7 +683,7 @@ "levelvalue": 3, "diffcult": 7, "weapondrop": 29, - "aiweapon": "0,0,50,40,10", + "aiweapon": "0,0,50,40,10,0", "aiusead": 4700, "aiusechanzi": [ 5, @@ -707,7 +707,7 @@ "levelvalue": 3, "diffcult": 7, "weapondrop": 30, - "aiweapon": "0,0,45,40,15", + "aiweapon": "0,0,45,40,15,0", "aiusead": 4750, "aiusechanzi": [ 5, @@ -731,7 +731,7 @@ "levelvalue": 3, "diffcult": 7, "weapondrop": 31, - "aiweapon": "0,0,40,45,15", + "aiweapon": "0,0,40,45,15,0", "aiusead": 4800, "aiusechanzi": [ 5, @@ -755,7 +755,7 @@ "levelvalue": 3, "diffcult": 7, "weapondrop": 32, - "aiweapon": "0,0,35,45,20", + "aiweapon": "0,0,35,45,20,0", "aiusead": 4850, "aiusechanzi": [ 5, @@ -779,7 +779,7 @@ "levelvalue": 4, "diffcult": 8, "weapondrop": 33, - "aiweapon": "0,0,30,50,20", + "aiweapon": "0,0,30,50,20,0", "aiusead": 4900, "aiusechanzi": [ 5, @@ -803,7 +803,7 @@ "levelvalue": 4, "diffcult": 8, "weapondrop": 34, - "aiweapon": "0,0,25,50,25", + "aiweapon": "0,0,25,50,25,0", "aiusead": 4950, "aiusechanzi": [ 5, @@ -827,7 +827,7 @@ "levelvalue": 4, "diffcult": 8, "weapondrop": 35, - "aiweapon": "0,0,20,55,25", + "aiweapon": "0,0,20,55,25,0", "aiusead": 5000, "aiusechanzi": [ 5, @@ -851,7 +851,7 @@ "levelvalue": 4, "diffcult": 8, "weapondrop": 36, - "aiweapon": "0,0,15,55,30", + "aiweapon": "0,0,15,55,30,0", "aiusead": 5050, "aiusechanzi": [ 5, @@ -875,7 +875,7 @@ "levelvalue": 4, "diffcult": 9, "weapondrop": 37, - "aiweapon": "0,0,10,60,30", + "aiweapon": "0,0,10,60,30,0", "aiusead": 5100, "aiusechanzi": [ 5, @@ -899,7 +899,7 @@ "levelvalue": 4, "diffcult": 9, "weapondrop": 38, - "aiweapon": "0,0,5,60,35", + "aiweapon": "0,0,5,60,35,0", "aiusead": 5150, "aiusechanzi": [ 5, @@ -923,7 +923,7 @@ "levelvalue": 4, "diffcult": 9, "weapondrop": 39, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,60,40,0", "aiusead": 5200, "aiusechanzi": [ 5, @@ -947,7 +947,7 @@ "levelvalue": 4, "diffcult": 9, "weapondrop": 40, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,60,40,0", "aiusead": 5250, "aiusechanzi": [ 5, @@ -971,7 +971,7 @@ "levelvalue": 4, "diffcult": 10, "weapondrop": 41, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,60,40,0", "aiusead": 5300, "aiusechanzi": [ 5, @@ -995,7 +995,7 @@ "levelvalue": 4, "diffcult": 10, "weapondrop": 42, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,60,40,0", "aiusead": 5350, "aiusechanzi": [ 5, @@ -1019,7 +1019,7 @@ "levelvalue": 4, "diffcult": 10, "weapondrop": 43, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,55,40,5", "aiusead": 5400, "aiusechanzi": [ 5, @@ -1043,7 +1043,7 @@ "levelvalue": 4, "diffcult": 10, "weapondrop": 44, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,55,40,5", "aiusead": 5450, "aiusechanzi": [ 5, @@ -1067,7 +1067,7 @@ "levelvalue": 4, "diffcult": 11, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,55,40,5", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1091,7 +1091,7 @@ "levelvalue": 4, "diffcult": 11, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,50,40,10", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1115,7 +1115,7 @@ "levelvalue": 4, "diffcult": 11, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,50,40,10", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1139,7 +1139,7 @@ "levelvalue": 4, "diffcult": 11, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,50,40,10", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1163,7 +1163,7 @@ "levelvalue": 4, "diffcult": 12, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,50,40,10", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1187,7 +1187,7 @@ "levelvalue": 4, "diffcult": 12, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,45,40,15", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1211,7 +1211,7 @@ "levelvalue": 4, "diffcult": 12, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,45,40,15", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1235,7 +1235,7 @@ "levelvalue": 4, "diffcult": 12, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,45,40,15", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1259,7 +1259,7 @@ "levelvalue": 4, "diffcult": 13, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,45,40,15", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1283,7 +1283,7 @@ "levelvalue": 4, "diffcult": 13, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,45,40,15", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1307,7 +1307,7 @@ "levelvalue": 5, "diffcult": 13, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,40,40,20", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1331,7 +1331,7 @@ "levelvalue": 5, "diffcult": 13, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,40,40,20", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1355,7 +1355,7 @@ "levelvalue": 5, "diffcult": 14, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,40,40,20", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1379,7 +1379,7 @@ "levelvalue": 5, "diffcult": 14, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,40,40,20", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1403,7 +1403,7 @@ "levelvalue": 5, "diffcult": 14, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,35,40,25", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1427,7 +1427,7 @@ "levelvalue": 5, "diffcult": 14, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,30,40,30", "aiusead": 5500, "aiusechanzi": [ 5, @@ -1451,7 +1451,7 @@ "levelvalue": 5, "diffcult": 15, "weapondrop": 50, - "aiweapon": "0,0,0,60,40", + "aiweapon": "0,0,0,25,40,35", "aiusead": 5500, "aiusechanzi": [ 5, diff --git a/assets/script/battle/ai/bridge/AiBattleBridge.ts b/assets/script/battle/ai/bridge/AiBattleBridge.ts index 94a877e..daa0718 100644 --- a/assets/script/battle/ai/bridge/AiBattleBridge.ts +++ b/assets/script/battle/ai/bridge/AiBattleBridge.ts @@ -1,5 +1,5 @@ import type { Node } from 'cc'; -import { aiLog, aiOp, aiWarn } from '../runtime/AiLog'; +import { aiOp } from '../runtime/AiLog'; import type { ProjectBattleHandle } from '../../runtime/BattleRuntime'; import type { MapManager } from '../../map/legacy/MapManager'; import type { AiMatchProfile } from '../match/AiMatchProfile'; @@ -13,15 +13,15 @@ import { } from '../runtime/AiBattlePersonality'; import { AiHeadlessBagHost } from '../runtime/AiHeadlessBagHost'; import { AiOpponentController } from '../runtime/AiOpponentController'; -import { AiHeroWeaponRegistry } from '../runtime/AiHeroWeaponRegistry'; -import { AiHeroWeaponAssigner } from '../runtime/AiHeroWeaponAssigner'; +import { AiGemSocketStore } from '../runtime/AiGemSocketStore'; +import { AiGemSocketAssigner } from '../runtime/AiGemSocketAssigner'; 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 { setAiGemSlotsResolver } from '../../combat/GemBattleEffect'; import { SkillAtkBuffBoardViewBind } from '../../combat/SkillAtkBuffBoardBridge'; import { AiMatchPlayerBuffCounts } from '../match/AiMatchPlayerBuffCounts'; import { BattleBuffCarryStore } from '../../../manager/battleBuff/BattleBuffCarryStore'; @@ -38,8 +38,8 @@ export class AiBattleBridge { 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 _gemStore = new AiGemSocketStore(); + private readonly _gemAssigner = new AiGemSocketAssigner(); private readonly _skillHost = new AiSkillHost(); private readonly _mapText = new AiMapTextHost(); private readonly _passiveCd = new AiPassiveCdHost(); @@ -307,7 +307,8 @@ export class AiBattleBridge { console.error('[AiBattleBridge] start aborted: headless bag bind failed'); return false; } - this._weaponRegistry.clear(); + this._gemAssigner.fill(this._gemStore, resolved.opponentRankId, resolved.seed); + setAiGemSlotsResolver(this._resolveAiGemSlots); this._battle = battle; this._map = map; this._profile = resolved; @@ -330,7 +331,6 @@ export class AiBattleBridge { 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] 就绪', @@ -338,6 +338,7 @@ export class AiBattleBridge { 'gold=' + this._bagHost.aiGold, 'nextCost=' + this._bagHost.nextRefreshCost, 'text=' + personality.textPolicy, + 'gemFilled=' + this._gemStore.countFilled(), ); return true; } @@ -391,16 +392,16 @@ export class AiBattleBridge { }; /** - * 供 TowerLoadout 注册的 AI 武器解析(避免循环 require)。 - * @param heroId Hero.id + * 供 GemBattleEffect 读 AI 五行槽。 + * @param element 五行 1~5 */ - private readonly _resolveAiWeaponForLoadout = (heroId: number): number | null => { - return this.getHeroBattleWeaponId(heroId); + private readonly _resolveAiGemSlots = (element: number): readonly number[] | null => { + return this._gemStore.getElementSlots(element); }; /** 战斗结束或切场景时停止 AI */ stop(): void { - setAiHeroWeaponResolver(null); + setAiGemSlotsResolver(null); SkillAtkBuffBoardViewBind.bindEnemyNotify(null); this._controller.stop(); this._passiveCd.dispose(); @@ -412,10 +413,11 @@ export class AiBattleBridge { this._mapText.clear(); this._skillHost.dispose(); this._bagHost.dispose(); - this._weaponRegistry.clear(); + this._gemStore.clear(); BattleBuffCarryStore.clear(); this._battle = null; this._map = null; + this._profile = null; } /** @@ -451,21 +453,11 @@ export class AiBattleBridge { } /** - * AI 战斗武器 id(旧 aiweapon 分配已下线,非 null 时亦可能为 0)。 - * @param heroId Hero.id + * 本局 AI 某五行宝石槽(只读副本)。 + * @param element 五行 1~5 */ - 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; + getAiElementGemSlots(element: number): number[] { + return this._gemStore.getElementSlots(element); } /** diff --git a/assets/script/battle/ai/index.ts b/assets/script/battle/ai/index.ts index 4857367..7556809 100644 --- a/assets/script/battle/ai/index.ts +++ b/assets/script/battle/ai/index.ts @@ -13,8 +13,8 @@ export { } from './runtime/AiBattlePersonality'; export { AiHeadlessBagHost } from './runtime/AiHeadlessBagHost'; export { AiOpponentController } from './runtime/AiOpponentController'; -export { AiHeroWeaponRegistry } from './runtime/AiHeroWeaponRegistry'; -export { AiHeroWeaponAssigner } from './runtime/AiHeroWeaponAssigner'; +export { AiGemSocketStore } from './runtime/AiGemSocketStore'; +export { AiGemSocketAssigner, AI_GEM_WEIGHT_COUNT } from './runtime/AiGemSocketAssigner'; export { AiSkillHost } from './runtime/AiSkillHost'; export { AiMapTextHost } from './runtime/AiMapTextHost'; export { AiPassiveCdHost } from './runtime/AiPassiveCdHost'; @@ -46,6 +46,3 @@ export { type AiPlannedAction, type AiMatchBuildInput, } from './types/AiTypes'; - -export type { AiHeroWeaponRecord } from './types/AiHeroWeaponTypes'; -export { AI_WEAPON_QUALITY_COUNT } from './types/AiHeroWeaponTypes'; diff --git a/assets/script/battle/ai/runtime/AiGemSocketAssigner.ts b/assets/script/battle/ai/runtime/AiGemSocketAssigner.ts new file mode 100644 index 0000000..62c23d3 --- /dev/null +++ b/assets/script/battle/ai/runtime/AiGemSocketAssigner.ts @@ -0,0 +1,163 @@ +import { TableNames } from '../../../game/ConfigTableData'; +import { GEM_SLOT_COUNT } from '../../../game/GameDataEnums'; +import { RankDataManager } from '../../../manager/RankDataManager'; +import { aiLog, aiOp, aiWarn } from './AiLog'; +import type { AiGemSocketStore } from './AiGemSocketStore'; + +/** Rank.aiweapon 六档:无 / 绿 / 蓝 / 紫 / 橙 / 红 */ +export const AI_GEM_WEIGHT_COUNT = 6; +/** 五行数量 */ +const ELEMENT_COUNT = 5; + +/** + * 开战按对手段位 `Rank.aiweapon` 一次性镶嵌 AI 宝石。 + * 20 槽各自独立掷骰:是否镶嵌 + 镶嵌何物;遵守「每五行最多 1 颗专属」。 + */ +export class AiGemSocketAssigner { + /** + * 灌满 store。 + * @param store 局内存档 + * @param opponentRankId 对手段位 + * @param seed 匹配种子 + */ + fill(store: AiGemSocketStore, opponentRankId: number, seed: number): void { + store.clear(); + const rank = RankDataManager.getRankConfig(opponentRankId); + if (!rank) { + aiWarn(`[AiGem] Rank 缺失 rankId=${opponentRankId},本局 AI 不镶嵌`); + return; + } + const weights = this._parseWeights(rank.aiweapon); + if (!weights) { + aiWarn(`[AiGem] aiweapon 无效 rankId=${opponentRankId} raw=${rank.aiweapon}`); + return; + } + const baseSeed = (Math.floor(Number(seed) || 0) >>> 0) || 1; + let filled = 0; + for (let element = 1; element <= ELEMENT_COUNT; element++) { + let hasExclusive = false; + for (let slot = 0; slot < GEM_SLOT_COUNT; slot++) { + const salt = element * 100 + slot; + const band = this._rollBand(weights, baseSeed, salt); + if (band == null) { + aiWarn(`[AiGem] 掷骰失败 element=${element} slot=${slot}`); + continue; + } + if (band === 0) { + store.setSlot(element, slot, 0); + continue; + } + const quality = band; + const gemId = this._pickGemId(element, quality, hasExclusive, baseSeed, salt ^ 0x9e3779b9); + if (gemId <= 0) { + store.setSlot(element, slot, 0); + continue; + } + const row = gg.data.table.getTableData(TableNames.Weapon, gemId); + const gemType = Math.floor(Number(row?.type) || 0); + if (gemType >= 2 && gemType <= 6) hasExclusive = true; + store.setSlot(element, slot, gemId); + filled += 1; + } + } + aiOp( + `[AiGem] 开战镶嵌完成 rank=${opponentRankId} filled=${filled}/20 weights=${rank.aiweapon}`, + ); + } + + /** + * 解析六档权重。 + * @param raw Rank.aiweapon + */ + private _parseWeights(raw: string): number[] | null { + const parts = String(raw ?? '').split(','); + if (parts.length !== AI_GEM_WEIGHT_COUNT) { + aiLog('[AiGem] aiweapon 长度须为 6', raw); + return null; + } + const out: number[] = []; + let total = 0; + for (let i = 0; i < AI_GEM_WEIGHT_COUNT; i++) { + const v = Math.max(0, Math.floor(Number(parts[i]) || 0)); + out.push(v); + total += v; + } + if (total <= 0) { + aiLog('[AiGem] aiweapon 权重和为 0', raw); + return null; + } + return out; + } + + /** + * 加权抽档:0=空,1~5=品质绿~红。 + * @param weights 六档 + * @param seed 局种子 + * @param salt 槽盐 + */ + private _rollBand(weights: number[], seed: number, salt: number): number | null { + let total = 0; + for (let i = 0; i < weights.length; i++) total += weights[i]; + if (total <= 0) return null; + let roll = this._unit(seed, salt) * total; + for (let i = 0; i < weights.length; i++) { + roll -= weights[i]; + if (roll < 0) return i; + } + return AI_GEM_WEIGHT_COUNT - 1; + } + + /** + * 在合法候选中抽一颗(通用 + 未占用专属)。 + * @param element 五行 + * @param quality Weapon.quality 1~5 + * @param hasExclusive 本五行是否已有专属 + * @param seed 局种子 + * @param salt 抽选盐 + */ + private _pickGemId(element: number, quality: number, hasExclusive: boolean, seed: number, salt: number): number { + const q = Math.floor(Number(quality) || 0); + if (q < 1 || q > 5) { + aiWarn(`[AiGem] 品质越界 q=${q} element=${element}`); + return 0; + } + const needExclusive = element + 1; + const list = gg.data.table.getTableList(TableNames.Weapon) ?? []; + const candidates: number[] = []; + for (let i = 0; i < list.length; i++) { + const row = list[i]; + const id = Math.floor(Number(row?.id) || 0); + if (id <= 0) continue; + if (Math.floor(Number(row.quality) || 0) !== q) continue; + const t = Math.floor(Number(row.type) || 0); + if (t === 1) { + candidates.push(id); + continue; + } + if (t === needExclusive && !hasExclusive) { + candidates.push(id); + } + } + if (candidates.length <= 0) { + aiWarn(`[AiGem] 无候选宝石 element=${element} quality=${q} exclusiveUsed=${hasExclusive}`); + return 0; + } + candidates.sort((a, b) => a - b); + const idx = Math.floor(this._unit(seed, salt) * candidates.length); + const pick = candidates[Math.min(candidates.length - 1, Math.max(0, idx))]; + return pick; + } + + /** + * 种子均匀 [0,1)。 + * @param seed 局种子 + * @param salt 扰动 + */ + private _unit(seed: number, salt: number): number { + let x = ((Math.floor(Number(seed) || 0) ^ Math.floor(Number(salt) || 0)) >>> 0) || 1; + x ^= x << 13; + x ^= x >>> 17; + x ^= x << 5; + return (x >>> 0) / 4294967296; + } +} diff --git a/assets/script/battle/ai/runtime/AiGemSocketAssigner.ts.meta b/assets/script/battle/ai/runtime/AiGemSocketAssigner.ts.meta new file mode 100644 index 0000000..df76f5c --- /dev/null +++ b/assets/script/battle/ai/runtime/AiGemSocketAssigner.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "9d5c95c8-e3be-4a32-8d68-07af92bb5a26", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/battle/ai/runtime/AiGemSocketStore.ts b/assets/script/battle/ai/runtime/AiGemSocketStore.ts new file mode 100644 index 0000000..0cf99f7 --- /dev/null +++ b/assets/script/battle/ai/runtime/AiGemSocketStore.ts @@ -0,0 +1,66 @@ +import { GEM_SLOT_COUNT } from '../../../game/GameDataEnums'; + +/** 五行数量 */ +const ELEMENT_COUNT = 5; + +/** + * 本局 AI 宝石镶嵌存档:五行 × 4 槽。 + * 开战灌入,局内只读,结束清空。 + */ +export class AiGemSocketStore { + /** element(1~5) → 长度 4 的宝石 id */ + private readonly _slots: number[][] = []; + + constructor() { + this.clear(); + } + + /** 清空全部槽 */ + clear(): void { + this._slots.length = 0; + for (let e = 0; e <= ELEMENT_COUNT; e++) { + this._slots[e] = [0, 0, 0, 0]; + } + } + + /** + * 读某五行 4 槽(副本)。 + * @param element Hero.herotype 1~5 + */ + getElementSlots(element: number): number[] { + const e = Math.floor(Number(element) || 0); + const out = [0, 0, 0, 0]; + if (e < 1 || e > ELEMENT_COUNT) return out; + const src = this._slots[e]; + for (let i = 0; i < GEM_SLOT_COUNT; i++) { + out[i] = Math.max(0, Math.floor(Number(src[i]) || 0)); + } + return out; + } + + /** + * 写入单槽。 + * @param element 五行 1~5 + * @param slot 槽 0~3 + * @param gemId Weapon.id;0=空 + */ + setSlot(element: number, slot: number, gemId: number): boolean { + const e = Math.floor(Number(element) || 0); + const s = Math.floor(Number(slot) || 0); + if (e < 1 || e > ELEMENT_COUNT || s < 0 || s >= GEM_SLOT_COUNT) return false; + this._slots[e][s] = Math.max(0, Math.floor(Number(gemId) || 0)); + return true; + } + + /** 已镶嵌槽数(gemId>0) */ + countFilled(): number { + let n = 0; + for (let e = 1; e <= ELEMENT_COUNT; e++) { + const row = this._slots[e]; + for (let s = 0; s < GEM_SLOT_COUNT; s++) { + if (row[s] > 0) n++; + } + } + return n; + } +} diff --git a/assets/script/battle/ai/runtime/AiGemSocketStore.ts.meta b/assets/script/battle/ai/runtime/AiGemSocketStore.ts.meta new file mode 100644 index 0000000..9cc33e7 --- /dev/null +++ b/assets/script/battle/ai/runtime/AiGemSocketStore.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "987914d6-2b9b-4799-a189-de1f6765bc51", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/battle/ai/types/AiHeroWeaponTypes.ts b/assets/script/battle/ai/types/AiHeroWeaponTypes.ts index b0331c0..b442e3a 100644 --- a/assets/script/battle/ai/types/AiHeroWeaponTypes.ts +++ b/assets/script/battle/ai/types/AiHeroWeaponTypes.ts @@ -8,5 +8,7 @@ export interface AiHeroWeaponRecord { quality: number; } -/** 武器品质档位数(Rank.aiweapon 五档:白绿蓝紫橙,quality 0~4) */ -export const AI_WEAPON_QUALITY_COUNT = 5; +/** + * @deprecated 已改为宝石六档,见 `AI_GEM_WEIGHT_COUNT`(无/绿/蓝/紫/橙/红)。 + */ +export const AI_WEAPON_QUALITY_COUNT = 6; diff --git a/assets/script/battle/combat/GemBattleEffect.ts b/assets/script/battle/combat/GemBattleEffect.ts index c54b8a8..0f1ce90 100644 --- a/assets/script/battle/combat/GemBattleEffect.ts +++ b/assets/script/battle/combat/GemBattleEffect.ts @@ -9,6 +9,17 @@ const LOG = '[GemBattle]'; /** modifier.source 前缀,刷新时整批清除 */ const SOURCE_PREFIX = 'gem_'; +/** AI 五行槽读取(AiBattleBridge 开战注册) */ +let _aiGemSlotsResolver: ((element: number) => readonly number[] | null) | null = null; + +/** + * 注册/清除 AI 宝石槽解析。 + * @param resolver element→4 槽;null 清除 + */ +export function setAiGemSlotsResolver(resolver: ((element: number) => readonly number[] | null) | null): void { + _aiGemSlotsResolver = resolver; +} + /** 五行镶嵌解析结果(局内用) */ export interface IGemBattleResolve { /** 提供技能的宝石 id(0=无) */ @@ -33,7 +44,7 @@ export interface IGemBattleAttr { /** * 局内宝石生效:读镶嵌 → 属性 modifier → 技能宝石 id。 - * 仅本机玩家;AI 返回空。 + * 本机读 `gg.gem`;AI 读开战灌入的槽。 */ export class GemBattleEffect { /** @@ -43,16 +54,22 @@ export class GemBattleEffect { */ static resolve(element: number, player: Player | null | undefined): IGemBattleResolve { const empty: IGemBattleResolve = { skillGemId: 0, skillId: 0, attrs: [] }; - if (!player || !player.isLocal) return empty; + if (!player) return empty; const e = Math.floor(Number(element) || 0); if (e < 1 || e > 5) return empty; let slots: number[]; - try { - slots = gg.gem.getElementSlots(e); - } catch (err) { - console.warn(LOG, 'getElementSlots 失败', e, err); - return empty; + if (player.isLocal) { + try { + slots = gg.gem.getElementSlots(e); + } catch (err) { + console.warn(LOG, 'getElementSlots 失败', e, err); + return empty; + } + } else { + const aiSlots = _aiGemSlotsResolver?.(e) ?? null; + if (!aiSlots || aiSlots.length < 4) return empty; + slots = [aiSlots[0] | 0, aiSlots[1] | 0, aiSlots[2] | 0, aiSlots[3] | 0]; } const attrs: IGemBattleAttr[] = []; diff --git a/assets/script/battle/combat/TowerLoadoutService.ts b/assets/script/battle/combat/TowerLoadoutService.ts index 081be79..8a57f3d 100644 --- a/assets/script/battle/combat/TowerLoadoutService.ts +++ b/assets/script/battle/combat/TowerLoadoutService.ts @@ -45,10 +45,7 @@ export function setAiHeroWeaponResolver(resolver: ((heroId: number) => number | export function resolveHeroBattleWeaponId(heroId: number, player?: Player | null): number { const hero = gg.data.table.getTableData(TableNames.Hero, heroId); if (isElementHero(hero)) { - if (player?.isLocal) { - return GemBattleEffect.resolve(Math.floor(Number(hero!.herotype)), player).skillGemId; - } - return 0; + return GemBattleEffect.resolve(Math.floor(Number(hero!.herotype)), player).skillGemId; } if (player && !player.isLocal) { if (_aiHeroWeaponResolver) { diff --git a/assets/script/interface/AutoTable.ts b/assets/script/interface/AutoTable.ts index 93c4c8d..89ee8f8 100644 --- a/assets/script/interface/AutoTable.ts +++ b/assets/script/interface/AutoTable.ts @@ -33,7 +33,7 @@ declare global { /** 装备掉落概率加值,万分比 */ readonly weapondrop: number; /** ai武将装备品质 -白,绿,蓝,紫,橙 */ +无,绿,蓝,紫,橙,红 */ readonly aiweapon: string; /** ai广告使用几率 */ readonly aiusead: number;