Update Healer Man server

This commit is contained in:
phenom committed 2026-10-07 13:05:04 -04:00
1 parent 69809f7228
commit ff20ab8779
101 files changed
+16713 -945

No files matched your search

+211 -3
View File
@@ -40,6 +40,8 @@ import {
} from "./wow335WarlockMetamorphosisRuntime";
import { WOW335_WARLOCK_SOUL_LINK_TAG } from "./wow335WarlockSoulLinkRuntime";
import { WOW335_TALENT_NODES } from "./wow335Talents.generated";
import { WOW335_HUNTER_DETERRENCE_PARENT_AURA_EFFECTS } from "./wow335DeterrenceRuntime";
import { WOW335_DETERRENCE_SOURCE_SIGNATURES } from "./wow335Deterrence";
export const BASE_CLASS_STARTER_SOURCE_LEVEL = 10;
@@ -50,6 +52,7 @@ interface Wow335Effect {
readonly basePoints: number;
readonly dieSides: number;
readonly pointsPerLevel: number;
readonly mechanic?: number;
readonly implicitTargetA: number;
readonly implicitTargetB: number;
readonly radius: number;
@@ -100,6 +103,126 @@ const data = generatedData as {
readonly classes: Readonly<Record<BaseClassId, readonly Wow335Chain[]>>;
};
const WOW335_HUNTER_DETERRENCE_CHAIN: Wow335Chain = Object.freeze({
id: "wow335-hunter-deterrence",
name: "Deterrence",
iconBasename: "ability_whirlwind",
iconPath: "/assets/ui/spells/ability_whirlwind.png",
sourceUnlockLevel: 60,
ranks: Object.freeze([Object.freeze({
spellId: 19263,
rank: 0,
level: 60,
sourceLevel: 60,
maxScalingLevel: 0,
description: "When activated, increases parry chance by 100% and grants a 100% chance to deflect spells. While Deterrence is active, you cannot attack. Lasts 5 sec.",
castTimeMs: 0,
cooldownMs: 90_000,
gcdMs: 0,
durationMs: 5_000,
channelDurationMs: 0,
powerType: 0,
powerCost: 0,
powerCostPercentage: 0,
rangeIndex: 1,
rangeMin: 0,
rangeMax: 0,
equippedItemClass: 2,
equippedItemSubclassMask: 173_555,
equippedItemInventoryMask: 0,
wowSpellDefense: Object.freeze({
damageClass: 0,
schoolMask: 1,
attributes0: 327_696,
attributes1: 16,
attributes3: 0,
attributes5: 0,
directDamageFromEffects: false,
sourceProfile: "wow335" as const,
}),
effects: Object.freeze(WOW335_DETERRENCE_SOURCE_SIGNATURES.selectedParent.effects.map((effect) => ({
...effect,
coefficient: [1, 1, 0][effect.index] ?? 0,
}))),
})]),
});
const WOW335_HUNTER_DETERRENCE_DESCRIPTION = WOW335_HUNTER_DETERRENCE_CHAIN.ranks[0]!.description;
function isExactHunterDeterrenceChain(chain: Wow335Chain): boolean {
const [rank] = chain.ranks;
const source = WOW335_DETERRENCE_SOURCE_SIGNATURES.selectedParent;
if (chain.id !== "wow335-hunter-deterrence"
|| chain.name !== "Deterrence"
|| chain.iconBasename !== "ability_whirlwind"
|| chain.iconPath !== "/assets/ui/spells/ability_whirlwind.png"
|| chain.sourceUnlockLevel !== 60
|| chain.ranks.length !== 1
|| !rank
|| rank.spellId !== source.spellId
|| rank.rank !== 0
|| rank.level !== 60
|| rank.sourceLevel !== 60
|| rank.maxScalingLevel !== 0
|| rank.description !== WOW335_HUNTER_DETERRENCE_DESCRIPTION
|| rank.castTimeMs !== 0
|| rank.cooldownMs !== 90_000
|| rank.gcdMs !== 0
|| rank.durationMs !== 5_000
|| rank.channelDurationMs !== 0
|| rank.powerType !== 0
|| rank.powerCost !== 0
|| rank.powerCostPercentage !== 0
|| rank.rangeIndex !== 1
|| rank.rangeMin !== 0
|| rank.rangeMax !== 0
|| rank.equippedItemClass !== 2
|| rank.equippedItemSubclassMask !== 173_555
|| rank.equippedItemInventoryMask !== 0) return false;
const defense = rank.wowSpellDefense;
if (!defense
|| defense.damageClass !== 0
|| defense.schoolMask !== 1
|| defense.attributes0 !== 327_696
|| defense.attributes1 !== 16
|| defense.attributes3 !== 0
|| defense.attributes5 !== 0
|| defense.directDamageFromEffects !== false
|| defense.sourceProfile !== "wow335") return false;
return rank.effects.length === source.effects.length
&& rank.effects.every((effect, index) => {
const expected = source.effects[index];
return expected !== undefined
&& effect.index === expected.index
&& effect.effectType === expected.effectType
&& effect.auraType === expected.auraType
&& effect.basePoints === expected.basePoints
&& effect.dieSides === expected.dieSides
&& effect.pointsPerLevel === expected.pointsPerLevel
&& (effect.mechanic ?? 0) === expected.mechanic
&& effect.implicitTargetA === expected.implicitTargetA
&& effect.implicitTargetB === expected.implicitTargetB
&& effect.radius === expected.radius
&& effect.periodMs === expected.periodMs
&& effect.miscValue === expected.miscValue
&& effect.triggerSpellId === expected.triggerSpellId
&& effect.coefficient === [1, 1, 0][index];
});
}
function hunterChainsWithSelectedDeterrence(): readonly Wow335Chain[] {
const chains = data.classes.hunter;
const candidates = chains.filter((chain) => chain.name === "Deterrence"
|| chain.ranks.some((rank) => [19_263, 65_871, 67_801].includes(rank.spellId)));
if (candidates.length === 0) return [...chains, WOW335_HUNTER_DETERRENCE_CHAIN];
if (candidates.length === 1 && isExactHunterDeterrenceChain(candidates[0]!)) return chains;
// A conflicting same-name/linked-ID import is source drift. Drop the whole
// candidate group and fail closed rather than substitute 65871 or 67801.
return chains.filter((chain) => !candidates.includes(chain));
}
const RESOURCE_BY_CLASS: Readonly<Record<BaseClassId, ResourceType>> = {
warrior: "rage",
paladin: "mana",
@@ -2386,7 +2509,15 @@ function abilityDefinition(classId: BaseClassId, chain: Wow335Chain): AbilityDef
?? auraMasteryTalentTooltip;
return talentTooltip ? { ...rank, description: talentTooltip } : rank;
});
const first = ranks[0];
const selectedDeterrence = classId === "hunter"
&& chain.id === "wow335-hunter-deterrence"
&& chain.name === "Deterrence"
&& castRanks.length === 1
&& castRanks[0]?.spellId === 19_263;
const mappedRanks = selectedDeterrence
? ranks.map((rank) => ({ ...rank, effects: WOW335_HUNTER_DETERRENCE_PARENT_AURA_EFFECTS }))
: ranks;
const first = mappedRanks[0];
const firstRaw = castRanks[0];
if (!first || !firstRaw) return null;
const effectiveSourceUnlockLevel = classId === "warrior" && chain.name === "Sweeping Strikes"
@@ -2458,7 +2589,7 @@ function abilityDefinition(classId: BaseClassId, chain: Wow335Chain): AbilityDef
talentNodeId: chain.talentNodeId && runtimeTalentNodeById(chain.talentNodeId)
? chain.talentNodeId
: undefined,
ranks,
ranks: mappedRanks,
};
}
@@ -2499,7 +2630,8 @@ function classicCooldownGroup(classId: BaseClassId, abilityName: string): string
export const WOW335_ABILITIES_BY_CLASS: Readonly<Record<BaseClassId, readonly AbilityDefinition[]>> =
Object.freeze(Object.fromEntries(BASE_CLASS_IDS.map((classId) => {
const generated = data.classes[classId]
const sourceChains = classId === "hunter" ? hunterChainsWithSelectedDeterrence() : data.classes[classId];
const generated = sourceChains
.map((chain) => abilityDefinition(classId, chain))
.filter((ability): ability is AbilityDefinition => Boolean(ability));
const specialized = classId === "warlock"
@@ -2514,6 +2646,82 @@ export const WOW335_ABILITIES_BY_CLASS: Readonly<Record<BaseClassId, readonly Ab
))];
}))) as unknown as Readonly<Record<BaseClassId, readonly AbilityDefinition[]>>;
export type Wow335HunterPreventionType = 0 | 1 | 2;
// Spell.dbc field 214 in the pinned 234-field layout (field 220 in the
// selected 239-field archive) is kept locally until the generator has a
// source-profile-aware way to carry this cast gate. Every value below was
// read from the pinned Spell.dbc row; missing IDs intentionally remain absent.
const WOW335_HUNTER_PREVENTION_SPELL_IDS_BY_TYPE: Readonly<Record<Wow335HunterPreventionType, readonly number[]>> = Object.freeze({
0: Object.freeze([
23_989, 1_494, 1_130, 14_323, 14_324, 14_325, 53_338, 883, 2_641, 6_991,
1_515, 19_883, 19_884, 781, 1_462, 19_885, 3_045, 19_880, 5_384,
19_878, 19_574, 19_882, 24_131, 24_134, 24_135, 27_069, 49_009, 49_010,
34_471, 19_879, 62_757, 19_263,
]),
1: Object.freeze([
13_163, 13_165, 14_318, 14_319, 14_320, 14_321, 14_322, 25_296, 27_044,
982, 136, 3_111, 3_661, 3_662, 13_542, 13_543, 13_544, 27_046, 48_989,
48_990, 6_197, 1_002, 1_513, 14_326, 14_327, 5_118, 13_795, 13_797, 14_298,
14_302, 14_299, 14_303, 14_300, 14_304, 14_301, 14_305, 27_023, 27_024,
49_053, 49_055, 49_054, 49_056, 34_074, 1_499, 14_310, 14_311, 13_809,
13_161, 19_577, 1_543, 13_813, 14_316, 14_317, 27_025, 49_066, 49_067,
13_159, 19_506, 20_043, 20_190, 27_045,
49_071, 34_026, 34_600, 61_846, 61_847, 60_192,
]),
2: Object.freeze([
75, 2_973, 14_260, 14_261, 14_262, 14_263, 14_264, 14_265, 14_266, 27_014,
48_995, 48_996, 1_978, 13_549, 13_550, 13_551, 13_552, 13_553, 13_554,
13_555, 25_295, 27_016, 49_000, 49_001, 3_044, 14_281, 14_282, 14_283,
14_284, 14_285, 14_286, 14_287, 27_019, 49_044, 49_045, 5_116, 20_736,
2_974, 19_503, 1_495, 14_269, 14_270, 14_271, 36_916, 53_339, 2_643,
14_288, 14_289, 14_290, 25_294, 27_021, 49_047, 49_048, 19_434, 27_065,
49_049, 49_050, 3_043, 19_306, 20_909, 20_910, 27_067, 48_998, 48_999,
34_490, 3_034, 1_510, 14_294, 14_295, 27_022, 19_386, 24_132, 24_133,
27_068, 49_011, 49_012, 3_674, 63_668, 63_669, 63_670, 63_671, 63_672,
56_641, 49_051, 49_052, 53_209, 53_301, 19_801, 34_477, 61_005, 61_006,
]),
});
const hunterPreventionTypesBySpellId: Record<number, Wow335HunterPreventionType> = {};
for (const type of [0, 1, 2] as const) {
for (const spellId of WOW335_HUNTER_PREVENTION_SPELL_IDS_BY_TYPE[type]) {
hunterPreventionTypesBySpellId[spellId] = type;
}
}
export const WOW335_HUNTER_PREVENTION_TYPES_BY_SPELL_ID: Readonly<Record<number, Wow335HunterPreventionType>> =
Object.freeze(hunterPreventionTypesBySpellId);
const HUNTER_SOURCE_RANK_IDENTITIES = new Map<number, { readonly id: string; readonly name: string }>();
const DUPLICATE_HUNTER_SOURCE_RANK_IDS = new Set<number>();
for (const ability of WOW335_ABILITIES_BY_CLASS.hunter) {
for (const rank of ability.ranks ?? []) {
if (HUNTER_SOURCE_RANK_IDENTITIES.has(rank.spellId)) {
HUNTER_SOURCE_RANK_IDENTITIES.delete(rank.spellId);
DUPLICATE_HUNTER_SOURCE_RANK_IDS.add(rank.spellId);
continue;
}
if (!DUPLICATE_HUNTER_SOURCE_RANK_IDS.has(rank.spellId)) {
HUNTER_SOURCE_RANK_IDENTITIES.set(rank.spellId, { id: ability.id, name: ability.name });
}
}
}
/** Returns the pinned source PreventionType only for a selected Hunter row. */
export function wow335HunterPreventionTypeForAbility(
ability: Pick<AbilityDefinition, "id" | "classId" | "source" | "name" | "dbcSpellId">,
): Wow335HunterPreventionType | undefined {
if (ability.classId !== "hunter" || ability.source !== "wow335-progression") return undefined;
const preventionType = WOW335_HUNTER_PREVENTION_TYPES_BY_SPELL_ID[ability.dbcSpellId];
const identity = HUNTER_SOURCE_RANK_IDENTITIES.get(ability.dbcSpellId);
if (preventionType === undefined || !identity || DUPLICATE_HUNTER_SOURCE_RANK_IDS.has(ability.dbcSpellId)) {
return undefined;
}
const publicAlias = identity.id.startsWith("wow335-") ? identity.id.slice("wow335-".length) : undefined;
if (ability.name !== identity.name || (ability.id !== identity.id && ability.id !== publicAlias)) return undefined;
return preventionType;
}
// These ground channels store their actual damage in same-name triggered
// child spells. Resolve the child once so each channel pulse can use its
// rank-specific DBC amount rather than creating a delayed area effect.