forked from phenom/healer-man
Release Healer Man 0.1.7
This commit is contained in:
1 parent
b12fb84859
commit
eca6b5db9b
99 files changed
+1931
-150
No files matched your search
+141
-41
@@ -1,13 +1,18 @@
|
||||
import type { ClassId } from "../app/characterCatalog";
|
||||
import {
|
||||
COA_RUNTIME_ABILITIES_BY_CLASS as COA_ABILITIES_BY_CLASS,
|
||||
COA_RUNTIME_CLASS_RESOURCES as COA_CLASS_RESOURCES,
|
||||
} from "./coaAbilityRuntimeCatalog";
|
||||
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
|
||||
import { ROM_ALL_ABILITIES, ROM_ELITE_ABILITIES, ROM_ORDINARY_ABILITIES_BY_CLASS, type RomSkillGroup } from "./romAbilityCatalog";
|
||||
BASE_CLASS_IDS,
|
||||
COA_CLASS_IDS,
|
||||
ROM_CLASS_IDS,
|
||||
type BaseClassId,
|
||||
type ClassId,
|
||||
type CoaClassId,
|
||||
type RomClassId,
|
||||
} from "../app/characterCatalog";
|
||||
import { loadCoaAbilityCatalogShard } from "./coaAbilityCatalogLoader";
|
||||
import { registerAbilityAnimationLookup } from "./abilityAnimationLookup";
|
||||
import type { AuraDefinition, DamageSchool, DispelCategory } from "./combatAuras";
|
||||
|
||||
export type RomSkillGroup = "primary" | "general" | "elite" | "passive";
|
||||
|
||||
export type ResourceType =
|
||||
| "mana"
|
||||
| "rage"
|
||||
@@ -301,7 +306,7 @@ function defineAbility(definition: AbilityDefinitionInput): AbilityDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
export const CLASS_RESOURCES: Readonly<Record<ClassId, ClassResourceProfile>> = {
|
||||
const classResources: Partial<Record<ClassId, ClassResourceProfile>> = {
|
||||
// The prototype has no auto-attack swing timer yet, so a small passive Rage
|
||||
// gain stands in for white-hit generation and keeps the full kit usable.
|
||||
warrior: { type: "rage", name: "Rage", color: "#b74439", maximum: 100, initial: 0, regenerationPerSecond: 5 },
|
||||
@@ -314,7 +319,6 @@ export const CLASS_RESOURCES: Readonly<Record<ClassId, ClassResourceProfile>> =
|
||||
mage: { type: "mana", name: "Mana", color: "#397dcc", maximum: 100, initial: 100, regenerationPerSecond: 4 },
|
||||
warlock: { type: "mana", name: "Mana", color: "#397dcc", maximum: 100, initial: 100, regenerationPerSecond: 4 },
|
||||
druid: { type: "mana", name: "Mana", color: "#397dcc", maximum: 100, initial: 100, regenerationPerSecond: 4 },
|
||||
...COA_CLASS_RESOURCES,
|
||||
"rom-warrior": { type: "rage", name: "Rage", color: "#b74439", maximum: 100, initial: 0, regenerationPerSecond: 0 },
|
||||
"rom-scout": { type: "focus", name: "Focus", color: "#55b995", maximum: 100, initial: 100, regenerationPerSecond: 6 },
|
||||
"rom-rogue": { type: "energy", name: "Energy", color: "#d8c43f", maximum: 100, initial: 100, regenerationPerSecond: 10 },
|
||||
@@ -327,6 +331,9 @@ export const CLASS_RESOURCES: Readonly<Record<ClassId, ClassResourceProfile>> =
|
||||
"rom-champion": { type: "rage", name: "Rage", color: "#bb604c", maximum: 100, initial: 0, regenerationPerSecond: 0 },
|
||||
};
|
||||
|
||||
/** Populated per mode/class by preloadAbilityCatalogsForClasses before gameplay mounts. */
|
||||
export const CLASS_RESOURCES = classResources as Readonly<Record<ClassId, ClassResourceProfile>>;
|
||||
|
||||
const WARRIOR = [
|
||||
defineAbility({
|
||||
id: "warrior-heroic-strike", classId: "warrior", dbcSpellId: 78, name: "Heroic Strike",
|
||||
@@ -789,48 +796,131 @@ function mergeWow335ClassicAbilities(
|
||||
];
|
||||
}
|
||||
|
||||
export const ABILITIES_BY_CLASS: Readonly<Record<ClassId, readonly AbilityDefinition[]>> = {
|
||||
warrior: mergeWow335ClassicAbilities(WARRIOR, WOW335_ABILITIES_BY_CLASS.warrior),
|
||||
paladin: mergeWow335ClassicAbilities(PALADIN, WOW335_ABILITIES_BY_CLASS.paladin),
|
||||
hunter: mergeWow335ClassicAbilities(HUNTER, WOW335_ABILITIES_BY_CLASS.hunter),
|
||||
rogue: mergeWow335ClassicAbilities(ROGUE, WOW335_ABILITIES_BY_CLASS.rogue),
|
||||
priest: mergeWow335ClassicAbilities(PRIEST, WOW335_ABILITIES_BY_CLASS.priest),
|
||||
"death-knight": mergeWow335ClassicAbilities(DEATH_KNIGHT, WOW335_ABILITIES_BY_CLASS["death-knight"]),
|
||||
shaman: mergeWow335ClassicAbilities(SHAMAN, WOW335_ABILITIES_BY_CLASS.shaman),
|
||||
mage: mergeWow335ClassicAbilities(MAGE, WOW335_ABILITIES_BY_CLASS.mage),
|
||||
warlock: mergeWow335ClassicAbilities(WARLOCK, WOW335_ABILITIES_BY_CLASS.warlock),
|
||||
druid: mergeWow335ClassicAbilities(DRUID, WOW335_ABILITIES_BY_CLASS.druid),
|
||||
...COA_ABILITIES_BY_CLASS,
|
||||
"rom-warrior": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-warrior"],
|
||||
"rom-scout": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-scout"],
|
||||
"rom-rogue": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-rogue"],
|
||||
"rom-mage": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-mage"],
|
||||
"rom-priest": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-priest"],
|
||||
"rom-knight": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-knight"],
|
||||
"rom-warden": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-warden"],
|
||||
"rom-druid": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-druid"],
|
||||
"rom-warlock": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-warlock"],
|
||||
"rom-champion": ROM_ORDINARY_ABILITIES_BY_CLASS["rom-champion"],
|
||||
const CLASSIC_CORE_ABILITIES: Readonly<Record<BaseClassId, readonly AbilityDefinition[]>> = {
|
||||
warrior: WARRIOR,
|
||||
paladin: PALADIN,
|
||||
hunter: HUNTER,
|
||||
rogue: ROGUE,
|
||||
priest: PRIEST,
|
||||
"death-knight": DEATH_KNIGHT,
|
||||
shaman: SHAMAN,
|
||||
mage: MAGE,
|
||||
warlock: WARLOCK,
|
||||
druid: DRUID,
|
||||
};
|
||||
const abilitiesByClass: Partial<Record<ClassId, readonly AbilityDefinition[]>> = {
|
||||
...CLASSIC_CORE_ABILITIES,
|
||||
};
|
||||
const abilityCatalog: Record<string, AbilityDefinition> = {};
|
||||
let romEliteAbilities: readonly AbilityDefinition[] = [];
|
||||
let romTriggeredAbilityResolver: ((spellId: number) => AbilityDefinition | null) | null = null;
|
||||
const coaTriggeredAbilities = new Map<number, AbilityDefinition>();
|
||||
let classicLoad: Promise<void> | null = null;
|
||||
let romLoad: Promise<void> | null = null;
|
||||
const coaLoads = new Map<CoaClassId, Promise<void>>();
|
||||
|
||||
export const ABILITY_CATALOG: Readonly<Record<string, AbilityDefinition>> = Object.freeze(
|
||||
Object.fromEntries([...Object.values(ABILITIES_BY_CLASS).flat(), ...ROM_ALL_ABILITIES].map((ability) => [ability.id, ability])),
|
||||
);
|
||||
/** Live read-only views retained for catalog consumers and validation tests. */
|
||||
export const ABILITIES_BY_CLASS = abilitiesByClass as Readonly<Record<ClassId, readonly AbilityDefinition[]>>;
|
||||
export const ABILITY_CATALOG = abilityCatalog as Readonly<Record<string, AbilityDefinition>>;
|
||||
|
||||
function registerClassAbilities(classId: ClassId, abilities: readonly AbilityDefinition[]): void {
|
||||
for (const previous of abilitiesByClass[classId] ?? []) delete abilityCatalog[previous.id];
|
||||
abilitiesByClass[classId] = abilities;
|
||||
for (const ability of abilities) abilityCatalog[ability.id] = ability;
|
||||
}
|
||||
|
||||
for (const classId of BASE_CLASS_IDS) {
|
||||
registerClassAbilities(classId, CLASSIC_CORE_ABILITIES[classId]);
|
||||
}
|
||||
|
||||
async function preloadClassicAbilityCatalog(): Promise<void> {
|
||||
if (classicLoad) return classicLoad;
|
||||
classicLoad = import("./wow335AbilityCatalog").then(({ WOW335_ABILITIES_BY_CLASS }) => {
|
||||
for (const classId of BASE_CLASS_IDS) {
|
||||
registerClassAbilities(
|
||||
classId,
|
||||
mergeWow335ClassicAbilities(CLASSIC_CORE_ABILITIES[classId], WOW335_ABILITIES_BY_CLASS[classId]),
|
||||
);
|
||||
}
|
||||
}).catch((error: unknown) => {
|
||||
classicLoad = null;
|
||||
throw error;
|
||||
});
|
||||
return classicLoad;
|
||||
}
|
||||
|
||||
async function preloadRomAbilityCatalog(): Promise<void> {
|
||||
if (romLoad) return romLoad;
|
||||
romLoad = import("./romAbilityCatalog").then((catalog) => {
|
||||
for (const classId of ROM_CLASS_IDS) {
|
||||
registerClassAbilities(classId, catalog.ROM_ORDINARY_ABILITIES_BY_CLASS[classId]);
|
||||
}
|
||||
romEliteAbilities = catalog.ROM_ELITE_ABILITIES;
|
||||
romTriggeredAbilityResolver = catalog.romTriggeredAbilityBySpellId;
|
||||
for (const ability of catalog.ROM_ALL_ABILITIES) abilityCatalog[ability.id] = ability;
|
||||
}).catch((error: unknown) => {
|
||||
romLoad = null;
|
||||
throw error;
|
||||
});
|
||||
return romLoad;
|
||||
}
|
||||
|
||||
async function preloadCoaAbilityCatalog(classId: CoaClassId): Promise<void> {
|
||||
const cached = coaLoads.get(classId);
|
||||
if (cached) return cached;
|
||||
const pending = loadCoaAbilityCatalogShard(classId).then((shard) => {
|
||||
registerClassAbilities(classId, shard.abilities);
|
||||
classResources[classId] = shard.resource;
|
||||
for (const [spellId, ability] of Object.entries(shard.triggeredAbilitiesBySpellId)) {
|
||||
coaTriggeredAbilities.set(Number(spellId), ability);
|
||||
}
|
||||
}).catch((error: unknown) => {
|
||||
coaLoads.delete(classId);
|
||||
throw error;
|
||||
});
|
||||
coaLoads.set(classId, pending);
|
||||
return pending;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads only the executable catalogs needed by the selected character and its
|
||||
* party mode. Callers gate gameplay mounting on this promise so combat APIs can
|
||||
* stay synchronous inside the simulation loop.
|
||||
*/
|
||||
export async function preloadAbilityCatalogsForClasses(classIds: readonly ClassId[]): Promise<void> {
|
||||
const requested = new Set(classIds);
|
||||
const loads: Promise<void>[] = [];
|
||||
if (BASE_CLASS_IDS.some((classId) => requested.has(classId))) loads.push(preloadClassicAbilityCatalog());
|
||||
if (ROM_CLASS_IDS.some((classId) => requested.has(classId))) loads.push(preloadRomAbilityCatalog());
|
||||
for (const classId of COA_CLASS_IDS) {
|
||||
if (requested.has(classId)) loads.push(preloadCoaAbilityCatalog(classId));
|
||||
}
|
||||
await Promise.all(loads);
|
||||
}
|
||||
|
||||
export function triggeredAbilityBySpellId(
|
||||
spellId: number,
|
||||
includeCoa = true,
|
||||
): AbilityDefinition | null {
|
||||
return romTriggeredAbilityResolver?.(spellId)
|
||||
?? (includeCoa ? coaTriggeredAbilities.get(spellId) : undefined)
|
||||
?? null;
|
||||
}
|
||||
|
||||
export function abilitiesForClass(classId: ClassId): readonly AbilityDefinition[] {
|
||||
return ABILITIES_BY_CLASS[classId];
|
||||
return abilitiesByClass[classId] ?? [];
|
||||
}
|
||||
|
||||
export function abilitiesForCharacter(
|
||||
classId: ClassId,
|
||||
secondaryClassId: ClassId | null | undefined,
|
||||
): readonly AbilityDefinition[] {
|
||||
if (!classId.startsWith("rom-") || !secondaryClassId?.startsWith("rom-")) return ABILITIES_BY_CLASS[classId];
|
||||
const primary = ABILITIES_BY_CLASS[classId];
|
||||
const secondaryGeneral = ABILITIES_BY_CLASS[secondaryClassId].filter((ability) => (
|
||||
if (!classId.startsWith("rom-") || !secondaryClassId?.startsWith("rom-")) return abilitiesForClass(classId);
|
||||
const primary = abilitiesForClass(classId);
|
||||
const secondaryGeneral = abilitiesForClass(secondaryClassId).filter((ability) => (
|
||||
ability.romSkillGroup === "general" || (ability.romSkillGroup === "passive" && ability.id.includes("-general-"))
|
||||
));
|
||||
const elites = ROM_ELITE_ABILITIES.filter((ability) => ability.classId === classId && ability.secondaryClassId === secondaryClassId);
|
||||
const elites = romEliteAbilities.filter((ability) => ability.classId === classId && ability.secondaryClassId === secondaryClassId);
|
||||
return [...primary, ...secondaryGeneral, ...elites];
|
||||
}
|
||||
|
||||
@@ -885,7 +975,7 @@ export function abilitiesUnlockedBetweenLevels(
|
||||
previousLevel: number,
|
||||
currentLevel: number,
|
||||
): readonly AbilityDefinition[] {
|
||||
return ABILITIES_BY_CLASS[classId].filter((ability) => (
|
||||
return abilitiesForClass(classId).filter((ability) => (
|
||||
ability.unlockLevel > previousLevel && ability.unlockLevel <= currentLevel
|
||||
));
|
||||
}
|
||||
@@ -920,7 +1010,7 @@ export function defaultActionBarForClass(classId: ClassId): readonly string[] {
|
||||
|| effect.kind === "rune"
|
||||
|| effect.kind === "trigger-spell"
|
||||
)) ? 0 : 1;
|
||||
const ordered = ABILITIES_BY_CLASS[classId]
|
||||
const ordered = abilitiesForClass(classId)
|
||||
.map((ability, index) => ({ ability, index }))
|
||||
.sort((left, right) => (
|
||||
left.ability.unlockLevel - right.ability.unlockLevel
|
||||
@@ -962,5 +1052,15 @@ export function defaultActionBarForCharacter(classId: ClassId, secondaryClassId?
|
||||
}
|
||||
|
||||
export function resourceProfileForClass(classId: ClassId): ClassResourceProfile {
|
||||
return CLASS_RESOURCES[classId];
|
||||
const profile = classResources[classId];
|
||||
if (!profile) throw new Error(`Ability catalog for ${classId} was used before it was loaded.`);
|
||||
return profile;
|
||||
}
|
||||
|
||||
if (import.meta.env.MODE === "test") {
|
||||
await preloadAbilityCatalogsForClasses([
|
||||
...BASE_CLASS_IDS,
|
||||
...COA_CLASS_IDS,
|
||||
...ROM_CLASS_IDS,
|
||||
]);
|
||||
}
|
||||
Reference in new issue
Block a user