From d4382d786415e1b03042855daa64aadcefb1f305 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Wed, 25 Feb 2026 07:24:16 +0100 Subject: [PATCH 1/6] AttackRestrict: add Static Ability check --- .../forge/game/combat/AttackRestriction.java | 11 +- .../java/forge/game/combat/CombatUtil.java | 4 +- .../game/combat/GlobalAttackRestrictions.java | 6 +- .../StaticAbilityAttackBlockRestrict.java | 120 ++++++++++++++++++ .../StaticAbilityAttackRestrict.java | 56 -------- .../StaticAbilityBlockRestrict.java | 38 ------ .../game/staticability/StaticAbilityMode.java | 7 +- .../custom_cards/sixth_head_of_the_hydra.txt | 2 +- forge-gui/res/cardsfolder/a/astral_arena.txt | 4 +- .../b/bright_palm_soul_awakener.txt | 2 +- .../res/cardsfolder/c/caverns_of_despair.txt | 4 +- forge-gui/res/cardsfolder/c/crawlspace.txt | 2 +- .../res/cardsfolder/d/dueling_grounds.txt | 4 +- .../res/cardsfolder/j/judoon_enforcers.txt | 2 +- .../m/mirri_weatherlight_duelist.txt | 4 +- .../res/cardsfolder/s/silent_arbiter.txt | 4 +- .../cardsfolder/t/the_eternal_wanderer.txt | 2 +- 17 files changed, 153 insertions(+), 119 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java delete mode 100644 forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackRestrict.java delete mode 100644 forge-game/src/main/java/forge/game/staticability/StaticAbilityBlockRestrict.java diff --git a/forge-game/src/main/java/forge/game/combat/AttackRestriction.java b/forge-game/src/main/java/forge/game/combat/AttackRestriction.java index fc0d365d61e..6396dd51a75 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackRestriction.java +++ b/forge-game/src/main/java/forge/game/combat/AttackRestriction.java @@ -4,6 +4,9 @@ import forge.game.GameEntity; import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.staticability.StaticAbility; +import forge.game.staticability.StaticAbilityAttackBlockRestrict; import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; @@ -66,12 +69,18 @@ public Set getViolation(final Map attac return violations; } + public List getStaticViolations(final Map attackers) { + CardCollection others = new CardCollection(attackers.keySet()); + others.remove(attacker); + return StaticAbilityAttackBlockRestrict.attackRestrict(attacker, others); + } + public boolean canAttack(final GameEntity defender, final Map attackers) { if (!canAttack(defender)) { return false; } - return getViolation(attackers).isEmpty(); + return getViolation(attackers).isEmpty() && getStaticViolations(attackers).isEmpty(); } public Set getTypes() { diff --git a/forge-game/src/main/java/forge/game/combat/CombatUtil.java b/forge-game/src/main/java/forge/game/combat/CombatUtil.java index 65d5ba94bc1..ed6bb13a850 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -32,7 +32,7 @@ import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.staticability.StaticAbility; -import forge.game.staticability.StaticAbilityBlockRestrict; +import forge.game.staticability.StaticAbilityAttackBlockRestrict; import forge.game.staticability.StaticAbilityCantAttackBlock; import forge.game.staticability.StaticAbilityMustBlock; import forge.game.trigger.TriggerType; @@ -433,7 +433,7 @@ public static boolean canBlock(final Card blocker, final Combat combat) { CardCollection allOtherBlockers = combat.getAllBlockers(); allOtherBlockers.remove(blocker); final int blockersFromOnePlayer = CardLists.count(allOtherBlockers, CardPredicates.isController(blocker.getController())); - if (blockersFromOnePlayer >= StaticAbilityBlockRestrict.blockRestrictNum(blocker.getController())) { + if (blockersFromOnePlayer >= StaticAbilityAttackBlockRestrict.blockRestrictNum(blocker.getController())) { return false; } diff --git a/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java b/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java index 80a47b5d864..0478f73708c 100644 --- a/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java +++ b/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java @@ -9,7 +9,7 @@ import forge.game.GameEntity; import forge.game.card.Card; import forge.game.player.Player; -import forge.game.staticability.StaticAbilityAttackRestrict; +import forge.game.staticability.StaticAbilityAttackBlockRestrict; import forge.util.collect.FCollectionView; public class GlobalAttackRestrictions { @@ -59,10 +59,10 @@ public static GlobalAttackRestrictions getGlobalRestrictions(final Player attack final Map defenderMax = Maps.newHashMapWithExpectedSize(possibleDefenders.size()); final Game game = attackingPlayer.getGame(); - Integer max = StaticAbilityAttackRestrict.globalAttackRestrict(game); + Integer max = StaticAbilityAttackBlockRestrict.globalAttackRestrictNum(game); for (final GameEntity defender : possibleDefenders) { - final Integer defMax = StaticAbilityAttackRestrict.attackRestrictNum(defender); + final Integer defMax = StaticAbilityAttackBlockRestrict.attackRestrictNum(defender); if (defMax != null) { defenderMax.put(defender, defMax); } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java new file mode 100644 index 00000000000..2e6a3bf5b0d --- /dev/null +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java @@ -0,0 +1,120 @@ +package forge.game.staticability; + +import java.util.Collection; +import java.util.List; + +import org.testng.collections.Lists; + +import forge.game.Game; +import forge.game.GameEntity; +import forge.game.ability.AbilityUtils; +import forge.game.card.Card; +import forge.game.card.CardPredicates; +import forge.game.player.Player; +import forge.game.zone.ZoneType; +import forge.util.Expressions; + +public class StaticAbilityAttackBlockRestrict { + + static public Integer globalAttackRestrictNum(Game game) { + Integer max = null; + for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.checkConditions(StaticAbilityMode.AttackRestrictNum) + || stAb.hasParam("ValidDefender")) { + continue; + } + int stMax = AbilityUtils.calculateAmount(stAb.getHostCard(), + stAb.getParamOrDefault("MaxAttackers", "1"), stAb); + if (null == max || stMax < max) { + max = stMax; + } + } + } + return max; + } + + static public Integer attackRestrictNum(GameEntity defender) { + final Game game = defender.getGame(); + Integer num = null; + for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.checkConditions(StaticAbilityMode.AttackRestrictNum) + || !stAb.hasParam("ValidDefender")) { + continue; + } + if (validRestrictNum(stAb, defender)) { + int stNum = AbilityUtils.calculateAmount(stAb.getHostCard(), + stAb.getParamOrDefault("MaxAttackers", "1"), stAb); + if (null == num || stNum < num) { + num = stNum; + } + } + } + } + return num; + } + + static public int blockRestrictNum(Player defender) { + final Game game = defender.getGame(); + int num = Integer.MAX_VALUE; + for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.checkConditions(StaticAbilityMode.BlockRestrictNum)) { + continue; + } + if (validRestrictNum(stAb, defender)) { + int stNum = AbilityUtils.calculateAmount(stAb.getHostCard(), + stAb.getParamOrDefault("MaxBlockers", "1"), stAb); + if (stNum < num) { + num = stNum; + } + } + + } + } + return num; + } + + static public boolean validRestrictNum(StaticAbility stAb, GameEntity defender) { + if (!stAb.matchesValidParam("ValidDefender", defender)) { + return false; + } + return true; + } + + static public List attackRestrict(final Card attacker, final Collection others) { + final Game game = attacker.getGame(); + List result = Lists.newArrayList(); + for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.checkConditions(StaticAbilityMode.AttackRestrict)) { + continue; + } + if (!stAb.matchesValidParam("ValidCard", attacker)) { + continue; + } + if (validRestrictCommon(stAb, attacker, others)) { + result.add(stAb); + } + } + } + return result; + } + + static public boolean validRestrictCommon(StaticAbility stAb, Card card, Collection others) { + long size; + if (stAb.hasParam("ValidOthers")) { + size = others.stream().filter(CardPredicates.restriction(stAb.getParam("ValidOthers").split(","), card.getController(), card, stAb)).count(); + } else { + size = others.size(); + } + String compare = stAb.getParamOrDefault("OthersCompare", "GE1"); + + String operator = compare.substring(0, 2); + String operand = compare.substring(2); + + final int operandValue = AbilityUtils.calculateAmount(card, operand, stAb); + return !Expressions.compare((int)size, operator, operandValue); + } +} diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackRestrict.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackRestrict.java deleted file mode 100644 index 121070f690f..00000000000 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackRestrict.java +++ /dev/null @@ -1,56 +0,0 @@ -package forge.game.staticability; - -import forge.game.Game; -import forge.game.GameEntity; -import forge.game.ability.AbilityUtils; -import forge.game.card.Card; -import forge.game.zone.ZoneType; - -public class StaticAbilityAttackRestrict { - - static public Integer globalAttackRestrict(Game game) { - Integer max = null; - for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { - for (final StaticAbility stAb : ca.getStaticAbilities()) { - if (!stAb.checkConditions(StaticAbilityMode.AttackRestrict) - || stAb.hasParam("ValidDefender")) { - continue; - } - int stMax = AbilityUtils.calculateAmount(stAb.getHostCard(), - stAb.getParamOrDefault("MaxAttackers", "1"), stAb); - if (null == max || stMax < max) { - max = stMax; - } - } - } - return max; - } - - static public Integer attackRestrictNum(GameEntity defender) { - final Game game = defender.getGame(); - Integer num = null; - for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { - for (final StaticAbility stAb : ca.getStaticAbilities()) { - if (!stAb.checkConditions(StaticAbilityMode.AttackRestrict) - || !stAb.hasParam("ValidDefender")) { - continue; - } - if (attackRestrict(stAb, defender)) { - int stNum = AbilityUtils.calculateAmount(stAb.getHostCard(), - stAb.getParamOrDefault("MaxAttackers", "1"), stAb); - if (null == num || stNum < num) { - num = stNum; - } - } - } - } - return num; - } - - static public boolean attackRestrict(StaticAbility stAb, GameEntity defender) { - if (!stAb.matchesValidParam("ValidDefender", defender)) { - return false; - } - return true; - } -} diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityBlockRestrict.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityBlockRestrict.java deleted file mode 100644 index ff909a00c18..00000000000 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityBlockRestrict.java +++ /dev/null @@ -1,38 +0,0 @@ -package forge.game.staticability; - -import forge.game.Game; -import forge.game.ability.AbilityUtils; -import forge.game.card.Card; -import forge.game.player.Player; -import forge.game.zone.ZoneType; - -public class StaticAbilityBlockRestrict { - - static public int blockRestrictNum(Player defender) { - final Game game = defender.getGame(); - int num = Integer.MAX_VALUE; - for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { - for (final StaticAbility stAb : ca.getStaticAbilities()) { - if (!stAb.checkConditions(StaticAbilityMode.BlockRestrict)) { - continue; - } - if (blockRestrict(stAb, defender)) { - int stNum = AbilityUtils.calculateAmount(stAb.getHostCard(), - stAb.getParamOrDefault("MaxBlockers", "1"), stAb); - if (stNum < num) { - num = stNum; - } - } - - } - } - return num; - } - - static public boolean blockRestrict(StaticAbility stAb, Player defender) { - if (!stAb.matchesValidParam("ValidDefender", defender)) { - return false; - } - return true; - } -} diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java index 087476579d8..ca1a13b9cc5 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java @@ -59,9 +59,6 @@ public enum StaticAbilityMode { // StaticAbilityNoCleanupDamage NoCleanupDamage, - // StaticAbilityBlockRestrict - BlockRestrict, - // StaticAbilityCantGainLosePayLife CantGainLife, CantLoseLife, @@ -77,8 +74,10 @@ public enum StaticAbilityMode { IgnoreHexproof, IgnoreShroud, - // StaticAbilityAttackRestrict + // StaticAbilityAttackBlockRestrict AttackRestrict, + AttackRestrictNum, + BlockRestrictNum, // StaticAbilityAssignNoCombatDamage AssignNoCombatDamage, diff --git a/forge-gui/res/adventure/common/custom_cards/sixth_head_of_the_hydra.txt b/forge-gui/res/adventure/common/custom_cards/sixth_head_of_the_hydra.txt index 4e951c976f0..0a6c2070bfc 100644 --- a/forge-gui/res/adventure/common/custom_cards/sixth_head_of_the_hydra.txt +++ b/forge-gui/res/adventure/common/custom_cards/sixth_head_of_the_hydra.txt @@ -6,7 +6,7 @@ PT:1/4 K:Defender R:Event$ GameLoss | ActiveZones$ Battlefield | ValidPlayer$ You | Layer$ CantHappen | Description$ You can't lose the game and your opponents can't win the game. R:Event$ GameWin | ActiveZones$ Battlefield | ValidPlayer$ Opponent | Layer$ CantHappen | Secondary$ True | Description$ You can't lose the game and your opponents can't win the game. -S:Mode$ AttackRestrict | MaxAttackers$ 6 | ValidDefender$ You | Description$ No more than six creatures can attack you each combat. +S:Mode$ AttackRestrictNum | MaxAttackers$ 6 | ValidDefender$ You | Description$ No more than six creatures can attack you each combat. R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | ValidCard$ Card.Self | ReplaceWith$ Exile | Description$ If CARDNAME would leave the battlefield, instead exile it with three time counters on it. It gains suspend. SVar:Exile:DB$ ChangeZone | Hidden$ True | WithCountersType$ TIME | WithCountersAmount$ 3 | Origin$ All | Destination$ Exile | Defined$ ReplacedCard | SubAbility$ GiveSuspend SVar:GiveSuspend:DB$ PumpAll | ValidCards$ Card.withoutSuspend+YouOwn | KW$ Suspend | PumpZone$ Exile | Duration$ Permanent diff --git a/forge-gui/res/cardsfolder/a/astral_arena.txt b/forge-gui/res/cardsfolder/a/astral_arena.txt index b16d2053506..cadfd5f6336 100644 --- a/forge-gui/res/cardsfolder/a/astral_arena.txt +++ b/forge-gui/res/cardsfolder/a/astral_arena.txt @@ -1,8 +1,8 @@ Name:Astral Arena ManaCost:no cost Types:Plane Kolbahan -S:Mode$ AttackRestrict | EffectZone$ Command | MaxAttackers$ 1 | Description$ No more than one creature can attack each combat. -S:Mode$ BlockRestrict | EffectZone$ Command | MaxBlockers$ 1 | Description$ No more than one creature can block each combat. +S:Mode$ AttackRestrictNum | EffectZone$ Command | MaxAttackers$ 1 | Description$ No more than one creature can attack each combat. +S:Mode$ BlockRestrictNum | EffectZone$ Command | MaxBlockers$ 1 | Description$ No more than one creature can block each combat. T:Mode$ ChaosEnsues | TriggerZones$ Command | Execute$ RolledChaos | TriggerDescription$ Whenever chaos ensues, CARDNAME deals 2 damage to each creature. SVar:RolledChaos:DB$ DamageAll | NumDmg$ 2 | ValidCards$ Creature SVar:AIRollPlanarDieParams:Mode$ Random | MinTurn$ 5 diff --git a/forge-gui/res/cardsfolder/b/bright_palm_soul_awakener.txt b/forge-gui/res/cardsfolder/b/bright_palm_soul_awakener.txt index 7b59c4c3e3c..cdc3af73297 100644 --- a/forge-gui/res/cardsfolder/b/bright_palm_soul_awakener.txt +++ b/forge-gui/res/cardsfolder/b/bright_palm_soul_awakener.txt @@ -6,7 +6,7 @@ K:Backup:1:BackupAbility SVar:BackupAbility:DB$ Animate | Triggers$ AttackTrig | sVars$ AE SVar:AttackTrig:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigDoubleCounters | TriggerDescription$ Whenever this creature attacks, double the number of +1/+1 counters on target creature. That creature can't be blocked by creatures with power 2 or less this turn. SVar:TrigDoubleCounters:DB$ MultiplyCounter | ValidTgts$ Creature | CounterType$ P1P1 | SubAbility$ DBEffect -SVar:DBEffect:DB$ Effect | StaticAbilities$ BlockRestrict | RememberObjects$ Targeted +SVar:DBEffect:DB$ Effect | StaticAbilities$ BlockRestrictNum | RememberObjects$ Targeted SVar:BlockRestrict:Mode$ CantBlockBy | ValidAttacker$ Card.IsRemembered | ValidBlocker$ Creature.powerLE2 | Description$ That creature can't be blocked by creatures with power 2 or less this turn. T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigDoubleCounters | TriggerDescription$ Whenever this creature attacks, double the number of +1/+1 counters on target creature. That creature can't be blocked by creatures with power 2 or less this turn. SVar:HasAttackEffect:TRUE diff --git a/forge-gui/res/cardsfolder/c/caverns_of_despair.txt b/forge-gui/res/cardsfolder/c/caverns_of_despair.txt index 2d6e1186d42..01fee67671c 100644 --- a/forge-gui/res/cardsfolder/c/caverns_of_despair.txt +++ b/forge-gui/res/cardsfolder/c/caverns_of_despair.txt @@ -1,8 +1,8 @@ Name:Caverns of Despair ManaCost:2 R R Types:World Enchantment -S:Mode$ AttackRestrict | MaxAttackers$ 2 | Description$ No more than two creatures can attack each combat. -S:Mode$ BlockRestrict | MaxBlockers$ 2 | Description$ No more than two creatures can block each combat. +S:Mode$ AttackRestrictNum | MaxAttackers$ 2 | Description$ No more than two creatures can attack each combat. +S:Mode$ BlockRestrictNum | MaxBlockers$ 2 | Description$ No more than two creatures can block each combat. SVar:NonStackingEffect:True AI:RemoveDeck:Random DeckHints:Type$Planeswalker|Artifact|Enchantment|Aura|Equipment diff --git a/forge-gui/res/cardsfolder/c/crawlspace.txt b/forge-gui/res/cardsfolder/c/crawlspace.txt index 9eda574faaa..001c667e6d9 100644 --- a/forge-gui/res/cardsfolder/c/crawlspace.txt +++ b/forge-gui/res/cardsfolder/c/crawlspace.txt @@ -1,6 +1,6 @@ Name:Crawlspace ManaCost:3 Types:Artifact -S:Mode$ AttackRestrict | MaxAttackers$ 2 | ValidDefender$ You | Description$ No more than two creatures can attack you each combat. +S:Mode$ AttackRestrictNum | MaxAttackers$ 2 | ValidDefender$ You | Description$ No more than two creatures can attack you each combat. SVar:NonStackingEffect:True Oracle:No more than two creatures can attack you each combat. diff --git a/forge-gui/res/cardsfolder/d/dueling_grounds.txt b/forge-gui/res/cardsfolder/d/dueling_grounds.txt index 047f83221fe..55dc78f6c97 100644 --- a/forge-gui/res/cardsfolder/d/dueling_grounds.txt +++ b/forge-gui/res/cardsfolder/d/dueling_grounds.txt @@ -1,7 +1,7 @@ Name:Dueling Grounds ManaCost:1 G W Types:Enchantment -S:Mode$ AttackRestrict | MaxAttackers$ 1 | Description$ No more than one creature can attack each combat. -S:Mode$ BlockRestrict | MaxBlockers$ 1 | Description$ No more than one creature can block each combat. +S:Mode$ AttackRestrictNum | MaxAttackers$ 1 | Description$ No more than one creature can attack each combat. +S:Mode$ BlockRestrictNum | MaxBlockers$ 1 | Description$ No more than one creature can block each combat. AI:RemoveDeck:Random Oracle:No more than one creature can attack each combat.\nNo more than one creature can block each combat. diff --git a/forge-gui/res/cardsfolder/j/judoon_enforcers.txt b/forge-gui/res/cardsfolder/j/judoon_enforcers.txt index 3ce507745c8..aba74730104 100644 --- a/forge-gui/res/cardsfolder/j/judoon_enforcers.txt +++ b/forge-gui/res/cardsfolder/j/judoon_enforcers.txt @@ -3,6 +3,6 @@ ManaCost:5 R W Types:Creature Alien Rhino Soldier PT:8/8 K:Trample -S:Mode$ AttackRestrict | MaxAttackers$ 1 | ValidDefender$ You | Description$ No more than one creature can attack you each combat. +S:Mode$ AttackRestrictNum | MaxAttackers$ 1 | ValidDefender$ You | Description$ No more than one creature can attack you each combat. K:Suspend:6:1 R W Oracle:Trample\nNo more than one creature can attack you each combat.\nSuspend 6—{1}{R}{W} (Rather than cast this card from your hand, you may pay {1}{R}{W} and exile it with six time counters on it. At the beginning of your upkeep, remove a time counter. When the last is removed, you may cast it without paying its mana cost. It has haste.) diff --git a/forge-gui/res/cardsfolder/m/mirri_weatherlight_duelist.txt b/forge-gui/res/cardsfolder/m/mirri_weatherlight_duelist.txt index f27ed0e736d..c738a1f89bd 100644 --- a/forge-gui/res/cardsfolder/m/mirri_weatherlight_duelist.txt +++ b/forge-gui/res/cardsfolder/m/mirri_weatherlight_duelist.txt @@ -3,8 +3,8 @@ ManaCost:1 G W Types:Legendary Creature Cat Warrior PT:3/2 K:First Strike -S:Mode$ AttackRestrict | IsPresent$ Card.Self+tapped | MaxAttackers$ 1 | ValidDefender$ You | Description$ As long as CARDNAME is tapped, no more than one creature can attack you each combat. +S:Mode$ AttackRestrictNum | IsPresent$ Card.Self+tapped | MaxAttackers$ 1 | ValidDefender$ You | Description$ As long as CARDNAME is tapped, no more than one creature can attack you each combat. T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigLimitBlock | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME attacks, each opponent can't block with more than one creature this combat. SVar:TrigLimitBlock:DB$ Effect | StaticAbilities$ STLimitBlock | Duration$ UntilEndOfCombat -SVar:STLimitBlock:Mode$ BlockRestrict | MaxBlockers$ 1 | ValidDefender$ Opponent | Description$ Each opponent can't block with more than one creature this combat. +SVar:STLimitBlock:Mode$ BlockRestrictNum | MaxBlockers$ 1 | ValidDefender$ Opponent | Description$ Each opponent can't block with more than one creature this combat. Oracle:First strike\nWhenever Mirri, Weatherlight Duelist attacks, each opponent can't block with more than one creature this combat.\nAs long as Mirri, Weatherlight Duelist is tapped, no more than one creature can attack you each combat. diff --git a/forge-gui/res/cardsfolder/s/silent_arbiter.txt b/forge-gui/res/cardsfolder/s/silent_arbiter.txt index 93b2d71c023..3b60d9cac8c 100644 --- a/forge-gui/res/cardsfolder/s/silent_arbiter.txt +++ b/forge-gui/res/cardsfolder/s/silent_arbiter.txt @@ -2,6 +2,6 @@ Name:Silent Arbiter ManaCost:4 Types:Artifact Creature Construct PT:1/5 -S:Mode$ AttackRestrict | MaxAttackers$ 1 | Description$ No more than one creature can attack each combat. -S:Mode$ BlockRestrict | MaxBlockers$ 1 | Description$ No more than one creature can block each combat. +S:Mode$ AttackRestrictNum | MaxAttackers$ 1 | Description$ No more than one creature can attack each combat. +S:Mode$ BlockRestrictNum | MaxBlockers$ 1 | Description$ No more than one creature can block each combat. Oracle:No more than one creature can attack each combat.\nNo more than one creature can block each combat. diff --git a/forge-gui/res/cardsfolder/t/the_eternal_wanderer.txt b/forge-gui/res/cardsfolder/t/the_eternal_wanderer.txt index 215751ddf4a..df4acb071e8 100644 --- a/forge-gui/res/cardsfolder/t/the_eternal_wanderer.txt +++ b/forge-gui/res/cardsfolder/t/the_eternal_wanderer.txt @@ -2,7 +2,7 @@ Name:The Eternal Wanderer ManaCost:4 W W Types:Legendary Planeswalker Loyalty:5 -S:Mode$ AttackRestrict | MaxAttackers$ 1 | ValidDefender$ Card.Self | Description$ No more than one creature can attack CARDNAME each combat. +S:Mode$ AttackRestrictNum | MaxAttackers$ 1 | ValidDefender$ Card.Self | Description$ No more than one creature can attack CARDNAME each combat. A:AB$ ChangeZone | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | Origin$ Battlefield | Destination$ Exile | ValidTgts$ Artifact,Creature | TargetMin$ 0 | TargetMax$ 1 | TgtPrompt$ Select up to one target artifact or creature | RememberChanged$ True | SubAbility$ DBDelTrig | SpellDescription$ Exile up to one target artifact or creature. Return that card to the battlefield under its owner's control at the beginning of that player's next end step. SVar:DBDelTrig:DB$ DelayedTrigger | Mode$ Phase | Phase$ End of Turn | ValidPlayer$ Player.IsTriggerRemembered | Execute$ TrigReturn | RememberObjects$ TargetedOwner & RememberedLKI | SubAbility$ DBCleanup | TriggerDescription$ Return that card to the battlefield under its owner's control at the beginning of that player's next end step. | StackDescription$ Return that card to the battlefield under its owner's control at the beginning of that player's next end step. SVar:TrigReturn:DB$ ChangeZone | Origin$ Exile | Destination$ Battlefield | Defined$ DelayTriggerRememberedLKI From e4949a96ee136a69a68a82d629ebc7723aaec8ff Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Wed, 25 Feb 2026 09:13:52 +0100 Subject: [PATCH 2/6] Update StaticAbilityAttackBlockRestrict.java --- .../game/staticability/StaticAbilityAttackBlockRestrict.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java index 2e6a3bf5b0d..78883d91d67 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java @@ -3,7 +3,7 @@ import java.util.Collection; import java.util.List; -import org.testng.collections.Lists; +import com.google.common.collect.Lists; import forge.game.Game; import forge.game.GameEntity; From 59d86a0c1bf2a18f84020bb6a16498272a499dac Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Thu, 5 Mar 2026 14:01:21 +0100 Subject: [PATCH 3/6] add ValidOthersRelative and fix ValidOthers --- .../game/staticability/StaticAbilityAttackBlockRestrict.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java index 78883d91d67..156c71566ca 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java @@ -105,6 +105,8 @@ static public List attackRestrict(final Card attacker, final Coll static public boolean validRestrictCommon(StaticAbility stAb, Card card, Collection others) { long size; if (stAb.hasParam("ValidOthers")) { + size = others.stream().filter(CardPredicates.restriction(stAb.getParam("ValidOthers").split(","), stAb.getHostCard().getController(), stAb.getHostCard(), stAb)).count(); + } else if (stAb.hasParam("ValidOthersRelative")) { size = others.stream().filter(CardPredicates.restriction(stAb.getParam("ValidOthers").split(","), card.getController(), card, stAb)).count(); } else { size = others.size(); From a12ac4ec13a3249ba59db11588697f476e3d8527 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Thu, 19 Mar 2026 07:17:37 +0100 Subject: [PATCH 4/6] ~ first AttackRestrict: black or green creature also attacks --- .../main/java/forge/game/combat/AttackConstraints.java | 8 +------- .../main/java/forge/game/combat/AttackRestriction.java | 9 --------- .../java/forge/game/combat/AttackRestrictionType.java | 6 ------ forge-gui/res/cardsfolder/s/scarred_puma.txt | 2 +- 4 files changed, 2 insertions(+), 23 deletions(-) diff --git a/forge-game/src/main/java/forge/game/combat/AttackConstraints.java b/forge-game/src/main/java/forge/game/combat/AttackConstraints.java index 9a02cc87a97..232d15157fa 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackConstraints.java +++ b/forge-game/src/main/java/forge/game/combat/AttackConstraints.java @@ -92,8 +92,6 @@ public Pair, Integer> getLegalAttackers() { ) || ( types.contains(AttackRestrictionType.NOT_ALONE) && myMax <= 1 ) || ( - types.contains(AttackRestrictionType.NEED_BLACK_OR_GREEN) && myMax <= 1 - ) || ( types.contains(AttackRestrictionType.NEED_GREATER_POWER) && myMax <= 1 )) { reqs.removeIf(findAll(attacker)); @@ -106,11 +104,7 @@ public Pair, Integer> getLegalAttackers() { // Next, remove creatures with constraints that can't be fulfilled. for (final Card attacker : myPossibleAttackers) { final Set types = restrictions.get(attacker).getTypes(); - if (types.contains(AttackRestrictionType.NEED_BLACK_OR_GREEN)) { - if (!myPossibleAttackers.anyMatch(AttackRestrictionType.NEED_BLACK_OR_GREEN.getPredicate(attacker))) { - attackersToRemove.add(attacker); - } - } else if (types.contains(AttackRestrictionType.NEED_GREATER_POWER)) { + if (types.contains(AttackRestrictionType.NEED_GREATER_POWER)) { if (!myPossibleAttackers.anyMatch(AttackRestrictionType.NEED_GREATER_POWER.getPredicate(attacker))) { attackersToRemove.add(attacker); } diff --git a/forge-game/src/main/java/forge/game/combat/AttackRestriction.java b/forge-game/src/main/java/forge/game/combat/AttackRestriction.java index 6396dd51a75..b7ed9126df7 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackRestriction.java +++ b/forge-game/src/main/java/forge/game/combat/AttackRestriction.java @@ -31,7 +31,6 @@ public AttackRestriction(final Card attacker, final FCollectionView if ((restrictions.contains(AttackRestrictionType.ONLY_ALONE) && ( restrictions.contains(AttackRestrictionType.NEED_GREATER_POWER) || - restrictions.contains(AttackRestrictionType.NEED_BLACK_OR_GREEN) || restrictions.contains(AttackRestrictionType.NOT_ALONE) || restrictions.contains(AttackRestrictionType.NEED_TWO_OTHERS)) ) || ( @@ -56,10 +55,6 @@ public Set getViolation(final Map attac && attackers.keySet().stream().noneMatch(AttackRestrictionType.NEED_GREATER_POWER.getPredicate(attacker))) { violations.add(AttackRestrictionType.NEED_GREATER_POWER); } - if (restrictions.contains(AttackRestrictionType.NEED_BLACK_OR_GREEN) - && attackers.keySet().stream().noneMatch(AttackRestrictionType.NEED_BLACK_OR_GREEN.getPredicate(attacker))) { - violations.add(AttackRestrictionType.NEED_BLACK_OR_GREEN); - } if (restrictions.contains(AttackRestrictionType.NOT_ALONE) && nAttackers <= 1) { violations.add(AttackRestrictionType.NOT_ALONE); } @@ -96,10 +91,6 @@ private void setRestrictions() { restrictions.add(AttackRestrictionType.NEED_GREATER_POWER); } - if (attacker.hasKeyword("CARDNAME can't attack unless a black or green creature also attacks.")) { - restrictions.add(AttackRestrictionType.NEED_BLACK_OR_GREEN); - } - if (attacker.hasKeyword("CARDNAME can't attack or block alone.") || attacker.hasKeyword("CARDNAME can't attack alone.")) { restrictions.add(AttackRestrictionType.NOT_ALONE); } diff --git a/forge-game/src/main/java/forge/game/combat/AttackRestrictionType.java b/forge-game/src/main/java/forge/game/combat/AttackRestrictionType.java index de7bfaf33ad..362675ee0fc 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackRestrictionType.java +++ b/forge-game/src/main/java/forge/game/combat/AttackRestrictionType.java @@ -1,6 +1,5 @@ package forge.game.combat; -import forge.card.MagicColor; import forge.game.card.Card; import forge.game.card.CardPredicates; @@ -10,7 +9,6 @@ public enum AttackRestrictionType { ONLY_ALONE, NEED_GREATER_POWER, - NEED_BLACK_OR_GREEN, NOT_ALONE, NEED_TWO_OTHERS, NEVER; @@ -19,10 +17,6 @@ public Predicate getPredicate(final Card attacker) { switch (this) { case NEED_GREATER_POWER: return CardPredicates.hasGreaterPowerThan(attacker.getNetPower()); - case NEED_BLACK_OR_GREEN: - return CardPredicates.isColor((byte) (MagicColor.BLACK | MagicColor.GREEN)) - // may explicitly not be black/green itself - .and(Predicate.not(attacker::equals)); case NOT_ALONE: return x -> true; default: diff --git a/forge-gui/res/cardsfolder/s/scarred_puma.txt b/forge-gui/res/cardsfolder/s/scarred_puma.txt index 1bfd6290117..77581b4d228 100644 --- a/forge-gui/res/cardsfolder/s/scarred_puma.txt +++ b/forge-gui/res/cardsfolder/s/scarred_puma.txt @@ -2,6 +2,6 @@ Name:Scarred Puma ManaCost:R Types:Creature Cat PT:2/1 -K:CARDNAME can't attack unless a black or green creature also attacks. +S:Mode$ AttackRestrict | ValidCard$ Card.Self | ValidOthers$ Creature.Black,Creature.Green | Description$ CARDNAME can't attack unless a black or green creature also attacks. DeckHints:Color$Black|Green Oracle:Scarred Puma can't attack unless a black or green creature also attacks. From 160bbaa205856a0a82f3f1b975add86d717a2f49 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Thu, 19 Mar 2026 10:26:44 +0100 Subject: [PATCH 5/6] Update bright_palm_soul_awakener.txt --- forge-gui/res/cardsfolder/b/bright_palm_soul_awakener.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui/res/cardsfolder/b/bright_palm_soul_awakener.txt b/forge-gui/res/cardsfolder/b/bright_palm_soul_awakener.txt index cdc3af73297..7b59c4c3e3c 100644 --- a/forge-gui/res/cardsfolder/b/bright_palm_soul_awakener.txt +++ b/forge-gui/res/cardsfolder/b/bright_palm_soul_awakener.txt @@ -6,7 +6,7 @@ K:Backup:1:BackupAbility SVar:BackupAbility:DB$ Animate | Triggers$ AttackTrig | sVars$ AE SVar:AttackTrig:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigDoubleCounters | TriggerDescription$ Whenever this creature attacks, double the number of +1/+1 counters on target creature. That creature can't be blocked by creatures with power 2 or less this turn. SVar:TrigDoubleCounters:DB$ MultiplyCounter | ValidTgts$ Creature | CounterType$ P1P1 | SubAbility$ DBEffect -SVar:DBEffect:DB$ Effect | StaticAbilities$ BlockRestrictNum | RememberObjects$ Targeted +SVar:DBEffect:DB$ Effect | StaticAbilities$ BlockRestrict | RememberObjects$ Targeted SVar:BlockRestrict:Mode$ CantBlockBy | ValidAttacker$ Card.IsRemembered | ValidBlocker$ Creature.powerLE2 | Description$ That creature can't be blocked by creatures with power 2 or less this turn. T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigDoubleCounters | TriggerDescription$ Whenever this creature attacks, double the number of +1/+1 counters on target creature. That creature can't be blocked by creatures with power 2 or less this turn. SVar:HasAttackEffect:TRUE From f19fc9ffedc52a85835a54f339f755ce823fb938 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Mon, 23 Mar 2026 07:24:12 +0100 Subject: [PATCH 6/6] All Attack and Block Restrictions --- .../forge/game/combat/AttackConstraints.java | 114 +----------------- .../forge/game/combat/AttackRequirement.java | 5 +- .../forge/game/combat/AttackRestriction.java | 55 +-------- .../game/combat/AttackRestrictionType.java | 26 ---- .../java/forge/game/combat/CombatUtil.java | 31 ++--- .../java/forge/game/phase/PhaseHandler.java | 22 +--- .../StaticAbilityAttackBlockRestrict.java | 18 ++- .../game/staticability/StaticAbilityMode.java | 1 + .../res/cardsfolder/b/bonded_construct.txt | 2 +- .../res/cardsfolder/b/bonded_horncrest.txt | 2 +- forge-gui/res/cardsfolder/c/craven_hulk.txt | 2 +- forge-gui/res/cardsfolder/e/ember_beast.txt | 2 +- forge-gui/res/cardsfolder/e/errantry.txt | 3 +- .../res/cardsfolder/j/jackal_familiar.txt | 2 +- forge-gui/res/cardsfolder/l/loyal_pegasus.txt | 2 +- .../res/cardsfolder/m/master_of_cruelties.txt | 2 +- .../res/cardsfolder/m/militia_rallier.txt | 2 +- forge-gui/res/cardsfolder/m/mogg_flunkies.txt | 2 +- forge-gui/res/cardsfolder/o/okk.txt | 4 +- .../res/cardsfolder/o/orcish_conscripts.txt | 2 +- .../p/pipsqueak_rebel_strongarm.txt | 2 +- forge-gui/res/cardsfolder/r/raging_kronch.txt | 2 +- .../res/cardsfolder/s/sightless_brawler.txt | 5 +- .../res/cardsfolder/t/trusty_companion.txt | 2 +- .../res/cardsfolder/w/wojek_bodyguard.txt | 2 +- .../res/tokenscripts/w_4_4_beast_lonely.txt | 2 +- 26 files changed, 53 insertions(+), 261 deletions(-) delete mode 100644 forge-game/src/main/java/forge/game/combat/AttackRestrictionType.java diff --git a/forge-game/src/main/java/forge/game/combat/AttackConstraints.java b/forge-game/src/main/java/forge/game/combat/AttackConstraints.java index 232d15157fa..ac2e17bc114 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackConstraints.java +++ b/forge-game/src/main/java/forge/game/combat/AttackConstraints.java @@ -10,7 +10,6 @@ import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -82,56 +81,6 @@ public Pair, Integer> getLegalAttackers() { final Map, Integer> possible = new LinkedHashMap<>(); final List reqs = getSortedFilteredRequirements(); - final CardCollection myPossibleAttackers = new CardCollection(possibleAttackers); - - // First, remove all requirements of creatures that aren't going attack this combat anyway - final CardCollection attackersToRemove = new CardCollection(); - for (final Card attacker : myPossibleAttackers) { - final Set types = restrictions.get(attacker).getTypes(); - if ((types.contains(AttackRestrictionType.NEED_TWO_OTHERS) && myMax <= 2 - ) || ( - types.contains(AttackRestrictionType.NOT_ALONE) && myMax <= 1 - ) || ( - types.contains(AttackRestrictionType.NEED_GREATER_POWER) && myMax <= 1 - )) { - reqs.removeIf(findAll(attacker)); - attackersToRemove.add(attacker); - } - } - myPossibleAttackers.removeAll(attackersToRemove); - attackersToRemove.clear(); - - // Next, remove creatures with constraints that can't be fulfilled. - for (final Card attacker : myPossibleAttackers) { - final Set types = restrictions.get(attacker).getTypes(); - if (types.contains(AttackRestrictionType.NEED_GREATER_POWER)) { - if (!myPossibleAttackers.anyMatch(AttackRestrictionType.NEED_GREATER_POWER.getPredicate(attacker))) { - attackersToRemove.add(attacker); - } - } - } - myPossibleAttackers.removeAll(attackersToRemove); - for (final Card toRemove : attackersToRemove) { - reqs.removeIf(findAll(toRemove)); - } - - // First, successively try each creature that must attack alone. - for (final Card attacker : myPossibleAttackers) { - if (restrictions.get(attacker).getTypes().contains(AttackRestrictionType.ONLY_ALONE)) { - final Attack attack = findFirst(reqs, attacker); - if (attack == null) { - // no requirements, we don't care anymore - continue; - } - final Map attackMap = ImmutableMap.of(attack.attacker, attack.defender); - final int violations = countViolations(attackMap); - if (violations != -1) { - possible.put(attackMap, violations); - } - // remove them from the requirements, as they'll not be relevant to this calculation any more - reqs.removeIf(findAll(attacker)); - } - } // Now try all others (plus empty attack) and count their violations final FCollection> legalAttackers = collectLegalAttackers(reqs, myMax); @@ -161,7 +110,7 @@ private List> collectLegalAttackers(final Map toDefender = new LinkedHashMap<>(); int attackersNeeded = 0; - outer: while (!reqs.isEmpty()) { + while (!reqs.isEmpty()) { final Iterator iterator = reqs.iterator(); final Attack req = iterator.next(); final boolean isReserved = reserved.contains(req.attacker); @@ -190,19 +139,8 @@ private List> collectLegalAttackers(final Map> predicateRestrictions = Lists.newLinkedList(); - for (final AttackRestrictionType rType : restriction.getTypes()) { - final Predicate predicate = rType.getPredicate(req.attacker); - if (predicate != null) { - predicateRestrictions.add(predicate); - } - } - if (!requirement.getCausesToAttack().isEmpty()) { final List clonedReqs = deepClone(reqs); for (final Entry> causesToAttack : requirement.getCausesToAttack().asMap().entrySet()) { @@ -216,34 +154,6 @@ private List> collectLegalAttackers(final Map predicateRestriction : predicateRestrictions) { - if (Sets.union(myAttackers.keySet(), reserved.asSet()).stream().anyMatch(predicateRestriction)) { - // predicate fulfilled already, ignore! - continue; - } - // otherwise: reserve first creature to match it! - final Attack match = findFirst(reqs, predicateRestriction); - if (match == null) { - // no match: remove this creature completely - reqs.removeIf(findAll(req.attacker)); - continue outer; - } - // found one! add it to reserve and lower local maximum - reserved.add(match.attacker); - localMaximum--; - - // if limited, try both with and without this creature - if (!haveTriedWithout && isLimited) { - // try without - final List clonedReqs = deepClone(reqs); - clonedReqs.removeIf(findAll(req.attacker)); - final CardCollection clonedReserved = new CardCollection(reserved); - result.addAll(collectLegalAttackers(myAttackers, clonedReqs, clonedReserved, localMaximum)); - haveTriedWithout = true; } } @@ -253,15 +163,6 @@ private List> collectLegalAttackers(final Map deepClone(final List original) { } return newList; } - private static Attack findFirst(final List reqs, final Predicate predicate) { - for (final Attack req : reqs) { - if (predicate.test(req.attacker)) { - return req; - } - } - return null; - } - private static Attack findFirst(final List reqs, final Card attacker) { - return findFirst(reqs, attacker::equals); - } private static Predicate findAll(final Card attacker) { return input -> input.attacker.equals(attacker); } diff --git a/forge-game/src/main/java/forge/game/combat/AttackRequirement.java b/forge-game/src/main/java/forge/game/combat/AttackRequirement.java index 076660656a2..277aa682977 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackRequirement.java +++ b/forge-game/src/main/java/forge/game/combat/AttackRequirement.java @@ -99,12 +99,11 @@ public int countViolations(final GameEntity defender, final Map constraints = combat.getAttackConstraints().getRestrictions(); // check if a restriction will apply such that the requirement is no longer relevant - if (attackers.size() != 1 || !constraints.get(attackers.entrySet().iterator().next().getKey()).getTypes().contains(AttackRestrictionType.ONLY_ALONE)) { + // TODO REFACTOR?! + if (attackers.size() != 1) { for (final Map.Entry> mustAttack : causesToAttack.asMap().entrySet()) { - if (constraints.get(mustAttack.getKey()).getTypes().contains(AttackRestrictionType.ONLY_ALONE)) continue; int max = Objects.requireNonNullElse(GlobalAttackRestrictions.getGlobalRestrictions(mustAttack.getKey().getController(), combat.getDefenders()).getMax(), Integer.MAX_VALUE); // only count violations if the forced creature can actually attack and has no cost incurred for doing so diff --git a/forge-game/src/main/java/forge/game/combat/AttackRestriction.java b/forge-game/src/main/java/forge/game/combat/AttackRestriction.java index b7ed9126df7..9866e6d67bf 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackRestriction.java +++ b/forge-game/src/main/java/forge/game/combat/AttackRestriction.java @@ -13,13 +13,11 @@ public class AttackRestriction { private final Card attacker; - private final Set restrictions = EnumSet.noneOf(AttackRestrictionType.class); private boolean cantAttack; private final FCollectionView cantAttackDefender; public AttackRestriction(final Card attacker, final FCollectionView possibleDefenders) { this.attacker = attacker; - setRestrictions(); final FCollection cantAttackDefender = new FCollection<>(); for (final GameEntity defender : possibleDefenders) { @@ -29,14 +27,7 @@ public AttackRestriction(final Card attacker, final FCollectionView } this.cantAttackDefender = cantAttackDefender; - if ((restrictions.contains(AttackRestrictionType.ONLY_ALONE) && ( - restrictions.contains(AttackRestrictionType.NEED_GREATER_POWER) || - restrictions.contains(AttackRestrictionType.NOT_ALONE) || - restrictions.contains(AttackRestrictionType.NEED_TWO_OTHERS)) - ) || ( - restrictions.contains(AttackRestrictionType.NEVER) - ) || ( - cantAttackDefender.size() == possibleDefenders.size())) { + if (cantAttackDefender.size() == possibleDefenders.size()) { cantAttack = true; } } @@ -45,25 +36,6 @@ public boolean canAttack(final GameEntity defender) { return !cantAttack && !cantAttackDefender.contains(defender); } - public Set getViolation(final Map attackers) { - final Set violations = EnumSet.noneOf(AttackRestrictionType.class); - final int nAttackers = attackers.size(); - if (restrictions.contains(AttackRestrictionType.ONLY_ALONE) && nAttackers > 1) { - violations.add(AttackRestrictionType.ONLY_ALONE); - } - if (restrictions.contains(AttackRestrictionType.NEED_GREATER_POWER) - && attackers.keySet().stream().noneMatch(AttackRestrictionType.NEED_GREATER_POWER.getPredicate(attacker))) { - violations.add(AttackRestrictionType.NEED_GREATER_POWER); - } - if (restrictions.contains(AttackRestrictionType.NOT_ALONE) && nAttackers <= 1) { - violations.add(AttackRestrictionType.NOT_ALONE); - } - if (restrictions.contains(AttackRestrictionType.NEED_TWO_OTHERS) && nAttackers <= 2) { - violations.add(AttackRestrictionType.NEED_TWO_OTHERS); - } - return violations; - } - public List getStaticViolations(final Map attackers) { CardCollection others = new CardCollection(attackers.keySet()); others.remove(attacker); @@ -75,29 +47,6 @@ public boolean canAttack(final GameEntity defender, final Map return false; } - return getViolation(attackers).isEmpty() && getStaticViolations(attackers).isEmpty(); - } - - public Set getTypes() { - return Collections.unmodifiableSet(restrictions); - } - - private void setRestrictions() { - if (attacker.hasKeyword("CARDNAME can only attack alone.")) { - restrictions.add(AttackRestrictionType.ONLY_ALONE); - } - - if (attacker.hasKeyword("CARDNAME can't attack unless a creature with greater power also attacks.")) { - restrictions.add(AttackRestrictionType.NEED_GREATER_POWER); - } - - if (attacker.hasKeyword("CARDNAME can't attack or block alone.") || attacker.hasKeyword("CARDNAME can't attack alone.")) { - restrictions.add(AttackRestrictionType.NOT_ALONE); - } - - if (attacker.hasKeyword("CARDNAME can't attack unless at least two other creatures attack.")) { - restrictions.add(AttackRestrictionType.NEED_TWO_OTHERS); - } + return getStaticViolations(attackers).isEmpty(); } - } diff --git a/forge-game/src/main/java/forge/game/combat/AttackRestrictionType.java b/forge-game/src/main/java/forge/game/combat/AttackRestrictionType.java deleted file mode 100644 index 362675ee0fc..00000000000 --- a/forge-game/src/main/java/forge/game/combat/AttackRestrictionType.java +++ /dev/null @@ -1,26 +0,0 @@ -package forge.game.combat; - -import forge.game.card.Card; -import forge.game.card.CardPredicates; - -import java.util.function.Predicate; - -public enum AttackRestrictionType { - - ONLY_ALONE, - NEED_GREATER_POWER, - NOT_ALONE, - NEED_TWO_OTHERS, - NEVER; - - public Predicate getPredicate(final Card attacker) { - switch (this) { - case NEED_GREATER_POWER: - return CardPredicates.hasGreaterPowerThan(attacker.getNetPower()); - case NOT_ALONE: - return x -> true; - default: - } - return null; - } -} diff --git a/forge-game/src/main/java/forge/game/combat/CombatUtil.java b/forge-game/src/main/java/forge/game/combat/CombatUtil.java index ed6bb13a850..88b87f5d337 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -46,6 +46,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** *

@@ -488,10 +489,7 @@ public static boolean canBlock(final Card blocker, final boolean nextTurn) { return false; } - boolean cantBlockAlone = blocker.hasKeyword("CARDNAME can't attack or block alone.") || blocker.hasKeyword("CARDNAME can't block alone."); - - final List list = blocker.getController().getCreaturesInPlay(); - return list.size() >= 2 || !cantBlockAlone; + return true; } public static boolean canBlockMoreCreatures(final Card blocker, final CardCollectionView blockedBy) { @@ -701,25 +699,12 @@ public static String validateBlocks(final Combat combat, final Player defending) // Creatures that aren't allowed to block unless certain restrictions are met. for (final Card blocker : blockers) { - boolean cantBlockAlone = blocker.hasKeyword("CARDNAME can't attack or block alone.") || blocker.hasKeyword("CARDNAME can't block alone."); - if (blockers.size() < 2 && cantBlockAlone) { - return TextUtil.concatWithSpace(blocker.toString(), "can't block alone."); - } else if (blockers.size() < 3 && blocker.hasKeyword("CARDNAME can't block unless at least two other creatures block.")) { - return TextUtil.concatWithSpace(blocker.toString(), "can't block unless at least two other creatures block."); - } else if (blocker.hasKeyword("CARDNAME can't block unless a creature with greater power also blocks.")) { - boolean found = false; - int power = blocker.getNetPower(); - // Note: This is O(n^2), but there shouldn't generally be many creatures with the above keyword. - for (Card blocker2 : blockers) { - if (blocker2.getNetPower() > power) { - found = true; - break; - } - } - if (!found) { - return TextUtil.concatWithSpace(blocker.toString(), "can't block unless a creature with greater power also blocks."); - } - } + CardCollection others = new CardCollection(blockers); + others.remove(blocker); + List list = StaticAbilityAttackBlockRestrict.blockRestrict(blocker, others); + if (list.isEmpty()) + continue; + return list.stream().map(StaticAbility::toString).collect(Collectors.joining(", ")); } for (final Card attacker : attackers) { diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index af92e394e5f..02735895beb 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -39,6 +39,7 @@ import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbilityAttackBlockRestrict; import forge.game.staticability.StaticAbilityNoCleanupDamage; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; @@ -701,24 +702,9 @@ private void declareBlockersTurnBasedAction() { reachedSteadyState = true; List remainingBlockers = CardLists.filterControlledBy(combat.getAllBlockers(), p); for (Card c : remainingBlockers) { - boolean removeBlocker = false; - boolean cantBlockAlone = c.hasKeyword("CARDNAME can't attack or block alone.") || c.hasKeyword("CARDNAME can't block alone."); - if (remainingBlockers.size() < 2 && cantBlockAlone) { - removeBlocker = true; - } else if (remainingBlockers.size() < 3 && c.hasKeyword("CARDNAME can't block unless at least two other creatures block.")) { - removeBlocker = true; - } else if (c.hasKeyword("CARDNAME can't block unless a creature with greater power also blocks.")) { - removeBlocker = true; - int power = c.getNetPower(); - // Note: This is O(n^2), but there shouldn't generally be many creatures with the above keyword. - for (Card c2 : remainingBlockers) { - if (c2.getNetPower() > power) { - removeBlocker = false; - break; - } - } - } - if (removeBlocker) { + CardCollection others = new CardCollection(remainingBlockers); + others.remove(c); + if (!StaticAbilityAttackBlockRestrict.blockRestrict(c, others).isEmpty()) { combat.undoBlockingAssignment(c); reachedSteadyState = false; } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java index 156c71566ca..bbd74f2d14c 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityAttackBlockRestrict.java @@ -83,25 +83,31 @@ static public boolean validRestrictNum(StaticAbility stAb, GameEntity defender) return true; } - static public List attackRestrict(final Card attacker, final Collection others) { - final Game game = attacker.getGame(); + static public List attackRestrict(final Card card, final Collection others) { + return restrictCommon(StaticAbilityMode.AttackRestrict, card, others); + } + static public List blockRestrict(final Card card, final Collection others) { + return restrictCommon(StaticAbilityMode.BlockRestrict, card, others); + } + + static public List restrictCommon(StaticAbilityMode mode, final Card card, final Collection others) { + final Game game = card.getGame(); List result = Lists.newArrayList(); for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { for (final StaticAbility stAb : ca.getStaticAbilities()) { - if (!stAb.checkConditions(StaticAbilityMode.AttackRestrict)) { + if (!stAb.checkConditions(mode)) { continue; } - if (!stAb.matchesValidParam("ValidCard", attacker)) { + if (!stAb.matchesValidParam("ValidCard", card)) { continue; } - if (validRestrictCommon(stAb, attacker, others)) { + if (validRestrictCommon(stAb, card, others)) { result.add(stAb); } } } return result; } - static public boolean validRestrictCommon(StaticAbility stAb, Card card, Collection others) { long size; if (stAb.hasParam("ValidOthers")) { diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java index ca1a13b9cc5..dd50ec95bb3 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java @@ -76,6 +76,7 @@ public enum StaticAbilityMode { // StaticAbilityAttackBlockRestrict AttackRestrict, + BlockRestrict, AttackRestrictNum, BlockRestrictNum, diff --git a/forge-gui/res/cardsfolder/b/bonded_construct.txt b/forge-gui/res/cardsfolder/b/bonded_construct.txt index bbd6037a8bf..3c6dbeec109 100644 --- a/forge-gui/res/cardsfolder/b/bonded_construct.txt +++ b/forge-gui/res/cardsfolder/b/bonded_construct.txt @@ -2,5 +2,5 @@ Name:Bonded Construct ManaCost:1 Types:Artifact Creature Construct PT:2/1 -K:CARDNAME can't attack alone. +S:Mode$ AttackRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack alone. Oracle:Bonded Construct can't attack alone. diff --git a/forge-gui/res/cardsfolder/b/bonded_horncrest.txt b/forge-gui/res/cardsfolder/b/bonded_horncrest.txt index f1119421edf..17bdfd85f65 100644 --- a/forge-gui/res/cardsfolder/b/bonded_horncrest.txt +++ b/forge-gui/res/cardsfolder/b/bonded_horncrest.txt @@ -2,5 +2,5 @@ Name:Bonded Horncrest ManaCost:3 R Types:Creature Dinosaur PT:5/5 -K:CARDNAME can't attack or block alone. +S:Mode$ AttackRestrict,BlockRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack or block alone. Oracle:Bonded Horncrest can't attack or block alone. diff --git a/forge-gui/res/cardsfolder/c/craven_hulk.txt b/forge-gui/res/cardsfolder/c/craven_hulk.txt index cb76bacf679..02b99fbc698 100644 --- a/forge-gui/res/cardsfolder/c/craven_hulk.txt +++ b/forge-gui/res/cardsfolder/c/craven_hulk.txt @@ -2,5 +2,5 @@ Name:Craven Hulk ManaCost:3 R Types:Creature Giant Coward PT:4/4 -K:CARDNAME can't block alone. +S:Mode$ BlockRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't block alone. Oracle:Craven Hulk can't block alone. diff --git a/forge-gui/res/cardsfolder/e/ember_beast.txt b/forge-gui/res/cardsfolder/e/ember_beast.txt index 255a9d31700..5b81ccf324d 100644 --- a/forge-gui/res/cardsfolder/e/ember_beast.txt +++ b/forge-gui/res/cardsfolder/e/ember_beast.txt @@ -2,5 +2,5 @@ Name:Ember Beast ManaCost:2 R Types:Creature Beast PT:3/4 -K:CARDNAME can't attack or block alone. +S:Mode$ AttackRestrict,BlockRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack or block alone. Oracle:Ember Beast can't attack or block alone. diff --git a/forge-gui/res/cardsfolder/e/errantry.txt b/forge-gui/res/cardsfolder/e/errantry.txt index 42ced9cafc8..d663d866465 100644 --- a/forge-gui/res/cardsfolder/e/errantry.txt +++ b/forge-gui/res/cardsfolder/e/errantry.txt @@ -3,5 +3,6 @@ ManaCost:1 R Types:Enchantment Aura K:Enchant:Creature SVar:AttachAILogic:Pump -S:Mode$ Continuous | Affected$ Creature.EnchantedBy | AddPower$ 3 | AddHiddenKeyword$ CARDNAME can only attack alone. | Description$ Enchanted creature gets +3/+0 and can only attack alone. +S:Mode$ Continuous | Affected$ Creature.EnchantedBy | AddPower$ 3 | Description$ Enchanted creature gets +3/+0 and can only attack alone. +S:Mode$ AttackRestrict | ValidCard$ Creature.EnchantedBy | OthersCompare$ LT1 | Secondary$ True | Description$ Enchanted creature can only attack alone. Oracle:Enchant creature\nEnchanted creature gets +3/+0 and can only attack alone. diff --git a/forge-gui/res/cardsfolder/j/jackal_familiar.txt b/forge-gui/res/cardsfolder/j/jackal_familiar.txt index 3ebe6441dad..df435fac000 100644 --- a/forge-gui/res/cardsfolder/j/jackal_familiar.txt +++ b/forge-gui/res/cardsfolder/j/jackal_familiar.txt @@ -2,5 +2,5 @@ Name:Jackal Familiar ManaCost:R Types:Creature Jackal PT:2/2 -K:CARDNAME can't attack or block alone. +S:Mode$ AttackRestrict,BlockRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack or block alone. Oracle:Jackal Familiar can't attack or block alone. diff --git a/forge-gui/res/cardsfolder/l/loyal_pegasus.txt b/forge-gui/res/cardsfolder/l/loyal_pegasus.txt index d590eec1cd6..80be8d827ba 100644 --- a/forge-gui/res/cardsfolder/l/loyal_pegasus.txt +++ b/forge-gui/res/cardsfolder/l/loyal_pegasus.txt @@ -3,5 +3,5 @@ ManaCost:W Types:Creature Pegasus PT:2/1 K:Flying -K:CARDNAME can't attack or block alone. +S:Mode$ AttackRestrict,BlockRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack or block alone. Oracle:Flying\nLoyal Pegasus can't attack or block alone. diff --git a/forge-gui/res/cardsfolder/m/master_of_cruelties.txt b/forge-gui/res/cardsfolder/m/master_of_cruelties.txt index 9a96febde13..978b531d2bd 100644 --- a/forge-gui/res/cardsfolder/m/master_of_cruelties.txt +++ b/forge-gui/res/cardsfolder/m/master_of_cruelties.txt @@ -4,7 +4,7 @@ Types:Creature Demon PT:1/4 K:First Strike K:Deathtouch -K:CARDNAME can only attack alone. +S:Mode$ AttackRestrict | ValidCard$ Card.Self | OthersCompare$ LT1 | Description$ CARDNAME can only attack alone. T:Mode$ AttackerUnblocked | ValidCard$ Card.Self | ValidDefender$ Player | Execute$ TrigDamage | TriggerDescription$ Whenever CARDNAME attacks a player and isn't blocked, that player's life total becomes 1. CARDNAME assigns no combat damage this combat. SVar:TrigDamage:DB$ SetLife | Defined$ TriggeredDefender | LifeAmount$ 1 | SubAbility$ DBNoCombatDamage SVar:DBNoCombatDamage:DB$ Effect | StaticAbilities$ SNoCombatDamage | Duration$ UntilHostLeavesPlayOrEndOfCombat diff --git a/forge-gui/res/cardsfolder/m/militia_rallier.txt b/forge-gui/res/cardsfolder/m/militia_rallier.txt index e9ecf7aee80..6b9d9fa61db 100644 --- a/forge-gui/res/cardsfolder/m/militia_rallier.txt +++ b/forge-gui/res/cardsfolder/m/militia_rallier.txt @@ -2,7 +2,7 @@ Name:Militia Rallier ManaCost:2 W Types:Creature Human Soldier PT:3/3 -K:CARDNAME can't attack alone. +S:Mode$ AttackRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack alone. T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigUntap | TriggerDescription$ Whenever CARDNAME attacks, untap target creature. SVar:TrigUntap:DB$ Untap | ValidTgts$ Creature | TgtPrompt$ Select target creature SVar:HasAttackEffect:TRUE diff --git a/forge-gui/res/cardsfolder/m/mogg_flunkies.txt b/forge-gui/res/cardsfolder/m/mogg_flunkies.txt index a45697c8d1a..1bf23bc222c 100644 --- a/forge-gui/res/cardsfolder/m/mogg_flunkies.txt +++ b/forge-gui/res/cardsfolder/m/mogg_flunkies.txt @@ -2,5 +2,5 @@ Name:Mogg Flunkies ManaCost:1 R Types:Creature Goblin PT:3/3 -K:CARDNAME can't attack or block alone. +S:Mode$ AttackRestrict,BlockRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack or block alone. Oracle:Mogg Flunkies can't attack or block alone. diff --git a/forge-gui/res/cardsfolder/o/okk.txt b/forge-gui/res/cardsfolder/o/okk.txt index f5acd7c49a6..7b7397a0193 100644 --- a/forge-gui/res/cardsfolder/o/okk.txt +++ b/forge-gui/res/cardsfolder/o/okk.txt @@ -2,6 +2,6 @@ Name:Okk ManaCost:1 R Types:Creature Goblin PT:4/4 -K:CARDNAME can't attack unless a creature with greater power also attacks. -K:CARDNAME can't block unless a creature with greater power also blocks. +S:Mode$ AttackRestrict | ValidCard$ Card.Self | ValidOthersRelative$ Creature.powerGTX | Description$ CARDNAME can't attack unless a creature with greater power also attacks. +S:Mode$ BlockRestrict | ValidCard$ Card.Self | ValidOthersRelative$ Creature.powerGTX | Description$ CARDNAME can't block unless a creature with greater power also blocks. Oracle:Okk can't attack unless a creature with greater power also attacks.\nOkk can't block unless a creature with greater power also blocks. diff --git a/forge-gui/res/cardsfolder/o/orcish_conscripts.txt b/forge-gui/res/cardsfolder/o/orcish_conscripts.txt index 71ad8a3707d..b0a66afb42a 100644 --- a/forge-gui/res/cardsfolder/o/orcish_conscripts.txt +++ b/forge-gui/res/cardsfolder/o/orcish_conscripts.txt @@ -2,6 +2,6 @@ Name:Orcish Conscripts ManaCost:R Types:Creature Orc PT:2/2 -K:CARDNAME can't attack unless at least two other creatures attack. +S:Mode$ AttackRestrict | ValidCard$ Card.Self | OthersCompare$ GE2 | Description$ CARDNAME can't attack unless at least two other creatures attack. K:CARDNAME can't block unless at least two other creatures block. Oracle:Orcish Conscripts can't attack unless at least two other creatures attack.\nOrcish Conscripts can't block unless at least two other creatures block. diff --git a/forge-gui/res/cardsfolder/p/pipsqueak_rebel_strongarm.txt b/forge-gui/res/cardsfolder/p/pipsqueak_rebel_strongarm.txt index 15ba899c5f6..b7cc38a8626 100644 --- a/forge-gui/res/cardsfolder/p/pipsqueak_rebel_strongarm.txt +++ b/forge-gui/res/cardsfolder/p/pipsqueak_rebel_strongarm.txt @@ -2,5 +2,5 @@ Name:Pipsqueak, Rebel Strongarm ManaCost:2 W Types:Legendary Creature Human Rebel Ally PT:4/4 -S:Mode$ Continuous | Affected$ Card.Self+counters_EQ0_P1P1 | AddHiddenKeyword$ CARDNAME can't attack alone. | Description$ NICKNAME can't attack alone unless he has a +1/+1 counter on him. +S:Mode$ AttackRestrict | ValidCard$ Card.Self+counters_EQ0_P1P1 | Description$ NICKNAME can't attack alone unless he has a +1/+1 counter on him. Oracle:Pipsqueak can't attack alone unless he has a +1/+1 counter on him. diff --git a/forge-gui/res/cardsfolder/r/raging_kronch.txt b/forge-gui/res/cardsfolder/r/raging_kronch.txt index 37c45faed32..7dd0e5e8afd 100644 --- a/forge-gui/res/cardsfolder/r/raging_kronch.txt +++ b/forge-gui/res/cardsfolder/r/raging_kronch.txt @@ -2,5 +2,5 @@ Name:Raging Kronch ManaCost:2 R Types:Creature Beast PT:4/3 -K:CARDNAME can't attack alone. +S:Mode$ AttackRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack alone. Oracle:Raging Kronch can't attack alone. diff --git a/forge-gui/res/cardsfolder/s/sightless_brawler.txt b/forge-gui/res/cardsfolder/s/sightless_brawler.txt index cb217d34005..f1b3e1469cc 100644 --- a/forge-gui/res/cardsfolder/s/sightless_brawler.txt +++ b/forge-gui/res/cardsfolder/s/sightless_brawler.txt @@ -3,6 +3,7 @@ ManaCost:1 W Types:Enchantment Creature Human Warrior PT:3/2 K:Bestow:4 W -K:CARDNAME can't attack alone. -S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddPower$ 3 | AddToughness$ 2 | AddHiddenKeyword$ CARDNAME can't attack alone. | Description$ Enchanted creature gets +3/+2 and can't attack alone. +S:Mode$ AttackRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack alone. +S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddPower$ 3 | AddToughness$ 2 | Description$ Enchanted creature gets +3/+2 and can't attack alone. +S:Mode$ AttackRestrict | ValidCard$ Card.EnchantedBy | Secondary$ True | Description$ Enchanted creature can't attack alone. Oracle:Bestow {4}{W} (If you cast this card for its bestow cost, it's an Aura spell with enchant creature. It becomes a creature again if it's not attached to a creature.)\nSightless Brawler can't attack alone.\nEnchanted creature gets +3/+2 and can't attack alone. diff --git a/forge-gui/res/cardsfolder/t/trusty_companion.txt b/forge-gui/res/cardsfolder/t/trusty_companion.txt index 080119832ff..383048a95de 100644 --- a/forge-gui/res/cardsfolder/t/trusty_companion.txt +++ b/forge-gui/res/cardsfolder/t/trusty_companion.txt @@ -3,5 +3,5 @@ ManaCost:1 W Types:Creature Hyena PT:3/2 K:Vigilance -K:CARDNAME can't attack alone. +S:Mode$ AttackRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack alone. Oracle:Vigilance\nTrusty Companion can't attack alone. diff --git a/forge-gui/res/cardsfolder/w/wojek_bodyguard.txt b/forge-gui/res/cardsfolder/w/wojek_bodyguard.txt index 10290fc1976..7440e53d24c 100644 --- a/forge-gui/res/cardsfolder/w/wojek_bodyguard.txt +++ b/forge-gui/res/cardsfolder/w/wojek_bodyguard.txt @@ -3,6 +3,6 @@ ManaCost:2 R Types:Creature Human Soldier PT:3/3 K:Mentor -K:CARDNAME can't attack or block alone. +S:Mode$ AttackRestrict,BlockRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack or block alone. DeckHas:Ability$Counters Oracle:Mentor (Whenever this creature attacks, put a +1/+1 counter on target attacking creature with lesser power.)\nWojek Bodyguard can't attack or block alone. diff --git a/forge-gui/res/tokenscripts/w_4_4_beast_lonely.txt b/forge-gui/res/tokenscripts/w_4_4_beast_lonely.txt index bb1abe69ded..a7d9e7d63f1 100644 --- a/forge-gui/res/tokenscripts/w_4_4_beast_lonely.txt +++ b/forge-gui/res/tokenscripts/w_4_4_beast_lonely.txt @@ -3,5 +3,5 @@ ManaCost:no cost Colors:white Types:Creature Beast PT:4/4 -K:CARDNAME can't attack or block alone. +S:Mode$ AttackRestrict,BlockRestrict | ValidCard$ Card.Self | Description$ CARDNAME can't attack or block alone. Oracle:This creature can't attack or block alone.