Skip to content

Convert Shadow to CantBlockBy/CanBlockIfShadow static abilities - #11877

Open
BigCrunch22 wants to merge 3 commits into
Card-Forge:masterfrom
BigCrunch22:shadow-rework
Open

BigCrunch22 wants to merge 3 commits into
Card-Forge:masterfrom
BigCrunch22:shadow-rework

Conversation

@BigCrunch22

Copy link
Copy Markdown
Contributor

Summary

This implements issue #2550, one item off the #3307 "remove hidden keywords" checklist. Shadow's blocking restriction currently lives as three hand-written hasKeyword(...) checks inside CombatUtil.canBlock(), instead of as composable StaticAbility restrictions the way Flying, Landwalk, Fear, etc. are already implemented. This PR moves it onto that same infrastructure and discards the keyword-string hack entirely.

What was there before

java
// rare case:
if (blocker.hasKeyword(Keyword.SHADOW)
        && blocker.hasKeyword("CARDNAME can block creatures with shadow as though they didn't have shadow.")) {
    return false;
}

if (attacker.hasKeyword(Keyword.SHADOW) && !blocker.hasKeyword(Keyword.SHADOW)
        && !blocker.hasKeyword("CARDNAME can block creatures with shadow as though they didn't have shadow.")) {
    return false;
}

if (!attacker.hasKeyword(Keyword.SHADOW) && blocker.hasKeyword(Keyword.SHADOW)) {
    return false;
}

Two problems with this:

  • It's a hardcoded special case in the combat engine rather than a reusable ability — exactly the pattern Remove Hidden Keywords Meta Issue #3307 is trying to remove.
  • The literal sentence "CARDNAME can block creatures with shadow as though they didn't have shadow." was reused, unmodified, as the "keyword" for four different cards — including two (Wall of Diffusion, Heartwood Dryad) whose real Oracle text says "as though it had shadow," not "as though they didn't have shadow." Those two phrasings are not mechanically interchangeable (see below), so those two cards were silently behaving like Aetherflame Wall instead of matching their own printed text.

What changed

1. Shadow itself → two generated CantBlockBy abilities

CardFactoryUtil now generates Shadow's two-directional restriction the same way Flying already generates its own (one CantBlockBy ability for the attacker side, one for the blocker side), instead of CombatUtil special-casing it. The three hardcoded checks above are deleted outright.

2. New StaticAbilityMode.CanBlockIfShadow

Four printed cards let a creature block a shadow creature despite the blocker not having shadow itself. That's exactly what CanBlockIfReach already does for Dragon Hunter ("can block Dragons as though it had reach"), so this adds a direct sibling mode, CanBlockIfShadow, plus two small helper methods in StaticAbilityCantAttackBlock, hooked into the existing ValidBlocker override check right next to the Reach one.

3. The four cards rewritten as plain static abilities — no keyword involved

  • Heartwood Dryad, Wall of Diffusion — one CanBlockIfShadow ability each, now using their correct Oracle wording.
  • Aetherflame Wall, Aether Web — the same CanBlockIfShadow, plus one extra ability: CantBlock | ValidCard$ Card.Self+withShadow. This reproduces the documented ruling that if Aetherflame Wall ever gains shadow, it can't block anything — not even other shadow creatures — without needing any special-cased combat logic to get there.

4. CreatureEvaluator AI heuristic

Was checking for the now-deleted keyword string; updated to check for the CanBlockIfShadow static ability instead.

Why the extra CantBlock ability

"Can block creatures with shadow as though they didn't have shadow" (Aetherflame Wall's wording) and "as though it had shadow" (Heartwood Dryad's wording) read as interchangeable but aren't: only the first creates an actual rules contradiction if the creature ever gains real shadow. Rather than model that interaction generically, it's declared directly as a second, always-on ability — if the card currently has shadow, it can't block, period. That one extra line reproduces the known ruling exactly.

Files changed

  • forge-game/.../card/CardFactoryUtil.java
  • forge-game/.../staticability/StaticAbilityMode.java
  • forge-game/.../staticability/StaticAbilityCantAttackBlock.java
  • forge-game/.../combat/CombatUtil.java
  • forge-ai/.../CreatureEvaluator.java
  • res/cardsfolder/a/aetherflame_wall.txt
  • res/cardsfolder/a/aether_web.txt
  • res/cardsfolder/h/heartwood_dryad.txt
  • res/cardsfolder/w/wall_of_diffusion.txt

Closes #2550.

Code changes made with heavy assistance from Claude

Move Shadow's block-legality logic out of hardcoded keyword-string checks in CombatUtil.canBlock() and into real StaticAbility-driven CantBlockBy restrictions generated by CardFactoryUtil, mirroring how Flying/Landwalk/Fear are already implemented (Card-Forge#3307).

Add StaticAbilityMode.CanBlockIfShadow, a direct sibling of the existing CanBlockIfReach/Dragon Hunter mechanism, to support the four cards that let a creature block shadow creatures without having shadow itself (Aetherflame Wall, Aether Web, Heartwood Dryad, Wall of Diffusion). Rewrite those four as plain static abilities with no keyword involved.

Fix a copy/paste bug found along the way: Wall of Diffusion and Heartwood Dryad were both scripted with Aetherflame Wall's ability text ("as though they didn't have shadow") instead of their own Oracle wording ("as though it had shadow") - the two are not mechanically interchangeable.

Update CreatureEvaluator's AI scoring heuristic, which was reading the now-removed keyword string.

Closes Card-Forge#2550.
Comment thread forge-gui/res/cardsfolder/a/aether_web.txt Outdated

@tool4ever tool4ever left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hanmac
not sure it answers all your questions but it seems cleaner

@Hanmac

Hanmac commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

I'm still not 100% sure what should happen if Heartwood Dryad is enchanted with Aether Web

🤔

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.

Shadow use CantBlockBy and static abilities

3 participants