Complete RoM pair elite mechanics and creature family rules
This commit is contained in:
1 parent
c8b3a3d95a
commit
0206fda9e2
21 files changed
+1082
-134
No files matched your search
+83
-19
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user