Skip to content

Issue.shadow cloak bug - #3672

Merged
bylins merged 4 commits into
masterfrom
issue.shadow-cloak-bug
Aug 2, 2026
Merged

Issue.shadow cloak bug#3672
bylins merged 4 commits into
masterfrom
issue.shadow-cloak-bug

Conversation

@Svjatovit

Copy link
Copy Markdown
Collaborator

Fixes #3657.

Problem

Shadow Cloak (kShadowCloak) cut spell passage far more than its stats imply, because magic-resist was gated twice on the same GET_MR (incl. the cloak's +21): the global CalcTotalSpellDmg gate and the cloak's scope="damage" absorption ward. Net landing was (1 − MR/100)². The affect stage had the same shape on GET_AR.

Changes (3 commits)

1. MR gated once per damage stageActionContext::mr_applied_ (mirrors ward_stop_): the first MR-derived gate to evaluate marks MR spent; any later MR gate defers. So MR gates at most once regardless of how many MR affects/wards are present (only one MR ward triggers). The +21 MR is intentionally kept — this fixes the double-count, not the buff magnitude.

2. AR gated once per affect stageCastAffect rolled GET_AR twice for a non-warcry debuff (a pre-roll number(1,999) <= AR*10 and a blanket number(1,100) <= AR, both ~AR/100). Added the parallel ar_applied_ flag. Balance note: removes the compounding, so debuffs land more often vs high-AR targets. The two axes now mirror: MR once per damage stage, AR once per affect stage.

3. Roll MR / ward / save per hit; add MR tester line — multi-hit spells (e.g. Magic Missile) resolved MR, the ward, and the save once per stage and dealt that single result to every hit, so a resisted spell missed with all its attacks. Moved the resolution into CastDamage's per-hit loop: each attack is independently absorbed / MR-resisted / saved. The ward had to move per-hit too, or a per-stage ward + per-hit gate would re-introduce the double-gating. Also added a spell_trace line when a hit is MR-resisted (was silent — indistinguishable from a saving throw), and dropped the Тестовое сообщение: prefix from the save tester line.

Notes

  • Magic-resist (GET_MR) is a damage-axis stat; affect-resist (GET_AR) is the affect axis — the flags are kept strictly per-axis.
  • A damage ward is now rolled after the instant-death gate, so a cloak no longer absorbs an instant-death spell outright (rare combo).
  • Ward absorption is still bypassed for god/implementor casters (WardEligible), unchanged — test the cloak absorb as a mortal.

Svjatovit and others added 4 commits August 1, 2026 19:50
Shadow Cloak (kShadowCloak) gated incoming magic damage on GET_MR twice:
the global CalcTotalSpellDmg gate AND its own scope="damage" absorption
ward (chance="kMagicResist"), both reading GET_MR (incl. the cloak's +21).
Result: P(land) = (1 - MR/100)^2 -- the cloak double-counted its own resist.

Add ActionContext::mr_applied_ (mirrors ward_stop_): the first MR-derived
gate to evaluate for a damage stage marks MR as spent, and any later MR gate
that stage defers -- so MR gates AT MOST ONCE regardless of how many MR
affects/wards are present (only one MR ward triggers). Reset per stage at
CastDamage entry. CalcTotalSpellDmg takes ActionContext& and skips its roll
when MR was already applied.

MR is a damage-axis stat: the flag logic is confined to want==kDamage. The
affect stage gates on GET_AR (affect resist), a separate axis, and is not
touched. Shadow Cloak's +21 MR is intentionally kept -- this fixes only the
double-gating, not the buff magnitude.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CastAffect gated a violent debuff on GET_AR twice for non-warcry spells:
a pre-roll (number(1,999) <= GET_AR*10) AND a blanket block (number(1,100)
<= GET_AR), both ~AR/100 and rolled independently. Net block was
1-(1-AR/100)^2 -- e.g. 36% at AR=20 instead of 20%.

Add ActionContext::ar_applied_ (the affect-stage counterpart to
mr_applied_): the first AR gate to evaluate marks AR spent for the
delivery, and the later gate defers. Reset per stage at CastAffect entry.
The pre-roll consumes the roll for non-warcry debuffs; a warcry (which
skips the pre-roll) is gated by the blanket block instead -- so AR gates
exactly once in every case.

Balance note: this removes the compounding, so debuffs land more often
against high-AR targets. The two axes now mirror: MR gates once per damage
stage, AR gates once per affect stage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multi-hit damage spells (e.g. Magic Missile) resolved magic-resist, the
damage ward, and the saving throw ONCE per stage: CalcTotalSpellDmg rolled
MR and computed one amount, the save halved it once, and that single result
was dealt to every hit. So one MR/save roll decided all hits together --
a resisted spell missed with all its attacks.

Move the resolution into CastDamage's per-hit loop so each attack is
independent (the point of multi-hit -- if one attack misses, others can
still land):
- CalcTotalSpellDmg now returns only the base per-hit amount (MR gate and
  its ctx param removed).
- Per hit: reset mr_applied; roll the damage-scoped ward (Shadow Cloak
  absorb); else roll the global GET_MR gate; else roll the saving throw for
  half. The ward had to move per-hit too, or a per-stage ward + per-hit gate
  would re-introduce the double-gating this branch fixes. mr_applied still
  ensures at most one MR roll per hit (ward OR global gate).
- Add a spell_trace tester line when a hit is MR-resisted (was silent, so
  testers could not tell MR from a saving throw).
- saving.cpp: drop the 'Тестовое сообщение: ' prefix from the save tester line.

The per-stage save roll (used by the instant-death gate) is kept; the damage
half-save is now per hit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the branch current with master a5c5b4d (split-currency fix #3669 +
others). Auto-merged cleanly, no conflicts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bylins
bylins merged commit 3ae08f8 into master Aug 2, 2026
20 checks passed
@bylins
bylins deleted the issue.shadow-cloak-bug branch August 2, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants