81 lines
4.1 KiB
TypeScript
81 lines
4.1 KiB
TypeScript
import { creatureRuntimeBaseStats } from "./azerothCoreReference";
|
|
import { bossDifficultyMultipliers } from "./dungeonDifficulty";
|
|
import { dungeonEnemyHealthBaseline, dungeonPartyHealthScale, scaledDungeonEnemyLevel } from "./dungeonLevelScaling";
|
|
import { finalDungeonBoss } from "./dungeonRunStore";
|
|
import type { DungeonDefinition } from "./dungeonTypes";
|
|
import type { PopulationEntityDefinition } from "./mobPopulation";
|
|
import type { ContentCategoryId } from "../app/contentCategories";
|
|
|
|
const WOW_BOSS_HEALTH_FLOOR: readonly (readonly [level: number, health: number])[] = [
|
|
// The starter anchors include enough room for authentic presence, imbue,
|
|
// disease, and shock rotations to retain the 25/45-second mechanic window.
|
|
[2, 950],
|
|
[4, 1_365],
|
|
[9, 7_000],
|
|
[21, 11_000],
|
|
[41, 23_000],
|
|
[80, 180_000],
|
|
];
|
|
const WOW_BOSS_TEST_HEALTH_FLOOR: readonly (readonly [level: number, health: number])[] = [
|
|
[2, 900],
|
|
[4, 1_300],
|
|
[9, 7_000],
|
|
[21, 11_000],
|
|
[41, 23_000],
|
|
[80, 180_000],
|
|
];
|
|
const BOSS_TEST_HEALTH_FLOOR_MULTIPLIER = 0.6;
|
|
|
|
function interpolatedFloor(level: number, anchors: readonly (readonly [number, number])[]): number {
|
|
const upperIndex = anchors.findIndex(([anchorLevel]) => level <= anchorLevel);
|
|
if (upperIndex <= 0) return anchors[0][1];
|
|
if (upperIndex < 0) return anchors[anchors.length - 1][1];
|
|
const [lowerLevel, lowerHealth] = anchors[upperIndex - 1];
|
|
const [upperLevel, upperHealth] = anchors[upperIndex];
|
|
const progress = (level - lowerLevel) / (upperLevel - lowerLevel);
|
|
return lowerHealth + (upperHealth - lowerHealth) * progress;
|
|
}
|
|
|
|
function partyBossHealthFloor(level: number, characterSource: ContentCategoryId, testing: boolean): number {
|
|
if (characterSource === "wow") return interpolatedFloor(
|
|
level,
|
|
testing ? WOW_BOSS_TEST_HEALTH_FLOOR : WOW_BOSS_HEALTH_FLOOR,
|
|
);
|
|
const baseline = 1_000 + level * 1_200 + level * level * 22;
|
|
// Each catalog is calibrated independently against its strongest canonical
|
|
// class-role cohort so every fight retains the 25/45-second mechanic window.
|
|
return baseline * 1.15 * dungeonPartyHealthScale(characterSource);
|
|
}
|
|
|
|
/** Campaign fights retain their full durability floor; testing rooms use a reduced floor. */
|
|
export function dungeonEnemyScaling(dungeon: DungeonDefinition, entity: PopulationEntityDefinition,
|
|
instanceId: string, characterLevel: number, difficultyId: string, bossDifficulty: number,
|
|
characterSource: ContentCategoryId = "wow") {
|
|
const boss = entity.kind === "boss";
|
|
const testing = dungeon.id.startsWith("boss-testing:");
|
|
const level = scaledDungeonEnemyLevel(characterLevel, boss);
|
|
const authoredHealth = entity.combat?.healthMultiplier ?? 1;
|
|
const authoredDamage = entity.combat?.damageMultiplier ?? 1;
|
|
const baseline = dungeonEnemyHealthBaseline(level, characterSource);
|
|
// Isolated testing rooms retain a packaged source boss's native health so
|
|
// developers can inspect it without changing campaign cross-catalog balance.
|
|
const native = testing && entity.combat?.serverEntry
|
|
? creatureRuntimeBaseStats(entity.combat.serverEntry, level)
|
|
: null;
|
|
const originalHealth = (native?.health ?? baseline) * authoredHealth;
|
|
const multipliers = boss ? bossDifficultyMultipliers(bossDifficulty) : { health: 1, damage: 1 };
|
|
const finalBoss = boss && !testing && finalDungeonBoss(dungeon, difficultyId)?.id === instanceId;
|
|
// Match durability to the character's game, including when crossing dungeon catalogs.
|
|
const partyHealthFloor = partyBossHealthFloor(level, characterSource, testing);
|
|
const bossFloor = partyHealthFloor * (finalBoss ? 1.8 : 1);
|
|
// A test room's sole objective must not promote every boss to a campaign
|
|
// finale. A reduced floor still prevents high-level source creatures with
|
|
// tiny extrapolated level-two health from dying before mechanics can run.
|
|
const testingBossFloor = partyHealthFloor * BOSS_TEST_HEALTH_FLOOR_MULTIPLIER;
|
|
const scaledHealth = boss
|
|
? Math.max(originalHealth, testing ? testingBossFloor : bossFloor)
|
|
: originalHealth;
|
|
return { level, maxHealth: scaledHealth * multipliers.health,
|
|
healthMultiplier: authoredHealth * multipliers.health, damageMultiplier: authoredDamage * multipliers.damage };
|
|
}
|