Skip to content

feat(corruption): the three 8.3 corruption drawback tiers - #75

Draft
svenbledt wants to merge 10 commits into
Hextv:mainfrom
svenbledt:corruption-system
Draft

feat(corruption): the three 8.3 corruption drawback tiers#75
svenbledt wants to merge 10 commits into
Hextv:mainfrom
svenbledt:corruption-system

Conversation

@svenbledt

@svenbledt svenbledt commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

This PR was rewritten. Its history was 62 commits carrying an oversized, essay-style comment
block on nearly every hunk. That was fair criticism and it has been addressed: the branch is now 12
commits, every comment states one why-fact in at most three lines, and the reasoning that used to
live in the code lives here instead. @Hextv's commit fix: add safeguards to some SQL updates was
pushed directly onto this branch and is preserved with its original authorship — it now sits in
PR #78.

This is the last of six PRs. The independent groundwork was split out so it can be reviewed
without the feature attached:

#77 build(cmake) MySQL discovery independent
#78 SQL updates directory rename independent — this PR stacks on it
#79 bonus-granted item effects independent — this PR stacks on it
#80 aura 285 → linked-aura handler independent — this PR stacks on it
#81 corruption penalty re-evaluation independent — this PR stacks on it

Everything below core/areatrigger: let a script resize the trigger the client draws in the commit
list belongs to one of those five. This PR's own contribution is the top five commits. It is
kept as a draft because one known defect is unresolved — see Known Issues.

Changes Proposed:

This PR proposes changes to:

  • Core (units, players, creatures, game systems).
  • Scripts (bosses, spell scripts, creature scripts).
  • Database (SAI, creatures, etc).

The three 8.3 corruption drawback tiers, plus the two core capabilities they need.

core/areatrigger: let a script resize the trigger the client draws

Nothing in this core ever wrote OverrideScaleCurve. AreaTrigger::Create fills in ExtraScaleCurve
from the misc template and leaves this one default-constructed, so every areatrigger is drawn at
exactly the size its template describes. A script whose trigger has to change size had no way to tell
the client, and the drawn ring then disagreed with whatever the server tested against.

SetOverrideScaleCurve writes a flat two-point curve at the requested multiple of the template size;
ClearOverrideScaleCurve returns the trigger to the template's own size. Rendering only — the
server-side shape is unchanged, so a caller that scales the visual must scale its own range test to
match.

core/spells: send runtime aura amounts for the corruption stat payloads

The five corruption stat payload spells carry DB2 base points of 0. The whole magnitude is supplied
at runtime by their aura-285 container, through HandleAuraLinked into CastCustomSpell. Without
SPELL_ATTR8_AURA_SEND_AMOUNT the amount never reaches the client (see
AuraApplication::BuildUpdatePacket), so the server holds the real value while the tooltip renders 0.

Grasping Tendrils — the 1+ corruption drawback

The first threshold penalty did nothing. Its snare 315176 carries BasePoints 0 in
SpellEffect.db2 and has no world DB override, so the aura applied at "movement speed reduced by
0%"
. Retail computed the magnitude server-side and shipped the client a placeholder — which is why
Blizzard's own 8.3 PTR tooltip read "slow your movement speed for 0 sec".

The curve is taken from its primary source rather than guessed. Wowhead's 8.3 PTR article
Corrupted Items — Corruption Debuff Breakpoints and Scaling of Debuffs
states outright that "the magnitude of the slow is equal to {Corruption + 10}", and tabulates it
from 10 corruption (20%) through 90 (100%). That is what the constants here produce.

The container also fired on heals. 315175 ships every TAKEN proc flag, including
PROC_FLAG_TAKEN_PERIODIC, which SpellMgr.h documents as "damage / healing"; with no
spell_proc row, the auto-generated entry takes PROC_SPELL_TYPE_MASK_ALL. Standing in a druid's
Efflorescence rooted the player once per healing tick. The new row restricts it to
PROC_SPELL_TYPE_DAMAGE and restores PROC_ATTR_TRIGGERED_CAN_PROC, which a hand-written row would
otherwise drop.

Eye of Corruption — the 20+ corruption drawback

