Release Healer Man 0.1.46
This commit is contained in:
1 parent
f82daf1c13
commit
9ff574ac0b
73 files changed
+33486
-174
No files matched your search
@@ -0,0 +1,151 @@
|
||||
import { creatureRuntimeBaseStats } from "./azerothCoreReference";
|
||||
import { scaledDungeonEnemyLevel } from "./dungeonLevelScaling";
|
||||
import type { DungeonDefinition } from "./dungeonTypes";
|
||||
import type {
|
||||
EnemyAbilityEffect,
|
||||
EnemyAttackDefinition,
|
||||
EnemyAtomicAbilityEffect,
|
||||
} from "./enemyAttacks";
|
||||
import type { PopulationEntityDefinition } from "./mobPopulation";
|
||||
|
||||
export const BOSS_PRESSURE_WINDOW_MS = 60_000;
|
||||
export const BOSS_PRESSURE_PARTY_SIZE = 5;
|
||||
|
||||
export interface BossAttackPressureEstimate {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly spellId?: number;
|
||||
readonly target: EnemyAttackDefinition["target"];
|
||||
readonly school: EnemyAttackDefinition["school"];
|
||||
readonly expectedCasts: number;
|
||||
readonly targetsPerCast: number;
|
||||
readonly damagePerTarget: number;
|
||||
readonly partyDamage: number;
|
||||
}
|
||||
|
||||
export interface BossPartyPressureEstimate {
|
||||
readonly key: string;
|
||||
readonly dungeonId: string;
|
||||
readonly dungeonTitle: string;
|
||||
readonly boss: string;
|
||||
readonly serverEntry: number | null;
|
||||
readonly playerLevel: number;
|
||||
readonly enemyLevel: number;
|
||||
readonly windowSeconds: number;
|
||||
readonly attackDamage: number;
|
||||
readonly partyDamage: number;
|
||||
readonly partyDamagePerSecond: number;
|
||||
readonly tankDamage: number;
|
||||
readonly randomTargetDamage: number;
|
||||
readonly groupwideDamage: number;
|
||||
readonly attacks: readonly BossAttackPressureEstimate[];
|
||||
}
|
||||
|
||||
function midpoint(range: readonly [number, number] | undefined, fallback: number): number {
|
||||
if (!range) return fallback;
|
||||
return (Math.max(0, range[0]) + Math.max(0, range[1])) / 2;
|
||||
}
|
||||
|
||||
function expectedCasts(attack: EnemyAttackDefinition, windowMs: number): number {
|
||||
const initial = midpoint(attack.initialCooldownMs, 450);
|
||||
if (windowMs < initial) return 0;
|
||||
const repeat = Math.max(1, midpoint(attack.repeatCooldownMs, attack.cooldownMs));
|
||||
const casts = 1 + (windowMs - initial) / repeat;
|
||||
return Math.min(attack.maximumCasts ?? Number.POSITIVE_INFINITY, casts);
|
||||
}
|
||||
|
||||
function atomicDamageMultiplier(effect: EnemyAtomicAbilityEffect): number {
|
||||
if (effect.kind === "damage") return Math.max(0, effect.multiplier);
|
||||
if (effect.kind === "leech") return Math.max(0, effect.damageMultiplier);
|
||||
if (effect.kind === "control") return Math.max(0, effect.damageMultiplier ?? 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function damageMultiplier(attack: EnemyAttackDefinition, effect: EnemyAbilityEffect | undefined): number {
|
||||
if (!effect) return Math.max(0, attack.damageMultiplier);
|
||||
if (effect.kind === "composite") {
|
||||
return effect.effects.reduce((total, entry) => total + atomicDamageMultiplier(entry), 0);
|
||||
}
|
||||
return atomicDamageMultiplier(effect);
|
||||
}
|
||||
|
||||
function targetsPerCast(attack: EnemyAttackDefinition, partySize: number): number {
|
||||
if (attack.target === "nearby-party") return partySize;
|
||||
if (attack.target === "primary" || attack.target === "random-party") return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparable direct-damage ceiling for one boss with a grouped party. It uses
|
||||
* live attack definitions and source creature stats, but deliberately excludes
|
||||
* armor/resistance, avoidable movement, summons, healing, and mutually exclusive
|
||||
* phase uptime. Those omissions keep the audit deterministic and make inflated
|
||||
* schedules or accidental party-wide targeting obvious.
|
||||
*/
|
||||
export function estimateBossPartyPressure(
|
||||
dungeon: DungeonDefinition,
|
||||
entityId: string,
|
||||
playerLevel = 80,
|
||||
windowMs = BOSS_PRESSURE_WINDOW_MS,
|
||||
partySize = BOSS_PRESSURE_PARTY_SIZE,
|
||||
): BossPartyPressureEstimate {
|
||||
const entity: PopulationEntityDefinition | undefined = dungeon.entities[entityId];
|
||||
if (!entity || entity.kind !== "boss") throw new Error(`${dungeon.id}:${entityId} is not a boss`);
|
||||
const enemyLevel = scaledDungeonEnemyLevel(playerLevel, true);
|
||||
const serverEntry = entity.combat?.serverEntry ?? null;
|
||||
const baseDamage = serverEntry
|
||||
? creatureRuntimeBaseStats(serverEntry, enemyLevel)?.averageAttackDamage
|
||||
: undefined;
|
||||
const attackDamage = (baseDamage ?? (12 + enemyLevel * 2)) * (entity.combat?.damageMultiplier ?? 1);
|
||||
const attacks = (entity.combat?.attacks ?? []).map((attack): BossAttackPressureEstimate => {
|
||||
const casts = expectedCasts(attack, windowMs);
|
||||
const targets = targetsPerCast(attack, partySize);
|
||||
const perTarget = attackDamage * damageMultiplier(attack, attack.effect);
|
||||
return {
|
||||
id: attack.id,
|
||||
name: attack.name,
|
||||
...(attack.spellId === undefined ? {} : { spellId: attack.spellId }),
|
||||
target: attack.target,
|
||||
school: attack.school,
|
||||
expectedCasts: casts,
|
||||
targetsPerCast: targets,
|
||||
damagePerTarget: perTarget,
|
||||
partyDamage: casts * targets * perTarget,
|
||||
};
|
||||
});
|
||||
const sum = (target: EnemyAttackDefinition["target"]): number => attacks
|
||||
.filter((attack) => attack.target === target)
|
||||
.reduce((total, attack) => total + attack.partyDamage, 0);
|
||||
const partyDamage = attacks.reduce((total, attack) => total + attack.partyDamage, 0);
|
||||
return {
|
||||
key: `${dungeon.id}/${entityId}`,
|
||||
dungeonId: dungeon.id,
|
||||
dungeonTitle: dungeon.title,
|
||||
boss: entity.name,
|
||||
serverEntry,
|
||||
playerLevel,
|
||||
enemyLevel,
|
||||
windowSeconds: windowMs / 1_000,
|
||||
attackDamage,
|
||||
partyDamage,
|
||||
partyDamagePerSecond: partyDamage / Math.max(1, windowMs / 1_000),
|
||||
tankDamage: sum("primary"),
|
||||
randomTargetDamage: sum("random-party"),
|
||||
groupwideDamage: sum("nearby-party"),
|
||||
attacks,
|
||||
};
|
||||
}
|
||||
|
||||
export function rankBossPartyPressure(
|
||||
definitions: readonly DungeonDefinition[],
|
||||
playerLevel = 80,
|
||||
windowMs = BOSS_PRESSURE_WINDOW_MS,
|
||||
partySize = BOSS_PRESSURE_PARTY_SIZE,
|
||||
): readonly BossPartyPressureEstimate[] {
|
||||
return definitions.flatMap((dungeon) => Object.entries(dungeon.entities)
|
||||
.filter(([, entity]) => entity.kind === "boss")
|
||||
.map(([entityId]) => estimateBossPartyPressure(dungeon, entityId, playerLevel, windowMs, partySize)))
|
||||
.sort((left, right) => right.partyDamage - left.partyDamage
|
||||
|| left.boss.localeCompare(right.boss)
|
||||
|| left.dungeonId.localeCompare(right.dungeonId));
|
||||
}
|
||||
Reference in new issue
Block a user