diff --git a/docs/ROM_FIVE_VALIDATION.md b/docs/ROM_FIVE_VALIDATION.md index aa5d3d09..4fbb0378 100644 --- a/docs/ROM_FIVE_VALIDATION.md +++ b/docs/ROM_FIVE_VALIDATION.md @@ -31,14 +31,17 @@ The five named builds and live NPCs use these race-legal pairings. Their seconda ## Accepted pair skill checks -`src/game/romPriestKnightOverrides.ts`, `src/game/romScoutRogueOverrides.ts`, and `src/game/romMageWarlockOverrides.ts` specialize the five accepted pairs from source components. `src/game/romCombatMechanics.ts` selects their conditional cast effects; `src/game/combatStore.ts` handles proc actions, damage schools, conditional cooldowns, line and chain targeting, and timed effects. `src/game/partyRuntime.ts` installs learned NPC passive auras on their first cast and passes passive and target tags into the shared selector. The three pair validation files are part of `npm run test:rom-five`; the focused run passed 196 tests across ten files on 2026-09-23. These include direct healing, area healing and damage, taunts, resource changes, offensive and defensive auras, crowd control, and source-conditional skill checks. Pure effect tests are evidence for translation and selection, not proof that every NPC cast and encounter timing is correct. +`src/game/romPriestKnightOverrides.ts`, `src/game/romScoutRogueOverrides.ts`, and `src/game/romMageWarlockOverrides.ts` specialize the five accepted pairs from source components. `src/game/romCombatMechanics.ts` selects their conditional cast effects; `src/game/combatStore.ts` handles proc actions, damage schools, conditional cooldowns, line and chain targeting, and timed effects. `src/game/partyRuntime.ts` installs learned NPC passive auras on their first cast and passes passive and target tags into the shared selector. The three pair validation files are part of `npm run test:rom-five`; the focused run passed 227 tests across eleven files on 2026-09-23. These include direct healing, area healing and damage, taunts, resource changes, offensive and defensive auras, crowd control, source-conditional skill checks, and selected live player/companion casts. Pure effect tests are evidence for translation and selection, not proof that every encounter timing is correct. + +The hidden source references are now executable through their parent skills where RuneWaker treats them as replacements: Priest/Knight Wave Armor `499967`, Knight/Scout Holy Chains `499925` and Blood Arrow `499924`, Scout/Rogue Reflected Shot `499535` and Wrist Attack `499537`, Rogue/Warrior Poison Spray `499477` and Whirlwind `499481`, and Mage/Warlock Soul Pain `498746`, Plasma Arrow `498749`, Elements of Pride `498755`/`498756`, and Elemental Extraction `499812`/`499851`. Priest/Knight Devotion Halo `499966` is a separate level-70 button: it gives the whole party Wisdom and reduces other members' actual Mana spending. Poisonous Spit supplies its own Poison Powder and Poison Spray consumes the selected target's Infection poison before spreading its upgraded poison. Toxic Splash can grant Rage on Infection. The source-only Holy Chains trigger `499910` has no measurable player-facing payload in the extracted data, so no extra effect is invented. + +The RuneWaker NPC object database now supplies Undead and Demon family labels through `src/game/romCreatureFamilies.ts`. Base Holy Chains targets those families; Evil Shackles opens living targets with a shorter root. Numbing Dagger rejects known Undead targets. Unknown or unregistered NPC families remain permissive. Ammo, Poison Powder, Rogue Throw projectiles, and other required bag reagents are treated as an unlimited, level-appropriate supply; casts do not consume an inventory stack. Remaining source and runtime gaps: -1. Source replacement skills referenced by several level-60/70 elites are absent from the playable generated catalog: Priest/Knight `499967`/`499966`, Knight/Scout `499925`/`499924`/`499910`, and Scout/Rogue or Rogue/Warrior `499535`/`499537`/`499477`/`499481`. Toxic Splash and Poison Battle Technique cannot execute their replacement-skill proc payloads yet. Several known magnitudes and chances use explicitly tagged hybrid estimates. -2. NPC passive tags now feed shared conditional casts. Player-side Poisonous Spit has a live proc test; companion proc execution and cooldown/resource side effects still need focused end-to-end coverage. This matters for paired NPCs even when the player-side tests pass. -3. Holy Chains and Evil Shackles need creature-family data for their Undead/Demon rule. Numbing Dagger also needs living/Humanoid target classification and projectile inventory consumption. Poisonous Spit currently recognizes a Poisonous aura as a stand-in for carrying Poison Powder; an actual Powder inventory item is not modeled. Knight/Scout bow-specific effects need a legal ranged loadout; the current Knight heirloom uses mace/shield. Rogue/Warrior One-Hand Axe Mastery cannot benefit the current dual-dagger layout. -4. Mage/Warlock damage remains low relative to Scout and Rogue. Check its AI rotation, cast cadence, and WoW-equivalent scaling before adjusting encounter tuning or promoting RoM. +1. Some source proc chances and magnitudes still use explicit hybrid estimates where the extracted source does not provide a direct WoW-stat conversion. NPC proc execution and resource side effects need more end-to-end coverage across encounters. +2. The current Knight heirloom uses mace/shield, so Knight/Scout bow effects require a separate legal ranged loadout. Rogue/Warrior One-Hand Axe Mastery does not benefit the current dual-dagger loadout. +3. Mage/Warlock damage remains low relative to Scout and Rogue. Check its AI rotation, cast cadence, and WoW-equivalent scaling before adjusting encounter tuning or promoting RoM. ## Combat diagnostic @@ -59,7 +62,7 @@ After the mechanics corrections, the targeted Shadoj run at level 60 with the sa ## Before Ready promotion 1. Resolve the persistent damage spread: Rogue is now far ahead of Scout and Mage, while Knight tank damage remains above both. Review source formulas, resource cadence, and rotations before tuning coefficients. -2. Finish source-specific restrictions that have no current inventory or creature-family authority. Numbing Dagger's projectile consumption and living/Humanoid target rule still need those systems. A source-row audit should continue beyond the corrected conditional skills; basic semantic coverage does not prove full tooltip parity. +2. Expand source-row and live NPC-cast audits beyond these corrected skills. Unknown NPC family IDs are permissive until their source records are classified; basic semantic coverage does not prove full tooltip parity. 3. Exercise behind-target requirements in a live spatial scene. The headless balance runner has no spatial target provider, so its Rogue damage can include casts that a live scene would reject. 4. Validate Priest throughput against encounters that actually demand sustained healing, and run RoM NPC seat tests after the mechanics above are resolved. The current manual run leaves Priest player-controlled. 5. Verify both Rogue daggers visually from an unobstructed angle. The live roster and dungeon screenshots in `artifacts/playtest/rom-five/` confirm rendered bodies and weapon silhouettes, but the off-hand dagger is unclear at their camera angles. Keep RoM entries at Needs Testing until the full class mechanics and role checks pass. diff --git a/docs/ROM_STATS_AND_GEARING.md b/docs/ROM_STATS_AND_GEARING.md index e6b029e9..fdaf785c 100644 --- a/docs/ROM_STATS_AND_GEARING.md +++ b/docs/ROM_STATS_AND_GEARING.md @@ -56,6 +56,6 @@ RoM equipment has base item properties plus additional attribute sources. [Gamef ## Online and validation boundary -`server/database.mjs` rejects a RoM/WoW source mixture when a grouped player selects a character and checks again when an activity starts. Until RoM AI companions are separately ready, online RoM groups launch players-only; the WoW-only randomized Bite-Sized Stockades activity is unavailable to them. This does not change local manual RoM party diagnostics. The five RoM profiles remain **Needs Testing** while elite-skill source gaps and the Mage damage outlier are investigated. +`server/database.mjs` rejects a RoM/WoW source mixture when a grouped player selects a character and checks again when an activity starts. Until RoM AI companions are separately ready, online RoM groups launch players-only; the WoW-only randomized Bite-Sized Stockades activity is unavailable to them. This does not change local manual RoM party diagnostics. The five RoM profiles remain **Needs Testing** while live NPC-cast coverage, equipment compatibility, and the Mage damage outlier are investigated. The identified level-60/70 source replacement skills now execute through their parent abilities, and Priest/Knight Devotion Halo is a separate playable skill. Validate the hybrid with legal slot/equip tests for all five builds, stat growth across 1–79, `npm run test:rom-five`, and targeted level/encounter diagnostics. Check Knight threat and survival, Priest effective healing and sustain, and the three DPS roles separately before promotion. If package rewards are added, compare their translated upgrades and combat outcomes. A common encounter definition can use source-specific tuning coefficients; no duplicate WoW/RoM dungeon catalogs are required by this audit. diff --git a/package.json b/package.json index 00901b6b..73c2cd7c 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "balance:lab:playtest": "node scripts/playtest-balance-lab.cjs", "readiness:playtest": "node scripts/playtest-companion-readiness.cjs", "test:ready-five": "vitest run src/game/companionSpecReadiness.test.ts src/game/readyFiveCertification.test.ts && python -m unittest scripts.test_balance_lab scripts.test_balance_lab_brackets", - "test:rom-five": "vitest run src/game/romFiveValidation.test.ts src/game/romPriestKnightPairValidation.test.ts src/game/romScoutRoguePairValidation.test.ts src/game/romMageWarlockPairValidation.test.ts src/game/romCombatRuntime.test.ts src/game/romCombatMechanics.test.ts src/game/romAbilityCatalogSemantics.test.ts src/game/romPlayerCatalog.test.ts src/game/specializationCatalog.test.ts src/game/romHeirloomReference.test.ts", + "test:rom-five": "vitest run src/game/romFiveValidation.test.ts src/game/romPriestKnightPairValidation.test.ts src/game/romScoutRoguePairValidation.test.ts src/game/romMageWarlockPairValidation.test.ts src/game/romCombatRuntime.test.ts src/game/romCombatMechanics.test.ts src/game/romAbilityCatalogSemantics.test.ts src/game/romPlayerCatalog.test.ts src/game/specializationCatalog.test.ts src/game/romHeirloomReference.test.ts src/game/romCreatureFamilies.test.ts", "balance:smoke": "vite-node scripts/combat-benchmark/run.ts --class mage --role damage --scenario damage-single-burst --seeds 1 --duration-scale 0.2", "prebuild": "npm run loot:generate && npm run web:prepare && npm run assets:ktx2:audit && npm run assets:performance:audit", "build": "node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build", diff --git a/scripts/runewaker-pipeline/export-rom-creature-families.mjs b/scripts/runewaker-pipeline/export-rom-creature-families.mjs new file mode 100644 index 00000000..969cf62b --- /dev/null +++ b/scripts/runewaker-pipeline/export-rom-creature-families.mjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { projectRoot, readJson, sourceRootFor } from "./lib/recipe.mjs"; + +const recipe = await readJson(path.join(projectRoot, "scripts/runewaker-pipeline/recipes/raksha-temple-population.json")); +const source = path.join(sourceRootFor(recipe), recipe.source.resourceRoot ?? "Resource", "data/npcobject.db"); +const bytes = await readFile(source); +const dataStart = 140; +const count = bytes.readInt32LE(132); +const recordSize = 2_224; +const raceOffset = 176; +if (count <= 0 || dataStart + count * recordSize > bytes.length) throw new Error("Unexpected NPCObjectDB layout"); +const knownIds = []; +const undeadIds = []; +const demonIds = []; +for (let index = 0; index < count; index += 1) { + const row = dataStart + index * recordSize; + if (bytes.readInt32LE(row + 4) !== 1) throw new Error(`Unexpected NPC object type at row ${index}`); + const id = bytes.readInt32LE(row); + if (id <= 0) continue; + knownIds.push(id); + const race = bytes.readInt32LE(row + raceOffset); + // ObjectStruct.h Race_ENUM names value 5 Undead. The source Demon Soldier + // and Demon Bodo NPC records use value 12. + if (race === 5) undeadIds.push(id); + if (race === 12) demonIds.push(id); +} +const sorted = (ids) => [...new Set(ids)].sort((left, right) => left - right); +const output = { + schemaVersion: 1, + sourceSha256: createHash("sha256").update(bytes).digest("hex"), + raceOffset, + knownIds: sorted(knownIds), + undeadIds: sorted(undeadIds), + demonIds: sorted(demonIds), +}; +await writeFile( + path.join(projectRoot, "src/game/generated/romCreatureFamilies.json"), + `${JSON.stringify(output)}\n`, +); +console.log(`Exported ${output.knownIds.length} NPC families (${output.undeadIds.length} undead, ${output.demonIds.length} demon).`); diff --git a/src/game/abilityCatalog.ts b/src/game/abilityCatalog.ts index 07812986..8606feb8 100644 --- a/src/game/abilityCatalog.ts +++ b/src/game/abilityCatalog.ts @@ -182,6 +182,8 @@ export type AbilityEffect = readonly recipient?: "ability-target" | "caster"; /** Party/raid area auras are applied to the player and every active party member. */ readonly scope?: "target" | "party"; + /** Some party effects benefit allies but exclude the caster. */ + readonly excludeCaster?: boolean; readonly control?: AbilityAuraControl; readonly sourceEffectType: number; readonly sourceAuraType: number; diff --git a/src/game/combatStore.ts b/src/game/combatStore.ts index ab39865a..071cb7ff 100644 --- a/src/game/combatStore.ts +++ b/src/game/combatStore.ts @@ -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; @@ -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((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((set, get) => ({ ...existing, name: registration.name || existing.name, serverEntry, + creatureFamily, boss, lootSourceId, level, @@ -4238,6 +4252,7 @@ export const useCombatStore = create((set, get) => ({ id, name: registration.name || "Unknown Creature", serverEntry, + creatureFamily, boss, lootSourceId, level, @@ -4931,9 +4946,6 @@ export const useCombatStore = create((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((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((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((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((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((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((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((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) { diff --git a/src/game/generated/romCreatureFamilies.json b/src/game/generated/romCreatureFamilies.json new file mode 100644 index 00000000..cf566e21 --- /dev/null +++ b/src/game/generated/romCreatureFamilies.json @@ -0,0 +1 @@ +{"schemaVersion":1,"sourceSha256":"b627b6eb269c30576a6558000a31abe2db472689ef9a60cc1c1e20207abd69f1","raceOffset":176,"knownIds":[100001,100002,100003,100004,100005,100006,100007,100008,100009,100010,100011,100012,100013,100014,100015,100016,100017,100018,100019,100020,100021,100022,100023,100024,100025,100026,100027,100028,100029,100030,100031,100032,100033,100034,100035,100036,100037,100038,100039,100040,100041,100042,100043,100044,100045,100046,100047,100048,100049,100050,100051,100052,100053,100054,100055,100056,100057,100058,100059,100060,100061,100062,100063,100064,100065,100066,100067,100068,100069,100070,100071,100072,100073,100074,100075,100076,100077,100078,100079,100080,100081,100082,100083,100084,100085,100086,100087,100088,100089,100090,100091,100092,100093,100094,100095,100096,100097,100098,100099,100100,100101,100102,100103,100104,100105,100106,100107,100108,100109,100110,100111,100112,100113,100114,100115,100116,100117,100118,100119,100120,100121,100122,100123,100124,100125,100126,100127,100128,100129,100130,100131,100132,100133,100134,100135,100136,100137,100138,100139,100140,100141,100142,100143,100144,100145,100146,100147,100148,100149,100150,100151,100152,100153,100154,100155,100156,100157,100158,100159,100160,100161,100162,100163,100164,100165,100166,100167,100168,100169,100170,100171,100172,100173,100174,100175,100176,100177,100178,100179,100180,100181,100182,100183,100184,100185,100186,100187,100188,100189,100190,100191,100192,100193,100194,100195,100196,100197,100198,100199,100200,100201,100202,100203,100204,100205,100206,100207,100208,100209,100210,100211,100212,100213,100214,100215,100216,100217,100218,100219,100220,100221,100222,100223,100224,100225,100226,100227,100228,100229,100230,100231,100232,100233,100234,100235,100236,100237,100238,100239,100240,100241,100242,100243,100244,100245,100246,100247,100248,100249,100250,100251,100252,100253,100254,100255,100256,100257,100258,100259,100260,100261,100262,100263,100264,100265,100266,100267,100268,100269,100270,100271,100272,100273,100274,100275,100276,100277,100278,100279,100280,100281,100282,100283,100284,100285,100286,100287,100288,100289,100290,100291,100292,100293,100294,100295,100296,100297,100298,100299,100300,100301,100302,100303,100304,100305,100306,100307,100308,100309,100310,100311,100312,100313,100314,100315,100316,100317,100318,100319,100320,100321,100322,100323,100324,100325,100326,100327,100328,100329,100330,100331,100332,100333,100334,100335,100336,100337,100338,100339,100340,100341,100342,100343,100344,100345,100346,100347,100348,100349,100350,100351,100352,100353,100354,100355,100356,100357,100358,100359,100360,100361,100362,100363,100364,100365,100366,100367,100368,100369,100370,100371,100372,100373,100374,100375,100376,100377,100378,100379,100380,100381,100382,100383,100384,100385,100386,100387,100388,100389,100390,100391,100392,100393,100394,100395,100396,100397,100398,100399,100400,100401,100402,100403,100404,100405,100406,100407,100408,100409,100410,100411,100412,100413,100414,100415,100416,100417,100418,100419,100420,100421,100422,100423,100424,100425,100426,100427,100428,100429,100430,100431,100432,100433,100434,100435,100436,100437,100438,100439,100440,100441,100442,100443,100444,100445,100446,100447,100448,100449,100450,100451,100452,100453,100454,100455,100456,100457,100458,100459,100460,100461,100462,100463,100464,100465,100466,100467,100468,100469,100470,100471,100472,100473,100474,100475,100476,100477,100478,100479,100480,100481,100482,100483,100484,100485,100486,100487,100488,100489,100490,100491,100492,100493,100494,100495,100496,100497,100498,100499,100500,100501,100502,100503,100504,100505,100506,100507,100508,100509,100510,100511,100512,100513,100514,100515,100516,100517,100518,100519,100520,100521,100522,100523,100524,100525,100526,100527,100528,100529,100530,100531,100532,100533,100534,100535,100536,100537,100538,100539,100540,100541,100542,100543,100544,100545,100546,100547,100548,100549,100550,100551,100552,100553,100554,100555,100556,100557,100558,100559,100560,100561,100562,100563,100564,100565,100566,100567,100568,100569,100570,100571,100572,100573,100574,100575,100576,100577,100578,100579,100580,100581,100582,100583,100584,100585,100586,100587,100588,100589,100590,100591,100592,100593,100594,100595,100596,100597,100598,100599,100600,100601,100602,100603,100604,100605,100606,100607,100608,100609,100610,100611,100612,100613,100614,100615,100616,100617,100618,100619,100620,100621,100622,100623,100624,100625,100626,100627,100628,100629,100630,100631,100632,100633,100634,100635,100636,100637,100638,100639,100640,100641,100642,100643,100644,100645,100646,100647,100648,100649,100650,100651,100652,100653,100654,100655,100656,100657,100658,100659,100660,100661,100662,100663,100664,100665,100666,100667,100668,100669,100670,100671,100672,100673,100674,100675,100676,100677,100678,100679,100680,100681,100682,100683,100684,100685,100686,100687,100688,100689,100690,100691,100692,100693,100694,100695,100696,100697,100698,100699,100700,100701,100702,100703,100704,100705,100706,100707,100708,100709,100710,100711,100712,100713,100714,100715,100716,100717,100718,100719,100720,100721,100722,100723,100724,100725,100726,100727,100728,100729,100730,100731,100732,100733,100734,100735,100736,100737,100738,100739,100740,100741,100742,100743,100744,100745,100746,100747,100748,100749,100750,100751,100752,100753,100754,100755,100756,100757,100758,100759,100760,100761,100762,100763,100764,100765,100766,100767,100768,100769,100770,100771,100772,100773,100774,100775,100776,100777,100778,100779,100780,100781,100782,100783,100784,100785,100786,100787,100788,100789,100790,100791,100792,100793,100794,100795,100796,100797,100798,100799,100800,100801,100802,100803,100804,100805,100806,100807,100808,100809,100810,100811,100812,100813,100814,100815,100816,100817,100818,100819,100820,100821,100822,100823,100824,100825,100826,100827,100828,100829,100830,100831,100832,100833,100834,100835,100836,100837,100838,100839,100840,100841,100842,100843,100844,100845,100846,100847,100848,100849,100850,100851,100852,100853,100854,100855,100856,100857,100858,100859,100860,100861,100862,100863,100864,100865,100866,100867,100868,100869,100870,100871,100872,100873,100874,100875,100876,100877,100878,100879,100880,100881,100882,100883,100884,100885,100886,100887,100888,100889,100890,100891,100892,100893,100894,100895,100896,100897,100898,100899,100900,100901,100902,100903,100904,100905,100906,100907,100908,100909,100910,100911,100912,100913,100914,100915,100916,100917,100918,100919,100920,100921,100922,100923,100924,100925,100926,100927,100928,100929,100930,100931,100932,100933,100934,100935,100936,100937,100938,100939,100940,100941,100942,100943,100944,100945,100946,100947,100948,100949,100950,100951,100952,100953,100954,100955,100956,100957,100958,100959,100960,100961,100962,100963,100964,100965,100966,100967,100968,100969,100970,100971,100972,100973,100974,100975,100976,100977,100978,100979,100980,100981,100982,100983,100984,100985,100986,100987,100988,100989,100990,100991,100992,100993,100994,100995,100996,100997,100998,100999,101000,101001,101002,101003,101004,101005,101006,101007,101008,101009,101010,101011,101012,101013,101014,101015,101016,101017,101018,101019,101020,101021,101022,101023,101024,101025,101026,101027,101028,101029,101030,101031,101032,101033,101034,101035,101036,101037,101038,101039,101040,101041,101042,101043,101044,101045,101046,101047,101048,101049,101050,101051,101052,101053,101054,101055,101056,101057,101058,101059,101060,101061,101062,101063,101064,101065,101066,101067,101068,101069,101070,101071,101072,101073,101074,101075,101076,101077,101078,101079,101080,101081,101082,101083,101084,101085,101086,101087,101088,101089,101090,101091,101092,101093,101094,101095,101096,101097,101098,101099,101100,101101,101102,101103,101104,101105,101106,101107,101108,101109,101110,101111,101112,101113,101114,101115,101116,101117,101118,101119,101120,101121,101122,101123,101124,101125,101126,101127,101128,101129,101130,101131,101132,101133,101134,101135,101136,101137,101138,101139,101140,101141,101142,101143,101144,101145,101146,101147,101148,101149,101150,101151,101152,101153,101154,101155,101156,101157,101158,101159,101160,101161,101162,101163,101164,101165,101166,101167,101168,101169,101170,101171,101172,101173,101174,101175,101176,101177,101178,101179,101180,101181,101182,101183,101184,101185,101186,101187,101188,101189,101190,101191,101192,101193,101194,101195,101196,101197,101198,101199,101200,101201,101202,101203,101204,101205,101206,101207,101208,101209,101210,101211,101212,101213,101214,101215,101216,101217,101218,101219,101220,101221,101222,101223,101224,101225,101226,101227,101228,101229,101230,101231,101232,101233,101234,101235,101236,101237,101238,101239,101240,101241,101242,101243,101244,101245,101246,101247,101248,101249,101250,101251,101252,101253,101254,101255,101256,101257,101258,101259,101260,101261,101262,101263,101264,101265,101266,101267,101268,101269,101270,101271,101272,101273,101274,101275,101276,101277,101278,101279,101280,101281,101282,101283,101284,101285,101286,101287,101288,101289,101290,101291,101292,101293,101294,101295,101296,101297,101298,101299,101300,101301,101302,101303,101304,101305,101306,101307,101308,101309,101310,101311,101312,101313,101314,101315,101316,101317,101318,101319,101320,101321,101322,101323,101324,101325,101326,101327,101328,101329,101330,101331,101332,101333,101334,101335,101336,101337,101338,101339,101340,101341,101342,101343,101344,101345,101346,101347,101348,101349,101350,101351,101352,101353,101354,101355,101356,101357,101358,101359,101360,101361,101362,101363,101364,101365,101366,101367,101368,101369,101370,101371,101372,101373,101374,101375,101376,101377,101378,101379,101380,101381,101382,101383,101384,101385,101386,101387,101388,101389,101390,101391,101392,101393,101394,101395,101396,101397,101398,101399,101400,101401,101402,101403,101404,101405,101406,101407,101408,101409,101410,101411,101412,101413,101414,101415,101416,101417,101418,101419,101420,101421,101422,101423,101424,101425,101426,101427,101428,101429,101430,101431,101432,101433,101434,101435,101436,101437,101438,101439,101440,101441,101442,101443,101444,101445,101446,101447,101448,101449,101450,101451,101452,101453,101454,101455,101456,101457,101458,101459,101460,101461,101462,101463,101464,101465,101466,101467,101468,101469,101470,101471,101472,101473,101474,101475,101476,101477,101478,101479,101480,101481,101482,101483,101484,101485,101486,101487,101488,101489,101490,101491,101492,101493,101494,101495,101496,101497,101498,101499,101500,101501,101502,101503,101504,101505,101506,101507,101508,101509,101510,101511,101512,101513,101514,101515,101516,101517,101518,101519,101520,101521,101522,101523,101524,101525,101526,101527,101528,101529,101530,101531,101532,101533,101534,101535,101536,101537,101538,101539,101540,101541,101542,101543,101544,101545,101546,101547,101548,101549,101550,101551,101552,101553,101554,101555,101556,101557,101558,101559,101560,101561,101562,101563,101564,101565,101566,101567,101568,101569,101570,101571,101572,101573,101574,101575,101576,101577,101578,101579,101580,101581,101582,101583,101584,101585,101586,101587,101588,101589,101590,101591,101592,101593,101594,101595,101596,101597,101598,101599,101600,101601,101602,101603,101604,101605,101606,101607,101608,101609,101610,101611,101612,101613,101614,101615,101616,101617,101618,101619,101620,101621,101622,101623,101624,101625,101626,101627,101628,101629,101630,101631,101632,101633,101634,101635,101636,101637,101638,101639,101640,101641,101642,101643,101644,101645,101646,101647,101648,101649,101650,101651,101652,101653,101654,101655,101656,101657,101658,101659,101660,101661,101662,101663,101664,101665,101666,101667,101668,101669,101670,101671,101672,101673,101674,101675,101676,101677,101678,101679,101680,101681,101682,101683,101684,101685,101686,101687,101688,101689,101690,101691,101692,101693,101694,101695,101696,101697,101698,101699,101700,101701,101702,101703,101704,101705,101706,101707,101708,101709,101710,101711,101712,101713,101714,101715,101716,101717,101718,101719,101720,101721,101722,101723,101724,101725,101726,101727,101728,101729,101730,101731,101732,101733,101734,101735,101736,101737,101738,101739,101740,101741,101742,101743,101744,101745,101746,101747,101748,101749,101750,101751,101752,101753,101754,101755,101756,101757,101758,101759,101760,101761,101762,101763,101764,101765,101766,101767,101768,101769,101770,101771,101772,101773,101774,101775,101776,101777,101778,101779,101780,101781,101782,101783,101784,101785,101786,101787,101788,101789,101790,101791,101792,101793,101794,101795,101796,101797,101798,101799,101800,101801,101802,101803,101804,101805,101806,101807,101808,101809,101810,101811,101812,101813,101814,101815,101816,101817,101818,101819,101820,101821,101822,101823,101824,101825,101826,101827,101828,101829,101830,101831,101832,101833,101834,101835,101836,101837,101838,101839,101840,101841,101842,101843,101844,101845,101846,101847,101848,101849,101850,101851,101852,101853,101854,101855,101856,101857,101858,101859,101860,101861,101862,101863,101864,101865,101866,101867,101868,101869,101870,101871,101872,101873,101874,101875,101876,101877,101878,101879,101880,101881,101882,101883,101884,101885,101886,101887,101888,101889,101890,101891,101892,101893,101894,101895,101896,101897,101898,101899,101900,101901,101902,101903,101904,101905,101906,101907,101908,101909,101910,101911,101912,101913,101914,101915,101916,101917,101918,101919,101920,101921,101922,101923,101924,101925,101926,101927,101928,101929,101930,101931,101932,101933,101934,101935,101936,101937,101938,101939,101940,101941,101942,101943,101944,101945,101946,101947,101948,101949,101950,101951,101952,101953,101954,101955,101956,101957,101958,101959,101960,101961,101962,101963,101964,101965,101966,101967,101968,101969,101970,101971,101972,101973,101974,101975,101976,101977,101978,101979,101980,101981,101982,101983,101984,101985,101986,101987,101988,101989,101990,101991,101992,101993,101994,101995,101996,101997,101998,101999,102000,102001,102002,102003,102004,102005,102006,102007,102008,102009,102010,102011,102012,102013,102014,102015,102016,102017,102018,102019,102020,102021,102022,102023,102024,102025,102026,102027,102028,102029,102030,102031,102032,102033,102034,102035,102036,102037,102038,102039,102040,102041,102042,102043,102044,102045,102046,102047,102048,102049,102050,102051,102052,102053,102054,102055,102056,102057,102058,102059,102060,102061,102062,102063,102064,102065,102066,102067,102068,102069,102070,102071,102072,102073,102074,102075,102076,102077,102078,102079,102080,102081,102082,102083,102084,102085,102086,102087,102088,102089,102090,102091,102092,102093,102094,102095,102096,102097,102098,102099,102100,102101,102102,102103,102104,102105,102106,102107,102108,102109,102110,102111,102112,102113,102114,102115,102116,102117,102118,102119,102120,102121,102122,102123,102124,102125,102126,102127,102128,102129,102130,102131,102132,102133,102134,102135,102136,102137,102138,102139,102140,102141,102142,102143,102144,102145,102146,102147,102148,102149,102150,102151,102152,102153,102154,102155,102156,102157,102158,102159,102160,102161,102162,102163,102164,102165,102166,102167,102168,102169,102170,102171,102172,102173,102174,102175,102176,102177,102178,102179,102180,102181,102182,102183,102184,102185,102186,102187,102188,102189,102190,102191,102192,102193,102194,102195,102196,102197,102198,102199,102200,102201,102202,102203,102204,102205,102206,102207,102208,102209,102210,102211,102212,102213,102214,102215,102216,102217,102218,102219,102220,102221,102222,102223,102224,102225,102226,102227,102228,102229,102230,102231,102232,102233,102234,102235,102236,102237,102238,102239,102240,102241,102242,102243,102244,102245,102246,102247,102248,102249,102250,102251,102252,102253,102254,102255,102256,102257,102258,102259,102260,102261,102262,102263,102264,102265,102266,102267,102268,102269,102270,102271,102272,102273,102274,102275,102276,102277,102278,102279,102280,102281,102282,102283,102284,102285,102286,102287,102288,102289,102290,102291,102292,102293,102294,102295,102296,102297,102298,102299,102300,102301,102302,102303,102304,102305,102306,102307,102308,102309,102310,102311,102312,102313,102314,102315,102316,102317,102318,102319,102320,102321,102322,102323,102324,102325,102326,102327,102328,102329,102330,102331,102332,102333,102334,102335,102336,102337,102338,102339,102340,102341,102342,102343,102344,102345,102346,102347,102348,102349,102350,102351,102352,102353,102354,102355,102356,102357,102358,102359,102360,102361,102362,102363,102364,102365,102366,102367,102368,102369,102370,102371,102372,102373,102374,102375,102376,102377,102378,102379,102380,102381,102382,102383,102384,102385,102386,102387,102388,102389,102390,102391,102392,102393,102394,102395,102396,102397,102398,102399,102400,102401,102402,102403,102404,102405,102406,102407,102408,102409,102410,102411,102412,102413,102414,102415,102416,102417,102418,102419,102420,102421,102422,102423,102424,102425,102426,102427,102428,102429,102430,102431,102432,102433,102434,102435,102436,102437,102438,102439,102440,102441,102442,102443,102444,102445,102446,102447,102448,102449,102450,102451,102452,102453,102454,102455,102456,102457,102458,102459,102460,102461,102462,102463,102464,102465,102466,102467,102468,102469,102470,102471,102472,102473,102474,102475,102476,102477,102478,102479,102480,102481,102482,102483,102484,102485,102486,102487,102488,102489,102490,102491,102492,102493,102494,102495,102496,102497,102498,102499,102500,102501,102502,102503,102504,102505,102506,102507,102508,102509,102510,102511,102512,102513,102514,102515,102516,102517,102518,102519,102520,102521,102522,102523,102524,102525,102526,102527,102528,102529,102530,102531,102532,102533,102534,102535,102536,102537,102538,102539,102540,102541,102542,102543,102544,102545,102546,102547,102548,102549,102550,102551,102552,102553,102554,102555,102556,102557,102558,102559,102560,102561,102562,102563,102564,102565,102566,102567,102568,102569,102570,102571,102572,102573,102574,102575,102576,102577,102578,102579,102580,102581,102582,102583,102584,102585,102586,102587,102588,102589,102590,102591,102592,102593,102594,102595,102596,102597,102598,102599,102600,102601,102602,102603,102604,102605,102606,102607,102608,102609,102610,102611,102612,102613,102614,102615,102616,102617,102618,102619,102620,102621,102622,102623,102624,102625,102626,102627,102628,102629,102630,102631,102632,102633,102634,102635,102636,102637,102638,102639,102640,102641,102642,102643,102644,102645,102646,102647,102648,102649,102650,102651,102652,102653,102654,102655,102656,102657,102658,102659,102660,102661,102662,102663,102664,102665,102666,102667,102668,102669,102670,102671,102672,102673,102674,102675,102676,102677,102678,102679,102680,102681,102682,102683,102684,102685,102686,102687,102688,102689,102690,102691,102692,102693,102694,102695,102696,102697,102698,102699,102700,102701,102702,102703,102704,102705,102706,102707,102708,102709,102710,102711,102712,102713,102714,102715,102716,102717,102718,102719,102720,102721,102722,102723,102724,102725,102726,102727,102728,102729,102730,102731,102732,102733,102734,102735,102736,102737,102738,102739,102740,102741,102742,102743,102744,102745,102746,102747,102748,102749,102750,102751,102752,102753,102754,102755,102756,102757,102758,102759,102760,102761,102762,102763,102764,102765,102766,102767,102768,102769,102770,102771,102772,102773,102774,102775,102776,102777,102778,102779,102780,102781,102782,102783,102784,102785,102786,102787,102788,102789,102790,102791,102792,102793,102794,102795,102796,102797,102798,102799,102800,102801,102802,102803,102804,102805,102806,102807,102808,102809,102810,102811,102812,102813,102814,102815,102816,102817,102818,102819,102820,102821,102822,102823,102824,102825,102826,102827,102828,102829,102830,102831,102832,102833,102834,102835,102836,102837,102838,102839,102840,102841,102842,102843,102844,102845,102846,102847,102848,102849,102850,102851,102852,102853,102854,102855,102856,102857,102858,102859,102860,102861,102862,102863,102864,102865,102866,102867,102868,102869,102870,102871,102872,102873,102874,102875,102876,102877,102878,102879,102880,102881,102882,102883,102884,102885,102886,102887,102888,102889,102890,102891,102892,102893,102894,102895,102896,102897,102898,102899,102900,102901,102902,102903,102904,102905,102906,102907,102908,102909,102910,102911,102912,102913,102914,102915,102916,102917,102918,102919,102920,102921,102922,102923,102924,102925,102926,102927,102928,102929,102930,102931,102932,102933,102934,102935,102936,102937,102938,102939,102940,102941,102942,102943,102944,102945,102946,102947,102948,102949,102950,102951,102952,102953,102954,102955,102956,102957,102958,102959,102960,102961,102962,102963,102964,102965,102966,102967,102968,102969,102970,102971,102972,102973,102974,102975,102976,102977,102978,102979,102980,102981,102982,102983,102984,102985,102986,102987,102988,102989,102990,102991,102992,102993,102994,102995,102996,102997,102998,102999,103000,103001,103002,103003,103004,103005,103006,103007,103008,103009,103010,103011,103012,103013,103014,103015,103016,103017,103018,103019,103020,103021,103022,103023,103024,103025,103026,103027,103028,103029,103030,103031,103032,103033,103034,103035,103036,103037,103038,103039,103040,103041,103042,103043,103044,103045,103046,103047,103048,103049,103050,103051,103052,103053,103054,103055,103056,103057,103058,103059,103060,103061,103062,103063,103064,103065,103066,103067,103068,103069,103070,103071,103072,103073,103074,103075,103076,103077,103078,103079,103080,103081,103082,103083,103084,103085,103086,103087,103088,103089,103090,103091,103092,103093,103094,103095,103096,103097,103098,103099,103100,103101,103102,103103,103104,103105,103106,103107,103108,103109,103110,103111,103112,103113,103114,103115,103116,103117,103118,103119,103120,103121,103122,103123,103124,103125,103126,103127,103128,103129,103130,103131,103132,103133,103134,103135,103136,103137,103138,103139,103140,103141,103142,103143,103144,103145,103146,103147,103148,103149,103150,103151,103152,103153,103154,103155,103156,103157,103158,103159,103160,103161,103162,103163,103164,103165,103166,103167,103168,103169,103170,103171,103172,103173,103174,103175,103176,103177,103178,103179,103180,103181,103182,103183,103184,103185,103186,103187,103188,103189,103190,103191,103192,103193,103194,103195,103196,103197,103198,103199,103200,103201,103202,103203,103204,103205,103206,103207,103208,103209,103210,103211,103212,103213,103214,103215,103216,103217,103218,103219,103220,103221,103222,103223,103224,103225,103226,103227,103228,103229,103230,103231,103232,103233,103234,103235,103236,103237,103238,103239,103240,103241,103242,103243,103244,103245,103246,103247,103248,103249,103250,103251,103252,103253,103254,103255,103256,103257,103258,103259,103260,103261,103262,103263,103264,103265,103266,103267,103268,103269,103270,103271,103272,103273,103274,103275,103276,103277,103278,103279,103280,103281,103282,103283,103284,103285,103286,103287,103288,103289,103290,103291,103292,103293,103294,103295,103296,103297,103298,103299,103300,103301,103302,103303,103304,103305,103306,103307,103308,103309,103310,103311,103312,103313,103314,103315,103316,103317,103318,103319,103320,103321,103322,103323,103324,103325,103326,103327,103328,103329,103330,103331,103332,103333,103334,103335,103336,103337,103338,103339,103340,103341,103342,103343,103344,103345,103346,103347,103348,103349,103350,103351,103352,103353,103354,103355,103356,103357,103358,103359,103360,103361,103362,103363,103364,103365,103366,103367,103368,103369,103370,103371,103372,103373,103374,103375,103376,103377,103378,103379,103380,103381,103382,103383,103384,103385,103386,103387,103388,103389,103390,103391,103392,103393,103394,103395,103396,103397,103398,103399,103400,103401,103402,103403,103404,103405,103406,103407,103408,103409,103410,103411,103412,103413,103414,103415,103416,103417,103418,103419,103420,103421,103422,103423,103424,103425,103426,103427,103428,103429,103430,103431,103432,103433,103434,103435,103436,103437,103438,103439,103440,103441,103442,103443,103444,103445,103446,103447,103448,103449,103450,103451,103452,103453,103454,103455,103456,103457,103458,103459,103460,103461,103462,103463,103464,103465,103466,103467,103468,103469,103470,103471,103472,103473,103474,103475,103476,103477,103478,103479,103480,103481,103482,103483,103484,103485,103486,103487,103488,103489,103490,103491,103492,103493,103494,103495,103496,103497,103498,103499,103500,103501,103502,103503,103504,103505,103506,103507,103508,103509,103510,103511,103512,103513,103514,103515,103516,103517,103518,103519,103520,103521,103522,103523,103524,103525,103526,103527,103528,103529,103530,103531,103532,103533,103534,103535,103536,103537,103538,103539,103540,103541,103542,103543,103544,103545,103546,103547,103548,103549,103550,103551,103552,103553,103554,103555,103556,103557,103558,103559,103560,103561,103562,103563,103564,103565,103566,103567,103568,103569,103570,103571,103572,103573,103574,103575,103576,103577,103578,103579,103580,103581,103582,103583,103584,103585,103586,103587,103588,103589,103590,103591,103592,103593,103594,103595,103596,103597,103598,103599,103600,103601,103602,103603,103604,103605,103606,103607,103608,103609,103610,103611,103612,103613,103614,103615,103616,103617,103618,103619,103620,103621,103622,103623,103624,103625,103626,103627,103628,103629,103630,103631,103632,103633,103634,103635,103636,103637,103638,103639,103640,103641,103642,103643,103644,103645,103646,103647,103648,103649,103650,103651,103652,103653,103654,103655,103656,103657,103658,103659,103660,103661,103662,103663,103664,103665,103666,103667,103668,103669,103670,103671,103672,103673,103674,103675,103676,103677,103678,103679,103680,103681,103682,103683,103684,103685,103686,103687,103688,103689,103690,103691,103692,103693,103694,103695,103696,103697,103698,103699,103700,103701,103702,103703,103704,103705,103706,103707,103708,103709,103710,103711,103712,103713,103714,103715,103716,103717,103718,103719,103720,103721,103722,103723,103724,103725,103726,103727,103728,103729,103730,103731,103732,103733,103734,103735,103736,103737,103738,103739,103740,103741,103742,103743,103744,103745,103746,103747,103748,103749,103750,103751,103752,103753,103754,103755,103756,103757,103758,103759,103760,103761,103762,103763,103764,103765,103766,103767,103768,103769,103770,103771,103772,103773,103774,103775,103776,103777,103778,103779,103780,103781,103782,103783,103784,103785,103786,103787,103788,103789,103790,103791,103792,103793,103794,103795,103796,103797,103798,103799,103800,103801,103802,103803,103804,103805,103806,103807,103808,103809,103810,103811,103812,103813,103814,103815,103816,103817,103818,103819,103820,103821,103822,103823,103824,103825,103826,103827,103828,103829,103830,103831,103832,103833,103834,103835,103836,103837,103838,103839,103840,103841,103842,103843,103844,103845,103846,103847,103848,103849,103850,103851,103852,103853,103854,103855,103856,103857,103858,103859,103860,103861,103862,103863,103864,103865,103866,103867,103868,103869,103870,103871,103872,103873,103874,103875,103876,103877,103878,103879,103880,103881,103882,103883,103884,103885,103886,103887,103888,103889,103890,103891,103892,103893,103894,103895,103896,103897,103898,103899,103900,103901,103902,103903,103904,103905,103906,103907,103908,103909,103910,103911,103912,103913,103914,103915,103916,103917,103918,103919,103920,103921,103922,103923,103924,103925,103926,103927,103928,103929,103930,103931,103932,103933,103934,103935,103936,103937,103938,103939,103940,103941,103942,103943,103944,103945,103946,103947,103948,103949,103950,103951,103952,103953,103954,103955,103956,103957,103958,103959,103960,103961,103962,103963,103964,103965,103966,103967,103968,103969,103970,103971,103972,103973,103974,103975,103976,103977,103978,103979,103980,103981,103982,103983,103984,103985,103986,103987,103988,103989,103990,103991,103992,103993,103994,103995,103996,103997,103998,103999,104000,104001,104002,104003,104004,104005,104006,104007,104008,104009,104010,104011,104012,104013,104014,104015,104016,104017,104018,104019,104020,104021,104022,104023,104024,104025,104026,104027,104028,104029,104030,104031,104032,104033,104034,104035,104036,104037,104038,104039,104040,104041,104042,104043,104044,104045,104046,104047,104048,104049,104050,104051,104052,104053,104054,104055,104056,104057,104058,104059,104060,104061,104062,104063,104064,104065,104066,104067,104068,104069,104070,104071,104072,104073,104074,104075,104076,104077,104078,104079,104080,104081,104082,104083,104084,104085,104086,104087,104088,104089,104090,104091,104092,104093,104094,104095,104096,104097,104098,104099,104100,104101,104102,104103,104104,104105,104106,104107,104108,104109,104110,104111,104112,104113,104114,104115,104116,104117,104118,104119,104120,104121,104122,104123,104124,104125,104126,104127,104128,104129,104130,104131,104132,104133,104134,104135,104136,104137,104138,104139,104140,104141,104142,104143,104144,104145,104146,104147,104148,104149,104150,104151,104152,104153,104154,104155,104156,104157,104158,104159,104160,104161,104162,104163,104164,104165,104166,104167,104168,104169,104170,104171,104172,104173,104174,104175,104176,104177,104178,104179,104180,104181,104182,104183,104184,104185,104186,104187,104188,104189,104190,104191,104192,104193,104194,104195,104196,104197,104198,104199,104200,104201,104202,104203,104204,104205,104206,104207,104208,104209,104210,104211,104212,104213,104214,104215,104216,104217,104218,104219,104220,104221,104222,104223,104224,104225,104226,104227,104228,104229,104230,104231,104232,104233,104234,104235,104236,104237,104238,104239,104240,104241,104242,104243,104244,104245,104246,104247,104248,104249,104250,104251,104252,104253,104254,104255,104256,104257,104258,104259,104260,104261,104262,104263,104264,104265,104266,104267,104268,104269,104270,104271,104272,104273,104274,104275,104276,104277,104278,104279,104280,104281,104282,104283,104284,104285,104286,104287,104288,104289,104290,104291,104292,104293,104294,104295,104296,104297,104298,104299,104300,104301,104302,104303,104304,104305,104306,104307,104308,104309,104310,104311,104312,104313,104314,104315,104316,104317,104318,104319,104320,104321,104322,104323,104324,104325,104326,104327,104328,104329,104330,104331,104332,104333,104334,104335,104336,104337,104338,104339,104340,104341,104342,104343,104344,104345,104346,104347,104348,104349,104350,104351,104352,104353,104354,104355,104356,104357,104358,104359,104360,104361,104362,104363,104364,104365,104366,104367,104368,104369,104370,104371,104372,104373,104374,104375,104376,104377,104378,104379,104380,104381,104382,104383,104384,104385,104386,104387,104388,104389,104390,104391,104392,104393,104394,104395,104396,104397,104398,104399,104400,104401,104402,104403,104404,104405,104406,104407,104408,104409,104410,104411,104412,104413,104414,104415,104416,104417,104418,104419,104420,104421,104422,104423,104424,104425,104426,104427,104428,104429,104430,104431,104432,104433,104434,104435,104436,104437,104438,104439,104440,104441,104442,104443,104444,104445,104446,104447,104448,104449,104450,104451,104452,104453,104454,104455,104456,104457,104458,104459,104460,104461,104462,104463,104464,104465,104466,104467,104468,104469,104470,104471,104472,104473,104474,104475,104476,104477,104478,104479,104480,104481,104482,104483,104484,104485,104486,104487,104488,104489,104490,104491,104492,104493,104494,104495,104496,104497,104498,104499,104500,104501,104502,104503,104504,104505,104506,104507,104508,104509,104510,104511,104512,104513,104514,104515,104516,104517,104518,104519,104520,104521,104522,104523,104524,104525,104526,104527,104528,104529,104530,104531,104532,104533,104534,104535,104536,104537,104538,104539,104540,104541,104542,104543,104544,104545,104546,104547,104548,104549,104550,104551,104552,104553,104554,104555,104556,104557,104558,104559,104560,104561,104562,104563,104564,104565,104566,104567,104568,104569,104570,104571,104572,104573,104574,104575,104576,104577,104578,104579,104580,104581,104582,104583,104584,104585,104586,104587,104588,104589,104590,104591,104592,104593,104594,104595,104596,104597,104598,104599,104600,104601,104602,104603,104604,104605,104606,104607,104608,104609,104610,104611,104612,104613,104614,104615,104616,104617,104618,104619,104620,104621,104622,104623,104624,104625,104626,104627,104628,104629,104630,104631,104632,104633,104634,104635,104636,104637,104638,104639,104640,104641,104642,104643,104644,104645,104646,104647,104648,104649,104650,104651,104652,104653,104654,104655,104656,104657,104658,104659,104660,104661,104662,104663,104664,104665,104666,104667,104668,104669,104670,104671,104672,104673,104674,104675,104676,104677,104678,104679,104680,104681,104682,104683,104684,104685,104686,104687,104688,104689,104690,104691,104692,104693,104694,104695,104696,104697,104698,104699,104700,104701,104702,104703,104704,104705,104706,104707,104708,104709,104710,104711,104712,104713,104714,104715,104716,104717,104718,104719,104720,104721,104722,104723,104724,104725,104726,104727,104728,104729,104730,104731,104732,104733,104734,104735,104736,104737,104738,104739,104740,104741,104742,104743,104744,104745,104746,104747,104748,104749,104750,104751,104752,104753,104754,104755,104756,104757,104758,104759,104760,104761,104762,104763,104764,104765,104766,104767,104768,104769,104770,104771,104772,104773,104774,104775,104776,104777,104778,104779,104780,104781,104782,104783,104784,104785,104786,104787,104788,104789,104790,104791,104792,104793,104794,104795,104796,104797,104798,104799,104800,104801,104802,104803,104804,104805,104806,104807,104808,104809,104810,104811,104812,104813,104814,104815,104816,104817,104818,104819,104820,104821,104822,104823,104824,104825,104826,104827,104828,104829,104830,104831,104832,104833,104834,104835,104836,104837,104838,104839,104840,104841,104842,104843,104844,104845,104846,104847,104848,104849,104850,104851,104852,104853,104854,104855,104856,104857,104858,104859,104860,104861,104862,104863,104864,104865,104866,104867,104868,104869,104870,104871,104872,104873,104874,104875,104876,104877,104878,104879,104880,104881,104882,104883,104884,104885,104886,104887,104888,104889,104890,104891,104892,104893,104894,104895,104896,104897,104898,104899,104900,104901,104902,104903,104904,104905,104906,104907,104908,104909,104910,104911,104912,104913,104914,104915,104916,104917,104918,104919,104920,104921,104922,104923,104924,104925,104926,104927,104928,104929,104930,104931,104932,104933,104934,104935,104936,104937,104938,104939,104940,104941,104942,104943,104944,104945,104946,104947,104948,104949,104950,104951,104952,104953,104954,104955,104956,104957,104958,104959,104960,104961,104962,104963,104964,104965,104966,104967,104968,104969,104970,104971,104972,104973,104974,104975,104976,104977,104978,104979,104980,104981,104982,104983,104984,104985,104986,104987,104988,104989,104990,104991,104992,104993,104994,104995,104996,104997,104998,104999,105000,105001,105002,105003,105004,105005,105006,105007,105008,105009,105010,105011,105012,105013,105014,105015,105016,105017,105018,105019,105020,105021,105022,105023,105024,105025,105026,105027,105028,105029,105030,105031,105032,105033,105034,105035,105036,105037,105038,105039,105040,105041,105042,105043,105044,105045,105046,105047,105048,105049,105050,105051,105052,105053,105054,105055,105056,105057,105058,105059,105060,105061,105062,105063,105064,105065,105066,105067,105068,105069,105070,105071,105072,105073,105074,105075,105076,105077,105078,105079,105080,105081,105082,105083,105084,105085,105086,105087,105088,105089,105090,105091,105092,105093,105094,105095,105096,105097,105098,105099,105100,105101,105102,105103,105104,105105,105106,105107,105108,105109,105110,105111,105112,105113,105114,105115,105116,105117,105118,105119,105120,105121,105122,105123,105124,105125,105126,105127,105128,105129,105130,105131,105132,105133,105134,105135,105136,105137,105138,105139,105140,105141,105142,105143,105144,105145,105146,105147,105148,105149,105150,105151,105152,105153,105154,105155,105156,105157,105158,105159,105160,105161,105162,105163,105164,105165,105166,105167,105168,105169,105170,105171,105172,105173,105174,105175,105176,105177,105178,105179,105180,105181,105182,105183,105184,105185,105186,105187,105188,105189,105190,105191,105192,105193,105194,105195,105196,105197,105198,105199,105200,105201,105202,105203,105204,105205,105206,105207,105208,105209,105210,105211,105212,105213,105214,105215,105216,105217,105218,105219,105220,105221,105222,105223,105224,105225,105226,105227,105228,105229,105230,105231,105232,105233,105234,105235,105236,105237,105238,105239,105240,105241,105242,105243,105244,105245,105246,105247,105248,105249,105250,105251,105252,105253,105254,105255,105256,105257,105258,105259,105260,105261,105262,105263,105264,105265,105266,105267,105268,105269,105270,105271,105272,105273,105274,105275,105276,105277,105278,105279,105280,105281,105282,105283,105284,105285,105286,105287,105288,105289,105290,105291,105292,105293,105294,105295,105296,105297,105298,105299,105300,105301,105302,105303,105304,105305,105306,105307,105308,105309,105310,105311,105312,105313,105314,105315,105316,105317,105318,105319,105320,105321,105322,105323,105324,105325,105326,105327,105328,105329,105330,105331,105332,105333,105334,105335,105336,105337,105338,105339,105340,105341,105342,105343,105344,105345,105346,105347,105348,105349,105350,105351,105352,105353,105354,105355,105356,105357,105358,105359,105360,105361,105362,105363,105364,105365,105366,105367,105368,105369,105370,105371,105372,105373,105374,105375,105376,105377,105378,105379,105380,105381,105382,105383,105384,105385,105386,105387,105388,105389,105390,105391,105392,105393,105394,105395,105396,105397,105398,105399,105400,105401,105402,105403,105404,105405,105406,105407,105408,105409,105410,105411,105412,105413,105414,105415,105416,105417,105418,105419,105420,105421,105422,105423,105424,105425,105426,105427,105428,105429,105430,105431,105432,105433,105434,105435,105436,105437,105438,105439,105440,105441,105442,105443,105444,105445,105446,105447,105448,105449,105450,105451,105452,105453,105454,105455,105456,105457,105458,105459,105460,105461,105462,105463,105464,105465,105466,105467,105468,105469,105470,105471,105472,105473,105474,105475,105476,105477,105478,105479,105480,105481,105482,105483,105484,105485,105486,105487,105488,105489,105490,105491,105492,105493,105494,105495,105496,105497,105498,105499,105500,105501,105502,105503,105504,105505,105506,105507,105508,105509,105510,105511,105512,105513,105514,105515,105516,105517,105518,105519,105520,105521,105522,105523,105524,105525,105526,105527,105528,105529,105530,105531,105532,105533,105534,105535,105536,105537,105538,105539,105540,105541,105542,105543,105544,105545,105546,105547,105548,105549,105550,105551,105552,105553,105554,105555,105556,105557,105558,105559,105560,105561,105562,105563,105564,105565,105566,105567,105568,105569,105570,105571,105572,105573,105574,105575,105576,105577,105578,105579,105580,105581,105582,105583,105584,105585,105586,105587,105588,105589,105590,105591,105592,105593,105594,105595,105596,105597,105598,105599,105600,105601,105602,105603,105604,105605,105606,105607,105608,105609,105610,105611,105612,105613,105614,105615,105616,105617,105618,105619,105620,105621,105622,105623,105624,105625,105626,105627,105628,105629,105630,105631,105632,105633,105634,105635,105636,105637,105638,105639,105640,105641,105642,105643,105644,105645,105646,105647,105648,105649,105650,105651,105652,105653,105654,105655,105656,105657,105658,105659,105660,105661,105662,105663,105664,105665,105666,105667,105668,105669,105670,105671,105672,105673,105674,105675,105676,105677,105678,105679,105680,105681,105682,105683,105684,105685,105686,105687,105688,105689,105690,105691,105692,105693,105694,105695,105696,105697,105698,105699,105700,105701,105702,105703,105704,105705,105706,105707,105708,105709,105710,105711,105712,105713,105714,105715,105716,105717,105718,105719,105720,105721,105722,105723,105724,105725,105726,105727,105728,105729,105730,105731,105732,105733,105734,105735,105736,105737,105738,105739,105740,105741,105742,105743,105744,105745,105746,105747,105748,105749,105750,105751,105752,105753,105754,105755,105756,105757,105758,105759,105760,105761,105762,105763,105764,105765,105766,105767,105768,105769,105770,105771,105772,105773,105774,105775,105776,105777,105778,105779,105780,105781,105782,105783,105784,105785,105786,105787,105788,105789,105790,105791,105792,105793,105794,105795,105796,105797,105798,105799,105800,105801,105802,105803,105804,105805,105806,105807,105808,105809,105810,105811,105812,105813,105814,105815,105816,105817,105818,105819,105820,105821,105822,105823,105824,105825,105826,105827,105828,105829,105830,105831,105832,105833,105834,105835,105836,105837,105838,105839,105840,105841,105842,105843,105844,105845,105846,105847,105848,105849,105850,105851,105852,105853,105854,105855,105856,105857,105858,105859,105860,105861,105862,105863,105864,105865,105866,105867,105868,105869,105870,105871,105872,105873,105874,105875,105876,105877,105878,105879,105880,105881,105882,105883,105884,105885,105886,105887,105888,105889,105890,105891,105892,105893,105894,105895,105896,105897,105898,105899,105900,105901,105902,105903,105904,105905,105906,105907,105908,105909,105910,105911,105912,105913,105914,105915,105916,105917,105918,105919,105920,105921,105922,105923,105924,105925,105926,105927,105928,105929,105930,105931,105932,105933,105934,105935,105936,105937,105938,105939,105940,105941,105942,105943,105944,105945,105946,105947,105948,105949,105950,105951,105952,105953,105954,105955,105956,105957,105958,105959,105960,105961,105962,105963,105964,105965,105966,105967,105968,105969,105970,105971,105972,105973,105974,105975,105976,105977,105978,105979,105980,105981,105982,105983,105984,105985,105986,105987,105988,105989,105990,105991,105992,105993,105994,105995,105996,105997,105998,105999,106000,106001,106002,106003,106004,106005,106006,106007,106008,106009,106010,106011,106012,106013,106014,106015,106016,106017,106018,106019,106020,106021,106022,106023,106024,106025,106026,106027,106028,106029,106030,106031,106032,106033,106034,106035,106036,106037,106038,106039,106040,106041,106042,106043,106044,106045,106046,106047,106048,106049,106050,106051,106052,106053,106054,106055,106056,106057,106058,106059,106060,106061,106062,106063,106064,106065,106066,106067,106068,106069,106070,106071,106072,106073,106074,106075,106076,106077,106078,106079,106080,106081,106082,106083,106084,106085,106086,106087,106088,106089,106090,106091,106092,106093,106094,106095,106096,106097,106098,106099,106100,106101,106102,106103,106104,106105,106106,106107,106108,106109,106110,106111,106112,106113,106114,106115,106116,106117,106118,106119,106120,106121,106122,106123,106124,106125,106126,106127,106128,106129,106130,106131,106132,106133,106134,106135,106136,106137,106138,106139,106140,106141,106142,106143,106144,106145,106146,106147,106148,106149,106150,106151,106152,106153,106154,106155,106156,106157,106158,106159,106160,106161,106162,106163,106164,106165,106166,106167,106168,106169,106170,106171,106172,106173,106174,106175,106176,106177,106178,106179,106180,106181,106182,106183,106184,106185,106186,106187,106188,106189,106190,106191,106192,106193,106194,106195,106196,106197,106198,106199,106200,106201,106202,106203,106204,106205,106206,106207,106208,106209,106210,106211,106212,106213,106214,106215,106216,106217,106218,106219,106220,106221,106222,106223,106224,106225,106226,106227,106228,106229,106230,106231,106232,106233,106234,106235,106236,106237,106238,106239,106240,106241,106242,106243,106244,106245,106246,106247,106248,106249,106250,106251,106252,106253,106254,106255,106256,106257,106258,106259,106260,106261,106262,106263,106264,106265,106266,106267,106268,106269,106270,106271,106272,106273,106274,106275,106276,106277,106278,106279,106280,106281,106282,106283,106284,106285,106286,106287,106288,106289,106290,106291,106292,106293,106294,106295,106296,106297,106298,106299,106300,106301,106302,106303,106304,106305,106306,106307,106308,106309,106310,106311,106312,106313,106314,106315,106316,106317,106318,106319,106320,106321,106322,106323,106324,106325,106326,106327,106328,106329,106330,106331,106332,106333,106334,106335,106336,106337,106338,106339,106340,106341,106342,106343,106344,106345,106346,106347,106348,106349,106350,106351,106352,106353,106354,106355,106356,106357,106358,106359,106360,106361,106362,106363,106364,106365,106366,106367,106368,106369,106370,106371,106372,106373,106374,106375,106376,106377,106378,106379,106380,106381,106382,106383,106384,106385,106386,106387,106388,106389,106390,106391,106392,106393,106394,106395,106396,106397,106398,106399,106400,106401,106402,106403,106404,106405,106406,106407,106408,106409,106410,106411,106412,106413,106414,106415,106416,106417,106418,106419,106420,106421,106422,106423,106424,106425,106426,106427,106428,106429,106430,106431,106432,106433,106434,106435,106436,106437,106438,106439,106440,106441,106442,106443,106444,106445,106446,106447,106448,106449,106450,106451,106452,106453,106454,106455,106456,106457,106458,106459,106460,106461,106462,106463,106464,106465,106466,106467,106468,106469,106470,106471,106472,106473,106474,106475,106476,106477,106478,106479,106480,106481,106482,106483,106484,106485,106486,106487,106488,106489,106490,106491,106492,106493,106494,106495,106496,106497,106498,106499,106500,106501,106502,106503,106504,106505,106506,106507,106508,106509,106510,106511,106512,106513,106514,106515,106516,106517,106518,106519,106520,106521,106522,106523,106524,106525,106526,106527,106528,106529,106530,106531,106532,106533,106534,106535,106536,106537,106538,106539,106540,106541,106542,106543,106544,106545,106546,106547,106548,106549,106550,106551,106552,106553,106554,106555,106556,106557,106558,106559,106560,106561,106562,106563,106564,106565,106566,106567,106568,106569,106570,106571,106572,106573,106574,106575,106576,106577,106578,106579,106580,106581,106582,106583,106584,106585,106586,106587,106588,106589,106590,106591,106592,106593,106594,106595,106596,106597,106598,106599,106600,106601,106602,106603,106604,106605,106606,106607,106608,106609,106610,106611,106612,106613,106614,106615,106616,106617,106618,106619,106620,106621,106622,106623,106624,106625,106626,106627,106628,106629,106630,106631,106632,106633,106634,106635,106636,106637,106638,106639,106640,106641,106642,106643,106644,106645,106646,106647,106648,106649,106650,106651,106652,106653,106654,106655,106656,106657,106658,106659,106660,106661,106662,106663,106664,106665,106666,106667,106668,106669,106670,106671,106672,106673,106674,106675,106676,106677,106678,106679,106680,106681,106682,106683,106684,106685,106686,106687,106688,106689,106690,106691,106692,106693,106694,106695,106696,106697,106698,106699,106700,106701,106702,106703,106704,106705,106706,106707,106708,106709,106710,106711,106712,106713,106714,106715,106716,106717,106718,106719,106720,106721,106722,106723,106724,106725,106726,106727,106728,106729,106730,106731,106732,106733,106734,106735,106736,106737,106738,106739,106740,106741,106742,106743,106744,106745,106746,106747,106748,106749,106750,106751,106752,106753,106754,106755,106756,106757,106758,106759,106760,106761,106762,106763,106764,106765,106766,106767,106768,106769,106770,106771,106772,106773,106774,106775,106776,106777,106778,106779,106780,106781,106782,106783,106784,106785,106786,106787,106788,106789,106790,106791,106792,106793,106794,106795,106796,106797,106798,106799,106800,106801,106802,106803,106804,106805,106806,106807,106808,106809,106810,106811,106812,106813,106814,106815,106816,106817,106818,106819,106820,106821,106822,106823,106824,106825,106826,106827,106828,106829,106830,106831,106832,106833,106834,106835,106836,106837,106838,106839,106840,106841,106842,106843,106844,106845,106846,106847,106848,106849,106850,106851,106852,106853,106854,106855,106856,106857,106858,106859,106860,106861,106862,106863,106864,106865,106866,106867,106868,106869,106870,106871,106872,106873,106874,106875,106876,106877,106878,106879,106880,106881,106882,106883,106884,106885,106886,106887,106888,106889,106890,106891,106892,106893,106894,106895,106896,106897,106898,106899,106900,106901,106902,106903,106904,106905,106906,106907,106908,106909,106910,106911,106912,106913,106914,106915,106916,106917,106918,106919,106920,106921,106922,106923,106924,106925,106926,106927,106928,106929,106930,106931,106932,106933,106934,106935,106936,106937,106938,106939,106940,106941,106942,106943,106944,106945,106946,106947,106948,106949,106950,106951,106952,106953,106954,106955,106956,106957,106958,106959,106960,106961,106962,106963,106964,106965,106966,106967,106968,106969,106970,106971,106972,106973,106974,106975,106976,106977,106978,106979,106980,106981,106982,106983,106984,106985,106986,106987,106988,106989,106990,106991,106992,106993,106994,106995,106996,106997,106998,106999,107000,107001,107002,107003,107004,107005,107006,107007,107008,107009,107010,107011,107012,107013,107014,107015,107016,107017,107018,107019,107020,107021,107022,107023,107024,107025,107026,107027,107028,107029,107030,107031,107032,107033,107034,107035,107036,107037,107038,107039,107040,107041,107042,107043,107044,107045,107046,107047,107048,107049,107050,107051,107052,107053,107054,107055,107056,107057,107058,107059,107060,107061,107062,107063,107064,107065,107066,107067,107068,107069,107070,107071,107072,107073,107074,107075,107076,107077,107078,107079,107080,107081,107082,107083,107084,107085,107086,107087,107088,107089,107090,107091,107092,107093,107094,107095,107096,107097,107098,107099,107100,107101,107102,107103,107104,107105,107106,107107,107108,107109,107110,107111,107112,107113,107114,107115,107116,107117,107118,107119,107120,107121,107122,107123,107124,107125,107126,107127,107128,107129,107130,107131,107132,107133,107134,107135,107136,107137,107138,107139,107140,107141,107142,107143,107144,107145,107146,107147,107148,107149,107150,107151,107152,107153,107154,107155,107156,107157,107158,107159,107160,107161,107162,107163,107164,107165,107166,107167,107168,107169,107170,107171,107172,107173,107174,107175,107176,107177,107178,107179,107180,107181,107182,107183,107184,107185,107186,107187,107188,107189,107190,107191,107192,107193,107194,107195,107196,107197,107198,107199,107200,107201,107202,107203,107204,107205,107206,107207,107208,107209,107210,107211,107212,107213,107214,107215,107216,107217,107218,107219,107220,107221,107222,107223,107224,107225,107226,107227,107228,107229,107230,107231,107232,107233,107234,107235,107236,107237,107238,107239,107240,107241,107242,107243,107244,107245,107246,107247,107248,107249,107250,107251,107252,107253,107254,107255,107256,107257,107258,107259,107260,107261,107262,107263,107264,107265,107266,107267,107268,107269,107270,107271,107272,107273,107274,107275,107276,107277,107278,107279,107280,107281,107282,107283,107284,107285,107286,107287,107288,107289,107290,107291,107292,107293,107294,107295,107296,107297,107298,107299,107300,107301,107302,107303,107304,107305,107306,107307,107308,107309,107310,107311,107312,107313,107314,107315,107316,107317,107318,107319,107320,107321,107322,107323,107324,107325,107326,107327,107328,107329,107330,107331,107332,107333,107334,107335,107336,107337,107338,107339,107340,107341,107342,107343,107344,107345,107346,107347,107348,107349,107350,107351,107352,107353,107354,107355,107356,107357,107358,107359,107360,107361,107362,107363,107364,107365,107366,107367,107368,107369,107370,107371,107372,107373,107374,107375,107376,107377,107378,107379,107380,107381,107382,107383,107384,107385,107386,107387,107388,107389,107390,107391,107392,107393,107394,107395,107396,107397,107398,107399,107400,107401,107402,107403,107404,107405,107406,107407,107408,107409,107410,107411,107412,107413,107414,107415,107416,107417,107418,107419,107420,107421,107422,107423,107424,107425,107426,107427,107428,107429,107430,107431,107432,107433,107434,107435,107436,107437,107438,107439,107440,107441,107442,107443,107444,107445,107446,107447,107448,107449,107450,107451,107452,107453,107454,107455,107456,107457,107458,107459,107460,107461,107462,107463,107464,107465,107466,107467,107468,107469,107470,107471,107472,107473,107474,107475,107476,107477,107478,107479,107480,107481,107482,107483,107484,107485,107486,107487,107488,107489,107490,107491,107492,107493,107494,107495,107496,107497,107498,107499,107500,107501,107502,107503,107504,107505,107506,107507,107508,107509,107510,107511,107512,107513,107514,107515,107516,107517,107518,107519,107520,107521,107522,107523,107524,107525,107526,107527,107528,107529,107530,107531,107532,107533,107534,107535,107536,107537,107538,107539,107540,107541,107542,107543,107544,107545,107546,107547,107548,107549,107550,107551,107552,107553,107554,107555,107556,107557,107558,107559,107560,107561,107562,107563,107564,107565,107566,107567,107568,107569,107570,107571,107572,107573,107574,107575,107576,107577,107578,107579,107580,107581,107582,107583,107584,107585,107586,107587,107588,107589,107590,107591,107592,107593,107594,107595,107596,107597,107598,107599,107600,107601,107602,107603,107604,107605,107606,107607,107608,107609,107610,107611,107612,107613,107614,107615,107616,107617,107618,107619,107620,107621,107622,107623,107624,107625,107626,107627,107628,107629,107630,107631,107632,107633,107634,107635,107636,107637,107638,107639,107640,107641,107642,107643,107644,107645,107646,107647,107648,107649,107650,107651,107652,107653,107654,107655,107656,107657,107658,107659,107660,107661,107662,107663,107664,107665,107666,107667,107668,107669,107670,107671,107672,107673,107674,107675,107676,107677,107678,107679,107680,107681,107682,107683,107684,107685,107686,107687,107688,107689,107690,107691,107692,107693,107694,107695,107696,107697,107698,107699,107700,107701,107702,107703,107704,107705,107706,107707,107708,107709,107710,107711,107712,107713,107714,107715,107716,107717,107718,107719,107720,107721,107722,107723,107724,107725,107726,107727,107728,107729,107730,107731,107732,107733,107734,107735,107736,107737,107738,107739,107740,107741,107742,107743,107744,107745,107746,107747,107748,107749,107750,107751,107752,107753,107754,107755,107756,107757,107758,107759,107760,107761,107762,107763,107764,107765,107766,107767,107768,107769,107770,107771,107772,107773,107774,107775,107776,107777,107778,107779,107780,107781,107782,107783,107784,107785,107786,107787,107788,107789,107790,107791,107792,107793,107794,107795,107796,107797,107798,107799,107800,107801,107802,107803,107804,107805,107806,107807,107808,107809,107810,107811,107812,107813,107814,107815,107816,107817,107818,107819,107820,107821,107822,107823,107824,107825,107826,107827,107828,107829,107830,107831,107832,107833,107834,107835,107836,107837,107838,107839,107840,107841,107842,107843,107844,107845,107846,107847,107848,107849,107850,107851,107852,107853,107854,107855,107856,107857,107858,107859,107860,107861,107862,107863,107864,107865,107866,107867,107868,107869,107870,107871,107872,107873,107874,107875,107876,107877,107878,107879,107880,107881,107882,107883,107884,107885,107886,107887,107888,107889,107890,107891,107892,107893,107894,107895,107896,107897,107898,107899,107900,107901,107902,107903,107904,107905,107906,107907,107908,107909,107910,107911,107912,107913,107914,107915,107916,107917,107918,107919,107920,107921,107922,107923,107924,107925,107926,107927,107928,107929,107930,107931,107932,107933,107934,107935,107936,107937,107938,107939,107940,107941,107942,107943,107944,107945,107946,107947,107948,107949,107950,107951,107952,107953,107954,107955,107956,107957,107958,107959,107960,107961,107962,107963,107964,107965,107966,107967,107968,107969,107970,107971,107972,107973,107974,107975,107976,107977,107978,107979,107980,107981,107982,107983,107984,107985,107986,107987,107988,107989,107990,107991,107992,107993,107994,107995,107996,107997,107998,107999,108000,108001,108002,108003,108004,108005,108006,108007,108008,108009,108010,108011,108012,108013,108014,108015,108016,108017,108018,108019,108020,108021,108022,108023,108024,108025,108026,108027,108028,108029,108030,108031,108032,108033,108034,108035,108036,108037,108038,108039,108040,108041,108042,108043,108044,108045,108046,108047,108048,108049,108050,108051,108052,108053,108054,108055,108056,108057,108058,108059,108060,108061,108062,108063,108064,108065,108066,108067,108068,108069,108070,108071,108072,108073,108074,108075,108076,108077,108078,108079,108080,108081,108082,108083,108084,108085,108086,108087,108088,108089,108090,108091,108092,108093,108094,108095,108096,108097,108098,108099,108100,108101,108102,108103,108104,108105,108106,108107,108108,108109,108110,108111,108112,108113,108114,108115,108116,108117,108118,108119,108120,108121,108122,108123,108124,108125,108126,108127,108128,108129,108130,108131,108132,108133,108134,108135,108136,108137,108138,108139,108140,108141,108142,108143,108144,108145,108146,108147,108148,108149,108150,108151,108152,108153,108154,108155,108156,108157,108158,108159,108160,108161,108162,108163,108164,108165,108166,108167,108168,108169,108170,108171,108172,108173,108174,108175,108176,108177,108178,108179,108180,108181,108182,108183,108184,108185,108186,108187,108188,108189,108190,108191,108192,108193,108194,108195,108196,108197,108198,108199,108200,108201,108202,108203,108204,108205,108206,108207,108208,108209,108210,108211,108212,108213,108214,108215,108216,108217,108218,108219,108220,108221,108222,108223,108224,108225,108226,108227,108228,108229,108230,108231,108232,108233,108234,108235,108236,108237,108238,108239,108240,108241,108242,108243,108244,108245,108246,108247,108248,108249,108250,108251,108252,108253,108254,108255,108256,108257,108258,108259,108260,108261,108262,108263,108264,108265,108266,108267,108268,108269,108270,108271,108272,108273,108274,108275,108276,108277,108278,108279,108280,108281,108282,108283,108284,108285,108286,108287,108288,108289,108290,108291,108292,108293,108294,108295,108296,108297,108298,108299,108300,108301,108302,108303,108304,108305,108306,108307,108308,108309,108310,108311,108312,108313,108314,108315,108316,108317,108318,108319,108320,108321,108322,108323,108324,108325,108326,108327,108328,108329,108330,108331,108332,108333,108334,108335,108336,108337,108338,108339,108340,108341,108342,108343,108344,108345,108346,108347,108348,108349,108350,108351,108352,108353,108354,108355,108356,108357,108358,108359,108360,108361,108362,108363,108364,108365,108366,108367,108368,108369,108370,108371,108372,108373,108374,108375,108376,108377,108378,108379,108380,108381,108382,108383,108384,108385,108386,108387,108388,108389,108390,108391,108392,108393,108394,108395,108396,108397,108398,108399,108400,108401,108402,108403,108404,108405,108406,108407,108408,108409,108410,108411,108412,108413,108414,108415,108416,108417,108418,108419,108420,108421,108422,108423,108424,108425,108426,108427,108428,108429,108430,108431,108432,108433,108434,108435,108436,108437,108438,108439,108440,108441,108442,108443,108444,108445,108446,108447,108448,108449,108450,108451,108452,108453,108454,108455,108456,108457,108458,108459,108460,108461,108462,108463,108464,108465,108466,108467,108468,108469,108470,108471,108472,108473,108474,108475,108476,108477,108478,108479,108480,108481,108482,108483,108484,108485,108486,108487,108488,108489,108490,108491,108492,108493,108494,108495,108496,108497,108498,108499,108500,108501,108502,108503,108504,108505,108506,108507,108508,108509,108510,108511,108512,108513,108514,108515,108516,108517,108518,108519,108520,108521,108522,108523,108524,108525,108526,108527,108528,108529,108530,108531,108532,108533,108534,108535,108536,108537,108538,108539,108540,108541,108542,108543,108544,108545,108546,108547,108548,108549,108550,108551,108552,108553,108554,108555,108556,108557,108558,108559,108560,108561,108562,108563,108564,108565,108566,108567,108568,108569,108570,108571,108572,108573,108574,108575,108576,108577,108578,108579,108580,108581,108582,108583,108584,108585,108586,108587,108588,108589,108590,108591,108592,108593,108594,108595,108596,108597,108598,108599,108600,108601,108602,108603,108604,108605,108606,108607,108608,108609,108610,108611,108612,108613,108614,108615,108616,108617,108618,108619,108620,108621,108622,108623,108624,108625,108626,108627,108628,108629,108630,108631,108632,108633,108634,108635,108636,108637,108638,108639,108640,108641,108642,108643,108644,108645,108646,108647,108648,108649,108650,108651,108652,108653,108654,108655,108656,108657,108658,108659,108660,108661,108662,108663,108664,108665,108666,108667,108668,108669,108670,108671,108672,108673,108674,108675,108676,108677,108678,108679,108680,108681,108682,108683,108684,108685,108686,108687,108688,108689,108690,108691,108692,108693,108694,108695,108696,108697,108698,108699,108700,108701,108702,108703,108704,108705,108706,108707,108708,108709,108710,108711,108712,108713,108714,108715,108716,108717,108718,108719,108720,108721,108722,108723,108724,108725,108726,108727,108728,108729,108730,108731,108732,108733,108734,108735,108736,108737,108738,108739,108740,108741,108742,108743,108744,108745,108746,108747,108748,108749,108750,108751,108752,108753,108754,108755,108756,108757,108758,108759,108760,108761,108762,108763,108764,108765,108766,108767,108768,108769,108770,108771,108772,108773,108774,108775,108776,108777,108778,108779,108780,108781,108782,108783,108784,108785,108786,108787,108788,108789,108790,108791,108792,108793,108794,108795,108796,108797,108798,108799,108800,108801,108802,108803,108804,108805,108806,108807,108808,108809,108810,108811,108812,108813,108814,108815,108816,108817,108818,108819,108820,108821,108822,108823,108824,108825,108826,108827,108828,108829,108830,108831,108832,108833,108834,108835,108836,108837,108838,108839,108840,108841,108842,108843,108844,108845,108846,108847,108848,108849,108850,108851,108852,108853,108854,108855,108856,108857,108858,108859,108860,108861,108862,108863,108864,108865,108866,108867,108868,108869,108870,108871,108872,108873,108874,108875,108876,108877,108878,108879,108880,108881,108882,108883,108884,108885,108886,108887,108888,108889,108890,108891,108892,108893,108894,108895,108896,108897,108898,108899,108900,108901,108902,108903,108904,108905,108906,108907,108908,108909,108910,108911,108912,108913,108914,108915,108916,108917,108918,108919,108920,108921,108922,108923,108924,108925,108926,108927,108928,108929,108930,108931,108932,108933,108934,108935,108936,108937,108938,108939,108940,108941,108942,108943,108944,108945,108946,108947,108948,108949,108950,108951,108952,108953,108954,108955,108956,108957,108958,108959,108960,108961,108962,108963,108964,108965,108966,108967,108968,108969,108970,108971,108972,108973,108974,108975,108976,108977,108978,108979,108980,108981,108982,108983,108984,108985,108986,108987,108988,108989,108990,108991,108992,108993,108994,108995,108996,108997,108998,108999,109000,109001,109002,109003,109004,109005,109006,109007,109008,109009,109010,109011,109012,109013,109014,109015,109016,109017,109018,109019,109020,109021,109022,109023,109024,109025,109026,109027,109028,109029,109030,109031,109032,109033,109034,109035,109036,109037,109038,109039,109040,109041,109042,109043,109044,109045,109046,109047,109048,109049,109050,109051,109052,109053,109054,109055,109056,109057,109058,109059,109060,109061,109062,109063,109064,109065,109066,109067,109068,109069,109070,109071,109072,109073,109074,109075,109076,109077,109078,109079,109080,109081,109082,109083,109084,109085,109086,109087,109088,109089,109090,109091,109092,109093,109094,109095,109096,109097,109098,109099,109100,109101,109102,109103,109104,109105,109106,109107,109108,109109,109110,109111,109112,109113,109114,109115,109116,109117,109118,109119,109120,109121,109122,109123,109124,109125,109126,109127,109128,109129,109130,109131,109132,109133,109134,109135,109136,109137,109138,109139,109140,109141,109142,109143,109144,109145,109146,109147,109148,109149,109150,109151,109152,109153,109154,109155,109156,109157,109158,109159,109160,109161,109162,109163,109164,109165,109166,109167,109168,109169,109170,109171,109172,109173,109174,109175,109176,109177,109178,109179,109180,109181,109182,109183,109184,109185,109186,109187,109188,109189,109190,109191,109192,109193,109194,109195,109196,109197,109198,109199,109200,109201,109202,109203,109204,109205,109206,109207,109208,109209,109210,109211,109212,109213,109214,109215,109216,109217,109218,109219,109220,109221,109222,109223,109224,109225,109226,109227,109228,109229,109230,109231,109232,109233,109234,109235,109236,109237,109238,109239,109240,109241,109242,109243,109244,109245,109246,109247,109248,109249,109250,109251,109252,109253,109254,109255,109256,109257,109258,109259,109260,109261,109262,109263,109264,109265,109266,109267,109268,109269,109270,109271,109272,109273,109274,109275,109276,109277,109278,109279,109280,109281,109282,109283,109284,109285,109286,109287,109288,109289,109290,109291,109292,109293,109294,109295,109296,109297,109298,109299,109300,109301,109302,109303,109304,109305,109306,109307,109308,109309,109310,109311,109312,109313,109314,109315,109316,109317,109318,109319,109320,109321,109322,109323,109324,109325,109326,109327,109328,109329,109330,109331,109332,109333,109334,109335,109336,109337,109338,109339,109340,109341,109342,109343,109344,109345,109346,109347,109348,109349,109350,109351,109352,109353,109354,109355,109356,109357,109358,109359,109360,109361,109362,109363,109364,109365,109366,109367,109368,109369,109370,109371,109372,109373,109374,109375,109376,109377,109378,109379,109380,109381,109382,109383,109384,109385,109386,109387,109388,109389,109390,109391,109392,109393,109394,109395,109396,109397,109398,109399,109400,109401,109402,109403,109404,109405,109406,109407,109408,109409,109410,109411,109412,109413,109414,109415,109416,109417,109418,109419,109420,109421,109422,109423,109424,109425,109426,109427,109428,109429,109430,109431,109432,109433,109434,109435,109436,109437,109438,109439,109440,109441,109442,109443,109444,109445,109446,109447,109448,109449,109450,109451,109452,109453,109454,109455,109456,109457,109458,109459,109460,109461,109462,109463,109464,109465,109466,109467,109468,109469,109470,109471,109472,109473,109474,109475,109476,109477,109478,109479,109480,109481,109482,109483,109484,109485,109486,109487,109488,109489,109490,109491,109492,109493,109494,109495,109496,109497,109498,109499,109500,109501,109502,109503,109504,109505,109506,109507,109508,109509,109510,109511,109512,109513,109514,109515,109516,109517,109518,109519,109520,109521,109522,109523,109524,109525,109526,109527,109528,109529,109530,109531,109532,109533,109534,109535,109536,109537,109538,109539,109540,109541,109542,109543,109544,109545,109546,109547,109548,109549,109550,109551,109552,109553,109554,109555,109556,109557,109558,109559,109560,109561,109562,109563,109564,109565,109566,109567,109568,109569,109570,109571,109572,109573,109574,109575,109576,109577,109578,109579,109580,109581,109582,109583,109584,109585,109586,109587,109588,109589,109590,109591,109592,109593,109594,109595,109596,109597,109598,109599,109600,109601,109602,109603,109604,109605,109606,109607,109608,109609,109610,109611,109612,109613,109614,109615,109616,109617,109618,109619,109620,109621,109622,109623,109624,109625,109626,109627,109628,109629,109630,109631,109632,109633,109634,109635,109636,109637,109638,109639,109640,109641,109642,109643,109644,109645,109646,109647,109648,109649,109650,109651,109652,109653,109654,109655,109656,109657,109658,109659,109660,109661,109662,109663,109664,109665,109666,109667,109668,109669,109670,109671,109672,109673,109674,109675,109676,109677,109678,109679,109680,109681,109682,109683,109684,109685,109686,109687,109688,109689,109690,109691,109692,109693,109694,109695,109696,109697,109698,109699,109700,109701,109702,109703,109704,109705,109706,109707,109708,109709,109710,109711,109712,109713,109714,109715,109716,109717,109718,109719,109720,109721,109722,109723,109724,109725,109726,109727,109728,109729,109730,109731,109732,109733,109734,109735,109736,109737,109738,109739,109740,109741,109742,109743,109744,109745,109746,109747,109748,109749,109750,109751,109752,109753,109754,109755,109756,109757,109758,109759,109760,109761,109762,109763,109764,109765,109766,109767,109768,109769,109770,109771,109772,109773,109774,109775,109776,109777,109778,109779,109780,109781,109782,109783,109784,109785,109786,109787,109788,109789,109790,109791,109792,109793,109794,109795,109796,109797,109798,109799,109800,109801,109802,109803,109804,109805,109806,109807,109808,109809,109810,109811,109812,109813,109814,109815,109816,109817,109818,109819,109820,109821,109822,109823,109824,109825,109826,109827,109828,109829,109830,109831,109832,109833,109834,109835,109836,109837,109838,109839,109840,109841,109842,109843,109844,109845,109846,109847,109848,109849,109850,109851,109852,109853,109854,109855,109856,109857,109858,109859,109860,109861,109862,109863,109864,109865,109866,109867,109868,109869,109870,109871,109872,109873,109874,109875,109876,109877,109878,109879,109880,109881,109882,109883,109884,109885,109886,109887,109888,109889,109890,109891,109892,109893,109894,109895,109896,109897,109898,109899,109900,109901,109902,109903,109904,109905,109906,109907,109908,109909,109910,109911,109912,109913,109914,109915,109916,109917,109918,109919,109920,109921,109922,109923,109924,109925,109926,109927,109928,109929,109930,109931,109932,109933,109934,109935,109936,109937,109938,109939,109940,109941,109942,109943,109944,109945,109946,109947,109948,109949,109950,109951,109952,109953,109954,109955,109956,109957,109958,109959,109960,109961,109962,109963,109964,109965,109966,109967,109968,109969,109970,109971,109972,109973,109974,109975,109976,109977,109978,109979,109980,109981,109982,109983,109984,109985,109986,109987,109988,109989,109990,109991,109992,109993,109994,109995,109996,109997,109998,109999,130000],"undeadIds":[100042,100046,100068,100071,100072,100073,100074,100108,100109,100110,100111,100112,100119,100151,100154,100193,100202,100206,100236,100264,100274,100340,100348,100349,100364,100391,100436,100437,100438,100562,100563,100611,100613,100614,100653,100661,100662,100668,100673,100706,100904,100906,100923,100924,101017,101022,101081,101090,101202,101203,101205,101206,101219,101227,101228,101255,101256,101257,101258,101259,101260,101261,101262,101265,101266,101281,101284,101286,101287,101296,101298,101331,101332,101339,101371,101372,101374,101375,101390,101391,101392,101393,101394,101395,101396,101409,101431,101434,101435,101436,101437,101452,101463,101477,101479,101481,101500,101503,101504,101505,101506,101534,101535,101542,101543,101544,101582,101583,101681,101699,101700,101702,101853,101855,101868,102126,102127,102128,102145,102146,102147,102148,102149,102150,102151,102152,102153,102154,102155,102161,102163,102164,102195,102275,102276,102296,102355,102405,102410,102581,102586,102747,102765,102766,102767,102769,102807,102836,102852,102854,102869,103032,103035,103036,103037,103038,103039,103064,103065,103304,103381,103386,103387,103388,103389,103390,103391,103392,103393,103515,103680,103681,103761,103762,103763,103764,103765,103766,103767,103768,103769,103771,103800,103925,103926,103927,103928,103929,103930,103933,103942,103943,103945,103974,104017,104018,104019,104020,104021,104022,104023,104024,104025,104026,104027,104028,104029,104030,104031,104032,104033,104034,104044,104052,104053,104113,104114,104115,104116,104117,104118,104119,104120,104121,104122,104123,104124,104127,104128,104129,104130,104131,104132,104183,104184,104224,104303,104461,104472,104473,104474,104559,104560,104620,104640,104641,104653,104654,104696,104707,104708,104726,104727,104728,104736,104738,104824,104836,104837,104852,104859,104861,104925,104928,104929,104932,104940,104965,104970,104971,104972,104979,104986,105018,105045,105076,105101,105102,105103,105113,105114,105190,105191,105232,105233,105260,105267,105282,105309,105310,105312,105313,105366,105367,105368,105369,105371,105374,105389,105395,105396,105397,105398,105414,105415,105428,105429,105430,105433,105434,105435,105436,105452,105453,105458,105462,105463,105465,105466,105484,105506,105514,105516,105517,105520,105522,105551,105587,105594,105598,105599,105603,105608,105609,105635,105636,105637,105641,105642,105647,105648,105652,105656,105657,105658,105659,105661,105663,105671,105679,105681,105697,105701,105702,105703,105741,105744,105745,105746,105747,105748,105766,105801,105807,105824,105827,105828,105829,105830,105831,105832,105833,105834,105858,105861,105862,105866,105867,105868,105869,105870,105873,105874,105876,105884,105885,105886,105887,105892,105896,105897,105935,105936,105937,105938,105960,105962,105978,105979,105980,106014,106015,106016,106017,106018,106019,106020,106021,106022,106023,106024,106025,106026,106027,106028,106029,106030,106031,106032,106033,106034,106043,106044,106048,106049,106050,106051,106052,106053,106054,106055,106080,106081,106082,106083,106084,106085,106086,106087,106088,106089,106090,106091,106092,106093,106094,106108,106187,106192,106199,106200,106206,106207,106208,106209,106219,106313,106314,106315,106316,106317,106318,106319,106320,106321,106322,106323,106324,106325,106326,106327,106338,106339,106340,106341,106342,106343,106344,106345,106346,106347,106348,106349,106350,106351,106352,106415,106416,106417,106420,106421,106431,106432,106433,106434,106444,106446,106447,106448,106450,106451,106489,106490,106495,106541,106622,106623,106624,106702,106703,106704,106705,106736,106737,106738,106751,106752,106753,106754,106757,106758,106759,106760,106761,106762,106765,106766,106767,106768,106791,106833,106842,106843,106858,107121,107122,107123,107124,107132,107133,107151,107326,107346,107347,107348,107376,107377,107378,107379,107380,107381,107456,107502,107503,107504,107505,107506,107511,107545,107546,107547,107548,107551,107552,107553,107556,107558,107571,107591,107592,107606,107615,107628,107647,107648,107649,107650,107651,107652,107653,107656,107658,107659,107660,107661,107662,107663,107664,107667,107676,107678,107680,107746,107747,107779,107780,107782,107817,107818,107819,107820,107864,107873,107875,107876,107878,107879,107880,107941,107942,107943,107987,107988,107989,107990,108004,108029,108050,108051,108052,108053,108067,108094,108150,108151,108152,108153,108175,108177,108212,108214,108215,108216,108221,108223,108224,108225,108233,108270,108276,108277,108281,108282,108290,108331,108346,108347,108349,108350,108387,108404,108406,108453,108463,108466,108476,108499,108502,108503,108665,108666,108667,108668,108677,108688,108689,108690,108691,108693,108694,108695,108696,108805,108806,108807,108815,108818,108819,108820,108821,108823,108824,108825,108856,108860,108907,108920,108925,108926,108927,108929,108996,109003,109004,109007,109008,109012,109039,109040,109041,109042,109043,109066,109067,109068,109076,109077,109078,109098,109099,109100,109113,109114,109115,109121,109125,109126,109130,109134,109135,109161,109162,109188],"demonIds":[100077,100260,100265,100372,100373,101159,101160,101380,101381,101528,102360,102386,102387,102408,102438,102691,102697,102698,102699,102700,102701,102702,102704,102705,102706,102707,102708,102725,102726,102804,102837,102838,102872,102873,102874,102875,102876,102877,102878,102879,102880,102882,102886,102909,102934,102938,102948,102950,102969,102987,102995,103040,103041,103043,103053,103054,103056,103060,103061,103062,103063,103066,103067,103068,103079,103082,103083,103084,103087,103088,103089,103090,103151,103152,103327,103333,103343,103344,103345,103346,103347,103348,103349,103350,103351,103352,103354,103370,103901,103931,103936,103975,104073,104074,104075,104102,104164,104464,104582,104583,104622,104666,104667,104669,104670,104697,104763,104764,104786,104838,104897,104920,104951,104968,104969,104984,104990,105019,105039,105099,105200,105289,105351,105352,105402,105403,105404,105405,105406,105515,105571,105588,105616,105617,105677,105733,105778,105779,105780,105781,105800,105804,105815,105818,105835,105878,105880,106079,106103,106169,106170,106527,106572,106596,106598,106620,106710,106711,106814,106816,106853,106857,106954,106955,106956,106959,107142,107459,107460,107513,107581,107582,107583,107584,107840,107863,107883,107978,107999,108000,108003,108007,108041,108062,108063,108066,108070,108400,108401]} diff --git a/src/game/partyAbilityAi.ts b/src/game/partyAbilityAi.ts index 76f795c6..606c6de5 100644 --- a/src/game/partyAbilityAi.ts +++ b/src/game/partyAbilityAi.ts @@ -13,7 +13,8 @@ import { type RotationFriendlyTarget, } from "./combatRotation"; import { spendDeathKnightRunes } from "./combatResources"; -import { equipmentAdjustedCombatAmount, type MobCombatState } from "./combatStore"; +import { resolveResourceValue } from "./combatAuras"; +import { equipmentAdjustedCombatAmount, useCombatStore, type MobCombatState } from "./combatStore"; import type { PartyMember } from "./partyStore"; import { baseResourcesFor } from "./wotlkStats"; import { companionResourceMaximum } from "./combatActorAuthority"; @@ -51,12 +52,20 @@ function currentResource(member: PartyMember, type: ResourceType): number { } function abilityCosts(member: PartyMember, ability: AbilityDefinition): readonly { resource: ResourceType; amount: number }[] { - return (ability.costs?.length ? ability.costs : [ability.cost]).map((cost) => ({ - resource: cost.resource, - amount: cost.percentage ? cost.amount / 100 * (cost.percentageBasis === "base-mana" + const auras = useCombatStore.getState().auras.filter((aura) => aura.targetId === member.id); + return (ability.costs?.length ? ability.costs : [ability.cost]).map((cost) => { + const maximum = cost.percentageBasis === "base-mana" ? baseResourcesFor(member.classId, member.level).mana - : cost.resource === "health" ? member.maxHealth : companionResourceMaximum(member, cost.resource)) : cost.amount, - })); + : cost.resource === "health" ? member.maxHealth : companionResourceMaximum(member, cost.resource); + const baseAmount = cost.percentage ? cost.amount / 100 * maximum : cost.amount; + const adjusted = resolveResourceValue( + resolveResourceValue(baseAmount, auras, cost.resource, "cost"), + auras, + "power", + "cost", + ); + return { resource: cost.resource, amount: Math.max(0, Math.round(adjusted)) }; + }); } export function partyAbilityIsReady( diff --git a/src/game/partyRuntime.ts b/src/game/partyRuntime.ts index 75d02c95..711c258b 100644 --- a/src/game/partyRuntime.ts +++ b/src/game/partyRuntime.ts @@ -82,6 +82,9 @@ import { ROGUE_ENVENOM_BUFF_TAG, } from "./roguePoison"; import { romAbilityNeedsShield, romEffectsForCast, romSelfDrainPercent, romTargetBleedCount } from "./romCombatMechanics"; +import { romCreatureFamilyTargetTags } from "./romCreatureFamilies"; +import { romScoutRogueWhirlwindBonusEffect } from "./romScoutRogueOverrides"; +import { ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_TAG, romFireLightningBurstReset } from "./romMageWarlockOverrides"; const OUT_OF_COMBAT_REGEN_FRACTION = 0.06; const ATTACK_ACQUISITION_RANGE = 30; @@ -768,9 +771,17 @@ function partyAbilityTargetIsValid( && aura.definition.disposition === "buff" && aura.definition.dispelCategory === "enrage" && !aura.definition.name.startsWith("Enraged Regeneration"))) return false; if (ability.source === "runewaker") { + if (ability.dbcSpellId === 491613 && mob?.creatureFamily === "living" + && !(member.classId === "rom-knight" && member.secondaryClassId === "rom-scout" && member.level >= 60)) return false; + if (ability.dbcSpellId === 490347 && mob?.creatureFamily === "undead") return false; if (ability.dbcSpellId === 490151 && (member.comboTargetId !== targetId || member.comboPoints <= 0)) return false; if (ability.dbcSpellId === 490347 && !combat.auras.some((aura) => aura.targetId === member.id && aura.definition.utilityTags?.includes("rom-poisonous"))) return false; + if (ability.dbcSpellId === 493017 && !combat.auras.some((aura) => aura.targetId === member.id + && aura.definition.utilityTags?.includes("rom-poisonous"))) return false; + if (ability.dbcSpellId === 492920 && !combat.effects.some((effect) => effect.kind === "dot" + && effect.targetId === targetId && effect.remainingTicks > 0 + && effect.sourceAbilityId === "rom-rogue-rom-warrior-elite-35")) return false; if (ability.dbcSpellId === 491171 && !combat.auras.some((aura) => aura.targetId === member.id && aura.definition.utilityTags?.includes("rom-charged"))) return false; if (ability.dbcSpellId === 491172 && !combat.auras.some((aura) => aura.targetId === member.id @@ -1225,10 +1236,19 @@ function executePartyAbility( ), targetTags: combat.auras.filter((aura) => aura.targetId === targetId) .flatMap((aura) => aura.definition.utilityTags ?? []).concat( + romCreatureFamilyTargetTags(combat.mobs[targetId]?.creatureFamily ?? "unknown"), combat.effects.filter((effect) => effect.targetId === targetId && effect.remainingTicks > 0) .flatMap((effect) => effect.tag ? [effect.tag] : []), ), }); + if (ability.dbcSpellId === 492920 && castEffects.some((effect) => effect.kind === "dot" + && effect.tag === "rom-622992")) { + useCombatStore.setState((state) => ({ + effects: state.effects.filter((effect) => !(effect.kind === "dot" + && effect.targetId === targetId + && effect.sourceAbilityId === "rom-rogue-rom-warrior-elite-35")), + })); + } for (const effect of castEffects) { if (ability.name === "Penance" && combat.mobs[targetId] && (effect.kind === "heal" || effect.kind === "hot")) continue; @@ -1272,6 +1292,20 @@ function executePartyAbility( } : undefined, ); effectiveAmount += result.amount; + if (effect.kind === "damage" && ability.dbcSpellId === 490133) { + const currentCombat = useCombatStore.getState(); + const bonus = romScoutRogueWhirlwindBonusEffect( + ability, + currentCombat.auras.filter((aura) => aura.targetId === member.id) + .flatMap((aura) => aura.definition.utilityTags ?? []), + currentCombat.effects.filter((active) => active.targetId === hostileTargetId && active.remainingTicks > 0) + .flatMap((active) => active.tag ? [active.tag] : []), + ); + if (bonus) effectiveAmount += currentCombat.damageMob( + hostileTargetId, partyAbilityAmount(runtimeMember, ability, bonus), now, + { actorId: member.id, role: member.role, abilityId: ability.id }, "physical", + ).amount; + } if ( result.amount > 0 && effect.kind === "damage" @@ -1383,7 +1417,7 @@ function executePartyAbility( : effect.recipient === "caster" ? [member.id] : combat.mobs[targetId] ? hostileTargets : friendlyTargets; - for (const recipientId of recipients) { + for (const recipientId of recipients.filter((id) => !effect.excludeCaster || id !== member.id)) { const appliedEffect = ability.name === "Rupture" ? { ...effect, aura: { ...effect.aura, durationMs: Math.max(4, Math.min(8, 3 + runtimeMember.comboPoints)) * 2_000 } } : ability.name === "Envenom" @@ -1563,6 +1597,34 @@ function executePartyAbility( if (consumesColdBlood) useCombatStore.getState().consumeActorAuraTag(member.id, "next-offensive-critical"); if (ability.dbcSpellId === 491171) useCombatStore.getState().consumeActorAuraTag(member.id, "rom-charged"); if (ability.dbcSpellId === 491172) useCombatStore.getState().consumeActorAuraTag(member.id, "rom-static-field-charge"); + if (ability.dbcSpellId === 498759) { + const currentCombat = useCombatStore.getState(); + const reset = romFireLightningBurstReset( + ability, + Boolean(currentCombat.mobs[targetId] && !currentCombat.mobs[targetId].dead + && currentCombat.mobs[targetId].health > 0), + currentCombat.auras.some((aura) => aura.targetId === member.id + && aura.definition.utilityTags?.includes(ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_TAG)), + ); + if (reset.reset) { + useCombatStore.getState().applyActorAura(member.id, member.id, ability, { + kind: "apply-aura", + recipient: "caster", + sourceEffectType: -1, + sourceAuraType: -1, + miscValue: 0, + triggerSpellId: 621514, + aura: { + id: "rom-fire-lightning-burst-reset-guard", + name: "Fire Lightning Burst reset guard", + disposition: "buff", + durationMs: reset.guardDurationMs, + utilityTags: [ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_TAG], + }, + }, now); + runtimeMember = { ...runtimeMember, cooldowns: { ...runtimeMember.cooldowns, [ability.id]: 0 } }; + } + } emitPartyAbility(member, ability, targetId, "release", now, !castStartAlreadyEmitted); emitPartyAbility(member, ability, targetId, "impact", now, !castStartAlreadyEmitted); if (effectiveAmount > 0 && ability.effects.some((effect) => ["heal", "hot", "shield", "resurrection"].includes(effect.kind))) { diff --git a/src/game/romAbilityCatalog.ts b/src/game/romAbilityCatalog.ts index fc89725d..2e2cc03d 100644 --- a/src/game/romAbilityCatalog.ts +++ b/src/game/romAbilityCatalog.ts @@ -14,7 +14,7 @@ import { type RomGeneratedAbilityLike, type RomSourceComponent, } from "./romAbilitySemantics"; -import { overrideRomPriestKnightEffects } from "./romPriestKnightOverrides"; +import { overrideRomPriestKnightEffects, romPriestKnightAdditionalAbilities } from "./romPriestKnightOverrides"; import { overrideRomScoutRogueEffects } from "./romScoutRogueOverrides"; import { overrideRomMageWarlockEffects } from "./romMageWarlockOverrides"; @@ -744,7 +744,11 @@ function ability(row: GeneratedSkill): AbilityDefinition { }; } -export const ROM_ALL_ABILITIES = Object.freeze(generated.map(ability)); +const generatedAbilities = generated.map(ability); +export const ROM_ALL_ABILITIES = Object.freeze([ + ...generatedAbilities, + ...romPriestKnightAdditionalAbilities(generatedAbilities), +]); const romAbilityByGeneratedId = new Map(ROM_ALL_ABILITIES.map((entry) => [entry.id, entry])); const triggeredComponentOwners = new Map(); diff --git a/src/game/romAbilityCatalogSemantics.test.ts b/src/game/romAbilityCatalogSemantics.test.ts index 69a97ba6..230103c3 100644 --- a/src/game/romAbilityCatalogSemantics.test.ts +++ b/src/game/romAbilityCatalogSemantics.test.ts @@ -114,7 +114,7 @@ describe("executable RuneWaker ability mappings", () => { })), ]); - expect(versions).toHaveLength(1_863); + expect(versions).toHaveLength(1_864); const scriptOnly = ROM_ALL_ABILITIES .filter((ability) => !ability.passive && ability.effects.every((effect) => effect.kind === "scripted")) .map((ability) => ability.name) diff --git a/src/game/romCombatMechanics.ts b/src/game/romCombatMechanics.ts index 6874b2b6..1650c33a 100644 --- a/src/game/romCombatMechanics.ts +++ b/src/game/romCombatMechanics.ts @@ -67,7 +67,7 @@ function scaleOutput(effect: AbilityEffect, factor: number): AbilityEffect { export function romEffectsForCast(ability: AbilityDefinition, context: RomCastMechanics): readonly AbilityEffect[] { if (ability.source !== "runewaker") return ability.effects; const priestKnightEffects = romPriestKnightCastEffects( - ability, ability.effects, context.passiveTags ?? [], context.casterMaxHealth, + ability, ability.effects, context.passiveTags ?? [], context.casterMaxHealth, context.targetTags ?? [], ); const mageWarlockEffects = romMageWarlockCastEffects(ability, priestKnightEffects, new Set(context.passiveTags ?? [])); const effects = romScoutRogueCastEffects(ability, mageWarlockEffects, context.passiveTags ?? [], context.targetTags ?? []); diff --git a/src/game/romCreatureFamilies.test.ts b/src/game/romCreatureFamilies.test.ts new file mode 100644 index 00000000..4ce553f0 --- /dev/null +++ b/src/game/romCreatureFamilies.test.ts @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { useCombatStore } from "./combatStore"; +import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; +import { romCreatureFamilyForServerEntry } from "./romCreatureFamilies"; + +describe("RuneWaker creature-family combat rules", () => { + beforeEach(() => clearMobRuntimeRegistry()); + + it("uses NPCObjectDB race records for living, Undead, and Demon targets", () => { + expect(romCreatureFamilyForServerEntry(103946)).toBe("living"); + expect(romCreatureFamilyForServerEntry(106432)).toBe("undead"); + expect(romCreatureFamilyForServerEntry(102698)).toBe("demon"); + expect(romCreatureFamilyForServerEntry(null)).toBe("unknown"); + }); + + it("restricts base Holy Chains and unlocks living targets with Evil Shackles", () => { + for (const level of [30, 60]) { + useCombatStore.getState().initializeCharacter({ + classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level, + }); + useCombatStore.setState({ playerPosition: [0, 0, 0] }); + useCombatStore.getState().registerMob("living", { + name: "Shadoj", serverEntry: 103946, maxHealth: 1_000_000, + }); + setMobPosition("living", [3, 0, 0]); + useCombatStore.getState().selectMob("living"); + expect(useCombatStore.getState().mobs.living?.creatureFamily).toBe("living"); + expect(useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-25", undefined, 1_000)) + .toMatchObject(level < 60 ? { ok: false, reason: "condition-not-met" } : { ok: true }); + } + useCombatStore.getState().initializeCharacter({ + classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 30, + }); + useCombatStore.getState().registerMob("undead", { + name: "Self-Loathing Ghost", serverEntry: 106432, maxHealth: 1_000_000, + }); + setMobPosition("undead", [3, 0, 0]); + useCombatStore.getState().selectMob("undead"); + expect(useCombatStore.getState().mobs.undead?.creatureFamily).toBe("undead"); + expect(useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-25", undefined, 1_000)) + .toMatchObject({ ok: true }); + }); + + it("accepts the Poisonous state without a bag item and rejects Numbing Dagger on Undead", () => { + useCombatStore.getState().initializeCharacter({ + classId: "rom-rogue", secondaryClassId: "rom-warrior", raceId: "rom-human", level: 50, + }); + useCombatStore.setState({ playerPosition: [0, 0, 0] }); + for (const [id, entry] of [["undead", 106432], ["living", 103946]] as const) { + useCombatStore.getState().registerMob(id, { name: id, serverEntry: entry, maxHealth: 1_000_000 }); + setMobPosition(id, [3, 0, 0]); + } + expect(useCombatStore.getState().castAbility("rom-rogue-primary-6", undefined, 1_000)) + .toMatchObject({ ok: true }); + useCombatStore.getState().tick(0.1, 2_100); + useCombatStore.getState().selectMob("undead"); + expect(useCombatStore.getState().castAbility("rom-rogue-primary-5", undefined, 2_600)) + .toMatchObject({ ok: false, reason: "condition-not-met" }); + useCombatStore.getState().selectMob("living"); + expect(useCombatStore.getState().castAbility("rom-rogue-primary-5", undefined, 2_600)) + .toMatchObject({ ok: true }); + }); +}); diff --git a/src/game/romCreatureFamilies.ts b/src/game/romCreatureFamilies.ts new file mode 100644 index 00000000..864ee82b --- /dev/null +++ b/src/game/romCreatureFamilies.ts @@ -0,0 +1,21 @@ +import source from "./generated/romCreatureFamilies.json"; + +export type CombatCreatureFamily = "living" | "undead" | "demon" | "unknown"; + +const known = new Set(source.knownIds); +const undead = new Set(source.undeadIds); +const demon = new Set(source.demonIds); + +/** RuneWaker NPCObjectDB race field, exported by export-rom-creature-families.mjs. */ +export function romCreatureFamilyForServerEntry(serverEntry: number | null | undefined): CombatCreatureFamily { + if (serverEntry == null || !known.has(serverEntry)) return "unknown"; + if (undead.has(serverEntry)) return "undead"; + if (demon.has(serverEntry)) return "demon"; + return "living"; +} + +export function romCreatureFamilyTargetTags(family: CombatCreatureFamily): readonly string[] { + if (family === "undead" || family === "demon") return ["rom-target-undead-demon"]; + if (family === "living") return ["rom-target-living"]; + return []; +} diff --git a/src/game/romMageWarlockOverrides.ts b/src/game/romMageWarlockOverrides.ts index f794178b..5ab15907 100644 --- a/src/game/romMageWarlockOverrides.ts +++ b/src/game/romMageWarlockOverrides.ts @@ -13,10 +13,16 @@ export const ROM_MAGE_WARLOCK_PASSIVE_TAGS = Object.freeze({ } as const); export const ROM_SOUL_STEPPING_CAST_TAG = "rom-mage-warlock-soul-stepping-cast"; +export const ROM_BREATH_ERASE_THREAT_TAG = "rom-mage-warlock-breath-erase-threat"; +export const ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_TAG = "rom-fire-lightning-burst-reset-guard"; +export const ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS = 6_000; -// Industry Fire's player tooltip gives no magnitude for its extra fire hit. -// One Soul Pain tick is a conservative source-derived starting value. -const INDUSTRY_FIRE_DOT_MULTIPLIER = 1.1; +// Hidden replacement skill 498746: 621504 is the initial Fire hit, and +// 621503 is its 24-second Fire DoT. These values come from MagicObject.db. +const INDUSTRY_FIRE_HIT_BASE = 140; +const INDUSTRY_FIRE_HIT_PER_LEVEL = 40; +const INDUSTRY_FIRE_DOT_BASE = 23.4; +const INDUSTRY_FIRE_DOT_PER_LEVEL = 10.4; function stateAura(id: string, tag: string, durationMs: number | null): AbilityEffect { return { @@ -60,6 +66,16 @@ export function overrideRomMageWarlockEffects( }); } + if (row.sourceSkillId === 498750) { + return translated.map((effect) => effect.kind === "apply-aura" ? { + ...effect, + aura: { + ...effect.aura, + utilityTags: [...(effect.aura.utilityTags ?? []), ROM_BREATH_ERASE_THREAT_TAG], + }, + } : effect); + } + if (row.sourceSkillId === 498745) return [stateAura("industry-fire", ROM_MAGE_WARLOCK_PASSIVE_TAGS.industryFire, null)]; if (row.sourceSkillId === 498748) return [stateAura("lightning-display", ROM_MAGE_WARLOCK_PASSIVE_TAGS.lightningDisplay, null)]; if (row.sourceSkillId === 498751) return [stateAura("elements-of-pride", ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementsOfPride, null)]; @@ -72,18 +88,6 @@ export function overrideRomMageWarlockEffects( return null; } -function scaleDamage(effect: AbilityEffect, factor: number): AbilityEffect { - if (effect.kind !== "damage" && effect.kind !== "dot") return effect; - return { - ...effect, - coefficient: effect.coefficient * factor, - ...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel * factor }), - ...(effect.basePoints === undefined ? {} : { basePoints: effect.basePoints * factor }), - ...(effect.pointsPerLevel === undefined ? {} : { pointsPerLevel: effect.pointsPerLevel * factor }), - ...(effect.bonusPowerCoefficient === undefined ? {} : { bonusPowerCoefficient: effect.bonusPowerCoefficient * factor }), - }; -} - function elementsOfPrideAura(school: "fire" | "nature", value: number): AbilityEffect { return { kind: "apply-aura", @@ -113,20 +117,25 @@ export function romMageWarlockCastEffects( if (ability.source !== "runewaker") return effects; if (ability.dbcSpellId === 497964 && passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.industryFire)) { - const transformed = effects.map((effect) => effect.kind === "dot" - ? { ...scaleDamage(effect, INDUSTRY_FIRE_DOT_MULTIPLIER), school: "fire" as const } - : effect); - const dot = transformed.find((effect) => effect.kind === "dot"); - if (dot?.kind !== "dot") return transformed; return [ - ...transformed, { kind: "damage", school: "fire", - coefficient: dot.coefficient, - basePoints: dot.basePoints ?? 0, - pointsPerLevel: dot.pointsPerLevel ?? 0, - sourceLevel: dot.sourceLevel ?? 1, + coefficient: 0, + basePoints: INDUSTRY_FIRE_HIT_BASE, + pointsPerLevel: INDUSTRY_FIRE_HIT_PER_LEVEL, + sourceLevel: 1, + }, + { + kind: "dot", + school: "fire", + coefficient: 0, + basePoints: INDUSTRY_FIRE_DOT_BASE, + pointsPerLevel: INDUSTRY_FIRE_DOT_PER_LEVEL, + sourceLevel: 1, + ticks: 24, + intervalMs: 1_000, + tag: "rom-industry-fire", }, ]; } @@ -134,19 +143,52 @@ export function romMageWarlockCastEffects( let result = effects; if (passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.lightningDisplay)) { result = result.map((effect) => { - if (effect.kind === "apply-aura" && effect.aura.utilityTags?.includes("rom-charged") - && ability.dbcSpellId === 491169) { - return { ...effect, aura: { ...effect.aura, durationMs: 18_000 } }; + if (effect.kind === "apply-aura" && [491169, 498749].includes(ability.dbcSpellId)) { + if (effect.aura.utilityTags?.includes("rom-charged")) { + return { ...effect, aura: { ...effect.aura, durationMs: 18_000 } }; + } + if (effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" + && modifier.stat === "magical-critical-strike")) { + return { + ...effect, + aura: { + ...effect.aura, + durationMs: 18_000, + modifiers: [ + ...effect.aura.modifiers, + { kind: "damage" as const, direction: "dealt" as const, school: "nature" as const, + operation: "percent" as const, value: 0.01 }, + ], + }, + }; + } } - return (effect.kind === "damage" || effect.kind === "dot") && effect.school === "nature" - ? scaleDamage(effect, 1.01) - : effect; + return effect; }); } if (passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementsOfPride)) { - if (ability.dbcSpellId === 491150) result = [...result, elementsOfPrideAura("nature", 0.02)]; - if (ability.dbcSpellId === 491170) result = [...result, elementsOfPrideAura("fire", 0.01)]; + if ([491150, 498755, 499812].includes(ability.dbcSpellId)) { + result = [...result, elementsOfPrideAura("nature", 0.02)]; + } + if ([491170, 498756, 499851].includes(ability.dbcSpellId)) { + result = [...result, elementsOfPrideAura("fire", 0.01)]; + } + } + + if (ability.dbcSpellId === 491170 && passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction)) { + // Hidden level-70 Electric Explosion 499851 replaces the normal branch's + // 620232 hit with 623668; its Charged 623669 hit keeps the same values. + const damage = result.filter((effect) => effect.kind === "damage"); + const normal = damage.at(-1); + if (damage.length >= 2 && normal) result = result.map((effect) => effect === normal ? { + kind: "damage" as const, + school: "nature" as const, + coefficient: 0, + basePoints: 65, + pointsPerLevel: 15, + sourceLevel: 1, + } : effect); } if (ability.dbcSpellId === 498757 && !passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.soulSpeed)) { @@ -162,7 +204,7 @@ export function romMageWarlockCriticalEffects( passiveTags: ReadonlySet, ): readonly AbilityEffect[] { if (!passiveTags.has(ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction) - || (ability.dbcSpellId !== 491170 && ability.dbcSpellId !== 499812)) return []; + || ![491150, 491170, 498755, 498756, 499812, 499851].includes(ability.dbcSpellId)) return []; return [{ kind: "apply-aura", recipient: "ability-target", @@ -184,3 +226,18 @@ export function romMageWarlockCriticalEffects( triggerSpellId: 499814, }]; } + +/** Lua script 02557: reset Burst on a surviving target once per six seconds. */ +export function romFireLightningBurstReset( + ability: AbilityDefinition, + targetAlive: boolean, + guardActive: boolean, +): { readonly reset: boolean; readonly guardDurationMs: number } { + const reset = ability.dbcSpellId === 498759 && targetAlive && !guardActive; + return { reset, guardDurationMs: reset ? ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS : 0 }; +} + +/** Breath Erase source 621506: ten percent less generated threat for five seconds. */ +export function romMageWarlockThreatScale(activeTags: ReadonlySet): number { + return activeTags.has(ROM_BREATH_ERASE_THREAT_TAG) ? 0.9 : 1; +} diff --git a/src/game/romMageWarlockPairValidation.test.ts b/src/game/romMageWarlockPairValidation.test.ts index 1680bb02..80b22184 100644 --- a/src/game/romMageWarlockPairValidation.test.ts +++ b/src/game/romMageWarlockPairValidation.test.ts @@ -1,12 +1,18 @@ import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForCharacter, abilityById, learnedAbilityForBuild } from "./abilityCatalog"; +import { PLAYER_AGGRO_ID } from "./aggro"; +import { applyAura as applyCombatAura } from "./combatAuras"; import { abilityDamageSchool, useCombatStore } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities"; import { ROM_MAGE_WARLOCK_PASSIVE_TAGS, + ROM_BREATH_ERASE_THREAT_TAG, + ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS, romMageWarlockCastEffects, romMageWarlockCriticalEffects, + romFireLightningBurstReset, + romMageWarlockThreatScale, } from "./romMageWarlockOverrides"; const ELITES = [ @@ -78,6 +84,8 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => { }), }), ])); + expect(elite(30).effects.some((effect) => effect.kind === "apply-aura" + && effect.aura.utilityTags?.includes(ROM_BREATH_ERASE_THREAT_TAG))).toBe(true); }); it("uses magic schools for Mage and Warlock magic projectiles and dark spells", () => { @@ -150,6 +158,23 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => { expect(useCombatStore.getState().mobs["weave-far"]!.health).toBe(100_000); }); + it("caps Surge of Malice at three targets even when four enemies are nearby", () => { + initialize(30); + for (const [id, x] of [ + ["surge-a", 2], ["surge-b", 3], ["surge-c", 4], ["surge-d", 5], ["surge-far", 20], + ] as const) { + useCombatStore.getState().registerMob(id, { name: id, maxHealth: 100_000 }); + setMobPosition(id, [x, 0, 0]); + } + useCombatStore.getState().selectMob("surge-a"); + expect(useCombatStore.getState().castAbility("rom-warlock-general-5", undefined, 1_000)) + .toMatchObject({ ok: true }); + const damaged = ["surge-a", "surge-b", "surge-c", "surge-d"] + .filter((id) => useCombatStore.getState().mobs[id]!.health < 100_000); + expect(damaged).toHaveLength(3); + expect(useCombatStore.getState().mobs["surge-far"]!.health).toBe(100_000); + }); + it("Static Resonance resets only Electrostatic Charge", () => { initialize(60); useCombatStore.setState({ cooldowns: { @@ -196,6 +221,19 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => { expect(useCombatStore.getState().mobs["moving-cast-dummy"]!.health).toBeLessThan(100_000); }); + it("Soul Speed adds its movement and spell speed buffs when Soul Stepping is cast", () => { + initialize(60); + expect(useCombatStore.getState().castAbility(elite(40).id, undefined, 1_000)) + .toMatchObject({ ok: true }); + const modifiers = useCombatStore.getState().auras + .filter((aura) => aura.targetId === PLAYER_AGGRO_ID) + .flatMap((aura) => aura.definition.modifiers ?? []); + expect(modifiers).toEqual(expect.arrayContaining([ + expect.objectContaining({ kind: "stat", stat: "movement-speed", value: 0.2 }), + expect.objectContaining({ kind: "stat", stat: "spell-haste", value: 20 }), + ])); + }); + it("Fire Lightning Burst hits twice and resets its own cooldown only if the target survives", () => { initialize(60); registerTarget(); @@ -206,6 +244,10 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => { expect(useCombatStore.getState().mobs["mage-warlock-dummy"]!.health).toBeLessThan(100_000); expect(useCombatStore.getState().cooldowns[elite(50).id] ?? 0).toBeLessThanOrEqual(3_100); expect(useCombatStore.getState().cooldowns["rom-mage-primary-2"]).toBe(90_000); + + expect(useCombatStore.getState().castAbility(elite(50).id, undefined, 4_600)).toMatchObject({ ok: true }); + useCombatStore.getState().tick(0.1, 6_700); + expect(useCombatStore.getState().cooldowns[elite(50).id]).toBeGreaterThan(6_700); }); it("does not apply conditional passive buffs or enemy defense penalties to the caster on login", () => { @@ -217,16 +259,52 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => { expect(elite(20).effects.some((effect) => effect.kind === "scripted")).toBe(false); }); - it("converts Soul Pain to fire with an extra hit only while Industry Fire is learned", () => { + it("uses Industry Fire's hidden Soul Pain payload during a live cast", () => { + initialize(60); + registerTarget("industry-fire-dummy"); + expect(useCombatStore.getState().castAbility("rom-warlock-general-4", undefined, 1_000)) + .toMatchObject({ ok: true }); + useCombatStore.getState().tick(0.1, 3_100); + const state = useCombatStore.getState(); + expect(state.mobs["industry-fire-dummy"]!.health).toBeLessThan(100_000); + expect(state.effects.some((effect) => effect.tag === "rom-industry-fire" + && effect.targetId === "industry-fire-dummy")).toBe(true); + }); + + it("triggers Lightning Display and Elements of Pride only after their source spells land", () => { + initialize(60); + registerTarget("elite-trigger-dummy"); + expect(useCombatStore.getState().castAbility("rom-mage-primary-52", undefined, 1_000)) + .toMatchObject({ ok: true }); + useCombatStore.getState().tick(0.1, 3_100); + expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID + && aura.definition.modifiers?.some((modifier) => modifier.kind === "stat" + && modifier.stat === "magical-critical-strike" && modifier.value === 20))).toBe(true); + + expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 4_600)) + .toMatchObject({ ok: true }); + useCombatStore.getState().tick(0.1, 7_700); + expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID + && aura.definition.id === "rom-mage-warlock-elements-nature" && aura.stacks === 1)).toBe(true); + }); + + it("uses hidden skill 498746's exact fire hit and DoT after Industry Fire", () => { const soulPain = abilityById("rom-warlock-general-4")!; const ordinary = romMageWarlockCastEffects(soulPain, soulPain.effects, new Set()); const upgraded = romMageWarlockCastEffects(soulPain, soulPain.effects, new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.industryFire])); expect(ordinary.some((effect) => effect.kind === "damage")).toBe(false); - expect(upgraded).toEqual(expect.arrayContaining([ - expect.objectContaining({ kind: "dot", school: "fire" }), - expect.objectContaining({ kind: "damage", school: "fire" }), - ])); + expect(upgraded).toEqual([ + expect.objectContaining({ + kind: "damage", school: "fire", coefficient: 0, + basePoints: 140, pointsPerLevel: 40, sourceLevel: 1, + }), + expect.objectContaining({ + kind: "dot", school: "fire", coefficient: 0, + basePoints: 23.4, pointsPerLevel: 10.4, sourceLevel: 1, + ticks: 24, intervalMs: 1_000, + }), + ]); }); it("extends Charged, creates conditional Elements stacks, and enhances Soul Stepping", () => { @@ -240,6 +318,20 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => { effect.kind === "apply-aura" && effect.aura.utilityTags?.includes("rom-charged") )); expect(charged).toMatchObject({ aura: { durationMs: 18_000 } }); + const lightningBuff = romMageWarlockCastEffects(plasma, plasma.effects, tags).find((effect) => ( + effect.kind === "apply-aura" && effect.aura.modifiers?.some((modifier) => ( + modifier.kind === "stat" && modifier.stat === "magical-critical-strike" + )) + )); + expect(lightningBuff).toMatchObject({ + aura: { + durationMs: 18_000, + modifiers: expect.arrayContaining([ + expect.objectContaining({ stat: "magical-critical-strike", value: 20 }), + expect.objectContaining({ kind: "damage", school: "nature", value: 0.01 }), + ]), + }, + }); const flame = abilityById("rom-mage-primary-1")!; expect(romMageWarlockCastEffects(flame, flame.effects, tags)).toEqual(expect.arrayContaining([ @@ -258,20 +350,138 @@ describe("accepted RoM Mage/Warlock dwarf pair", () => { }); it("keeps Elemental Extraction's defense debuff on a critical-hit target", () => { - const explosion = abilityById("rom-mage-primary-54")!; - expect(romMageWarlockCriticalEffects(explosion, new Set())).toEqual([]); - expect(romMageWarlockCriticalEffects(explosion, - new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction]))).toEqual([ - expect.objectContaining({ - kind: "apply-aura", recipient: "ability-target", - aura: expect.objectContaining({ - disposition: "debuff", durationMs: 10_000, - modifiers: expect.arrayContaining([ - expect.objectContaining({ stat: "armor", value: -0.3 }), - expect.objectContaining({ stat: "magical-defense", value: -0.3 }), - ]), + for (const id of ["rom-mage-primary-1", "rom-mage-primary-54"]) { + const attack = abilityById(id)!; + expect(romMageWarlockCriticalEffects(attack, new Set())).toEqual([]); + expect(romMageWarlockCriticalEffects(attack, + new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction]))).toEqual([ + expect.objectContaining({ + kind: "apply-aura", recipient: "ability-target", + aura: expect.objectContaining({ + disposition: "debuff", durationMs: 10_000, + modifiers: expect.arrayContaining([ + expect.objectContaining({ stat: "armor", value: -0.3 }), + expect.objectContaining({ stat: "magical-defense", value: -0.3 }), + ]), + }), }), - }), - ]); + ]); + } + }); + + it("triggers Elemental Extraction's target debuff on a live critical Flame hit", () => { + initialize(79); + registerTarget("critical-extraction-dummy"); + useCombatStore.setState((state) => ({ + auras: applyCombatAura(state.auras, { + definition: { + id: "mage-warlock-test-guaranteed-crit", + name: "Guaranteed critical test trigger", + disposition: "buff", + durationMs: 10_000, + utilityTags: ["next-offensive-critical"], + }, + sourceId: PLAYER_AGGRO_ID, + targetId: PLAYER_AGGRO_ID, + now: 1_000, + }).auras, + })); + expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 1_000)) + .toMatchObject({ ok: true }); + useCombatStore.getState().tick(0.1, 4_100); + expect(useCombatStore.getState().auras.some((aura) => aura.targetId === "critical-extraction-dummy" + && aura.definition.id === "rom-mage-warlock-elemental-extraction-debuff")).toBe(true); + }); + + it("uses hidden 499851's stronger normal Electric Explosion branch at level 70", () => { + const explosion = abilityById("rom-mage-primary-54")!; + const original = explosion.effects.filter((effect) => effect.kind === "damage"); + const upgraded = romMageWarlockCastEffects(explosion, explosion.effects, + new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction])) + .filter((effect) => effect.kind === "damage"); + expect(upgraded).toHaveLength(2); + expect(upgraded[0]).toEqual(original[0]); + expect(upgraded[1]).toMatchObject({ + school: "nature", coefficient: 0, basePoints: 65, pointsPerLevel: 15, sourceLevel: 1, + }); + }); + + it("honors Fire Lightning Burst's source six-second reset guard", () => { + const burst = elite(50); + expect(romFireLightningBurstReset(burst, true, false)).toEqual({ + reset: true, guardDurationMs: ROM_FIRE_LIGHTNING_BURST_RESET_GUARD_MS, + }); + expect(romFireLightningBurstReset(burst, true, true)).toEqual({ reset: false, guardDurationMs: 0 }); + expect(romFireLightningBurstReset(burst, false, false)).toEqual({ reset: false, guardDurationMs: 0 }); + expect(romFireLightningBurstReset(elite(45), true, false)).toEqual({ reset: false, guardDurationMs: 0 }); + }); + + it("exposes Breath Erase's five-second threat reduction to the runtime", () => { + const breath = elite(30).effects.find((effect) => effect.kind === "apply-aura" + && effect.aura.utilityTags?.includes(ROM_BREATH_ERASE_THREAT_TAG)); + expect(breath).toMatchObject({ aura: { durationMs: 5_000 } }); + expect(romMageWarlockThreatScale(new Set())).toBe(1); + expect(romMageWarlockThreatScale(new Set([ROM_BREATH_ERASE_THREAT_TAG]))).toBe(0.9); + }); + + it("reduces live spell threat while Breath Erase is active", () => { + const castFlame = (useBreath: boolean) => { + initialize(60); + registerTarget("breath-threat-dummy"); + if (useBreath) { + expect(useCombatStore.getState().castAbility(elite(30).id, undefined, 1_000)) + .toMatchObject({ ok: true }); + } + expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 2_600)) + .toMatchObject({ ok: true }); + useCombatStore.getState().tick(0.1, 5_700); + const target = useCombatStore.getState().mobs["breath-threat-dummy"]!; + return { + damage: target.maxHealth - target.health, + threat: target.threatByActor[PLAYER_AGGRO_ID] ?? 0, + }; + }; + const baseline = castFlame(false); + const reduced = castFlame(true); + expect(reduced.damage).toBeGreaterThan(0); + expect(reduced.damage).toBe(baseline.damage); + expect((reduced.threat - 1) / (baseline.threat - 1)).toBeCloseTo(0.9, 5); + }); + + it("applies Elemental Extraction's defense reduction to the target's live magic mitigation", () => { + const targetId = "extraction-defense-dummy"; + const castFlame = (useDebuff: boolean) => { + initialize(60); + useCombatStore.getState().registerMob(targetId, { + name: targetId, + maxHealth: 100_000, + resistances: { fire: 1_000 }, + }); + setMobPosition(targetId, [3, 0, 0]); + useCombatStore.getState().selectMob(targetId); + if (useDebuff) { + const rider = romMageWarlockCriticalEffects(abilityById("rom-mage-primary-1")!, + new Set([ROM_MAGE_WARLOCK_PASSIVE_TAGS.elementalExtraction]))[0]; + expect(rider.kind).toBe("apply-aura"); + if (rider.kind !== "apply-aura") throw new Error("Elemental Extraction debuff missing"); + useCombatStore.setState((state) => ({ + auras: applyCombatAura(state.auras, { + definition: rider.aura, + sourceId: PLAYER_AGGRO_ID, + targetId, + now: 1_000, + }).auras, + })); + } + expect(useCombatStore.getState().castAbility("rom-mage-primary-1", undefined, 1_000)) + .toMatchObject({ ok: true }); + useCombatStore.getState().tick(0.1, 4_100); + return useCombatStore.getState().mobs[targetId]!.maxHealth + - useCombatStore.getState().mobs[targetId]!.health; + }; + const baselineDamage = castFlame(false); + const reducedDefenseDamage = castFlame(true); + expect(baselineDamage).toBeGreaterThan(0); + expect(reducedDefenseDamage).toBeGreaterThan(baselineDamage); }); }); diff --git a/src/game/romPlayerCatalog.test.ts b/src/game/romPlayerCatalog.test.ts index 6d220bee..a3bd912e 100644 --- a/src/game/romPlayerCatalog.test.ts +++ b/src/game/romPlayerCatalog.test.ts @@ -40,23 +40,26 @@ describe("generated RuneWaker player catalog", () => { expect(audit.hashes.interfaceFdb).toMatch(/^[a-f0-9]{64}$/); }); - it("resolves every ordered pair to all ten executable elite milestones", () => { - expect(ROM_ELITE_ABILITIES).toHaveLength(660); + it("resolves every ordered pair to all ten elite milestones and the source-backed Devotion Halo", () => { + expect(ROM_ELITE_ABILITIES).toHaveLength(661); const pairGroups = new Map(); for (const ability of ROM_ELITE_ABILITIES) { const key = `${ability.classId}:${ability.secondaryClassId}`; pairGroups.set(key, [...(pairGroups.get(key) ?? []), ability]); } expect(pairGroups.size).toBe(66); - for (const elites of pairGroups.values()) { - expect(elites.map((ability) => ability.unlockLevel)).toEqual(ELITE_LEVELS); + for (const [pair, elites] of pairGroups) { + expect(elites.map((ability) => ability.unlockLevel)).toEqual(pair === "rom-priest:rom-knight" + ? [...ELITE_LEVELS, 70] : ELITE_LEVELS); expect(elites.every((ability) => ability.source === "runewaker" && ability.effects.length > 0)).toBe(true); } - expect(ROM_ALL_ABILITIES).toHaveLength(929); - expect(ROM_ALL_ABILITIES.every((ability) => ( + expect(ROM_ALL_ABILITIES).toHaveLength(930); + expect(ROM_ALL_ABILITIES.filter((ability) => ability.id !== "rom-priest-rom-knight-elite-70-devotion-halo").every((ability) => ( /^\/assets\/ui\/rom-spells\/\d+\.png$/.test(ability.icon) && ability.icon === generated.skills.find((skill) => skill.id === ability.id)?.icon ))).toBe(true); + expect(ROM_ALL_ABILITIES.find((ability) => ability.id === "rom-priest-rom-knight-elite-70-devotion-halo")?.icon) + .toBe(generated.skills.find((skill) => skill.sourceSkillId === 499957)?.icon); expect(generated.skills.every((ability) => ( ability.iconResolution === "native-interface-fdb" && /^\/assets\/ui\/rom-spells\/\d+\.png$/.test(ability.icon) diff --git a/src/game/romPriestKnightOverrides.ts b/src/game/romPriestKnightOverrides.ts index b450dbc3..8a8d6398 100644 --- a/src/game/romPriestKnightOverrides.ts +++ b/src/game/romPriestKnightOverrides.ts @@ -150,6 +150,7 @@ export function romPriestKnightCastEffects( effects: readonly AbilityEffect[], passiveTags: readonly string[] | ReadonlySet, casterMaxHealth: number, + targetTags: readonly string[] | ReadonlySet = [], ): readonly AbilityEffect[] { if (ability.source !== "runewaker" || casterMaxHealth <= 0) return effects; switch (ability.dbcSpellId) { @@ -202,12 +203,127 @@ export function romPriestKnightCastEffects( triggerSpellId: 490066, }]; case 491613: { // Evil Shackles halves duration on non-undead/demon targets. - if (!hasTag(passiveTags, "rom-evil-shackles") || hasTag(passiveTags, "rom-target-undead-demon")) return effects; + if (!hasTag(passiveTags, "rom-evil-shackles") || hasTag(targetTags, "rom-target-undead-demon")) return effects; return effects.map((effect) => effect.kind === "apply-aura" && effect.control?.kind === "root" ? { ...effect, aura: { ...effect.aura, durationMs: Math.max(1_000, Math.round((effect.aura.durationMs ?? 10_000) / 2)) } } : effect); } + case 490273: // Source 499967 keeps Wave Armor's shield and adds heal component 623009. + if (!hasTag(passiveTags, "rom-splendor-ripple")) return effects; + return [...effects, { + kind: "heal", + recipient: "ability-target", + coefficient: 0, + basePoints: 106.25, + pointsPerLevel: 21.25, + sourceLevel: 1, + }]; + case 499966: // Devotion Halo refreshes Light Guidance while the halo is active. + if (!hasTag(passiveTags, "rom-light-guidance")) return effects; + return [...effects, { + kind: "apply-aura", + recipient: "caster", + aura: { + id: "rom-pair-light-guidance-healing", + name: "Light Guidance", + disposition: "buff", + durationMs: 900_000, + modifiers: [{ kind: "healing", direction: "done", operation: "percent", value: 0.05 }], + dispelCategory: "magic", + }, + sourceEffectType: -1, + sourceAuraType: 149, + miscValue: 5, + triggerSpellId: 623015, + }]; + case 490434: // Source 499924 adds component 623167 to the base Blood Arrow. + if (!hasTag(passiveTags, "rom-blood-punishment")) return effects; + return [...effects, { + kind: "apply-aura", + recipient: "caster", + aura: { + id: "rom-pair-blood-arrow-healing-received", + name: "Blood Punishment", + disposition: "buff", + durationMs: 30_000, + modifiers: [{ kind: "healing", direction: "taken", operation: "percent", value: 0.5 }], + utilityTags: ["rom-blood-arrow-upgrade"], + dispelCategory: "magic", + }, + sourceEffectType: -1, + sourceAuraType: 144, + miscValue: 50, + triggerSpellId: 623167, + }]; default: return effects; } } + +/** Source skill 499966 is granted by the level-70 Priest/Knight elite pair. */ +export function romPriestKnightAdditionalAbilities( + catalog: readonly AbilityDefinition[], +): readonly AbilityDefinition[] { + const lightGuidance = catalog.find((ability) => ability.dbcSpellId === 499957 + && ability.classId === "rom-priest" && ability.secondaryClassId === "rom-knight"); + if (!lightGuidance) return []; + const effects: readonly AbilityEffect[] = [{ + kind: "apply-aura", + recipient: "caster", + scope: "party", + aura: { + id: "rom-pair-devotion-halo", + name: "Devotion Halo", + disposition: "buff", + durationMs: 900_000, + modifiers: [{ kind: "stat", stat: "wisdom", operation: "percent", value: 0.05 }], + dispelCategory: "magic", + }, + sourceEffectType: -1, + sourceAuraType: 164, + miscValue: 5, + triggerSpellId: 623016, + }, { + kind: "apply-aura", + recipient: "caster", + scope: "party", + excludeCaster: true, + aura: { + id: "rom-pair-devotion-halo-mana", + name: "Devotion Halo Mana", + disposition: "buff", + durationMs: 900_000, + modifiers: [{ kind: "resource", resource: "mana", property: "cost", operation: "percent", value: -0.06 }], + dispelCategory: "magic", + }, + sourceEffectType: -1, + sourceAuraType: 164, + miscValue: -6, + triggerSpellId: 623016, + }]; + return [{ + id: "rom-priest-rom-knight-elite-70-devotion-halo", + classId: "rom-priest", + secondaryClassId: "rom-knight", + dbcSpellId: 499966, + name: "Devotion Halo", + unlockLevel: 70, + icon: lightGuidance.icon, + sigil: "DH", + description: "Raises party Wisdom by 5% and lowers party mana costs by 6% for 15 minutes.", + target: "self", + range: { min: 0, max: 5 }, + radius: 20, + castMode: "instant", + castTimeMs: 0, + cooldownMs: 0, + gcdMs: 1_500, + cost: { resource: "mana", amount: 0 }, + costs: [], + effects, + passive: false, + source: "runewaker", + sourceUnlockLevel: 70, + romSkillGroup: "elite", + }]; +} diff --git a/src/game/romPriestKnightPairValidation.test.ts b/src/game/romPriestKnightPairValidation.test.ts index 80ff451c..2db293a9 100644 --- a/src/game/romPriestKnightPairValidation.test.ts +++ b/src/game/romPriestKnightPairValidation.test.ts @@ -5,8 +5,9 @@ import { useCombatStore } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry"; import { usePartyStore } from "./partyStore"; +import { commitPartyAbilityUse } from "./partyAbilityAi"; import { ROM_ALL_ABILITIES, ROM_ELITE_ABILITIES } from "./romAbilityCatalog"; -import { romPriestKnightCastEffects } from "./romPriestKnightOverrides"; +import { romPriestKnightAdditionalAbilities, romPriestKnightCastEffects } from "./romPriestKnightOverrides"; const PAIRED_SKILLS = [ ["rom-priest", "rom-knight", 15, 492355, "Enhanced Grace of Life", "passive"], @@ -58,7 +59,7 @@ describe("accepted RoM Priest/Knight and Knight/Scout pairs", () => { for (const [primary, secondary] of [["rom-priest", "rom-knight"], ["rom-knight", "rom-scout"]] as const) { const pair = ROM_ELITE_ABILITIES.filter((ability) => ability.classId === primary && ability.secondaryClassId === secondary); - expect(pair, `${primary}/${secondary}`).toHaveLength(10); + expect(pair, `${primary}/${secondary}`).toHaveLength(primary === "rom-priest" ? 11 : 10); const kit = abilitiesForCharacter(primary, secondary); for (const skill of pair) expect(kit).toContain(skill); } @@ -197,11 +198,58 @@ describe("accepted RoM Priest/Knight and Knight/Scout pairs", () => { .find((effect) => effect.control?.kind === "root")?.aura.durationMs; const ordinary = romPriestKnightCastEffects(chains, chains.effects, ["rom-evil-shackles"], 1_000); const undead = romPriestKnightCastEffects(chains, chains.effects, - ["rom-evil-shackles", "rom-target-undead-demon"], 1_000); + ["rom-evil-shackles"], 1_000, ["rom-target-undead-demon"]); expect(rootDuration(ordinary)).toBe(rootDuration(chains.effects)! / 2); expect(rootDuration(undead)).toBe(rootDuration(chains.effects)); }); + it("adds source 499967's extra Wave Armor heal only with Splendor Ripple", () => { + const waveArmor = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490273)!; + const ordinary = romPriestKnightCastEffects(waveArmor, waveArmor.effects, [], 1_000); + const upgraded = romPriestKnightCastEffects(waveArmor, waveArmor.effects, ["rom-splendor-ripple"], 1_000); + expect(ordinary.filter((effect) => effect.kind === "heal")).toHaveLength(0); + expect(upgraded).toEqual(expect.arrayContaining([ + expect.objectContaining({ kind: "heal", basePoints: 106.25, pointsPerLevel: 21.25 }), + ])); + }); + + it("adds source 499924's Blood Arrow healing bonus only with Blood Punishment", () => { + const bloodArrow = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === 490434)!; + const ordinary = romPriestKnightCastEffects(bloodArrow, bloodArrow.effects, [], 1_000); + const upgraded = romPriestKnightCastEffects(bloodArrow, bloodArrow.effects, ["rom-blood-punishment"], 1_000); + expect(ordinary.some((effect) => effect.kind === "apply-aura" + && effect.aura.utilityTags?.includes("rom-blood-arrow-upgrade"))).toBe(false); + expect(upgraded.some((effect) => effect.kind === "apply-aura" + && effect.aura.durationMs === 30_000 + && effect.aura.modifiers?.some((modifier) => modifier.kind === "healing" + && modifier.direction === "taken" && modifier.value === 0.5))).toBe(true); + }); + + it("builds source 499966 Devotion Halo and grants conditional Light Guidance healing", () => { + const [halo] = romPriestKnightAdditionalAbilities(ROM_ALL_ABILITIES); + expect(halo).toMatchObject({ + id: "rom-priest-rom-knight-elite-70-devotion-halo", + dbcSpellId: 499966, + classId: "rom-priest", + secondaryClassId: "rom-knight", + unlockLevel: 70, + radius: 20, + passive: false, + }); + expect(abilitiesForCharacter("rom-priest", "rom-knight").some((ability) => ability.dbcSpellId === 499966)).toBe(true); + expect(halo!.effects.some((effect) => effect.kind === "apply-aura" + && effect.scope === "party" && effect.aura.durationMs === 900_000 + && effect.aura.modifiers?.some((modifier) => modifier.kind === "stat" + && modifier.stat === "wisdom" && modifier.value === 0.05))).toBe(true); + const ordinary = romPriestKnightCastEffects(halo!, halo!.effects, [], 1_000); + const guided = romPriestKnightCastEffects(halo!, halo!.effects, ["rom-light-guidance"], 1_000); + expect(ordinary.some((effect) => effect.kind === "apply-aura" + && effect.aura.modifiers?.some((modifier) => modifier.kind === "healing"))).toBe(false); + expect(guided.some((effect) => effect.kind === "apply-aura" + && effect.aura.modifiers?.some((modifier) => modifier.kind === "healing" + && modifier.direction === "done" && modifier.value === 0.05))).toBe(true); + }); + it("heals multiple nearby allies with Priest Group Heal", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 30, role: "healer", @@ -251,6 +299,32 @@ describe("accepted RoM Priest/Knight and Knight/Scout pairs", () => { expect(useCombatStore.getState().mobs["arrow-b"]!.forcedTarget?.actorId).toBe(PLAYER_AGGRO_ID); }); + it.each([ + [30, "living", false, 0], + [30, "undead", true, 10_000], + [60, "living", true, 5_000], + [60, "demon", true, 10_000], + ] as const)("uses Holy Chains against %s-level %s for the correct duration", ( + level, creatureFamily, allowed, durationMs, + ) => { + useCombatStore.getState().initializeCharacter({ + classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level, role: "tank", + }); + useCombatStore.getState().registerMob("chains-target", { + name: "Chains Target", maxHealth: 10_000, creatureFamily, + }); + setMobPosition("chains-target", [3, 0, 0]); + useCombatStore.getState().selectMob("chains-target"); + const cast = useCombatStore.getState().castAbility("rom-knight-rom-scout-elite-25", undefined, 1_000); + if (!allowed) { + expect(cast).toMatchObject({ ok: false, reason: "condition-not-met" }); + return; + } + expect(cast).toMatchObject({ ok: true }); + const root = useCombatStore.getState().mobs["chains-target"]!.statuses.find((status) => status.kind === "root"); + expect(root?.endsAt).toBe(1_000 + durationMs); + }); + it("damages multiple nearby enemies with Knight Whirlwind Shield", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 30, role: "tank", @@ -322,6 +396,82 @@ describe("accepted RoM Priest/Knight and Knight/Scout pairs", () => { expect(useCombatStore.getState().activeCast?.originalDurationMs).toBeGreaterThan(1_000); }); + it("Splendor Ripple adds healing when the Priest casts Wave Armor at level 60", () => { + useCombatStore.getState().initializeCharacter({ + classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 60, role: "healer", + }); + usePartyStore.getState().initializeParty("priest-ripple", 60, useCombatStore.getState().health, "healer", 2, "rom"); + const member = usePartyStore.getState().members[0]!; + usePartyStore.getState().damageMember(member.id, Math.floor(member.maxHealth * 0.5)); + registerPartyRuntimePosition(member.id, [3, 0, 0]); + usePartyStore.getState().selectMember(member.id); + const injured = usePartyStore.getState().members[0]!.health; + expect(useCombatStore.getState().castAbility("rom-priest-primary-1", undefined, 1_000)) + .toMatchObject({ ok: true }); + expect(usePartyStore.getState().members[0]!.health).toBeGreaterThan(injured); + }); + + it("casts Devotion Halo on the party and gains Light Guidance at level 70", () => { + useCombatStore.getState().initializeCharacter({ + classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 70, role: "healer", + }); + usePartyStore.getState().initializeParty("priest-halo", 70, useCombatStore.getState().health, "healer", 2, "rom"); + const member = usePartyStore.getState().members[0]!; + registerPartyRuntimePosition(member.id, [3, 0, 0]); + expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-70-devotion-halo", undefined, 1_000)) + .toMatchObject({ ok: true }); + expect(useCombatStore.getState().auras.some((aura) => aura.targetId === member.id + && aura.definition.id === "rom-pair-devotion-halo")).toBe(true); + expect(useCombatStore.getState().auras.some((aura) => aura.targetId === member.id + && aura.definition.id === "rom-pair-devotion-halo-mana")).toBe(true); + expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID + && aura.definition.id === "rom-pair-devotion-halo-mana")).toBe(false); + expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID + && aura.definition.id === "rom-pair-light-guidance-healing")).toBe(true); + }); + + it("reduces a companion's actual Mana spending while Devotion Halo is active", () => { + useCombatStore.getState().initializeCharacter({ + classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 70, role: "healer", + }); + usePartyStore.getState().initializeParty("priest-halo-cost", 70, useCombatStore.getState().health, "healer", 2, "rom"); + const partyMember = usePartyStore.getState().members[0]!; + registerPartyRuntimePosition(partyMember.id, [3, 0, 0]); + const member = { + ...partyMember, + classId: "rom-priest" as const, + secondaryClassId: "rom-knight" as const, + resourceType: "mana" as const, + resource: 100_000, + maxResource: 100_000, + resourcePools: { mana: 100_000 }, + cooldowns: {}, + globalCooldownEndsAt: 0, + }; + const spell = abilitiesForCharacter("rom-priest", "rom-knight") + .find((ability) => !ability.passive && ability.unlockLevel <= 70 + && ability.cost.resource === "mana" && ability.cost.amount >= 10)!; + const ordinary = commitPartyAbilityUse(member, spell, 2_000)!; + expect(ordinary).not.toBeNull(); + expect(useCombatStore.getState().castAbility("rom-priest-rom-knight-elite-70-devotion-halo", undefined, 1_000)) + .toMatchObject({ ok: true }); + const discounted = commitPartyAbilityUse(member, spell, 2_000)!; + expect(discounted).not.toBeNull(); + expect(discounted.resourcePools.mana).toBeGreaterThan(ordinary.resourcePools.mana!); + }); + + it("adds Blood Punishment's healing bonus during Knight/Scout Blood Arrow", () => { + useCombatStore.getState().initializeCharacter({ + classId: "rom-knight", secondaryClassId: "rom-scout", raceId: "rom-human", level: 79, role: "tank", + }); + expect(useCombatStore.getState().castAbility("rom-scout-general-4", undefined, 1_000)) + .toMatchObject({ ok: true }); + expect(useCombatStore.getState().auras.some((aura) => aura.targetId === PLAYER_AGGRO_ID + && aura.definition.id === "rom-pair-blood-arrow-healing-received" + && aura.definition.modifiers?.some((modifier) => modifier.kind === "healing" + && modifier.direction === "taken" && modifier.value === 0.5))).toBe(true); + }); + it("summons the Priest/Knight Light Fairy companion", () => { useCombatStore.getState().initializeCharacter({ classId: "rom-priest", secondaryClassId: "rom-knight", raceId: "rom-human", level: 60, role: "healer", diff --git a/src/game/romScoutRogueOverrides.ts b/src/game/romScoutRogueOverrides.ts index f7f1e093..73c3ebf9 100644 --- a/src/game/romScoutRogueOverrides.ts +++ b/src/game/romScoutRogueOverrides.ts @@ -161,10 +161,10 @@ export function overrideRomScoutRogueEffects( customProc(493257, "hit", "rom-reclaim-energy", { chance: 0.25 })); case 499476: return taggedPassive(499476, "Toxic Splash", "rom-toxic-splash", - customProc(499476, "hit", "rom-toxic-splash")); + // 493296 replaces Infection and can generate 20 rage on a successful hit. + customProc(499476, "hit", "rom-infection-rage", { chance: 0.25 })); case 499480: - return taggedPassive(499480, "Poison Battle Technique", "rom-poison-battle-technique", - customProc(499480, "hit", "rom-poison-battle-technique")); + return taggedPassive(499480, "Poison Battle Technique", "rom-poison-battle-technique"); default: return null; } @@ -180,7 +180,8 @@ export function romScoutRogueCastEffects( const passives = new Set(passiveTags); const target = new Set(targetTags); if (ability.dbcSpellId === 490457) { - const masteryFactor = passives.has("rom-master-reflected-shot") ? 1.2 : 1; + // Replacement 499535 / component 623051 raises ranged DPS from 0.75 to 0.85. + const masteryFactor = passives.has("rom-master-reflected-shot") ? 0.85 / 0.75 : 1; return effects.map((effect) => effect.kind === "damage" ? { ...effect, coefficient: effect.coefficient * masteryFactor, @@ -192,6 +193,40 @@ export function romScoutRogueCastEffects( falloffPerJump: passives.has("rom-poisonous-spit") ? 1 : effect.falloffPerJump, } : effect); } + if (ability.dbcSpellId === 490451 && passives.has("rom-meridian-attack")) { + // 499537/623061 replaces Wrist Attack: -30 physical and magical damage for 10s. + return effects.map((effect) => effect.kind === "apply-aura" && effect.recipient !== "caster" + ? { + ...effect, + aura: { + ...effect.aura, + durationMs: 10_000, + modifiers: [ + { kind: "damage" as const, direction: "dealt" as const, school: "physical" as const, operation: "percent" as const, value: -0.30 }, + { kind: "damage" as const, direction: "dealt" as const, attackKind: "spell" as const, operation: "percent" as const, value: -0.30 }, + ], + }, + } : effect); + } + if (ability.dbcSpellId === 492920 && passives.has("rom-toxic-splash")) { + // 499477 replaces Poison Spray. 493296 is its additional infection hit; + // 622992 is the six-second poison it spreads to nearby targets. + return [ + ...effects.filter((effect) => effect.kind !== "dot"), + // 620287: 0.85 main-hand DPS + 0.20 Dexterity, then +7.5% skill-level DPS. + { kind: "damage", coefficient: 1.11375, coefficientPerLevel: 0.06375, sourceLevel: 1 }, + { kind: "dot", coefficient: 0, basePoints: 16.2, pointsPerLevel: 4.2, + sourceLevel: 1, ticks: 3, intervalMs: 2_000, tag: "rom-622992" }, + ]; + } + if (ability.dbcSpellId === 490133 && passives.has("rom-poison-battle-technique")) { + // 499481/620286 raises Whirlwind's main-hand DPS coefficient 0.70 -> 0.75. + return effects.map((effect) => effect.kind === "damage" ? { + ...effect, + coefficient: effect.coefficient * 0.75 / 0.70, + ...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel * 0.75 / 0.70 }), + } : effect); + } if (ability.dbcSpellId === 490454 && passives.has("rom-lasso-master")) { return [...effects, { kind: "status", @@ -216,6 +251,17 @@ export function romScoutRogueCastEffects( return effects; } +/** 499481/622993 adds a hit only to targets already carrying 622992's poison. */ +export function romScoutRogueWhirlwindBonusEffect( + ability: AbilityDefinition, + passiveTags: readonly string[], + targetTags: readonly string[], +): Extract | null { + if (ability.dbcSpellId !== 490133 || !passiveTags.includes("rom-poison-battle-technique") + || !targetTags.includes("rom-622992")) return null; + return { kind: "damage", coefficient: 0.50, coefficientPerLevel: 0.03, sourceLevel: 1 }; +} + export type RomScoutRogueProcActionSpec = | { readonly kind: "aura"; @@ -246,13 +292,13 @@ export type RomScoutRogueProcActionSpec = /** * A pure result for combat-store proc execution. `triggeringSourceSkillId` is * the RuneWaker skill ID of the attack that generated the hit/crit event. - * The level-60/70 replacement skills 499477 and 499481 are absent from the - * playable source catalog; those unresolved actions deliberately return null. + * The level-60/70 replacement skills 499477 and 499481 are represented by + * their learned passive tags and base-ID cast upgrades, not custom procs. */ export function romScoutRogueProcActionSpec( actionId: string, triggeringSourceSkillId: number, - casterPoisonous: boolean, + _casterPoisonous: boolean, ): RomScoutRogueProcActionSpec | null { switch (actionId) { case "rom-exploiting-shot": @@ -286,21 +332,21 @@ export function romScoutRogueProcActionSpec( provenance: "hybrid-estimate", }; case "rom-poisonous-spit": - if (!casterPoisonous || ![490457, 490424].includes(triggeringSourceSkillId)) return null; + if (![490457, 490424].includes(triggeringSourceSkillId)) return null; return { kind: "dot", recipient: "target", effect: { kind: "dot", coefficient: 0, - basePoints: 16, - pointsPerLevel: 8, + basePoints: 7.1, + pointsPerLevel: 2.1, sourceLevel: 1, - ticks: 5, + ticks: 4, intervalMs: 2_000, tag: "rom-poisonous-spit", }, - provenance: "hybrid-estimate", // 501690 poison scale is a proxy; Poison Powder payload is not in this catalog. + provenance: "source", // 499535/503522. Poison Powder is supplied without inventory gating. }; case "rom-reclaim-energy": if (triggeringSourceSkillId === 490306) return { @@ -310,8 +356,12 @@ export function romScoutRogueProcActionSpec( kind: "resource", recipient: "caster", resource: "rage", amount: 10, provenance: "hybrid-estimate", }; return null; - case "rom-toxic-splash": // Requires absent replacement skill 499477 and triggered payload 493296. - case "rom-poison-battle-technique": // Requires absent replacement skill 499481. + case "rom-infection-rage": + return triggeringSourceSkillId === 492625 + ? { kind: "resource", recipient: "caster", resource: "rage", amount: 20, provenance: "hybrid-estimate" } + : null; + case "rom-toxic-splash": // 499477 is applied as an upgrade of base 492920. + case "rom-poison-battle-technique": // 499481 is applied as an upgrade of base 490133. return null; default: return null; diff --git a/src/game/romScoutRoguePairValidation.test.ts b/src/game/romScoutRoguePairValidation.test.ts index c85be672..2c336adc 100644 --- a/src/game/romScoutRoguePairValidation.test.ts +++ b/src/game/romScoutRoguePairValidation.test.ts @@ -1,9 +1,8 @@ import { beforeEach, describe, expect, it } from "vitest"; import { abilitiesForCharacter, learnedAbilityForBuild, type AbilityDefinition } from "./abilityCatalog"; -import { PLAYER_AGGRO_ID } from "./aggro"; import { useCombatStore } from "./combatStore"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; -import { romScoutRogueCastEffects, romScoutRogueProcActionSpec } from "./romScoutRogueOverrides"; +import { romScoutRogueCastEffects, romScoutRogueProcActionSpec, romScoutRogueWhirlwindBonusEffect } from "./romScoutRogueOverrides"; const scoutRogue = abilitiesForCharacter("rom-scout", "rom-rogue"); const rogueWarrior = abilitiesForCharacter("rom-rogue", "rom-warrior"); @@ -122,7 +121,7 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => { expect(new Set(damage).size).toBe(1); }); - it("Master Reflected Shot applies a labeled 20% hybrid damage bonus", () => { + it("Master Reflected Shot uses replacement 499535's 0.85 / 0.75 DPS ratio", () => { const ability = skill(scoutRogue, "rom-scout-primary-11"); const base = ability.effects.find((effect) => effect.kind === "damage"); const mastered = romScoutRogueCastEffects(ability, ability.effects, @@ -131,7 +130,7 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => { expect(base?.kind).toBe("damage"); expect(mastered?.kind).toBe("damage"); if (base?.kind !== "damage" || mastered?.kind !== "damage") return; - expect(mastered.coefficient).toBeCloseTo(base.coefficient * 1.2); + expect(mastered.coefficient).toBeCloseTo(base.coefficient * 0.85 / 0.75); expect(mastered.falloffPerJump).toBe(1); }); @@ -160,12 +159,11 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => { expect(proc?.kind === "apply-aura" ? proc.aura.procs?.[0]?.internalCooldownMs : null).toBe(20_000); }); - it("proc resolver poisons only Reflected or Combo Shot while carrying Poison Powder", () => { + it("proc resolver poisons Reflected or Combo Shot with unlimited Poison Powder", () => { for (const sourceId of [490457, 490424]) { - expect(romScoutRogueProcActionSpec("rom-poisonous-spit", sourceId, false)).toBeNull(); - expect(romScoutRogueProcActionSpec("rom-poisonous-spit", sourceId, true)).toMatchObject({ - kind: "dot", recipient: "target", provenance: "hybrid-estimate", - effect: { kind: "dot", ticks: 5, intervalMs: 2_000 }, + expect(romScoutRogueProcActionSpec("rom-poisonous-spit", sourceId, false)).toMatchObject({ + kind: "dot", recipient: "target", provenance: "source", + effect: { kind: "dot", ticks: 4, intervalMs: 2_000, basePoints: 7.1, pointsPerLevel: 2.1 }, }); } expect(romScoutRogueProcActionSpec("rom-poisonous-spit", 490423, true)).toBeNull(); @@ -175,15 +173,6 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => { it("executes the Scout/Rogue poison rider on a successful reflected hit", () => { prime("rom-scout", 60); - useCombatStore.getState().synchronizeActors(); - const poison = skill(rogueWarrior, "rom-rogue-primary-6"); - const state = poison.effects - .filter((effect): effect is Extract => effect.kind === "apply-aura") - .find((effect) => effect.aura.utilityTags?.includes("rom-poisonous") === true); - expect(state).toBeDefined(); - expect(useCombatStore.getState().applyActorAura( - PLAYER_AGGRO_ID, PLAYER_AGGRO_ID, poison, state!, 1_000, - )).toBe(true); expect(useCombatStore.getState().castAbility("rom-scout-primary-11", undefined, 2_600)) .toMatchObject({ ok: true }); expect(useCombatStore.getState().effects.some((effect) => effect.tag === "rom-poisonous-spit" @@ -202,11 +191,112 @@ describe("accepted RoM Scout/Rogue and Rogue/Warrior pairings", () => { expect(proc?.kind === "apply-aura" ? proc.aura.procs?.[0]?.chance : null).toBe(0.25); }); - it("keeps absent 499477/499481 replacement-skill procs explicitly unresolved", () => { + it("Toxic Splash's 493296 Infection upgrade can return 20 rage on its hit", () => { + expect(romScoutRogueProcActionSpec("rom-infection-rage", 490338, false)).toBeNull(); + expect(romScoutRogueProcActionSpec("rom-infection-rage", 492625, false)).toMatchObject({ + kind: "resource", recipient: "caster", resource: "rage", amount: 20, + provenance: "hybrid-estimate", + }); + const proc = skill(rogueWarrior, "rom-rogue-rom-warrior-elite-60").effects.find((effect) => effect.kind === "apply-aura"); + expect(proc?.kind === "apply-aura" ? proc.aura.procs?.[0]?.chance : null).toBe(0.25); + }); + + it("maps 499477/499481 through cast upgrades rather than redundant procs", () => { expect(romScoutRogueProcActionSpec("rom-toxic-splash", 499477, true)).toBeNull(); expect(romScoutRogueProcActionSpec("rom-poison-battle-technique", 499481, true)).toBeNull(); }); + it("Meridian Attack upgrades Wrist Attack's physical and spell damage penalty", () => { + const ability = skill(scoutRogue, "rom-scout-general-6"); + expect(ability.dbcSpellId).toBe(490451); + const upgraded = romScoutRogueCastEffects(ability, ability.effects, ["rom-meridian-attack"], []); + const debuff = upgraded.find((effect) => effect.kind === "apply-aura" && effect.recipient !== "caster"); + expect(debuff?.kind === "apply-aura" ? debuff.aura : null).toMatchObject({ + durationMs: 10_000, + modifiers: expect.arrayContaining([ + { kind: "damage", direction: "dealt", school: "physical", operation: "percent", value: -0.3 }, + { kind: "damage", direction: "dealt", attackKind: "spell", operation: "percent", value: -0.3 }, + ]), + }); + }); + + it("applies Meridian Attack's ten-second Wrist Attack debuff at level 79", () => { + prime("rom-scout", 79); + expect(useCombatStore.getState().castAbility("rom-scout-general-6", undefined, 1_000)) + .toMatchObject({ ok: true }); + const debuff = useCombatStore.getState().auras.find((aura) => aura.targetId === "target" + && aura.definition.modifiers?.some((modifier) => modifier.kind === "damage" + && modifier.direction === "dealt" && modifier.attackKind === "spell")); + expect(debuff?.definition.durationMs).toBe(10_000); + }); + + it("Toxic Splash upgrades Poison Spray to 493296's hit and 622992's six-second poison", () => { + const ability = skill(rogueWarrior, "rom-rogue-rom-warrior-elite-40"); + const upgraded = romScoutRogueCastEffects(ability, ability.effects, ["rom-toxic-splash"], []); + expect(upgraded.filter((effect) => effect.kind === "damage")).toHaveLength(2); + expect(upgraded.filter((effect) => effect.kind === "damage")[1]).toMatchObject({ + kind: "damage", coefficient: 1.11375, coefficientPerLevel: 0.06375, + }); + expect(upgraded.find((effect) => effect.kind === "dot")).toMatchObject({ + kind: "dot", ticks: 3, intervalMs: 2_000, tag: "rom-622992", + basePoints: 16.2, pointsPerLevel: 4.2, + }); + }); + + it("spreads Toxic Splash's replacement poison to nearby enemies at level 79", () => { + prime("rom-rogue", 79); + useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, rage: 100 } })); + expect(useCombatStore.getState().castAbility("rom-rogue-primary-6", undefined, 1_000)) + .toMatchObject({ ok: true }); + useCombatStore.getState().tick(1.1, 2_100); + const infectionCast = useCombatStore.getState().castAbility("rom-rogue-rom-warrior-elite-35", undefined, 2_600); + expect(infectionCast, infectionCast.reason).toMatchObject({ ok: true }); + expect(useCombatStore.getState().castAbility("rom-rogue-rom-warrior-elite-40", undefined, 4_200)) + .toMatchObject({ ok: true, affectedMobIds: expect.arrayContaining(["target", "nearby"]) }); + expect(useCombatStore.getState().effects.some((effect) => effect.tag === "rom-622992" + && effect.targetId === "nearby")).toBe(true); + expect(useCombatStore.getState().effects.some((effect) => effect.kind === "dot" + && effect.sourceAbilityId === "rom-rogue-rom-warrior-elite-35" && effect.targetId === "target")).toBe(false); + }); + + it("Poison Battle Technique upgrades Whirlwind and adds a per-target poisoned hit", () => { + const ability = skill(rogueWarrior, "rom-warrior-general-2"); + const base = ability.effects.find((effect) => effect.kind === "damage"); + const upgraded = romScoutRogueCastEffects(ability, ability.effects, ["rom-poison-battle-technique"], []) + .find((effect) => effect.kind === "damage"); + expect(base?.kind).toBe("damage"); + expect(upgraded?.kind).toBe("damage"); + if (base?.kind !== "damage" || upgraded?.kind !== "damage") return; + expect(upgraded.coefficient).toBeCloseTo(base.coefficient * 0.75 / 0.70); + expect(romScoutRogueWhirlwindBonusEffect(ability, ["rom-poison-battle-technique"], [])) + .toBeNull(); + expect(romScoutRogueWhirlwindBonusEffect(ability, ["rom-poison-battle-technique"], ["rom-622992"])) + .toMatchObject({ kind: "damage", coefficient: 0.5, coefficientPerLevel: 0.03 }); + }); + + it("Whirlwind grants its Poison Battle Technique rider only to sprayed targets at level 79", () => { + prime("rom-rogue", 79); + useCombatStore.getState().registerMob("clean", { name: "clean", maxHealth: 1_000_000 }); + setMobPosition("clean", [-4, 0, 0]); // Whirlwind range, outside Spray's target-centered radius. + useCombatStore.setState((state) => ({ resourcePools: { ...state.resourcePools, rage: 100 } })); + expect(useCombatStore.getState().castAbility("rom-rogue-primary-6", undefined, 1_000)).toMatchObject({ ok: true }); + useCombatStore.getState().tick(1.1, 2_100); + expect(useCombatStore.getState().castAbility("rom-rogue-rom-warrior-elite-35", undefined, 2_600)) + .toMatchObject({ ok: true }); + expect(useCombatStore.getState().castAbility("rom-rogue-rom-warrior-elite-40", undefined, 4_200)) + .toMatchObject({ ok: true }); + expect(useCombatStore.getState().effects.some((effect) => effect.tag === "rom-622992" && effect.targetId === "target")) + .toBe(true); + expect(useCombatStore.getState().effects.some((effect) => effect.tag === "rom-622992" && effect.targetId === "clean")) + .toBe(false); + expect(useCombatStore.getState().castAbility("rom-warrior-general-2", undefined, 5_800)) + .toMatchObject({ ok: true, affectedMobIds: expect.arrayContaining(["target", "clean"]) }); + const hits = useCombatStore.getState().combatEvents.filter((event) => event.abilityId === "rom-warrior-general-2" + && event.kind === "damage"); + expect(hits.filter((event) => event.targetActorId === "target")).toHaveLength(2); + expect(hits.filter((event) => event.targetActorId === "clean")).toHaveLength(1); + }); + it("Rogue Combo Throw has two hits and Poison grants a proc state (490338, 490329)", () => { expect(skill(rogueWarrior, "rom-rogue-primary-15").effects.filter((effect) => effect.kind === "damage")).toHaveLength(2); prime("rom-rogue", 60);