The Eye spawned and did nothing. Container 315169 procs on damage dealt and triggers 315154,
which creates areatrigger 22815 — but 22815 carried no ScriptName and no template actions, and
nothing cast the damage spell 315161. The Eye appeared, sat for its eight seconds and left. A
template action could not fix it either: DoActions fires once on enter, while the Eye has to tick
for as long as the player stays in range.

What the client supplies is read, not hardcoded. The 8 second duration is SpellDuration index
31, the 2 second tick is 315154 effect 1, the school is SpellMisc SchoolMask 32, and the
per-stack amplification is 315161 effect 1 — the same row the client renders "damage taken increased
by $s2%"
from.

What it does not supply is the damage and the radius — the two things the tooltip promises scale
with corruption. Every scaling field on 315161 effect 0 is zero: EffectBasePoints,
EffectBonusCoefficient, BonusCoefficientFromAP, Coefficient, Variance,
EffectRealPointsPerLevel, ResourceCoefficient and EffectPointsPerResource. ContentTuningID is
0 and there is no hotfix row. Retail computed both server-side. Both come instead from the same
Wowhead PTR sampling (news 295810): 875 × Corruption − 1000
per tick
, and a radius of Corruption / 5 yards. The stated formulas are used rather than a fit
to the article's own table, because the table's drift past 50 corruption is an artefact of Inevitable
Doom amplifying all damage taken.

The ring the player sees and the range the damage covers are the same edge by construction — both
come from one division against the template radius. An earlier attempt scaled the radius but drove
the graphic with SetObjectScale, and the damage then cut off outside the visible ring. The range
test is deliberately 2d and centre-to-centre: the zone is a cylinder, and both IsWithinDist2d and
GetInsideUnits() route through IsInDist, which pads the radius by the player's CombatReach and
so damages a player standing outside the edge they can see.

Grand Delusions — the 40+ corruption drawback

Container 315184 triggers 315186, which summons creature 161895 — the Thing From Beyond —
and nothing happened after that. The creature was marked SmartAI with no smart_scripts rows of its
own, so it spawned, stood still and despawned.

