Skip to content

[Gear] Zatha'tek: model Necrotic Tear as a per-target debuff#2

Open
NN0323 wants to merge 1 commit into
zathatek-breath-of-corruptionfrom
fix/zathatek-breath-of-corruption-target-debuff
Open

[Gear] Zatha'tek: model Necrotic Tear as a per-target debuff#2
NN0323 wants to merge 1 commit into
zathatek-breath-of-corruptionfrom
fix/zathatek-breath-of-corruption-target-debuff

Conversation

@NN0323

@NN0323 NN0323 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the one substantive issue found in code review of zathatek-breath-of-corruption: Necrotic Tear (spell 1305391) was modeled as a single player-wide buff instead of a per-target debuff.

In-game, Necrotic Tear is a stacking debuff applied to each target that persists until that target dies, granting +5% Breath of Corruption damage per stack (max 10). The implementation used make_buff(e.player, ...) — one shared stack counter — and amplified damage off that shared count regardless of which target was hit.

Why it mattered

Single-target results were correct, so this was silent in single-target (Patchwerk) smoke tests. But in multi-target / cleave / target-swap sims the shared player buff:

  • let stacks built up on one target inflate Breath of Corruption damage dealt to a different target (composite_da_multiplier read the global stack count, not s->target's), and
  • never reset when a target died (infinite-duration player buff), so stacks only ever ramped up — an overestimate in add-wave / DungeonSlice styles.

The fix

Convert Necrotic Tear to the per-target debuff infrastructure already used elsewhere in unique_gear_midnight.cpp (void_execution_mandate, torments_duality, potion_of_zealotry):

  • ctor: target_debuff = e.player->find_spell( 1305391 );
  • composite_da_multiplier: if ( auto debuff = find_debuff( s->target ) ) m *= 1.0 + debuff->check() * stack_mod;
  • impact: get_debuff( s->target )->trigger();

get_debuff(t) lazily creates the debuff per target via make_buff(actor_pair_t(t, player), …, target_debuff), so max_stack (10) and the infinite duration are still inherited from the spell data and per-target stacks now reset on target death for free. Deliberately does not copy potion_of_zealotry's target-swap expiry logic — Necrotic Tear persists per target until death.

Also: const-qualify stack_mod, drop the now-redundant stack guard (the lazy find_debuff null check is the necessary one), and clarify the spell-id header comment (the +5%/stack value lives on the driver's effectN(2), not the tear spell).

Behavior impact

  • Single-target: identical output — same spell data, same stacks/duration, same effectN(1) damage and effectN(2) +5%/stack.
  • Multi-target / target-swap: corrected — stacks are now per-target and reset on death.

Verification

  • Reviewed by three independent code-review agents; all converged on this as the single must-fix issue, with the rest of the implementation (RPPM 2.5 + haste scaling, damage/role_mult sourcing, max-stack/duration, constructor arg order) verified correct against the PTR DBC.
  • The fix was adversarially re-verified against the engine API (action_t::target_debuff / get_debuff / find_debuff in action.hpp/action.cpp) and the neighboring precedents: API type-checks, single-target equivalence holds, impact ordering preserved, no dangling necrotic_tear references.
  • Not locally compiled — no C++ toolchain was available on PATH in this environment (VS solution only). Change is static-analysis verified; a local/CI build is recommended before merge. Note the project builds with -Wall -Wextra -Wpedantic but not -Werror.

🤖 Generated with Claude Code

Necrotic Tear (spell 1305391) is a stacking debuff applied to each target
that persists until that target dies. It was modeled as a single player-wide
buff via make_buff(e.player, ...), with the damage multiplier keyed off that
shared stack count regardless of which target was hit.

Single-target results were correct, but in multi-target / cleave / target-swap
sims the shared buff (a) let stacks built on one target inflate Breath of
Corruption damage dealt to another, and (b) never reset on a target's death,
so stacks only ever ramped up. The bug was silent in single-target smoke tests.

Convert it to the per-target debuff infrastructure already used elsewhere in
this file (void_execution_mandate, torments_duality, potion_of_zealotry):
set target_debuff to spell 1305391 and read/trigger via find_debuff(s->target)
and get_debuff(s->target). max_stack (10) and the infinite duration are still
inherited from the spell data. The +5%/stack value (driver effectN(2)) and the
base Nature damage (driver effectN(1)) are unchanged, so single-target output
is identical; only multi-target/target-death behavior is corrected.

Also const-qualify stack_mod and drop the now-redundant stack guard (the
per-target debuff is created lazily, so the null check from find_debuff is the
necessary guard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant