Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,270 @@
|
||||
import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog";
|
||||
import type { ActiveAura, AuraDefinition } from "./combatAuras";
|
||||
|
||||
/** August Vol'Jin Witch Doctor ingredient actions promoted for the first party. */
|
||||
export type CoaWitchDoctorIngredientId =
|
||||
| "jungle-shrooms"
|
||||
| "frog-bones"
|
||||
| "bloodthistle"
|
||||
| "fish-oil";
|
||||
|
||||
interface IngredientSource {
|
||||
readonly id: CoaWitchDoctorIngredientId;
|
||||
readonly spellId: number;
|
||||
/** Null means taught by Mixologist rather than represented by its own talent node. */
|
||||
readonly talentEntryId: number | null;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
const INGREDIENTS: readonly IngredientSource[] = [
|
||||
{ id: "jungle-shrooms", spellId: 801660, talentEntryId: 12645, name: "Jungle Shrooms" },
|
||||
{ id: "frog-bones", spellId: 801663, talentEntryId: 29741, name: "Frog Bones" },
|
||||
{ id: "bloodthistle", spellId: 801664, talentEntryId: 6021, name: "Bloodthistle" },
|
||||
// Cauldron Brewer's DBC says Mixologist teaches this ingredient directly.
|
||||
{ id: "fish-oil", spellId: 801662, talentEntryId: null, name: "Fish Oil" },
|
||||
];
|
||||
|
||||
export const COA_WITCH_DOCTOR_CAULDRON_BREWER_SPELL_ID = 92085;
|
||||
export const COA_WITCH_DOCTOR_POTION_TOSS_TALENT_ENTRY_ID = 12646;
|
||||
export const COA_WITCH_DOCTOR_POTION_TOSS_SPELL_ID = 801661;
|
||||
export const COA_WITCH_DOCTOR_MIXOLOGIST_TALENT_ENTRY_ID = 6645;
|
||||
export const COA_WITCH_DOCTOR_MIXOLOGIST_SPELL_ID = 561069;
|
||||
export const COA_WITCH_DOCTOR_SPLASH_POTION_TALENT_ENTRY_ID = 7128;
|
||||
export const COA_WITCH_DOCTOR_SPLASH_POTION_SPELL_ID = 802710;
|
||||
export const COA_WITCH_DOCTOR_FROG_BONES_POTION_SPELL_ID = 802971;
|
||||
|
||||
const INGREDIENT_TAG_PREFIX = "coa-witch-doctor-active-ingredient:";
|
||||
const ACTIVE_AURA_ID_PREFIX = "coa-witch-doctor:active-ingredient:";
|
||||
|
||||
/** Global Spell.dbc 802971: 8s absorb; base 100, 5 points/level from spell level 10,
|
||||
* plus 35% Spirit and 80% bonus healing. DBC maxLevel is 0, so it has no level cap.
|
||||
*/
|
||||
export const COA_WITCH_DOCTOR_FROG_BONES_POTION_SHIELD: Extract<AbilityEffect, { kind: "shield" }> = {
|
||||
kind: "shield",
|
||||
coefficient: 0,
|
||||
basePoints: 100,
|
||||
dieSides: 1,
|
||||
pointsPerLevel: 5,
|
||||
sourceLevel: 10,
|
||||
spiritCoefficient: 0.35,
|
||||
bonusPowerCoefficient: 0.8,
|
||||
durationMs: 8_000,
|
||||
};
|
||||
|
||||
/** Global Spell.dbc 803699: reduced-effect Splash Potion version of Frog Bones. */
|
||||
export const COA_WITCH_DOCTOR_FROG_BONES_SPLASH_SHIELD: Extract<AbilityEffect, { kind: "shield" }> = {
|
||||
kind: "shield",
|
||||
coefficient: 0,
|
||||
basePoints: 25,
|
||||
dieSides: 1,
|
||||
pointsPerLevel: 1,
|
||||
sourceLevel: 15,
|
||||
durationMs: 8_000,
|
||||
scope: "party",
|
||||
center: "ability-target",
|
||||
radius: 10,
|
||||
maximumTargets: 8,
|
||||
};
|
||||
|
||||
function ingredientForAbility(ability: AbilityDefinition): IngredientSource | null {
|
||||
if (ability.classId !== "witch-doctor") return null;
|
||||
return INGREDIENTS.find((ingredient) => ingredient.spellId === ability.dbcSpellId
|
||||
&& (ingredient.talentEntryId === null
|
||||
? ability.talentEntryId === undefined
|
||||
: ability.talentEntryId === ingredient.talentEntryId)) ?? null;
|
||||
}
|
||||
|
||||
/** Exact promoted identity for an Ingredient cast action, or null for every other ability. */
|
||||
export function coaWitchDoctorIngredientIdForAbility(
|
||||
ability: AbilityDefinition,
|
||||
): CoaWitchDoctorIngredientId | null {
|
||||
return ingredientForAbility(ability)?.id ?? null;
|
||||
}
|
||||
|
||||
function isApplyAura(effect: AbilityEffect): effect is Extract<AbilityEffect, { kind: "apply-aura" }> {
|
||||
return effect.kind === "apply-aura";
|
||||
}
|
||||
|
||||
function ingredientStateAura(ingredient: IngredientSource): AuraDefinition {
|
||||
return {
|
||||
id: `${ACTIVE_AURA_ID_PREFIX}${ingredient.id}`,
|
||||
name: `Cauldron: ${ingredient.name}`,
|
||||
description: `Active Witch Doctor cauldron ingredient: ${ingredient.name}.`,
|
||||
disposition: "buff",
|
||||
hideFromAuraStrip: true,
|
||||
durationMs: null,
|
||||
maxStacks: 1,
|
||||
stackBehavior: "refresh",
|
||||
utilityTags: [`${INGREDIENT_TAG_PREFIX}${ingredient.id}`],
|
||||
};
|
||||
}
|
||||
|
||||
function ingredientStateEffect(ingredient: IngredientSource): Extract<AbilityEffect, { kind: "apply-aura" }> {
|
||||
return {
|
||||
kind: "apply-aura",
|
||||
aura: ingredientStateAura(ingredient),
|
||||
recipient: "caster",
|
||||
sourceEffectType: 6,
|
||||
sourceAuraType: 0,
|
||||
miscValue: 0,
|
||||
triggerSpellId: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function activeIngredientIds(
|
||||
auras: readonly ActiveAura[],
|
||||
actorId: string,
|
||||
now: number,
|
||||
): ReadonlySet<CoaWitchDoctorIngredientId> {
|
||||
const active = new Set<CoaWitchDoctorIngredientId>();
|
||||
for (const ingredient of INGREDIENTS) {
|
||||
const tag = `${INGREDIENT_TAG_PREFIX}${ingredient.id}`;
|
||||
if (auras.some((aura) => aura.definition.id === `${ACTIVE_AURA_ID_PREFIX}${ingredient.id}`
|
||||
&& aura.definition.utilityTags?.includes(tag)
|
||||
&& aura.sourceId === actorId
|
||||
&& aura.targetId === actorId
|
||||
&& aura.stacks > 0
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now))) {
|
||||
active.add(ingredient.id);
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
/** Source-backed max: Cauldron Brewer allows one ingredient; Mixologist allows two. */
|
||||
export function coaWitchDoctorIngredientCapacity(
|
||||
talentRanks: Readonly<Record<string, number>>,
|
||||
): 1 | 2 {
|
||||
return (talentRanks[`coa-entry-${COA_WITCH_DOCTOR_MIXOLOGIST_TALENT_ENTRY_ID}`] ?? 0) > 0 ? 2 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check before paying for an Ingredient cast. The source establishes the capacity
|
||||
* and same-ingredient exclusivity; it does not define which active brew is replaced
|
||||
* when capacity is full, so this helper leaves a full cauldron untouched.
|
||||
*/
|
||||
export function coaWitchDoctorCanBrewIngredient(
|
||||
ability: AbilityDefinition,
|
||||
auras: readonly ActiveAura[],
|
||||
actorId: string,
|
||||
talentRanks: Readonly<Record<string, number>>,
|
||||
now: number,
|
||||
): boolean {
|
||||
const ingredient = ingredientForAbility(ability);
|
||||
if (!ingredient) return true;
|
||||
const active = activeIngredientIds(auras, actorId, now);
|
||||
if (active.has(ingredient.id)) return true;
|
||||
return active.size < coaWitchDoctorIngredientCapacity(talentRanks);
|
||||
}
|
||||
|
||||
function isFrogBonesPotionShield(effect: AbilityEffect): boolean {
|
||||
return effect.kind === "shield"
|
||||
&& effect.basePoints === COA_WITCH_DOCTOR_FROG_BONES_POTION_SHIELD.basePoints
|
||||
&& effect.pointsPerLevel === COA_WITCH_DOCTOR_FROG_BONES_POTION_SHIELD.pointsPerLevel
|
||||
&& effect.sourceLevel === COA_WITCH_DOCTOR_FROG_BONES_POTION_SHIELD.sourceLevel
|
||||
&& effect.spiritCoefficient === COA_WITCH_DOCTOR_FROG_BONES_POTION_SHIELD.spiritCoefficient
|
||||
&& effect.bonusPowerCoefficient === COA_WITCH_DOCTOR_FROG_BONES_POTION_SHIELD.bonusPowerCoefficient
|
||||
&& effect.durationMs === COA_WITCH_DOCTOR_FROG_BONES_POTION_SHIELD.durationMs;
|
||||
}
|
||||
|
||||
export function isCoaWitchDoctorSplashPotion(ability: AbilityDefinition): boolean {
|
||||
return ability.classId === "witch-doctor"
|
||||
&& ability.talentEntryId === COA_WITCH_DOCTOR_SPLASH_POTION_TALENT_ENTRY_ID
|
||||
&& ability.dbcSpellId === COA_WITCH_DOCTOR_SPLASH_POTION_SPELL_ID;
|
||||
}
|
||||
|
||||
function isSplashPotionBaseHeal(effect: AbilityEffect): effect is Extract<AbilityEffect, { kind: "heal" }> {
|
||||
return effect.kind === "heal"
|
||||
&& effect.basePoints === 54
|
||||
&& effect.dieSides === 3
|
||||
&& effect.pointsPerLevel === 1.3710700273513794
|
||||
&& effect.sourceLevel === 17
|
||||
&& effect.bonusPowerCoefficient === 0.224494
|
||||
&& effect.spiritCoefficient === 0.08
|
||||
&& effect.scope === "party"
|
||||
&& effect.radius === 10
|
||||
&& effect.maximumTargets === 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project the selected source action before resolving its effects. Ingredient
|
||||
* state is added as an actor-owned aura effect, so the ordinary successful-cast
|
||||
* path applies it only after cast validation and payment have succeeded.
|
||||
*/
|
||||
export function coaWitchDoctorBrewingAbilityForCast(
|
||||
ability: AbilityDefinition,
|
||||
auras: readonly ActiveAura[],
|
||||
actorId: string,
|
||||
now: number,
|
||||
): AbilityDefinition {
|
||||
if (ability.classId !== "witch-doctor") return ability;
|
||||
|
||||
const ingredient = ingredientForAbility(ability);
|
||||
const isPotionToss = ability.talentEntryId === COA_WITCH_DOCTOR_POTION_TOSS_TALENT_ENTRY_ID
|
||||
&& ability.dbcSpellId === COA_WITCH_DOCTOR_POTION_TOSS_SPELL_ID;
|
||||
const isSplash = isCoaWitchDoctorSplashPotion(ability);
|
||||
if (!ingredient && !isPotionToss && !isSplash) return ability;
|
||||
|
||||
if (isSplash) {
|
||||
const baseHeal = ability.effects.find(isSplashPotionBaseHeal);
|
||||
if (!baseHeal) return { ...ability, effects: [] };
|
||||
|
||||
// 802710 expands Jungle, Fish Oil, Frog Bones, and SLS combo payloads into
|
||||
// one static effect list. Source text says riders depend on ingredients in
|
||||
// the Cauldron; until their individual rider semantics are implemented,
|
||||
// retain the verified area heal and project only Frog Bones' sourced 803699
|
||||
// absorb when that ingredient marker is active.
|
||||
const effects: AbilityEffect[] = [baseHeal];
|
||||
if (activeIngredientIds(auras, actorId, now).has("frog-bones")) {
|
||||
effects.push(COA_WITCH_DOCTOR_FROG_BONES_SPLASH_SHIELD);
|
||||
}
|
||||
return effects.length === ability.effects.length
|
||||
&& effects.every((effect, index) => effect === ability.effects[index])
|
||||
? ability
|
||||
: { ...ability, effects };
|
||||
}
|
||||
|
||||
let effects = [...ability.effects];
|
||||
if (ingredient?.id === "frog-bones") {
|
||||
// 801663 is the cauldron toggle. The generic importer mistakes Aura 4's
|
||||
// damage-taken modifier for a short shield; the actual Potion shield is the
|
||||
// separate 802971 payload and belongs on Potion Toss's selected ally.
|
||||
effects = effects.filter((effect) => effect.kind !== "shield"
|
||||
&& !(isApplyAura(effect) && effect.sourceAuraType === 4));
|
||||
effects = effects.map((effect) => {
|
||||
if (!isApplyAura(effect) || effect.sourceAuraType !== 23) return effect;
|
||||
return {
|
||||
...effect,
|
||||
aura: { ...effect.aura, maxStacks: 1, stackBehavior: "refresh" },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (ingredient) {
|
||||
const stateAuraId = `${ACTIVE_AURA_ID_PREFIX}${ingredient.id}`;
|
||||
if (!effects.some((effect) => isApplyAura(effect) && effect.aura.id === stateAuraId)) {
|
||||
effects.push(ingredientStateEffect(ingredient));
|
||||
}
|
||||
}
|
||||
|
||||
if (isPotionToss && activeIngredientIds(auras, actorId, now).has("frog-bones")
|
||||
&& !effects.some(isFrogBonesPotionShield)) {
|
||||
// 802971's shield effect uses Potion Toss's unit target, not the caster and
|
||||
// not the whole party. The DBC maxLevel is 0, so no maxScalingLevel is set.
|
||||
effects.push(COA_WITCH_DOCTOR_FROG_BONES_POTION_SHIELD);
|
||||
}
|
||||
|
||||
return effects.length === ability.effects.length
|
||||
&& effects.every((effect, index) => effect === ability.effects[index])
|
||||
? ability
|
||||
: { ...ability, effects };
|
||||
}
|
||||
|
||||
/** Active ingredient markers owned by this actor only; expired markers do not count. */
|
||||
export function coaWitchDoctorActiveIngredients(
|
||||
auras: readonly ActiveAura[],
|
||||
actorId: string,
|
||||
now: number,
|
||||
): ReadonlySet<CoaWitchDoctorIngredientId> {
|
||||
return activeIngredientIds(auras, actorId, now);
|
||||
}
|
||||
Reference in new issue
Block a user