import { preloadAbilityCatalogsForClasses } from "../abilityCatalog"; import { PLAYER_AGGRO_ID } from "../aggro"; import { installCombatClock } from "../combatClock"; import { subscribeCombatEvents } from "../combatEvents"; import { resetCombatSpatialProvider } from "../combatSpatial"; import { useCombatStore } from "../combatStore"; import { dungeonEnemyScaling } from "../dungeonBossScaling"; import { requireDungeonDefinition } from "../dungeonRegistry"; import { clearMobRuntimeRegistry, setMobPosition } from "../mobRuntimeRegistry"; import { advanceDungeonPartyCombat, partyCombatRangeBand, resetPartyRuntimeClock } from "../partyRuntime"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "../partyRuntimeRegistry"; import { usePartyStore } from "../partyStore"; import { specializationsForClass } from "../specializationCatalog"; import { BenchmarkController } from "./benchmarkController"; import type { BenchmarkBuildDefinition, BenchmarkScenarioDefinition } from "./benchmarkTypes"; const scenario: BenchmarkScenarioDefinition = { id: "dungeon-boss", version: 1, name: "Dungeon boss", description: "Full party against an authored dungeon boss", family: "damage", roles: ["damage", "tank", "healer"], durationMs: 60_000, tickMs: 100, hostileTargets: 1, partyLayout: "melee", attacks: [], }; /** Uses the live boss stats, companion AI and baseline player policy with a controlled clock. */ export async function runDungeonBossBenchmark( build: BenchmarkBuildDefinition, bossId: string, seed: string, mechanics = false, ) { await preloadAbilityCatalogsForClasses([build.classId]); const dungeon = requireDungeonDefinition("ragefire-chasm"); const spawn = dungeon.staticSpawns.find((entry) => entry.id === bossId)!; const entity = dungeon.entities[spawn.entityId]; const scaling = dungeonEnemyScaling(dungeon, entity, bossId, build.level, "normal", 1, build.source); const specs = specializationsForClass(build.classId).filter((spec) => spec.role === build.role); const specialization = specs.find((spec) => spec.displayName === build.orientation) ?? specs[0]; clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); resetCombatSpatialProvider(); let now = 1_000_000; const restoreClock = installCombatClock(() => now); let playerDamage = 0; let partyDamage = 0; let incomingDamage = 0; let healing = 0; let casts = 0; let selfHealing = 0; let allyHealing = 0; const healingByAbility: Record = {}; const damageByAbility: Record = {}; const stop = subscribeCombatEvents((event) => { if (event.kind === "damage" && event.targetActorId === bossId) { if (event.sourceActorId === PLAYER_AGGRO_ID) playerDamage += event.effectiveAmount; else partyDamage += event.effectiveAmount; const key = `${event.sourceActorId}:${event.abilityId ?? "unknown"}`; damageByAbility[key] = (damageByAbility[key] ?? 0) + event.effectiveAmount; } if (event.kind === "damage" && event.sourceActorId === bossId) incomingDamage += event.effectiveAmount; if (event.kind === "healing") { healing += event.effectiveAmount; if (event.targetActorId === PLAYER_AGGRO_ID) selfHealing += event.effectiveAmount; else allyHealing += event.effectiveAmount; const key = `${event.sourceActorId}:${event.abilityId ?? "unknown"}:${event.targetActorId}`; healingByAbility[key] = (healingByAbility[key] ?? 0) + event.effectiveAmount; } }); try { const combat = useCombatStore.getState(); combat.initializeCharacter({ classId: build.classId, secondaryClassId: build.secondaryClassId, specializationId: specialization?.id, role: build.role, level: build.level, talentRanks: build.talentRanks, inventory: build.inventory, equipment: build.equipment }); combat.setPlayerPosition([-2, 0, 0]); usePartyStore.getState().initializeParty(seed, build.level, useCombatStore.getState().maxHealth, build.role, 5, build.source); combat.synchronizeActors(); combat.applyDefaultPartyBuffs(now); combat.applyPrecombatPreparation(now); combat.registerMob(bossId, { name: entity.name, boss: true, serverEntry: entity.combat?.serverEntry, ...scaling, attacks: entity.combat?.attacks, xpReward: 0 }); setMobPosition(bossId, [0, 0, 0]); for (const member of usePartyStore.getState().members) { registerPartyRuntimePosition(member.id, [-partyCombatRangeBand(member).preferred, 0, 0]); updatePartyRuntimeTargetVisibility(member.id, bossId, true); } combat.selectMob(bossId); const tank = usePartyStore.getState().members.find((member) => member.role === "tank"); combat.damageMob(bossId, 1, now, { actorId: build.role === "tank" ? PLAYER_AGGRO_ID : tank!.id, role: "tank", abilityId: "benchmark-pull", }, "physical"); const controller = new BenchmarkController(); for (let elapsed = 100; elapsed <= scenario.durationMs; elapsed += 100) { now = 1_000_000 + elapsed; combat.tick(.1, now); advanceDungeonPartyCombat(now); if (mechanics) combat.advanceMobCombat(now); if (controller.act(now, build.role, scenario, false).cast) casts += 1; const state = useCombatStore.getState(); const mob = state.mobs[bossId]; if (mob.dead || (mechanics && state.health <= 0)) { return { seconds: mob.dead ? ((mob.diedAt ?? now) - 1_000_000) / 1000 : null, elapsedSeconds: elapsed / 1000, outcome: mob.dead ? "defeated" : "player-death", maxHealth: scaling.maxHealth, playerDamage, partyDamage, incomingDamage, healing, selfHealing, allyHealing, healingByAbility, casts, damageByAbility, playerAlive: state.health > 0, deaths: usePartyStore.getState().members.filter((member) => member.health <= 0).length, attacks: mob.attackRevision, specialization: specialization?.displayName, party: usePartyStore.getState().members.map((member) => `${member.classId}:${member.specialization}`) }; } } return { seconds: null, elapsedSeconds: scenario.durationMs / 1000, outcome: "timeout", maxHealth: scaling.maxHealth, remainingHealth: useCombatStore.getState().mobs[bossId].health, playerDamage, partyDamage, incomingDamage, healing, selfHealing, allyHealing, healingByAbility, casts, damageByAbility, playerAlive: useCombatStore.getState().health > 0, deaths: usePartyStore.getState().members.filter((member) => member.health <= 0).length, attacks: useCombatStore.getState().mobs[bossId].attackRevision, specialization: specialization?.displayName, party: usePartyStore.getState().members.map((member) => `${member.classId}:${member.specialization}`) }; } finally { stop(); restoreClock(); clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); resetCombatSpatialProvider(); } }