diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index d1947a73875..8678c600e34 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -22,7 +22,6 @@ import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.GameObject; -import forge.game.GlobalRuleChange; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; @@ -43,7 +42,6 @@ import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.spellability.SpellAbility; -import forge.game.spellability.SpellPermanent; import forge.game.spellability.TargetRestrictions; import forge.game.staticability.StaticAbility; import forge.game.trigger.Trigger; @@ -78,9 +76,7 @@ public class AttachAi extends SpellAbilityAi { } } - if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule) - && source.getType().isLegendary() && sa instanceof SpellPermanent - && ai.isCardInPlay(source.getName())) { + if (source.isAura() && sa.isSpell() && !source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) { // Don't play the second copy of a legendary enchantment already in play // TODO: Add some extra checks for where the AI may want to cast a replacement aura diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java index fa364648ec1..8d3a77c2d1d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -11,7 +11,6 @@ import forge.card.MagicColor; import forge.game.Game; import forge.game.GameEntity; import forge.game.GameObject; -import forge.game.GlobalRuleChange; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; @@ -276,11 +275,8 @@ public class ChangeZoneAi extends SpellAbilityAi { //Ninjutsu if (sa.hasParam("Ninjutsu")) { - if (source.getType().isLegendary() - && !ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { - if (ai.getZone(ZoneType.Battlefield).contains(CardPredicates.nameEquals(source.getName()))) { - return false; - } + if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) { + return false; } if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) { return false; @@ -732,18 +728,16 @@ public class ChangeZoneAi extends SpellAbilityAi { } } // predict Legendary cards already present - if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { - boolean nothingWillReturn = true; - for (final Card c : retrieval) { - if (!(c.getType().isLegendary() && ai.isCardInPlay(c.getName()))) { - nothingWillReturn = false; - break; - } - } - if (nothingWillReturn) { - return false; + boolean nothingWillReturn = true; + for (final Card c : retrieval) { + if (!(!c.ignoreLegendRule() && ai.isCardInPlay(c.getName()))) { + nothingWillReturn = false; + break; } } + if (nothingWillReturn) { + return false; + } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java index 79f59f18334..89f06af68ea 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java @@ -20,11 +20,13 @@ package forge.ai.ability; import java.util.List; import java.util.Map; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; + import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.Game; -import forge.game.GlobalRuleChange; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; @@ -37,6 +39,7 @@ import forge.game.player.Player; import forge.game.player.PlayerController.BinaryChoiceType; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; +import forge.game.zone.ZoneType; /** *

@@ -97,13 +100,19 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { return true; } else { // currently only Clockspinning - boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule); // logic to remove some counter CardCollection countersList = CardLists.filter(list, CardPredicates.hasCounters()); if (!countersList.isEmpty()) { - if (!ai.isCardInPlay("Marit Lage") || noLegendary) { + CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage"); + boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, new Predicate() { + @Override + public boolean apply(Card input) { + return input.ignoreLegendRule(); + } + }); + if (maritEmpty) { CardCollectionView depthsList = CardLists.filter(countersList, CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterEnumType.ICE)); @@ -198,9 +207,6 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { @Override public CounterType chooseCounterType(List options, SpellAbility sa, Map params) { final Player ai = sa.getActivatingPlayer(); - final Game game = ai.getGame(); - - boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule); Card tgt = (Card) params.get("Target"); @@ -229,7 +235,15 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { } else { // this counters are treat first to be removed if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) { - if (!ai.isCardInPlay("Marit Lage") || noLegendary) { + CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage"); + boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, new Predicate() { + @Override + public boolean apply(Card input) { + return input.ignoreLegendRule(); + } + }); + + if (maritEmpty) { return CounterType.get(CounterEnumType.ICE); } } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) { @@ -260,12 +274,10 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map params) { if (kindOfChoice.equals(BinaryChoiceType.AddOrRemove)) { final Player ai = sa.getActivatingPlayer(); - final Game game = ai.getGame(); + Card tgt = (Card) params.get("Target"); CounterType type = (CounterType) params.get("CounterType"); - boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule); - if (tgt.getController().isOpponentOf(ai)) { if (type.is(CounterEnumType.LOYALTY) && tgt.isPlaneswalker()) { return false; @@ -274,7 +286,15 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { return ComputerUtil.isNegativeCounter(type, tgt); } else { if (type.is(CounterEnumType.ICE) && "Dark Depths".equals(tgt.getName())) { - if (!ai.isCardInPlay("Marit Lage") || noLegendary) { + CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage"); + boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, new Predicate() { + @Override + public boolean apply(Card input) { + return input.ignoreLegendRule(); + } + }); + + if (maritEmpty) { return false; } } else if (type.is(CounterEnumType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java index 92f636973d3..d5df83a1a60 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java @@ -3,7 +3,9 @@ package forge.ai.ability; import java.util.List; import java.util.Map; +import com.google.common.base.Predicate; import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; @@ -11,7 +13,6 @@ import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.GameEntity; -import forge.game.GlobalRuleChange; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -117,16 +118,21 @@ public class CountersRemoveAi extends SpellAbilityAi { // Filter AI-specific targets if provided list = ComputerUtil.filterAITgts(sa, ai, list, false); - boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule); + CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage"); + boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, new Predicate() { + @Override + public boolean apply(Card input) { + return input.ignoreLegendRule(); + } + }); if (type.matches("All")) { // Logic Part for Vampire Hexmage // Break Dark Depths - if (!ai.isCardInPlay("Marit Lage") || noLegendary) { + if (maritEmpty) { CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), CardPredicates.hasCounter(CounterEnumType.ICE, 3)); - if (!depthsList.isEmpty()) { sa.getTargets().add(depthsList.getFirst()); return true; @@ -161,7 +167,7 @@ public class CountersRemoveAi extends SpellAbilityAi { } // try to remove them from Dark Depths and Planeswalkers too - if (!ai.isCardInPlay("Marit Lage") || noLegendary) { + if (maritEmpty) { CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), CardPredicates.hasCounter(CounterEnumType.ICE)); diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java index 56b41a80289..ac6bba86efe 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java @@ -11,8 +11,6 @@ import forge.ai.ComputerUtilMana; import forge.ai.SpellAbilityAi; import forge.card.CardType.Supertype; import forge.card.mana.ManaCost; -import forge.game.Game; -import forge.game.GlobalRuleChange; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; @@ -52,28 +50,26 @@ public class PermanentAi extends SpellAbilityAi { @Override protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { final Card card = sa.getHostCard(); - final Game game = ai.getGame(); // check on legendary - if (card.getType().isLegendary() - && !game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { // TODO check the risk we'd lose the effect with bad timing - if (ai.isCardInPlay(card.getName())) { // TODO check for keyword - if (!card.hasSVar("AILegendaryException")) { - // AiPlayDecision.WouldDestroyLegend - return false; - } else { - String specialRule = card.getSVar("AILegendaryException"); - if ("TwoCopiesAllowed".equals(specialRule)) { - // One extra copy allowed on the battlefield, e.g. Brothers Yamazaki - if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())) > 1) { - return false; - } - } else if ("AlwaysAllowed".equals(specialRule)) { - // Nothing to do here, check for Legendary is disabled - } else { - // Unknown hint, assume two copies not allowed + if (!card.ignoreLegendRule() && ai.isCardInPlay(card.getName())) { + // TODO check the risk we'd lose the effect with bad timing + // TODO check for keyword + if (!card.hasSVar("AILegendaryException")) { + // AiPlayDecision.WouldDestroyLegend + return false; + } else { + String specialRule = card.getSVar("AILegendaryException"); + if ("TwoCopiesAllowed".equals(specialRule)) { + // One extra copy allowed on the battlefield, e.g. Brothers Yamazaki + if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())) > 1) { return false; } + } else if ("AlwaysAllowed".equals(specialRule)) { + // Nothing to do here, check for Legendary is disabled + } else { + // Unknown hint, assume two copies not allowed + return false; } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java index f34a321b6fd..8a7a58d4951 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java @@ -2,10 +2,8 @@ package forge.ai.ability; import com.google.common.base.Predicate; -import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import forge.ai.*; -import forge.game.GlobalRuleChange; import forge.game.card.Card; import forge.game.card.CardPredicates; import forge.game.phase.PhaseType; @@ -54,16 +52,16 @@ public class RepeatAi extends SpellAbilityAi { if (sa.usesTargeting()) { if (logic.startsWith("CopyBestCreature")) { Card best = null; - if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule) || logic.endsWith("IgnoreLegendary")) { - best = ComputerUtilCard.getBestAI(Iterables.filter(ai.getCreaturesInPlay(), Predicates.and( - CardPredicates.isTargetableBy(sa), new Predicate() { - @Override - public boolean apply(Card card) { - return !card.getType().isLegendary(); - } - }))); + Iterable targetableAi = Iterables.filter(ai.getCreaturesInPlay(), CardPredicates.isTargetableBy(sa)); + if (!logic.endsWith("IgnoreLegendary")) { + best = ComputerUtilCard.getBestAI(Iterables.filter(targetableAi, new Predicate() { + @Override + public boolean apply(Card card) { + return card.ignoreLegendRule(); + } + })); } else { - best = ComputerUtilCard.getBestAI(Iterables.filter(ai.getCreaturesInPlay(), CardPredicates.isTargetableBy(sa))); + best = ComputerUtilCard.getBestAI(targetableAi); } if (best == null && mandatory && sa.canTarget(sa.getHostCard())) { best = sa.getHostCard(); diff --git a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java index b010942e831..4b3451a6855 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java @@ -9,7 +9,6 @@ import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.card.CardStateName; -import forge.game.GlobalRuleChange; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -238,7 +237,7 @@ public class SetStateAi extends SpellAbilityAi { private boolean isSafeToTransformIntoLegendary(Player aiPlayer, Card source) { // Prevent transform into legendary creature if copy already exists // Check first if Legend Rule does still apply - if (!aiPlayer.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { + if (!source.ignoreLegendRule()) { if (!source.hasAlternateState()) { System.err.println("Warning: SetState without ALTERNATE on " + source.getName() + "."); return false; diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java index 4838a3aa551..e0a8bbc3639 100644 --- a/forge-core/src/main/java/forge/card/CardDb.java +++ b/forge-core/src/main/java/forge/card/CardDb.java @@ -20,6 +20,8 @@ package forge.card; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.*; + +import forge.StaticData; import forge.card.CardEdition.CardInSet; import forge.card.CardEdition.Type; import forge.deck.generation.IDeckGenPool; @@ -45,14 +47,16 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { private final Map uniqueCardsByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); private final Map rulesByName; private final Map facesByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); - private static Map artPrefs = new HashMap<>(); + private static Map artPrefs = Maps.newHashMap(); private final Map alternateName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); - private final Map artIds = new HashMap<>(); + private final Map artIds = Maps.newHashMap(); private final CardEdition.Collection editions; private List filtered; + private Map nonLegendaryCreatureNames = Maps.newHashMap(); + public enum CardArtPreference { LATEST_ART_ALL_EDITIONS(false, true), LATEST_ART_CORE_EXPANSIONS_REPRINT_ONLY(true, true), @@ -489,7 +493,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { return null; // 1. First off, try using all possible search parameters, to narrow down the actual cards looked for. String reqEditionCode = request.edition; - PaperCard result = null; if (reqEditionCode != null && reqEditionCode.length() > 0) { // This get is robust even against expansion aliases (e.g. TE and TMP both valid for Tempest) - // MOST of the extensions have two short codes, 141 out of 221 (so far) @@ -843,6 +846,26 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { return facesByName.get(getName(name)); } + public boolean isNonLegendaryCreatureName(final String name) { + Boolean bool = nonLegendaryCreatureNames.get(name); + if (bool != null) { + return bool; + } + // check if the name is from a face + // in general token creatures does not have this + final ICardFace face = StaticData.instance().getCommonCards().getFaceByName(name); + if (face == null) { + nonLegendaryCreatureNames.put(name, false); + return false; + } + // TODO add check if face is legal in the format of the game + // name does need to be a non-legendary creature + final CardType type = face.getType(); + bool = type.isCreature() && !type.isLegendary(); + nonLegendaryCreatureNames.put(name, bool); + return bool; + } + @Override public Collection getAllCards() { return Collections.unmodifiableCollection(allCardsByName.values()); diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 897fbae53a4..57fce01e2a6 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -17,7 +17,6 @@ */ package forge.game; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; @@ -31,12 +30,12 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Predicate; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ComparisonChain; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; import forge.GameCommand; @@ -1697,38 +1696,71 @@ public class GameAction { } private boolean handleLegendRule(Player p, CardCollection noRegCreats) { - final List a = CardLists.getType(p.getCardsIn(ZoneType.Battlefield), "Legendary"); - if (a.isEmpty() || game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { - return false; - } - boolean recheck = false; - // TODO legend rule exception into static ability - List yamazaki = CardLists.getKeyword(a, "Legend rule doesn't apply to CARDNAME."); - a.removeAll(yamazaki); + final List a = Lists.newArrayList(); - Multimap uniqueLegends = ArrayListMultimap.create(); - for (Card c : a) { - if (!c.isFaceDown()) { - uniqueLegends.put(c.getName(), c); + // check for ignore legend rule + for (Card c : CardLists.getType(p.getCardsIn(ZoneType.Battlefield), "Legendary")) { + if (!c.ignoreLegendRule()) { + a.add(c); } } - // TODO handle Spy Kit + if (a.isEmpty()) { + return false; + } + boolean recheck = false; + + // Corner Case 1: Legendary with non legendary creature names + CardCollection nonLegendaryNames = new CardCollection(Iterables.filter(a, new Predicate() { + @Override + public boolean apply(Card input) { + return input.hasNonLegendaryCreatureNames(); + } + + })); + + Multimap uniqueLegends = Multimaps.index(a, CardPredicates.Accessors.fnGetNetName); + CardCollection removed = new CardCollection(); for (String name : uniqueLegends.keySet()) { - Collection cc = uniqueLegends.get(name); + // skip the ones with empty names + if (name.isEmpty()) { + continue; + } + CardCollection cc = new CardCollection(uniqueLegends.get(name)); + // check if it is a non legendary creature name + // if yes, then add the other legendary with Spy Kit too + if (!name.isEmpty() && StaticData.instance().getCommonCards().isNonLegendaryCreatureName(name)) { + cc.addAll(nonLegendaryNames); + } if (cc.size() < 2) { continue; } recheck = true; - Card toKeep = p.getController().chooseSingleEntityForEffect(new CardCollection(cc), new SpellAbility.EmptySa(ApiType.InternalLegendaryRule, new Card(-1, game), p), + Card toKeep = p.getController().chooseSingleEntityForEffect(cc, new SpellAbility.EmptySa(ApiType.InternalLegendaryRule, new Card(-1, game), p), "You have multiple legendary permanents named \""+name+"\" in play.\n\nChoose the one to stay on battlefield (the rest will be moved to graveyard)", null); cc.remove(toKeep); - noRegCreats.addAll(cc); + removed.addAll(cc); } + // Corner Case 2: with all non legendary creature names + CardCollection emptyNameAllNonLegendary = new CardCollection(nonLegendaryNames); + // remove the ones that got already removed by other legend rule above + emptyNameAllNonLegendary.removeAll(removed); + if (emptyNameAllNonLegendary.size() > 1) { + + recheck = true; + + Card toKeep = p.getController().chooseSingleEntityForEffect(emptyNameAllNonLegendary, new SpellAbility.EmptySa(ApiType.InternalLegendaryRule, new Card(-1, game), p), + "You have multiple legendary permanents with non legendary creature names in play.\n\nChoose the one to stay on battlefield (the rest will be moved to graveyard)", null); + emptyNameAllNonLegendary.remove(toKeep); + removed.addAll(emptyNameAllNonLegendary); + + } + noRegCreats.addAll(removed); + return recheck; } diff --git a/forge-game/src/main/java/forge/game/GlobalRuleChange.java b/forge-game/src/main/java/forge/game/GlobalRuleChange.java index e0b5007432a..3e8312427e0 100644 --- a/forge-game/src/main/java/forge/game/GlobalRuleChange.java +++ b/forge-game/src/main/java/forge/game/GlobalRuleChange.java @@ -28,7 +28,6 @@ public enum GlobalRuleChange { noCreatureETBTriggers ("Creatures entering the battlefield don't cause abilities to trigger."), noCreatureDyingTriggers ("Creatures dying don't cause abilities to trigger."), noNight ("It can't become night."), - noLegendRule ("The legend rule doesn't apply."), /* onlyOneAttackerATurn ("No more than one creature can attack each turn."), */ onlyOneAttackerACombat ("No more than one creature can attack each combat."), onlyOneBlocker ("No more than one creature can block each combat."), diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 8e8a6e0ebac..004f778d712 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -60,6 +60,7 @@ import forge.game.staticability.StaticAbilityCantPutCounter; import forge.game.staticability.StaticAbilityCantSacrifice; import forge.game.staticability.StaticAbilityCantTarget; import forge.game.staticability.StaticAbilityCantTransform; +import forge.game.staticability.StaticAbilityIgnoreLegendRule; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerType; @@ -5189,15 +5190,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } else { // check if this card has a name from a face // in general token creatures does not have this - final ICardFace face = StaticData.instance().getCommonCards().getFaceByName(getName()); - if (face == null) { - return false; - } // TODO add check if face is legal in the format of the game // name does need to be a non-legendary creature - final CardType type = face.getType(); - if (type != null && type.isCreature() && !type.isLegendary()) - return true; + return StaticData.instance().getCommonCards().isNonLegendaryCreatureName(getName()); } } return sharesNameWith(c1.getName()); @@ -5220,15 +5215,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars { if (!shares && hasNonLegendaryCreatureNames()) { // check if the name is from a face // in general token creatures does not have this - final ICardFace face = StaticData.instance().getCommonCards().getFaceByName(name); - if (face == null) { - return false; - } // TODO add check if face is legal in the format of the game // name does need to be a non-legendary creature - final CardType type = face.getType(); - if (type.isCreature() && !type.isLegendary()) - return true; + return StaticData.instance().getCommonCards().isNonLegendaryCreatureName(name); } return shares; } @@ -7107,4 +7096,16 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } return getGame().getCombat().isAttacking(this); } + + public boolean ignoreLegendRule() { + // not legendary + if (!getType().isLegendary()) { + return true; + } + // empty name and no "has non legendary creature names" + if (this.getName().isEmpty() && !hasNonLegendaryCreatureNames()) { + return true; + } + return StaticAbilityIgnoreLegendRule.ignoreLegendRule(this); + } } diff --git a/forge-game/src/main/java/forge/game/card/CardPredicates.java b/forge-game/src/main/java/forge/game/card/CardPredicates.java index 0ad73836de9..c1c49ce427c 100644 --- a/forge-game/src/main/java/forge/game/card/CardPredicates.java +++ b/forge-game/src/main/java/forge/game/card/CardPredicates.java @@ -760,6 +760,13 @@ public final class CardPredicates { return a.getCMC(); } }; + + public static final Function fnGetNetName = new Function() { + @Override + public String apply(Card a) { + return a.getName(); + } + }; } } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityIgnoreLegendRule.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityIgnoreLegendRule.java new file mode 100644 index 00000000000..0b7211e0d49 --- /dev/null +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityIgnoreLegendRule.java @@ -0,0 +1,33 @@ +package forge.game.staticability; + +import forge.game.Game; +import forge.game.card.Card; +import forge.game.zone.ZoneType; + +public class StaticAbilityIgnoreLegendRule { + + static String MODE = "IgnoreLegendRule"; + + public static boolean ignoreLegendRule(final Card card) { + final Game game = card.getGame(); + for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.getParam("Mode").equals(MODE) || stAb.isSuppressed() || !stAb.checkConditions()) { + continue; + } + + if (applyIgnoreLegendRuleAbility(stAb, card)) { + return true; + } + } + } + return false; + } + + public static boolean applyIgnoreLegendRuleAbility(final StaticAbility stAb, final Card card) { + if (!stAb.matchesValidParam("ValidCard", card)) { + return false; + } + return true; + } +} diff --git a/forge-gui/res/cardsfolder/b/brothers_yamazaki.txt b/forge-gui/res/cardsfolder/b/brothers_yamazaki.txt index a44e961b09c..31002da06ac 100644 --- a/forge-gui/res/cardsfolder/b/brothers_yamazaki.txt +++ b/forge-gui/res/cardsfolder/b/brothers_yamazaki.txt @@ -3,8 +3,7 @@ ManaCost:2 R Types:Legendary Creature Human Samurai PT:2/1 K:Bushido:1 -S:Mode$ Continuous | Affected$ Permanent.namedBrothers Yamazaki | CheckSVar$ X | SVarCompare$ EQ2 | AddHiddenKeyword$ Legend rule doesn't apply to CARDNAME. | Description$ If there are exactly two permanents named Brothers Yamazaki on the battlefield, the "legend rule" doesn't apply to them. -SVar:X:Count$Valid Permanent.namedBrothers Yamazaki +S:Mode$ IgnoreLegendRule | ValidCard$ Permanent.namedBrothers Yamazaki | IsPresent$ Permanent.namedBrothers Yamazaki | PresentCompare$ EQ2 | Description$ If there are exactly two permanents named Brothers Yamazaki on the battlefield, the "legend rule" doesn't apply to them. S:Mode$ Continuous | Affected$ Creature.Other+namedBrothers Yamazaki | AddPower$ 2 | AddToughness$ 2 | AddKeyword$ Haste | Description$ Each other creature named CARDNAME gets +2/+2 and has haste. DeckHints:Name$Brothers Yamazaki SVar:AILegendaryException:TwoCopiesAllowed diff --git a/forge-gui/res/cardsfolder/m/mirror_box.txt b/forge-gui/res/cardsfolder/m/mirror_box.txt index b54cf2f77ae..46192807181 100644 --- a/forge-gui/res/cardsfolder/m/mirror_box.txt +++ b/forge-gui/res/cardsfolder/m/mirror_box.txt @@ -1,7 +1,7 @@ Name:Mirror Box ManaCost:3 Types:Artifact -S:Mode$ Continuous | Affected$ Permanent.YouCtrl | AddHiddenKeyword$ Legend rule doesn't apply to CARDNAME. | Description$ The "legend rule" doesn't apply to permanents you control. +S:Mode$ IgnoreLegendRule | ValidCard$ Permanent.YouCtrl | Description$ The "legend rule" doesn't apply to permanents you control. S:Mode$ Continuous | Affected$ Creature.Legendary+YouCtrl | AddPower$ 1 | AddToughness$ 1 | Description$ Each legendary creature you control gets +1/+1. S:Mode$ Continuous | Affected$ Creature.nonToken+YouCtrl | AddPower$ AffectedX | AddToughness$ AffectedX | Description$ Each nontoken creature you control gets +1/+1 for each other creature you control with the same name as that creature. SVar:AffectedX:Count$Valid Creature.Other+sharesNameWith+YouCtrl diff --git a/forge-gui/res/cardsfolder/m/mirror_gallery.txt b/forge-gui/res/cardsfolder/m/mirror_gallery.txt index 7201641b78f..6f3526a97b7 100644 --- a/forge-gui/res/cardsfolder/m/mirror_gallery.txt +++ b/forge-gui/res/cardsfolder/m/mirror_gallery.txt @@ -1,7 +1,7 @@ Name:Mirror Gallery ManaCost:5 Types:Artifact -S:Mode$ Continuous | GlobalRule$ The legend rule doesn't apply. | Description$ The "legend rule" doesn't apply. +S:Mode$ IgnoreLegendRule | Description$ The "legend rule" doesn't apply. SVar:NonStackingEffect:True AI:RemoveDeck:Random Oracle:The "legend rule" doesn't apply. diff --git a/forge-gui/res/cardsfolder/s/sakashima_of_a_thousand_faces.txt b/forge-gui/res/cardsfolder/s/sakashima_of_a_thousand_faces.txt index d33828681bf..89e6111b219 100644 --- a/forge-gui/res/cardsfolder/s/sakashima_of_a_thousand_faces.txt +++ b/forge-gui/res/cardsfolder/s/sakashima_of_a_thousand_faces.txt @@ -4,7 +4,7 @@ Types:Legendary Creature Human Rogue PT:3/1 K:ETBReplacement:Copy:DBCopy:Optional SVar:DBCopy:DB$ Clone | Choices$ Creature.YouCtrl+Other | AddKeywords$ Partner | AddStaticAbilities$ NoLegendRule | SpellDescription$ You may have CARDNAME enter the battlefield as a copy of another creature you control, except it has CARDNAME's other abilities. -SVar:NoLegendRule:Mode$ Continuous | Affected$ Permanent.YouCtrl | AddHiddenKeyword$ Legend rule doesn't apply to CARDNAME. | Description$ The "legend rule" doesn't apply to permanents you control. -S:Mode$ Continuous | Affected$ Permanent.YouCtrl | AddHiddenKeyword$ Legend rule doesn't apply to CARDNAME. | Description$ The "legend rule" doesn't apply to permanents you control. +SVar:NoLegendRule:Mode$ IgnoreLegendRule | ValidCard$ Permanent.YouCtrl | Description$ The "legend rule" doesn't apply to permanents you control. +S:Mode$ IgnoreLegendRule | ValidCard$ Permanent.YouCtrl | Description$ The "legend rule" doesn't apply to permanents you control. K:Partner Oracle:You may have Sakashima of a Thousand Faces enter the battlefield as a copy of another creature you control, except it has Sakashima of a Thousand Faces's other abilities.\nThe "legend rule" doesn't apply to permanents you control.\nPartner (You can have two commanders if both have partner.)