Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -0,0 +1,196 @@
|
||||
import sourceCatalog from "./generated/enemyDamageTypeSources.generated.json";
|
||||
|
||||
export type EnemyDamageOrigin = "wow335" | "runewaker";
|
||||
export type EnemyAttackKind = "melee" | "ranged" | "spell";
|
||||
export type EnemyDamageSchool =
|
||||
| "physical"
|
||||
| "arcane"
|
||||
| "fire"
|
||||
| "frost"
|
||||
| "nature"
|
||||
| "shadow"
|
||||
| "holy";
|
||||
|
||||
export interface WowEnemyDamageTypeSource {
|
||||
readonly spellId: number;
|
||||
readonly name: string;
|
||||
readonly damageClass: number;
|
||||
readonly schoolMask: number;
|
||||
readonly schools: readonly EnemyDamageSchool[];
|
||||
readonly school?: EnemyDamageSchool;
|
||||
readonly attackKind?: EnemyAttackKind;
|
||||
readonly damageEffects: readonly number[];
|
||||
readonly source: string;
|
||||
}
|
||||
|
||||
export interface RunewakerEnemyDamageTypeComponent {
|
||||
readonly componentId: number;
|
||||
/** MagicObject.Attack.CalType; direct packet class, independent of EffectType. */
|
||||
readonly calculationType: number;
|
||||
/** Raw MagicObject.EffectType, retained as source metadata. */
|
||||
readonly effectType: number;
|
||||
readonly magicType: number;
|
||||
/** Formula-specific handling; not a packet class and not the MagicCollect hit gate. */
|
||||
readonly directFormula?: "throw" | "fixed" | "target-hp-percent";
|
||||
/** Absorption branch selected by the direct formula and raw component flags. */
|
||||
readonly formulaMitigation?: "weapon-absorb" | "magic-absorb" | "bypassed";
|
||||
readonly sourceDamageClass?: "physical" | "magical";
|
||||
readonly attackKind?: EnemyAttackKind;
|
||||
readonly sourceChannel?: "physical" | "earth" | "water" | "fire" | "wind" | "light" | "darkness";
|
||||
/** Approximate mapping into HealerMan's seven-school resistance model. */
|
||||
readonly school?: EnemyDamageSchool;
|
||||
}
|
||||
|
||||
export interface RunewakerEnemyDamageTypeSource {
|
||||
readonly spellId: number;
|
||||
readonly name: string;
|
||||
readonly components: readonly RunewakerEnemyDamageTypeComponent[];
|
||||
readonly source: string;
|
||||
}
|
||||
|
||||
export type EnemyDamageTypeSource = WowEnemyDamageTypeSource | RunewakerEnemyDamageTypeSource;
|
||||
|
||||
export interface ResolveEnemyDamageTypeInput {
|
||||
readonly origin?: EnemyDamageOrigin;
|
||||
readonly spellId?: number;
|
||||
readonly componentId?: number;
|
||||
/** Raw WoW Spell.dbc DmgClass when present. */
|
||||
readonly damageClass?: number;
|
||||
/** Runtime delivery classification, authored or normalized by the importer. */
|
||||
readonly attackKind?: EnemyAttackKind;
|
||||
/** Raw WoW Spell.dbc SchoolMask; bit values are preserved. */
|
||||
readonly schoolMask?: number;
|
||||
/** Authored runtime school used when source school data is absent or ambiguous. */
|
||||
readonly school?: EnemyDamageSchool;
|
||||
}
|
||||
|
||||
export interface ResolvedEnemyDamageType {
|
||||
readonly attackKind?: EnemyAttackKind;
|
||||
readonly damageClass?: number;
|
||||
readonly sourceDamageClass?: "none" | "magic" | "melee" | "ranged" | "physical" | "magical";
|
||||
readonly schoolMask?: number;
|
||||
readonly schools: readonly EnemyDamageSchool[];
|
||||
/** Present only when source evidence identifies exactly one runtime school. */
|
||||
readonly school?: EnemyDamageSchool;
|
||||
readonly unknownSchoolMaskBits?: number;
|
||||
readonly sourceVerified: boolean;
|
||||
}
|
||||
|
||||
type GeneratedCatalog = {
|
||||
readonly wow335: Readonly<Record<string, WowEnemyDamageTypeSource>>;
|
||||
readonly runewaker: Readonly<Record<string, RunewakerEnemyDamageTypeSource>>;
|
||||
};
|
||||
|
||||
const catalog = sourceCatalog as GeneratedCatalog;
|
||||
const WOW_SCHOOL_BITS: readonly (readonly [number, EnemyDamageSchool])[] = [
|
||||
[1, "physical"],
|
||||
[2, "holy"],
|
||||
[4, "fire"],
|
||||
[8, "nature"],
|
||||
[16, "frost"],
|
||||
[32, "shadow"],
|
||||
[64, "arcane"],
|
||||
];
|
||||
const KNOWN_WOW_SCHOOL_BITS = WOW_SCHOOL_BITS.reduce((mask, [bit]) => mask | bit, 0);
|
||||
|
||||
export function enemyDamageTypeSourceForSpell(
|
||||
origin: EnemyDamageOrigin,
|
||||
spellId: number,
|
||||
): EnemyDamageTypeSource | undefined {
|
||||
if (!Number.isSafeInteger(spellId) || spellId <= 0) return undefined;
|
||||
const key = String(spellId);
|
||||
return origin === "wow335" ? catalog.wow335[key] : catalog.runewaker[key];
|
||||
}
|
||||
|
||||
export function enemyDamageTypeComponentForSpell(
|
||||
spellId: number,
|
||||
componentId: number,
|
||||
): RunewakerEnemyDamageTypeComponent | undefined {
|
||||
const source = enemyDamageTypeSourceForSpell("runewaker", spellId);
|
||||
if (!source || !("components" in source)) return undefined;
|
||||
return source.components.find((component) => component.componentId === componentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts WotLK SchoolMask bits without imposing a strongest-school rule.
|
||||
* Composite masks remain a list; callers should keep the authored single-school
|
||||
* fallback until the damage resolver supports combined-school mitigation.
|
||||
*/
|
||||
export function wowSchoolsFromSchoolMask(mask: number): {
|
||||
readonly schools: readonly EnemyDamageSchool[];
|
||||
readonly unknownBits: number;
|
||||
} {
|
||||
if (!Number.isSafeInteger(mask) || mask < 0) return { schools: [], unknownBits: 0 };
|
||||
return {
|
||||
schools: WOW_SCHOOL_BITS.filter(([bit]) => (mask & bit) !== 0).map(([, school]) => school),
|
||||
unknownBits: (mask & ~KNOWN_WOW_SCHOOL_BITS) >>> 0,
|
||||
};
|
||||
}
|
||||
|
||||
function sourceComponentForResolution(
|
||||
source: RunewakerEnemyDamageTypeSource | undefined,
|
||||
componentId: number | undefined,
|
||||
): RunewakerEnemyDamageTypeComponent | undefined {
|
||||
if (!source) return undefined;
|
||||
if (componentId !== undefined) {
|
||||
return source.components.find((component) => component.componentId === componentId);
|
||||
}
|
||||
if (source.components.length === 1) return source.components[0];
|
||||
const first = source.components[0];
|
||||
if (first && source.components.every((component) => (
|
||||
component.sourceDamageClass === first.sourceDamageClass
|
||||
&& component.attackKind === first.attackKind
|
||||
&& component.school === first.school
|
||||
&& component.sourceChannel === first.sourceChannel
|
||||
))) return first;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function attackKindForDamageClass(value: number | undefined): EnemyAttackKind | undefined {
|
||||
if (value === 1) return "spell";
|
||||
if (value === 2) return "melee";
|
||||
if (value === 3) return "ranged";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveEnemyDamageType(input: ResolveEnemyDamageTypeInput): ResolvedEnemyDamageType {
|
||||
const source = input.origin && input.spellId !== undefined
|
||||
? enemyDamageTypeSourceForSpell(input.origin, input.spellId)
|
||||
: undefined;
|
||||
const wowSource = source && "schoolMask" in source ? source : undefined;
|
||||
const runewakerSource = source && "components" in source ? source : undefined;
|
||||
const runewakerComponent = sourceComponentForResolution(runewakerSource, input.componentId);
|
||||
const damageClass = input.damageClass ?? wowSource?.damageClass;
|
||||
const schoolMask = input.schoolMask ?? wowSource?.schoolMask;
|
||||
const maskResult = schoolMask === undefined
|
||||
? { schools: [] as readonly EnemyDamageSchool[], unknownBits: 0 }
|
||||
: wowSchoolsFromSchoolMask(schoolMask);
|
||||
const exactWoWSchool = schoolMask !== undefined
|
||||
&& maskResult.schools.length === 1
|
||||
&& maskResult.unknownBits === 0
|
||||
? maskResult.schools[0]
|
||||
: undefined;
|
||||
const sourceDamageClass = input.origin === "runewaker"
|
||||
? runewakerComponent?.sourceDamageClass
|
||||
: damageClass === 0 ? "none"
|
||||
: damageClass === 1 ? "magic"
|
||||
: damageClass === 2 ? "melee"
|
||||
: damageClass === 3 ? "ranged" : undefined;
|
||||
const attackKind = input.origin === "runewaker"
|
||||
? runewakerComponent?.attackKind ?? input.attackKind
|
||||
: input.attackKind ?? attackKindForDamageClass(damageClass);
|
||||
const school = exactWoWSchool
|
||||
?? runewakerComponent?.school
|
||||
?? input.school;
|
||||
|
||||
return {
|
||||
...(attackKind ? { attackKind } : {}),
|
||||
...(damageClass !== undefined ? { damageClass } : {}),
|
||||
...(sourceDamageClass ? { sourceDamageClass } : {}),
|
||||
...(schoolMask !== undefined ? { schoolMask } : {}),
|
||||
schools: maskResult.schools.length ? maskResult.schools : runewakerComponent?.school ? [runewakerComponent.school] : [],
|
||||
...(school ? { school } : {}),
|
||||
...(maskResult.unknownBits ? { unknownSchoolMaskBits: maskResult.unknownBits } : {}),
|
||||
sourceVerified: Boolean(wowSource || runewakerComponent),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user