Its appearance comes from a clone aura, not the template. 161895's only creature_template_model
row is display 11686 — the invisible stalker shared by 8467 templates for bunnies and kill credit.
That reads like an oversight and is not one: retail's Thing From Beyond is a copy of the player it
chases, so the model was never meant to come from the template. The clone has to be a real aura,
because WorldSession::HandleMirrorImageDataRequest answers the client's request for the copy's gear
only if the unit carries SPELL_AURA_CLONE_CASTER, and reads the appearance off that aura's caster.
It is applied rather than cast: Clone Caster is a positive spell and this summon is hostile
(SummonProperties 4793 sets faction 14 over the template's friendly 35), so CheckCast answers
SPELL_FAILED_BAD_TARGETS — and forgiving that requires TRIGGERED_IGNORE_TARGET_CHECK, which sits
outside TRIGGERED_FULL_MASK and is therefore unreachable from CastSpell(..., true).

The Thing is levelled to its summoner. GetEffectiveResistChance adds
(victim level − attacker level) × 5 resistance, so a Thing below its target's level had roughly half
its damage resisted away before it landed.

The strike is measured the way the chase generator measures. IsWithinMeleeRange squares the
height difference into the distance while ChaseMovementGenerator stops on a 2d test, so on uneven
ground the pursuit arrived and then failed its own strike check for the whole eight seconds. One
observed pursuit ended at 2d 5.72 with dz −1.34 — 5.88 in three dimensions, against a melee range
of 5.08.

Damage goes through SpellNonMeleeDamage, not DealDamage, so it appears in the combat log and
passes through absorbs, resistances and damage-taken modifiers instead of executing unconditionally.
The tooltip figure — "reaching you deals about your health in damage" — is the cost of a whole
pursuit, so it is divided across the strikes the Thing has time to land: being caught late costs less
than being caught at once. Connecting does not spend the Thing, because Cascading Disaster's tooltip
says being struck applies Grasping Tendrils and Eye of Corruption, which a pursuer that vanished on
contact could not do.

Pursuit speed is an approximation and is isolated in the GrandDelusions namespace for retuning.
The tooltip states the speed rises with corruption but gives no numbers,
creature_template.speed_run is TrinityCore's stock default on all three pursuer creatures, and
there is no movement-speed aura anywhere in the corruption block.

Removed from the previous revision

An earlier revision played the Thing From Beyond's own spell visual kits on the summon and added
.debug visualkit / .debug spellvisual to iterate on them. The visual kits did not produce the
retail appearance — the clone aura does — so that code and its two debug commands are gone rather
than left in place. The TC_LOG_DEBUG instrumentation used during that investigation is gone too.

AI-assisted Pull Requests

  • AI tools were used. Claude Code, model Claude Opus 5. Used for the DB2 field surveys, the
    call-site tracing, drafting, and the comment cleanup / history rewrite described at the top.
    Every line has been reviewed and is defensible by the author.

Issues Addressed:

  • Closes nothing tracked.

SOURCE:

The changes have been validated through:

  • Live research (checked on live servers, e.g Classic WotLK, Retail, etc.)
  • Sniffs (remember to share them with the open source community!)
  • Video evidence, knowledge databases or other public sources (e.g forums, Wowhead, etc.)
  • The changes promoted by this pull request come partially or entirely from another project (cherry-pick).

Two distinct sources, kept separate on purpose:

  1. The 8.3.7 client data, for everything it actually describes — durations, tick periods, school
    masks, amplification percentages, summon properties, faction. Cited inline above by DB2 table and
    field so a reviewer can check each one.
  2. Wowhead's 8.3 PTR sampling, news 295810, for the two
    things retail computed server-side and never shipped: the Grasping Tendrils slow curve and the Eye
    of Corruption damage and radius. These are the numbers most worth a second opinion, because no
    client-side ground truth exists for them.

Where neither source has an answer — pursuit speed — that is stated as an approximation rather than
presented as retail behaviour.

Tests Performed:

  • Tested in-game by the author.
  • Tested in-game by other community members/someone else other than the author.
  • This pull request requires further testing and may have edge cases to be tested.

Builds clean on Windows / VS 2022 x64 RelWithDebInfo. Verified in-game, per tier:

  • Grasping Tendrils — the snare applies on damage taken, not on heals, and its magnitude tracks
    effective corruption.
  • Eye of Corruption — the Eye spawns, draws at a corruption-scaled radius, and ticks while the
    player stands in it.
  • Grand Delusions — the Thing spawns as a copy of its summoner, pursues, and its strike is
    reported as spell damage in the combat log.

Not tested: the numbers themselves against retail. Nobody has sniffed 8.3 corruption damage; the
formulas are as good as the Wowhead sampling they come from.

How to Test the Changes:

  • This pull request requires further testing. Steps below.
  1. .additem <itemId> 1 <bonusListId> with a corruption bonus list — e.g. 6552 (Infinite Stars,
    20 corruption). The third argument is a bonus list id, not a spell id.
  2. .debug corruption (from core/player: re-evaluate corruption penalties without re-applying them #81) to confirm the effective total and which threshold rows qualify.
  3. 1+ — take damage and confirm the snare applies with a non-zero magnitude that tracks the total.
    Then stand in a HoT or an Efflorescence and confirm it does not fire on heals.
  4. 20+ — deal damage and confirm the Eye spawns, that the ring you see matches the range that
    damages you, and that it ticks every 2 seconds.
  5. 40+ — confirm the Thing From Beyond spawns looking like you, chases, and that its strike lands
    in the combat log rather than executing silently. Test on sloped ground specifically — that is the
    case the 2d/3d measurement mismatch broke.

Known Issues and TODO List:

  • Unequip does not remove the effect (inherited from core/items: apply item effects granted by bonus lists #79, not introduced here). A corrupted
    item's affix keeps firing after the item is unequipped, until it leaves the bags or the player
    relogs. Root cause is not established: the effects are confirmed TriggerType = ON_EQUIP, and
    ApplyItemEquipSpell(item, false) is reached on unequip, but the removal is not matching.
    This is why the PR is still a draft.
  • Some items that should carry a corruption effect do not — @Hextv reports 172227
    [Shard of the Black Empire]. Triaged as bonus-list data rather than code, but unconfirmed.
  • Only the three drawback tiers are implemented. The corruption affixes — Echoing Void, Twilight
    Devastation, Infinite Stars and the rest — are not.
  • Pursuit speed for the Thing From Beyond is an approximation with no source (see above).
  • The Eye's damage and radius formulas rest entirely on one Wowhead PTR article. If anyone has a
    sniff, it should replace them.
  • If you hand-applied the previous revision's migrations, note that its seven incremental
    files are replaced by three final-state ones. The three reach the same end state on their own,
    with one exception: the old 2026_08_01_02 briefly moved the AI onto creature 160966 and
    2026_08_01_03 moved it back. If you applied _02 but not _03, clear that row by hand —
    UPDATE creature_template SET AIName = 'SmartAI', ScriptName = '' WHERE entry = 160966;

@svenbledt

Copy link
Copy Markdown
Collaborator Author

@Hextv its a draft PR so i could need asisst on the last gaps i clearly documented but those are the last ones then were done here!

@Hextv

Hextv commented Aug 1, 2026

Copy link
Copy Markdown
Owner

I found that some items that should have the corruption spellid don't.

For example 172227 [Shard of the Black Empire].

@svenbledt

Copy link
Copy Markdown
Collaborator Author

I also found a problem:

When unequipped items with corruption the corruption effects still trigger until u remove them from your bags or relog.

ITEM_BONUS_ITEM_EFFECT_ID = 23 is declared in DBCEnums.h but had no case
in BonusData::AddBonus, BonusData had no storage for effects, and every
consumer read proto->Effects - the static ItemTemplate list. An item
whose effect arrives through a bonus list therefore contributed its
stats but never its effect spell. This is the whole reason corrupted
gear does nothing on this core, but it is not corruption-specific: any
bonus-granted effect was silently dropped.

BonusData gains an effect set, seeded from the template in Initialize
and appended to by AddBonus. Item::GetEffects() returns it and
Item::GetEffectCount() its size. The migration rule is that a call site
uses GetEffects() if and only if it holds an Item*; sites that hold only
an ItemTemplate const* keep proto->Effects and carry a comment saying
why - a loot filter runs before any Item exists, an auction listing is
an entry plus bonus ids, a toy is used from the collection, and
UpdatePotionCooldown is keyed off an item id kept after the Item is
gone.

Charge handling follows the effect set rather than the template. Charge
slots are keyed by effect index into a 5-wide update field, so every
walk is clamped to MAX_ITEM_SPELLS and indexed with uint8. Item::Create
seeds from the template only and every caller applies bonus lists
afterwards, so SeedSpellCharges fills the slots a newly applied bonus
list just added without touching charges already spent. SaveToDB and
LoadFromDB agree on the clamped width, and LoadFromDB now runs after
SetBonuses and reads however many tokens are present instead of
discarding every charge on a count mismatch.

Four defects surfaced while migrating the call sites and are fixed here:

- Spell::TakeCastItem judged "no charges left" from whichever effect it
  looked at last, and read a charge array that a stacked item never
  writes to. It now folds the verdict across every charged effect and
  only trusts a persisted count.
- Spell::TakeReagents dereferenced m_CastItem in a block that had
  already cleared it, if a second reagent slot named the same item.
- LootItem::AllowedForPlayer indexed Effects[1] unconditionally.
- WorldSession::HandleUseItemOpcode rejected an in-combat use because a
  passive effect on the item was flagged unusable in combat, rather than
  judging the on-use effect being cast.

MAX_BONUS_ITEM_EFFECTS caps the set at 16 so malformed hotfix data
cannot grow it without limit; unknown effect ids are skipped silently,
since logging would fire once per item instance.

Verified by building worldserver on Windows / VS 2022 x64 RelWithDebInfo
and in-game: a corrupted item's effect spell now applies on equip, and
charged on-use items are consumed at the right time.
Aura 285 was named SPELL_AURA_MOD_ATTACK_POWER_OF_ARMOR and handled by
HandleAuraModAttackPowerOfArmor, which added the target's armor to its
attack power. The 8.3.7 client data does not describe that aura. It
carries 359 rows of aura 285 across 336 spells, every one of them with a
non-zero trigger spell, which is the shape of SPELL_AURA_LINKED (284) -
not of a stat conversion. The old handler was inert in practice: nothing
in this build's data reaches it.

Aura 285 is therefore renamed SPELL_AURA_LINKED_2 and pointed at
HandleAuraLinked, matching what the rows actually contain. The 284 + 285
link graph in this build was checked for safety before repointing: it
has zero cycles and a maximum depth of 2, so no chain of linked auras
can recurse.

HandleAuraModAttackPowerOfArmor and its UpdateAttackPowerAndDamage
counterpart are removed rather than left unreachable, and the two
comments in StatSystem.cpp that cited the aura by name are corrected -
UpdateArmor still refreshes melee attack power, just not for that
reason.

This is a generic change and deserves regression testing beyond the work
that motivated it: any spell using aura 284 or 285 is in scope. Built
clean on Windows / VS 2022 x64 RelWithDebInfo.
Player::UpdateCorruption had three problems, all of which left the
CorruptionEffects.db2 threshold penalties out of step with the player.

It re-applied live auras. The loop ended in an unconditional
CastSpell(this, aura, true), so every corruption rating change re-cast
penalties that were already present, restarting their durations and
resetting their proc state. It now casts only what is missing and lets
an aura that is already applied recalculate its own amounts in place.

It was never re-evaluated on an area change. Each CorruptionEffects row
carries a PlayerConditionID, and the sync ran only from
UpdateRating(CR_CORRUPTION | CR_CORRUPTION_RESISTANCE) - a gear or
rating-aura change. A player who walked into an area where the condition
stopped matching kept the penalty indefinitely, and one who walked out
never regained it. UpdateArea now schedules a sync.

It was never re-evaluated on resurrect. RemoveAllAurasOnDeath strips the
penalties and neither a rating change nor an area transition follows, so
reviving where you fell left the player uncorrupted until they happened
to cross an area border. That is why a corpse run appeared to fix it and
.revive did not.

Syncing at each mutation site is wrong in the other direction: a bulk
item rebuild strips every item's corruption before re-applying it, so a
per-mutation sync walks the total down to zero and tears down each
tier's aura on the way. The sync is now coalesced behind a dirty flag
and runs once per Player::Update tick. The two item-level rebuilds that
did not bracket themselves with SetCanModifyStats now do, and
UpdateItemLevelAreaBasedScaling settles its derived stats afterwards -
without that the max health it scales the player to was one rebuild
stale.

GetEffectiveCorruption is split out of UpdateCorruption so callers that
need the value do not repeat the rating subtraction.

Verified in-game: penalties apply and remove as corruption crosses each
threshold, survive a gear swap, and return on resurrect.
Corruption penalties are driven entirely by CorruptionEffects.db2, and
when one fails to apply there is no way to see which of the three gates
rejected it - the threshold, the PlayerConditionID, or the row being
flagged disabled. The only alternative is equipping corrupted gear and
inferring the answer from what happens.

.debug corruption prints the selected player's effective corruption
alongside the two ratings it derives from, then every CorruptionEffects
row with its threshold, aura, condition, the verdict on that player and
whether the aura is actually applied. A server whose client data has no
CorruptionEffects rows at all reports that explicitly, since it is
otherwise indistinguishable from a core fault.

RBAC-gated behind RBAC_PERM_COMMAND_DEBUG like the rest of the table.
Nothing in this core ever wrote OverrideScaleCurve. AreaTrigger::Create
fills in ExtraScaleCurve from the misc template and leaves this one
default-constructed, so every areatrigger is drawn at exactly the size
its template describes. That is fine while a spell keeps a fixed shape,
but a script whose trigger has to change size has no way to tell the
client, and the drawn ring then disagrees with whatever the server
tests against.

SetOverrideScaleCurve writes a flat two-point curve at the requested
multiple of the template size; ClearOverrideScaleCurve returns the
trigger to the template's own size. Rendering only - the server-side
shape is unchanged, so a caller that scales the visual must scale its
own range test to match.
The five corruption stat payload spells carry DB2 base points of 0 - the
whole magnitude is supplied at runtime by their aura-285 container,
through HandleAuraLinked into CastCustomSpell. Without
SPELL_ATTR8_AURA_SEND_AMOUNT the amount never reaches the client (see
AuraApplication::BuildUpdatePacket), so the server holds the real value
while the tooltip renders 0.
The first corruption threshold penalty did nothing. Its snare 315176
carries BasePoints 0 in SpellEffect.db2 and no world DB override, so the
aura applied at "movement speed reduced by 0%" - retail computed the
magnitude server-side and shipped the client a placeholder, which is why
Blizzard's own 8.3 PTR tooltip read "slow your movement speed for 0
sec".

The curve is confirmed at its primary source rather than guessed:
Wowhead's 8.3 PTR article "Corrupted Items - Corruption Debuff
Breakpoints and Scaling of Debuffs" (news 295810) states outright that
"the magnitude of the slow is equal to {Corruption + 10}" and tabulates
it from 10 corruption (20%) to 90 (100%), which is what the constants
here produce.

The container also fired on heals. 315175 ships every TAKEN proc flag,
including PROC_FLAG_TAKEN_PERIODIC, which SpellMgr.h documents as
"damage / healing"; with no spell_proc row the auto-generated entry
takes PROC_SPELL_TYPE_MASK_ALL. Standing in a druid's Efflorescence
rooted the player once per healing tick. The new row restricts it to
PROC_SPELL_TYPE_DAMAGE and restores PROC_ATTR_TRIGGERED_CAN_PROC, which
a hand-written row would otherwise drop.

Verified in-game: the snare applies on damage taken, not on heals, and
its magnitude tracks effective corruption.
The Eye spawned and did nothing. Container 315169 procs on damage dealt
and triggers 315154, which creates areatrigger 22815, but 22815 carried
no ScriptName and no template actions and nothing cast the damage spell
315161 - so the Eye appeared, sat for its eight seconds and left. A
template action could not fix it either: DoActions fires once on enter,
while the Eye has to tick for as long as the player stays in range.

What the client supplies is read rather than hardcoded. The 8 second
duration is SpellDuration index 31, the 2 second tick is 315154 effect
1, the school is SpellMisc SchoolMask 32, and the per-stack amplification
is 315161 effect 1 - the same row the client renders "damage taken
increased by $s2%" from.

What it does not supply is the damage and the radius, the two things the
tooltip promises scale with corruption. Every scaling field on 315161
effect 0 is zero - EffectBasePoints, EffectBonusCoefficient,
BonusCoefficientFromAP, Coefficient, Variance, EffectRealPointsPerLevel,
ResourceCoefficient and EffectPointsPerResource - ContentTuningID is 0
and there is no hotfix row. Retail computed both server-side. Both come
instead from Wowhead's 8.3 PTR sampling (news 295810): 875 * Corruption
- 1000 per tick, and a radius of Corruption / 5 yards. The stated
formulas are used rather than a fit to the article's own table, because
the table's drift past 50 corruption is an artefact of Inevitable Doom
amplifying all damage taken.

The ring the player sees and the range the damage covers are now the
same edge by construction, both taken from one division against the
template radius. An earlier attempt scaled the radius but drove the
graphic with SetObjectScale, and the damage cut off outside the visible
ring. The range test is deliberately 2d and centre-to-centre: the zone
is a cylinder, and both IsWithinDist2d and GetInsideUnits() route
through IsInDist, which pads the radius by the player's CombatReach and
so damages a player standing outside the edge they can see.

Verified in-game: the Eye spawns, draws at a corruption-scaled radius
and ticks while the player stands in it.
Container 315184 triggers 315186, which summons creature 161895 - the
Thing From Beyond - and nothing happened after that. The creature was
marked SmartAI with no smart_scripts rows of its own, so it spawned,
stood still and despawned.

Its appearance comes from a clone aura rather than the template. 161895's
only creature_template_model row is display 11686, the invisible stalker
shared by 8467 templates for bunnies and kill credit, which reads like an
oversight and is not one: retail's Thing From Beyond is a copy of the
player it chases, so the model was never meant to come from the template.
The clone has to be a real aura, because
WorldSession::HandleMirrorImageDataRequest answers the client's request
for the copy's gear only if the unit carries SPELL_AURA_CLONE_CASTER, and
reads the appearance off that aura's caster. It is applied rather than
cast: Clone Caster is a positive spell and this summon is hostile
(SummonProperties 4793 sets faction 14 over the template's friendly 35),
so CheckCast answers SPELL_FAILED_BAD_TARGETS, and forgiving that
requires TRIGGERED_IGNORE_TARGET_CHECK - which sits outside
TRIGGERED_FULL_MASK and is therefore unreachable from
CastSpell(..., true).

The Thing is levelled to its summoner. GetEffectiveResistChance adds
(victim level - attacker level) * 5 resistance, so a Thing below its
target's level had roughly half its damage resisted away before it
landed.

The strike is measured the way the chase generator measures.
IsWithinMeleeRange squares the height difference into the distance while
ChaseMovementGenerator stops on a 2d test, so on uneven ground the
pursuit arrived and then failed its own strike check for the whole eight
seconds - one observed pursuit ended at 2d 5.72 with dz -1.34, which is
5.88 in three dimensions against a melee range of 5.08.

Damage goes through SpellNonMeleeDamage rather than DealDamage, so it
appears in the combat log and passes through absorbs, resistances and
damage-taken modifiers instead of executing unconditionally. The tooltip
figure - "reaching you deals about your health in damage" - is the cost
of a whole pursuit, so it is divided across the strikes the Thing has
time to land, and being caught late costs less than being caught at
once. Connecting does not spend the Thing: Cascading Disaster's tooltip
says being struck applies Grasping Tendrils and Eye of Corruption, which
a pursuer that vanished on contact could not do.

Pursuit speed is an approximation and is isolated in the GrandDelusions
namespace for retuning. The tooltip states the speed rises with
corruption but gives no numbers, creature_template.speed_run is
TrinityCore's stock default on all three pursuer creatures, and there is
no movement-speed aura anywhere in the corruption block.

Verified in-game: the Thing spawns as a copy of its summoner, pursues,
and its strike is reported as spell damage in the combat log.
@svenbledt svenbledt changed the title feat(corruption): BfA corruption system - bonus item effects, threshold sync, and the first three tiers [DRAFT / INCOMPLETE] feat(corruption): the three 8.3 corruption drawback tiers Aug 2, 2026
@Hextv

Hextv commented Aug 3, 2026

Copy link
Copy Markdown
Owner

[FOUND BUG] Corruption proc triggering globally on passive/utility actions (e.g., learning mounts)

svenbledt added a commit to svenbledt/BFA-HavenCore that referenced this pull request Aug 5, 2026
315169 had no `spell_proc` row, so LoadSpellProcs generated one with
SpellTypeMask = PROC_SPELL_TYPE_MASK_ALL, and CanSpellTriggerProcOnEvent
skips the type filter when the mask admits everything. Its DONE flags
include PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_POS, so any positive
damage-class-none cast summoned an Eye - reported on PR Hextv#75 as learning
a mount spawning one.

SpellTypeMask 3 keeps damage and heals and drops
PROC_SPELL_TYPE_NO_DMG_HEAL, the bucket Spell.cpp assigns to a cast that
neither heals nor damages. SpellPhaseMask has to be set because these
flags are DONE and the load path does not default it, and has to stay
HIT-only: Spell::finish raises a second proc at PROC_SPELL_PHASE_CAST
passing MASK_ALL as the event's own type mask, which no SpellTypeMask can
reject.

Also corrects 2026_08_01_01's claim that 315169 kept
PROC_ATTR_TRIGGERED_CAN_PROC. addTriggerFlag is gated on
TAKEN_HIT_PROC_FLAG_MASK and 315169's flags are all DONE, so it never had
the attribute.

Not verified in game. Data-only change; confirming it needs the migration
applied, a worldserver restart and a 20+ corruption character.
315169 had no `spell_proc` row, so LoadSpellProcs generated one with
SpellTypeMask = PROC_SPELL_TYPE_MASK_ALL, and CanSpellTriggerProcOnEvent
skips the type filter when the mask admits everything. Its DONE flags
include PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_POS, so any positive
damage-class-none cast summoned an Eye - reported on PR Hextv#75 as learning a
mount spawning one.

SpellTypeMask 3 keeps damage and heals and drops
PROC_SPELL_TYPE_NO_DMG_HEAL, the bucket Spell.cpp assigns to a cast that
neither heals nor damages. SpellPhaseMask has to be set because these
flags are DONE and the load path does not default it, and has to stay
HIT-only: Spell::finish raises a second proc at PROC_SPELL_PHASE_CAST
passing MASK_ALL as the event's own type mask, which no SpellTypeMask can
reject. No AttributesMask, unlike the rows in 2026_08_02_00 and
2026_08_02_02: addTriggerFlag is gated on TAKEN_HIT_PROC_FLAG_MASK, so
315169 never carried PROC_ATTR_TRIGGERED_CAN_PROC.

Not verified in game. Data-only change; confirming it needs the migration
applied, a worldserver restart and a 20+ corruption character.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants