Complete RoM pair elite mechanics and creature family rules

This commit is contained in:
phenom committed 2026-09-23 19:38:13 -04:00
1 parent c8b3a3d95a
commit 0206fda9e2
21 files changed
+1082 -134

No files matched your search

+83 -19
View File
@@ -156,8 +156,9 @@ import {
type HeirloomContext,
} from "./heirloomEquipment";
import { romAbilityNeedsShield, romEffectsForCast, romSelfDrainPercent, romSingleTargetControl, romTargetBleedCount } from "./romCombatMechanics";
import { romMageWarlockCriticalEffects } from "./romMageWarlockOverrides";
import { romScoutRogueProcActionSpec } from "./romScoutRogueOverrides";
import { ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_TAG, romFireLightningBurstReset, romMageWarlockCriticalEffects } from "./romMageWarlockOverrides";
import { romScoutRogueProcActionSpec, romScoutRogueWhirlwindBonusEffect } from "./romScoutRogueOverrides";
import { romCreatureFamilyForServerEntry, romCreatureFamilyTargetTags, type CombatCreatureFamily } from "./romCreatureFamilies";
import { defaultPartyRoleForClass, type PartyRole } from "./partyRoles";
import { specializationById } from "./specializationCatalog";
import {
@@ -393,6 +394,7 @@ export interface MobCombatState {
readonly id: string;
readonly name: string;
readonly serverEntry: number | null;
readonly creatureFamily: CombatCreatureFamily;
readonly summonedBy?: string;
readonly spawnGroupId?: string;
readonly boss: boolean;
@@ -592,6 +594,7 @@ export interface CharacterCombatSeed {
export interface MobRegistration {
readonly name: string;
readonly serverEntry?: number;
readonly creatureFamily?: CombatCreatureFamily;
readonly boss?: boolean;
readonly level?: number;
readonly resistances?: Partial<MobMagicResistances>;
@@ -1220,7 +1223,9 @@ function addHealingThreatInWork(
const engagedIds = Object.values(work.mobs)
.filter((mob) => mob.engaged && !mob.dead)
.map((mob) => mob.id);
const perMob = threatFromHealing(effectiveHealing, engagedIds.length) * (source.threatScale ?? 1);
const perMob = threatFromHealing(effectiveHealing, engagedIds.length)
* (source.threatScale ?? 1)
* resolveAuraStatValue(1, aurasForEntity(work.auras, source.actorId), "threat");
if (perMob <= 0) return;
for (const id of engagedIds) {
const mob = work.mobs[id];
@@ -1390,21 +1395,24 @@ function playerCharacterStats(
});
}
function npcCombatDefender(mob: MobCombatState, attackerLevel: number): CombatDefenderStats {
function npcCombatDefender(mob: MobCombatState, attackerLevel: number, auras: readonly ActiveAura[]): CombatDefenderStats {
const levelAdvantage = Math.max(0, mob.level - attackerLevel);
const resistance = (value: number, school: DamageSchool): number => Math.max(0,
resolveAuraStatValue(resolveAuraStatValue(value, auras, `resistance:${school}`), auras, "magical-defense"),
);
return {
level: mob.level,
armor: mob.armor,
armor: Math.max(0, resolveAuraStatValue(mob.armor, auras, "armor")),
dodgePercent: 5 + levelAdvantage * 0.5,
parryPercent: 5 + levelAdvantage * 0.5,
blockPercent: 0,
blockValue: 0,
resistances: {
arcane: mob.resistances.arcane,
fire: mob.resistances.fire,
frost: mob.resistances.frost,
nature: mob.resistances.nature,
shadow: mob.resistances.shadow,
arcane: resistance(mob.resistances.arcane, "arcane"),
fire: resistance(mob.resistances.fire, "fire"),
frost: resistance(mob.resistances.frost, "frost"),
nature: resistance(mob.resistances.nature, "nature"),
shadow: resistance(mob.resistances.shadow, "shadow"),
},
};
}
@@ -1471,7 +1479,7 @@ function resolvePlayerDamageResult(
attackKind,
damageSchool: school,
attacker,
defender: mob.characterDefender ?? npcCombatDefender(mob, stats.level),
defender: mob.characterDefender ?? npcCombatDefender(mob, stats.level, aurasForEntity(work.auras, mob.id)),
rules: immune
? {
...rules,
@@ -2070,6 +2078,7 @@ function summonedMobCombatState(
id,
name: template?.name ?? `Summoned Creature ${creatureEntry}`,
serverEntry: creatureEntry,
creatureFamily: romCreatureFamilyForServerEntry(creatureEntry),
summonedBy: summoner.id,
...(spawnGroupId ? { spawnGroupId } : {}),
boss: false,
@@ -2842,7 +2851,8 @@ function damageMobInWork(
: addActorThreat(
engagedMob.threatByActor,
source.actorId,
threatFromDamage(applied, source.role, source.threatScale),
threatFromDamage(applied, source.role, source.threatScale)
* resolveAuraStatValue(1, aurasForEntity(work.auras, source.actorId), "threat"),
);
work.mobs[id] = {
...engagedMob,
@@ -4139,6 +4149,9 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const existing = state.mobs[id];
const boss = registration.boss ?? existing?.boss ?? false;
const serverEntry = registration.serverEntry ?? existing?.serverEntry ?? null;
const creatureFamily = registration.creatureFamily
?? (registration.serverEntry === undefined && existing?.creatureFamily
? existing.creatureFamily : romCreatureFamilyForServerEntry(serverEntry));
const lootSourceId = registration.lootSourceId ?? existing?.lootSourceId ?? null;
const level = Math.max(1, Math.trunc(registration.level ?? existing?.level ?? state.level));
const resistances: MobMagicResistances = {
@@ -4208,6 +4221,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
...existing,
name: registration.name || existing.name,
serverEntry,
creatureFamily,
boss,
lootSourceId,
level,
@@ -4238,6 +4252,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
id,
name: registration.name || "Unknown Creature",
serverEntry,
creatureFamily,
boss,
lootSourceId,
level,
@@ -4931,9 +4946,6 @@ export const useCombatStore = create<CombatState>((set, get) => ({
else if (ability.id === "rom-mage-primary-55" && !hasRuneWakerState("rom-charged")) failure = "condition-not-met";
else if (ability.id === "rom-mage-primary-56" && !hasRuneWakerState("rom-static-field-charge")) failure = "condition-not-met";
else if (ability.dbcSpellId === 493017 && !hasRuneWakerState("rom-poisonous")) failure = "condition-not-met";
else if (ability.dbcSpellId === 492920 && !state.effects.some((effect) =>
effect.kind === "dot" && effect.targetId === targetId && effect.remainingTicks > 0
&& effect.sourceAbilityId === "rom-rogue-rom-warrior-elite-35")) failure = "condition-not-met";
else if (abilityRequiresOutOfCombat(ability)
&& !(ability.source === "runewaker" && state.classId === "rom-rogue" && hasRuneWakerState("rom-vanish"))
&& Object.values(state.mobs).some((mob) => mob.engaged && !mob.dead)) failure = "condition-not-met";
@@ -5013,6 +5025,13 @@ export const useCombatStore = create<CombatState>((set, get) => ({
if (!failure && ability?.name === "Power Word: Shield"
&& hasWeakenedSoul(state.auras, targetId ?? PLAYER_AURA_ENTITY_ID, now)) failure = "condition-not-met";
if (!failure && ability?.source === "runewaker") {
const family = targetId ? state.mobs[targetId]?.creatureFamily : undefined;
if (ability.dbcSpellId === 491613 && family === "living"
&& !hasRuneWakerState("rom-evil-shackles")) failure = "condition-not-met";
if (ability.dbcSpellId === 490347 && family === "undead") failure = "condition-not-met";
if (ability.dbcSpellId === 492920 && !state.effects.some((effect) =>
effect.kind === "dot" && effect.targetId === targetId && effect.remainingTicks > 0
&& effect.sourceAbilityId === "rom-rogue-rom-warrior-elite-35")) failure = "condition-not-met";
if (ability.dbcSpellId === 490151
&& (!targetId || state.comboTargetId !== targetId || state.comboPoints <= 0)) failure = "condition-not-met";
if (ability.dbcSpellId === 490347 && !hasRuneWakerState("rom-poisonous")) failure = "condition-not-met";
@@ -5352,6 +5371,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
.flatMap((aura) => aura.definition.utilityTags ?? []),
targetTags: work.auras.filter((aura) => aura.targetId === targetId)
.flatMap((aura) => aura.definition.utilityTags ?? []).concat(
romCreatureFamilyTargetTags(targetId && work.mobs[targetId]
? work.mobs[targetId].creatureFamily : "unknown"),
work.effects.filter((effect) => effect.targetId === targetId && effect.remainingTicks > 0)
.flatMap((effect) => [
...(effect.tag ? [effect.tag] : []),
@@ -5360,6 +5381,12 @@ export const useCombatStore = create<CombatState>((set, get) => ({
]),
),
});
if (ability.dbcSpellId === 492920 && targetId
&& castEffects.some((effect) => effect.kind === "dot" && effect.tag === "rom-622992")) {
work.effects = work.effects.filter((effect) => !(effect.kind === "dot"
&& effect.targetId === targetId
&& effect.sourceAbilityId === "rom-rogue-rom-warrior-elite-35"));
}
const effectsToResolve = completingCast?.mode === "channel"
? castEffects.filter((effect) => !["damage", "heal", "dot", "hot", "resource-drain"].includes(effect.kind))
: castEffects;
@@ -5415,6 +5442,24 @@ export const useCombatStore = create<CombatState>((set, get) => ({
ability.name === "Shiv" ? { canCrit: false } : {},
);
grantDamageResourceInWork(work, effect, result.amount, castThreatSource.actorId, ability.id, now);
if (ability.dbcSpellId === 490133) {
const bonus = romScoutRogueWhirlwindBonusEffect(
ability,
aurasForEntity(work.auras, PLAYER_AURA_ENTITY_ID).flatMap((aura) => aura.definition.utilityTags ?? []),
work.effects.filter((active) => active.targetId === id && active.remainingTicks > 0)
.flatMap((active) => active.tag ? [active.tag] : []),
);
if (bonus) {
const bonusAmount = talentAdjustedAmount(
equipmentAdjustedCombatAmount(bonus, work.progression.level, work.gearStats, state.classId, ability),
modifiers, "damage-percent", ability,
);
resolveAndDamageMobInWork(
state.classId, work, id, bonusAmount, now, castThreatSource,
"melee", "physical", `${castThreatSource.actorId}:${ability.id}:${id}:${now}:poison-bonus`, ability,
);
}
}
}
} else if (effect.kind === "finisher-damage") {
const comboPoints = work.comboPoints;
@@ -5980,7 +6025,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
: effectiveTarget === "hostile"
? affectedMobIds
: [targetId ?? PLAYER_AURA_ENTITY_ID];
for (const recipientId of recipients) {
for (const recipientId of recipients.filter((id) => !effect.excludeCaster || id !== PLAYER_AURA_ENTITY_ID)) {
const previousAuras = work.auras;
if (effect.control && work.mobs[recipientId]?.characterDefender && work.mobs[recipientId].mechanicImmunities.includes(effect.control.kind as AzerothCoreMechanic)) continue;
const auraResult = applyCombatAura(previousAuras, {
@@ -6430,9 +6475,26 @@ export const useCombatStore = create<CombatState>((set, get) => ({
}
}
}
if (ability.dbcSpellId === 498759 && targetId && work.mobs[targetId]
&& !work.mobs[targetId].dead && work.mobs[targetId].health > 0) {
const burstReset = romFireLightningBurstReset(
ability,
Boolean(targetId && work.mobs[targetId] && !work.mobs[targetId].dead && work.mobs[targetId].health > 0),
aurasForEntity(work.auras, PLAYER_AURA_ENTITY_ID).some((aura) =>
aura.definition.utilityTags?.includes(ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_TAG)),
);
if (burstReset.reset) {
cooldowns[ability.id] = 0;
work.auras = [...applyCombatAura(work.auras, {
definition: {
id: "rom-fire-lightning-burst-reset-guard",
name: "Fire Lightning Burst reset guard",
disposition: "buff",
durationMs: burstReset.guardDurationMs,
utilityTags: [ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_TAG],
},
sourceId: PLAYER_AURA_ENTITY_ID,
targetId: PLAYER_AURA_ENTITY_ID,
now,
}).auras];
}
const primaryCombatResult = work.combatResults.length === 1 ? work.combatResults[0] : null;
@@ -6875,7 +6937,9 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const engagedIds = Object.values(state.mobs)
.filter((mob) => mob.engaged && !mob.dead)
.map((mob) => mob.id);
const perMob = threatFromHealing(effectiveHealing, engagedIds.length) * (source.threatScale ?? 1);
const perMob = threatFromHealing(effectiveHealing, engagedIds.length)
* (source.threatScale ?? 1)
* resolveAuraStatValue(1, aurasForEntity(state.auras, source.actorId), "threat");
if (perMob <= 0) return state;
const mobs = { ...state.mobs };
for (const id of engagedIds) {