From 56f8d7f23ab48ff2926bd74a728260ce72ac9deb Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 2 Jan 2022 12:05:56 +0100 Subject: [PATCH 1/7] Fix Epic targeting --- .../main/java/forge/game/player/Player.java | 36 +++++++------------ .../forge/game/spellability/SpellAbility.java | 15 ++++++-- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 26b41b445f9..6bf017134ed 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -529,8 +529,7 @@ public class Player extends GameEntity implements Comparable { } // Run triggers - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.LifeAmount, lifeGain); runParams.put(AbilityKey.Source, source); runParams.put(AbilityKey.SourceSA, sa); @@ -602,8 +601,7 @@ public class Player extends GameEntity implements Comparable { lifeLostThisTurn += toLose; // Run triggers - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.LifeAmount, toLose); runParams.put(AbilityKey.FirstTime, firstLost); game.getTriggerHandler().runTrigger(TriggerType.LifeLost, runParams, false); @@ -630,8 +628,7 @@ public class Player extends GameEntity implements Comparable { loseLife(lifePayment, false, false); // Run triggers - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.LifeAmount, lifePayment); game.getTriggerHandler().runTrigger(TriggerType.PayLife, runParams, false); @@ -1212,8 +1209,7 @@ public class Player extends GameEntity implements Comparable { getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave)); surveilThisTurn++; - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.NumThisTurn, surveilThisTurn); getGame().getTriggerHandler().runTrigger(TriggerType.Surveil, runParams, false); } @@ -1329,7 +1325,7 @@ public class Player extends GameEntity implements Comparable { } view.updateNumDrawnThisTurn(this); - final Map runParams = Maps.newHashMap(); + final Map runParams = AbilityKey.mapFromPlayer(this); // CR 121.8 card was drawn as part of another sa (e.g. paying with Chromantic Sphere), hide it temporarily if (game.getTopLibForPlayer(this) != null && getPaidForSA() != null && cause != null && getPaidForSA() != cause.getRootAbility()) { @@ -1341,7 +1337,6 @@ public class Player extends GameEntity implements Comparable { // Run triggers runParams.put(AbilityKey.Card, c); runParams.put(AbilityKey.Number, numDrawnThisTurn); - runParams.put(AbilityKey.Player, this); game.getTriggerHandler().runTrigger(TriggerType.Drawn, runParams, false); } } @@ -1545,8 +1540,7 @@ public class Player extends GameEntity implements Comparable { } } } - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.Card, c); runParams.put(AbilityKey.Cause, cause); runParams.put(AbilityKey.IsMadness, discardMadness); @@ -1564,8 +1558,7 @@ public class Player extends GameEntity implements Comparable { public final void addTokensCreatedThisTurn(Card token) { numTokenCreatedThisTurn++; - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.Num, numTokenCreatedThisTurn); runParams.put(AbilityKey.Card, token); game.getTriggerHandler().runTrigger(TriggerType.TokenCreated, runParams, false); @@ -1581,8 +1574,7 @@ public class Player extends GameEntity implements Comparable { public final void addForetoldThisTurn() { numForetoldThisTurn++; - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.Num, numForetoldThisTurn); game.getTriggerHandler().runTrigger(TriggerType.Foretell, runParams, false); } @@ -1717,8 +1709,7 @@ public class Player extends GameEntity implements Comparable { getZone(ZoneType.Library).setCards(getController().cheatShuffle(list)); // Run triggers - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.Source, sa); game.getTriggerHandler().runTrigger(TriggerType.Shuffled, runParams, false); @@ -2244,8 +2235,7 @@ public class Player extends GameEntity implements Comparable { } public final void addInvestigatedThisTurn() { investigatedThisTurn++; - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.Num, investigatedThisTurn); game.getTriggerHandler().runTrigger(TriggerType.Investigated, runParams, false); } @@ -2265,10 +2255,9 @@ public class Player extends GameEntity implements Comparable { sacrificedThisTurn.add(cpy); // Run triggers - final Map runParams = AbilityKey.newMap(); + final Map runParams = AbilityKey.mapFromPlayer(this); // use a copy that preserves last known information about the card (e.g. for Savra, Queen of the Golgari + Painter's Servant) runParams.put(AbilityKey.Card, cpy); - runParams.put(AbilityKey.Player, this); runParams.put(AbilityKey.Cause, source); runParams.put(AbilityKey.CostStack, game.costPaymentStack); runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek()); @@ -3513,8 +3502,7 @@ public class Player extends GameEntity implements Comparable { // Change this if something would make multiple player learn at the same time // Discard Trigger outside Effect - final Map runParams = AbilityKey.newMap(); - runParams.put(AbilityKey.Player, this); + final Map runParams = AbilityKey.mapFromPlayer(this); runParams.put(AbilityKey.Cards, new CardCollection(c)); runParams.put(AbilityKey.Cause, sa); runParams.put(AbilityKey.FirstTime, firstDiscard); diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index bfc4f8af46d..5f5cb789f2d 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -504,7 +504,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } public boolean isCycling() { - return this.isAlternativeCost(AlternativeCost.Cycling); + return isAlternativeCost(AlternativeCost.Cycling); } public boolean isBoast() { @@ -515,6 +515,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return this.hasParam("Ninjutsu"); } + public boolean isEpic() { + AbilitySub sub = this.getSubAbility(); + while (sub != null && !sub.hasParam("Epic")) { + sub = sub.getSubAbility(); + } + return sub != null && sub.hasParam("Epic"); + } + // If this is not null, then ability was made in a factory public ApiType getApi() { return api; @@ -777,7 +785,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void resetOnceResolved() { //resetPaidHash(); // FIXME: if uncommented, breaks Dragon Presence, e.g. Orator of Ojutai + revealing a Dragon from hand. // Is it truly necessary at this point? The paid hash seems to be reset on all SA instance operations. - resetTargets(); + // Epic spell keeps original targets + if (!isEpic()) { + resetTargets(); + } resetTriggeringObjects(); resetTriggerRemembered(); From dc7d226b04981be2906c73c865190ac6a8859085 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 2 Jan 2022 13:18:04 +0000 Subject: [PATCH 2/7] Protection: use Static CantAttach + CantTarget --- .../main/java/forge/ai/ability/AttachAi.java | 27 +-- .../java/forge/ai/ability/DamageDealAi.java | 6 +- .../main/java/forge/ai/ability/FightAi.java | 21 +-- .../src/main/java/forge/game/GameEntity.java | 26 ++- .../ability/effects/AnimateEffectBase.java | 2 +- .../src/main/java/forge/game/card/Card.java | 123 +------------- .../java/forge/game/card/CardFactory.java | 4 +- .../java/forge/game/card/CardFactoryUtil.java | 155 ++++++++++++------ .../java/forge/game/card/CardPredicates.java | 9 - .../main/java/forge/game/player/Player.java | 65 +------- .../forge/game/player/PlayerFactoryUtil.java | 42 +++-- .../game/staticability/StaticAbility.java | 8 +- .../StaticAbilityCantAttach.java | 57 +++++-- .../StaticAbilityContinuous.java | 4 +- .../main/java/forge/game/zone/ZoneType.java | 2 +- forge-gui/res/cardsfolder/d/dream_leash.txt | 3 +- .../res/cardsfolder/e/enthralling_hold.txt | 3 +- 17 files changed, 214 insertions(+), 343 deletions(-) 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 95a5a82e6b2..7eb100d2c00 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -125,15 +125,8 @@ public class AttachAi extends SpellAbilityAi { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) { final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source); - final ZoneType origin = ZoneType.listValueOf(effectExile.getParam("Origin")).get(0); final TargetRestrictions exile_tgt = effectExile.getTargetRestrictions(); - final CardCollection list = CardLists.getValidCards(ai.getGame().getCardsIn(origin), exile_tgt.getValidTgts(), ai, source, effectExile); - final CardCollection targets = CardLists.filter(list, new Predicate() { - @Override - public boolean apply(final Card c) { - return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF)); - } - }); + final CardCollection targets = CardLists.filter(CardUtil.getValidCardsToTarget(exile_tgt, effectExile), CardPredicates.canBeAttached(source)); return !targets.isEmpty(); } @@ -1325,8 +1318,7 @@ public class AttachAi extends SpellAbilityAi { // Is a SA that moves target attachment if ("MoveTgtAura".equals(sa.getParam("AILogic"))) { - CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa); - list = CardLists.filter(list, Predicates.not(CardPredicates.isProtectedFrom(attachSource))); + CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(tgt, sa)); list = CardLists.filter(list, Predicates.or(CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()), new Predicate() { @Override public boolean apply(final Card card) { @@ -1336,7 +1328,7 @@ public class AttachAi extends SpellAbilityAi { return !list.isEmpty() ? ComputerUtilCard.getBestAI(list) : null; } else if ("Unenchanted".equals(sa.getParam("AILogic"))) { - CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa); + CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(tgt, sa)); CardCollection preferred = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card card) { @@ -1361,18 +1353,7 @@ public class AttachAi extends SpellAbilityAi { if (tgt == null) { list = AbilityUtils.getDefinedCards(attachSource, sa.getParam("Defined"), sa); } else { - list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa); - list = CardLists.filter(list, CardPredicates.canBeAttached(attachSource)); - - // TODO If Attaching without casting, don't need to actually target. - // I believe this is the only case where mandatory will be true, so just - // check that when starting that work - // But we shouldn't attach to things with Protection - if (!mandatory) { - list = CardLists.getTargetableCards(list, sa); - } else { - list = CardLists.filter(list, Predicates.not(CardPredicates.isProtectedFrom(attachSource))); - } + list = CardLists.filter(CardUtil.getValidCardsToTarget(tgt, sa), CardPredicates.canBeAttached(attachSource)); } if (list.isEmpty()) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index a41b4c42dab..cbe6159d056 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -598,9 +598,9 @@ public class DamageDealAi extends DamageAiBase { lastTgt = humanCreature; dmg -= assignedDamage; } - if (!source.hasProtectionFrom(humanCreature)) { - dmgTaken += humanCreature.getNetPower(); - } + // protection is already checked by target above + dmgTaken += humanCreature.getNetPower(); + if (dmg == 0) { return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/FightAi.java b/forge-ai/src/main/java/forge/ai/ability/FightAi.java index 20f75ed3a8d..a1e5b42bff1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java @@ -1,21 +1,18 @@ package forge.ai.ability; import java.util.List; -import java.util.Map; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCombat; import forge.ai.SpellAbilityAi; -import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; -import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; @@ -263,15 +260,13 @@ public class FightAi extends SpellAbilityAi { for (Trigger t : aiCreature.getTriggers()) { if (t.getMode() == TriggerType.SpellCast) { SpellAbility sa = t.ensureAbility(); - final Map params = t.getMapParams(); if (sa == null) { continue; } if (ApiType.PutCounter.equals(sa.getApi())) { - if ("Card.Self".equals(params.get("TargetsValid")) && "You".equals(params.get("ValidActivatingPlayer"))) { - SpellAbility heroic = AbilityFactory.getAbility(aiCreature.getSVar(params.get("Execute")),aiCreature); - if ("Self".equals(heroic.getParam("Defined")) && "P1P1".equals(heroic.getParam("CounterType"))) { - return AbilityUtils.calculateAmount(aiCreature, heroic.getParam("CounterNum"), heroic); + if ("Card.Self".equals(t.getParam("TargetsValid")) && "You".equals(t.getParam("ValidActivatingPlayer"))) { + if ("Self".equals(sa.getParam("Defined")) && "P1P1".equals(sa.getParam("CounterType"))) { + return AbilityUtils.calculateAmount(aiCreature, sa.getParam("CounterNum"), sa); } break; } @@ -299,12 +294,14 @@ public class FightAi extends SpellAbilityAi { if (opponent.getSVar("Targeting").equals("Dies")) { return true; } - if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed() || opponent.getShieldCount() > 0 - || ComputerUtil.canRegenerate(opponent.getController(), opponent)) { + // the damage prediction is later + int damage = fighter.getNetPower() + pumpAttack; + if (damage <= 0 || opponent.getShieldCount() > 0 || ComputerUtil.canRegenerate(opponent.getController(), opponent)) { return false; } - if (fighter.hasKeyword(Keyword.DEATHTOUCH) - || ComputerUtilCombat.getDamageToKill(opponent, true) <= fighter.getNetPower() + pumpAttack) { + // try to predict the damage that fighter would deal to opponent + // this should also handle if the opponents creature can be destroyed or not + if (ComputerUtilCombat.getEnoughDamageToKill(opponent, damage, fighter, false) <= damage) { return true; } return false; diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java index 4f3940b258c..714e86ae957 100644 --- a/forge-game/src/main/java/forge/game/GameEntity.java +++ b/forge-game/src/main/java/forge/game/GameEntity.java @@ -39,7 +39,7 @@ import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; -import forge.game.staticability.StaticAbility; +import forge.game.staticability.StaticAbilityCantAttach; import forge.game.zone.ZoneType; public abstract class GameEntity extends GameObject implements IIdentifiable { @@ -222,15 +222,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { return false; } - // CantTarget static abilities - for (final Card ca : getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { - for (final StaticAbility stAb : ca.getStaticAbilities()) { - if (stAb.applyAbility("CantAttach", attach, this)) { - return false; - } - } - } - + // check for rules if (attach.isAura() && !canBeEnchantedBy(attach)) { return false; } @@ -241,8 +233,13 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { return false; } + // check for can't attach static + if (StaticAbilityCantAttach.cantAttach(this, attach, checkSBA)) { + return false; + } + // true for all - return !hasProtectionFrom(attach, checkSBA); + return true; } protected boolean canBeEquippedBy(final Card aura) { @@ -260,6 +257,8 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { } protected boolean canBeEnchantedBy(final Card aura) { + // TODO need to check for multiple Enchant Keywords + SpellAbility sa = aura.getFirstAttachSpell(); TargetRestrictions tgt = null; if (sa != null) { @@ -269,11 +268,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { return !((tgt != null) && !isValid(tgt.getValidTgts(), aura.getController(), aura, sa)); } - public boolean hasProtectionFrom(final Card source) { - return hasProtectionFrom(source, false); - } - public abstract boolean hasProtectionFrom(final Card source, final boolean checkSBA); - // Counters! public boolean hasCounters() { return !counters.isEmpty(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java index 24e62094dfe..70ac1823195 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java @@ -127,7 +127,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect { // itself a static ability) final List addedStaticAbilities = Lists.newArrayList(); for (final String s : stAbs) { - addedStaticAbilities.add(new StaticAbility(AbilityUtils.getSVar(sa, s), c, sa.getCardState())); + addedStaticAbilities.add(StaticAbility.create(AbilityUtils.getSVar(sa, s), c, sa.getCardState(), false)); } final GameCommand unanimate = new GameCommand() { 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 b80d00c8571..f626587f320 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -1912,9 +1912,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { for (KeywordInterface inst : keywords) { String keyword = inst.getOriginal(); try { - if (keyword.startsWith("SpellCantTarget")) { - continue; - } if (keyword.startsWith("CantBeCounteredBy")) { final String[] p = keyword.split(":"); sbLong.append(p[2]).append("\r\n"); @@ -4596,8 +4593,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } public final StaticAbility addStaticAbility(final String s) { if (!s.trim().isEmpty()) { - final StaticAbility stAb = new StaticAbility(s, this, currentState); - stAb.setIntrinsic(true); + final StaticAbility stAb = StaticAbility.create(s, this, currentState, true); currentState.addStaticAbility(stAb); return stAb; } @@ -5693,103 +5689,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public Card getMeldedWith() { return meldedWith; } public void setMeldedWith(Card meldedWith) { this.meldedWith = meldedWith; } - public boolean hasProtectionFrom(final Card source) { - return hasProtectionFrom(source, false, false); - } - - @Override - public boolean hasProtectionFrom(final Card source, final boolean checkSBA) { - return hasProtectionFrom(source, checkSBA, false); - } - public boolean hasProtectionFrom(final Card source, final boolean checkSBA, final boolean damageSource) { - if (source == null) { - return false; - } - - if (isImmutable()) { - return true; - } - - // Protection only works on the Battlefield - if (!isInPlay()) { - return false; - } - - final boolean colorlessDamage = damageSource && source.hasKeyword("Colorless Damage Source"); - - for (final KeywordInterface inst : getKeywords(Keyword.PROTECTION)) { - String kw = inst.getOriginal(); - if (kw.equals("Protection from white")) { - if (source.isWhite() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from blue")) { - if (source.isBlue() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from black")) { - if (source.isBlack() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from red")) { - if (source.isRed() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from green")) { - if (source.isGreen() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from all colors")) { - if (!source.isColorless() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from colorless")) { - if (source.isColorless() || colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from everything")) { - return true; - } else if (kw.startsWith("Protection:")) { // uses isValid; Protection:characteristic:desc:exception - final String[] kws = kw.split(":"); - String characteristic = kws[1]; - - if (characteristic.startsWith("Player")) { - // TODO need to handle that better in CardProperty - if (source.getController().isValid(characteristic.split(","), getController(), this, null)) { - return true; - } - } else { - // if damageSource then it does only check damage color.. - if (damageSource) { - if (characteristic.endsWith("White") || characteristic.endsWith("Blue") - || characteristic.endsWith("Black") || characteristic.endsWith("Red") - || characteristic.endsWith("Green") || characteristic.endsWith("Colorless") - || characteristic.endsWith("MonoColor") || characteristic.endsWith("MultiColor")) { - characteristic += "Source"; - } - } - - final String[] characteristics = characteristic.split(","); - final String[] exceptions = kws.length > 3 ? kws[3].split(",") : null; // check "This effect cannot remove sth" - if (source.isValid(characteristics, getController(), this, null) - && (!checkSBA || exceptions == null || !source.isValid(exceptions, getController(), this, null))) { - return true; - } - } - } else if (kw.startsWith("Protection from opponent of ")) { - final String playerName = kw.substring("Protection from opponent of ".length()); - if (source.getController().isOpponentOf(playerName)) { - return true; - } - } else if (kw.startsWith("Protection from ")) { - final String protectType = CardType.getSingularType(kw.substring("Protection from ".length())); - if (source.getType().hasStringType(protectType)) { - return true; - } - } - } - return false; - } public String getProtectionKey() { String protectKey = ""; boolean pR = false; boolean pG = false; boolean pB = false; boolean pU = false; boolean pW = false; @@ -5933,30 +5832,10 @@ public class Card extends GameEntity implements Comparable, IHasSVars { return true; } - if (hasProtectionFrom(sa.getHostCard())) { - return false; - } - if (isPhasedOut()) { return false; } - final Card source = sa.getHostCard(); - - if (sa.isSpell()) { - // TODO replace with Static Ability - for (KeywordInterface inst : source.getKeywords()) { - String kw = inst.getOriginal(); - if (!kw.startsWith("SpellCantTarget")) { - continue; - } - final String[] k = kw.split(":"); - final String[] restrictions = k[1].split(","); - if (isValid(restrictions, source.getController(), source, null)) { - return false; - } - } - } return true; } diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index 235bf63ae4d..540ebe740ec 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -745,9 +745,7 @@ public class CardFactory { for (final String s : str.split(",")) { if (origSVars.containsKey(s)) { final String actualStatic = origSVars.get(s); - final StaticAbility grantedStatic = new StaticAbility(actualStatic, out, sa.getCardState()); - grantedStatic.setIntrinsic(true); - state.addStaticAbility(grantedStatic); + state.addStaticAbility(StaticAbility.create(actualStatic, out, sa.getCardState(), true)); } } } diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 1e4af37ddbf..56fd67c5de7 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -677,13 +677,13 @@ public class CardFactoryUtil { } public static String getProtectionValid(final String kw, final boolean damage) { - String validSource = "Card."; // TODO extend for Emblem too + String validSource = ""; if (kw.startsWith("Protection:")) { final String[] kws = kw.split(":"); String characteristic = kws[1]; if (characteristic.startsWith("Player")) { - validSource += "ControlledBy " + characteristic; + validSource = "ControlledBy " + characteristic; } else { if (damage && (characteristic.endsWith("White") || characteristic.endsWith("Blue") || characteristic.endsWith("Black") || characteristic.endsWith("Red") @@ -691,34 +691,37 @@ public class CardFactoryUtil { || characteristic.endsWith("MonoColor") || characteristic.endsWith("MultiColor"))) { characteristic += "Source"; } - validSource = characteristic; + return characteristic; } } else if (kw.startsWith("Protection from ")) { String protectType = kw.substring("Protection from ".length()); if (protectType.equals("white")) { - validSource += "White" + (damage ? "Source" : ""); + validSource = "White" + (damage ? "Source" : ""); } else if (protectType.equals("blue")) { - validSource += "Blue" + (damage ? "Source" : ""); + validSource = "Blue" + (damage ? "Source" : ""); } else if (protectType.equals("black")) { - validSource += "Black" + (damage ? "Source" : ""); + validSource = "Black" + (damage ? "Source" : ""); } else if (protectType.equals("red")) { - validSource += "Red" + (damage ? "Source" : ""); + validSource = "Red" + (damage ? "Source" : ""); } else if (protectType.equals("green")) { - validSource += "Green" + (damage ? "Source" : ""); + validSource = "Green" + (damage ? "Source" : ""); } else if (protectType.equals("colorless")) { - validSource += "Colorless" + (damage ? "Source" : ""); + validSource = "Colorless" + (damage ? "Source" : ""); } else if (protectType.equals("all colors")) { - validSource += "nonColorless" + (damage ? "Source" : ""); + validSource = "nonColorless" + (damage ? "Source" : ""); } else if (protectType.equals("everything")) { - validSource = ""; + return ""; } else if (protectType.startsWith("opponent of ")) { final String playerName = protectType.substring("opponent of ".length()); - validSource += "ControlledBy Player.OpponentOf PlayerNamed_" + playerName; + validSource = "ControlledBy Player.OpponentOf PlayerNamed_" + playerName; } else { validSource = CardType.getSingularType(protectType); } } - return validSource; + if (validSource.isEmpty()) { + return validSource; + } + return "Card." + validSource + ",Emblem." + validSource; } public static ReplacementEffect makeEtbCounter(final String kw, final CardState card, final boolean intrinsic) { @@ -3398,8 +3401,6 @@ public class CardFactoryUtil { public static void addStaticAbility(final KeywordInterface inst, final CardState state, final boolean intrinsic) { String keyword = inst.getOriginal(); - String effect = null; - Map svars = Maps.newHashMap(); if (keyword.startsWith("Affinity")) { final String[] k = keyword.split(":"); @@ -3419,20 +3420,24 @@ public class CardFactoryUtil { sb.append("Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Amount$ AffinityX | EffectZone$ All"); sb.append("| Description$ Affinity for ").append(desc); sb.append(" (").append(inst.getReminderText()).append(")"); - effect = sb.toString(); + String effect = sb.toString(); - svars.put("AffinityX", "Count$Valid " + t + ".YouCtrl"); + StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic); + + st.setSVar("AffinityX", "Count$Valid " + t + ".YouCtrl"); + inst.addStaticAbility(st); } else if (keyword.equals("Changeling")) { - effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self" + + String effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self" + " | CharacteristicDefining$ True | AddAllCreatureTypes$ True | Secondary$ True" + " | Description$ Changeling (" + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Cipher")) { StringBuilder sb = new StringBuilder(); sb.append("Mode$ Continuous | EffectZone$ Exile | Affected$ Card.EncodedWithSource"); sb.append(" | AddTrigger$ CipherTrigger"); sb.append(" | Description$ Cipher (").append(inst.getReminderText()).append(")"); - effect = sb.toString(); + String effect = sb.toString(); sb = new StringBuilder(); @@ -3445,8 +3450,12 @@ public class CardFactoryUtil { String ab = "DB$ Play | Defined$ OriginalHost | WithoutManaCost$ True | CopyCard$ True"; - svars.put("CipherTrigger", trig); - svars.put("PlayEncoded", ab); + StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic); + + st.setSVar("CipherTrigger", trig); + st.setSVar("PlayEncoded", ab); + + inst.addStaticAbility(st); } else if (keyword.startsWith("Class")) { final String[] k = keyword.split(":"); final String level = k[1]; @@ -3475,24 +3484,32 @@ public class CardFactoryUtil { } } - effect = "Mode$ Continuous | Affected$ Card.Self | ClassLevel$ " + level + " | " + params; + String effect = "Mode$ Continuous | Affected$ Card.Self | ClassLevel$ " + level + " | " + params; if (descAdded) { effect += " | Description$ " + desc.toString(); } + + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.startsWith("Dash")) { - effect = "Mode$ Continuous | Affected$ Card.Self+dashed | AddKeyword$ Haste"; + String effect = "Mode$ Continuous | Affected$ Card.Self+dashed | AddKeyword$ Haste"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Daybound")) { - effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Daybound | Secondary$ True | Description$ This permanent can't be transformed except by its daybound ability."; + String effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Daybound | Secondary$ True | Description$ This permanent can't be transformed except by its daybound ability."; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Decayed")) { - effect = "Mode$ Continuous | Affected$ Card.Self | AddHiddenKeyword$ CARDNAME can't block. | " + + String effect = "Mode$ Continuous | Affected$ Card.Self | AddHiddenKeyword$ CARDNAME can't block. | " + "Secondary$ True"; - svars.put("SacrificeEndCombat", "True"); + StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic); + st.setSVar("SacrificeEndCombat", "True"); + inst.addStaticAbility(st); } else if (keyword.equals("Defender")) { - effect = "Mode$ CantAttack | ValidCard$ Card.Self | DefenderKeyword$ True | Secondary$ True"; + String effect = "Mode$ CantAttack | ValidCard$ Card.Self | DefenderKeyword$ True | Secondary$ True"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Devoid")) { - effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self" + + String effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self" + " | CharacteristicDefining$ True | SetColor$ Colorless | Secondary$ True" + " | Description$ Devoid (" + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.startsWith("Escalate")) { final String[] k = keyword.split(":"); final String manacost = k[1]; @@ -3506,15 +3523,18 @@ public class CardFactoryUtil { } sb.append(cost.toSimpleString()); - effect = "Mode$ RaiseCost | ValidCard$ Card.Self | Type$ Spell | Secondary$ True" + String effect = "Mode$ RaiseCost | ValidCard$ Card.Self | Type$ Spell | Secondary$ True" + " | Amount$ Escalate | Cost$ "+ manacost +" | EffectZone$ All" + " | Description$ " + sb.toString() + " (" + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Fear")) { - effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+nonBlack | Secondary$ True " + + String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+nonBlack | Secondary$ True " + " | Description$ Fear ( " + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Flying")) { - effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutFlying+withoutReach | Secondary$ True " + + String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutFlying+withoutReach | Secondary$ True " + " | Description$ Flying ( " + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.startsWith("Hexproof")) { final StringBuilder sbDesc = new StringBuilder("Hexproof"); final StringBuilder sbValid = new StringBuilder(); @@ -3526,55 +3546,84 @@ public class CardFactoryUtil { sbValid.append("| ValidSource$ ").append(k[1]); } - effect = "Mode$ CantTarget | Hexproof$ True | ValidCard$ Card.Self | Secondary$ True" + String effect = "Mode$ CantTarget | Hexproof$ True | ValidCard$ Card.Self | Secondary$ True" + sbValid.toString() + " | Activator$ Opponent | Description$ " + sbDesc.toString() + " (" + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Horsemanship")) { - effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutHorsemanship | Secondary$ True " + + String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutHorsemanship | Secondary$ True " + " | Description$ Horsemanship ( " + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Intimidate")) { - effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+notSharesColorWith | Secondary$ True " + + String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+notSharesColorWith | Secondary$ True " + " | Description$ Intimidate ( " + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Nightbound")) { - effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Nightbound | Secondary$ True | Description$ This permanent can't be transformed except by its nightbound ability."; + String effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Nightbound | Secondary$ True | Description$ This permanent can't be transformed except by its nightbound ability."; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.startsWith("Protection")) { String valid = getProtectionValid(keyword, false); - effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self "; + + // Block + String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Secondary$ True "; + String desc = "Protection ( " + inst.getReminderText() + ")"; if (!valid.isEmpty()) { effect += "| ValidBlocker$ " + valid; } - effect += " | Secondary$ True | Description$ Protection ( " + inst.getReminderText() + ")"; + effect += " | Description$ " + desc; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); + + // Target + effect = "Mode$ CantTarget | Protection$ True | ValidCard$ Card.Self | Secondary$ True "; + if (!valid.isEmpty()) { + effect += "| ValidSource$ " + valid; + } + effect += " | Description$ " + desc; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); + + // Attach + effect = "Mode$ CantAttach | Protection$ True | Target$ Card.Self | Secondary$ True "; + if (!valid.isEmpty()) { + effect += "| ValidCard$ " + valid; + } + // This effect doesn't remove something + if (keyword.startsWith("Protection:")) { + final String[] kws = keyword.split(":"); + if (kws.length > 3) { + effect += "| Exceptions$ " + kws[3]; + } + } + effect += " | Description$ " + desc; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Shroud")) { - effect = "Mode$ CantTarget | Shroud$ True | ValidCard$ Card.Self | Secondary$ True" + String effect = "Mode$ CantTarget | Shroud$ True | ValidCard$ Card.Self | Secondary$ True" + " | Description$ Shroud (" + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Skulk")) { - effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.powerGTX | Secondary$ True " + + String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.powerGTX | Secondary$ True " + " | Description$ Skulk ( " + inst.getReminderText() + ")"; - svars.put("X", "Count$CardPower"); + StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic); + st.setSVar("X", "Count$CardPower"); + inst.addStaticAbility(st); } else if (keyword.startsWith("Strive")) { final String[] k = keyword.split(":"); final String manacost = k[1]; - effect = "Mode$ RaiseCost | ValidCard$ Card.Self | Type$ Spell | Amount$ Strive | Cost$ "+ manacost +" | EffectZone$ All" + + String effect = "Mode$ RaiseCost | ValidCard$ Card.Self | Type$ Spell | Amount$ Strive | Cost$ "+ manacost +" | EffectZone$ All" + " | Description$ Strive - " + inst.getReminderText(); + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Unleash")) { - effect = "Mode$ Continuous | Affected$ Card.Self+counters_GE1_P1P1 | AddHiddenKeyword$ CARDNAME can't block."; + String effect = "Mode$ Continuous | Affected$ Card.Self+counters_GE1_P1P1 | AddHiddenKeyword$ CARDNAME can't block."; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Undaunted")) { - effect = "Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Secondary$ True" + String effect = "Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Secondary$ True" + "| Amount$ Undaunted | EffectZone$ All | Description$ Undaunted (" + inst.getReminderText() + ")"; + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("MayFlashSac")) { - effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self | Secondary$ True | MayPlay$ True" + String effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self | Secondary$ True | MayPlay$ True" + " | MayPlayNotSorcerySpeed$ True | MayPlayWithFlash$ True | MayPlayText$ Sacrifice at the next cleanup step" + " | AffectedZone$ Exile,Graveyard,Hand,Library,Stack | Description$ " + inst.getReminderText(); - } - - if (effect != null) { - StaticAbility st = new StaticAbility(effect, state.getCard(), state); - st.setIntrinsic(intrinsic); - for (Map.Entry e : svars.entrySet()) { - st.setSVar(e.getKey(), e.getValue()); - } - inst.addStaticAbility(st); + inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } } 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 4ca270b5072..201eb8b0a57 100644 --- a/forge-game/src/main/java/forge/game/card/CardPredicates.java +++ b/forge-game/src/main/java/forge/game/card/CardPredicates.java @@ -224,15 +224,6 @@ public final class CardPredicates { }; } - public static final Predicate isProtectedFrom(final Card source) { - return new Predicate() { - @Override - public boolean apply(final Card c) { - return c.hasProtectionFrom(source); - } - }; - } - public static final Predicate restriction(final String[] restrictions, final Player sourceController, final Card source, final CardTraitBase spellAbility) { return new Predicate() { @Override diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 26b41b445f9..45aebb90a6b 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -1097,70 +1097,7 @@ public class Player extends GameEntity implements Comparable { return false; } - return !hasProtectionFrom(sa.getHostCard()); - } - - public boolean hasProtectionFromDamage(final Card source) { - return hasProtectionFrom(source, false, true); - } - - @Override - public boolean hasProtectionFrom(final Card source, final boolean checkSBA) { - return hasProtectionFrom(source, checkSBA, false); - } - public boolean hasProtectionFrom(final Card source, final boolean checkSBA, final boolean damageSource) { - final boolean colorlessDamage = damageSource && source.hasKeyword("Colorless Damage Source"); - for (KeywordInterface ki : keywords) { - String kw = ki.getOriginal(); - if (kw.startsWith("Protection")) { - if (kw.startsWith("Protection:")) { // uses isValid - final String characteristic = kw.split(":")[1]; - if (characteristic.startsWith("Player")) { - // Protection:PlayerUID - if (source.getController().isValid(characteristic, this, null, null)) { - return true; - } - } else { - final String[] characteristics = characteristic.split(","); - if (source.isValid(characteristics, this, null, null)) { - return true; - } - } - } else if (kw.equals("Protection from white")) { - if (source.isWhite() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from blue")) { - if (source.isBlue() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from black")) { - if (source.isBlack() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from red")) { - if (source.isRed() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from green")) { - if (source.isGreen() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from all colors")) { - if (!source.isColorless() && !colorlessDamage) { - return true; - } - } else if (kw.equals("Protection from everything")) { - return true; - } else if (kw.startsWith("Protection from ")) { - final String protectType = CardType.getSingularType(kw.substring("Protection from ".length())); - if (source.getType().hasStringType(protectType)) { - return true; - } - } - } - } - return false; + return true; } public void surveil(int num, SpellAbility cause, CardZoneTable table) { diff --git a/forge-game/src/main/java/forge/game/player/PlayerFactoryUtil.java b/forge-game/src/main/java/forge/game/player/PlayerFactoryUtil.java index a77b38a8afa..ebfe3ae327b 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/player/PlayerFactoryUtil.java @@ -11,7 +11,7 @@ public class PlayerFactoryUtil { public static void addStaticAbility(final KeywordInterface inst, final Player player) { String keyword = inst.getOriginal(); - String effect = null; + if (keyword.startsWith("Hexproof")) { final StringBuilder sbDesc = new StringBuilder("Hexproof"); final StringBuilder sbValid = new StringBuilder(); @@ -23,18 +23,40 @@ public class PlayerFactoryUtil { sbValid.append("| ValidSource$ ").append(k[1]); } - effect = "Mode$ CantTarget | Hexproof$ True | ValidPlayer$ Player.You | Secondary$ True " + String effect = "Mode$ CantTarget | Hexproof$ True | ValidPlayer$ Player.You | Secondary$ True " + sbValid.toString() + " | Activator$ Opponent | EffectZone$ Command | Description$ " + sbDesc.toString() + " (" + inst.getReminderText() + ")"; - } else if (keyword.equals("Shroud")) { - effect = "Mode$ CantTarget | Shroud$ True | ValidPlayer$ Player.You | Secondary$ True " - + "| EffectZone$ Command | Description$ Shroud (" + inst.getReminderText() + ")"; - } - if (effect != null) { + final Card card = player.getKeywordCard(); - StaticAbility st = new StaticAbility(effect, card, card.getCurrentState()); - st.setIntrinsic(false); - inst.addStaticAbility(st); + inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false)); + } else if (keyword.equals("Shroud")) { + String effect = "Mode$ CantTarget | Shroud$ True | ValidPlayer$ Player.You | Secondary$ True " + + "| EffectZone$ Command | Description$ Shroud (" + inst.getReminderText() + ")"; + + final Card card = player.getKeywordCard(); + inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false)); + } else if (keyword.startsWith("Protection")) { + String valid = CardFactoryUtil.getProtectionValid(keyword, false); + String effect = "Mode$ CantTarget | Protection$ True | ValidCard$ Player.You | Secondary$ True "; + if (!valid.isEmpty()) { + effect += "| ValidSource$ " + valid; + } + final Card card = player.getKeywordCard(); + inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false)); + + // Attach + effect = "Mode$ CantAttach | Protection$ True | Target$ Player.You | Secondary$ True "; + if (!valid.isEmpty()) { + effect += "| ValidCard$ " + valid; + } + // This effect doesn't remove something + if (keyword.startsWith("Protection:")) { + final String[] kws = keyword.split(":"); + if (kws.length > 3) { + effect += "| Exceptions$ " + kws[3]; + } + } + inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false)); } } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java index 9e024edc5e3..b0f5e11eb7b 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -234,6 +234,12 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone this(parseParams(params, host), host, state); } + public static StaticAbility create(final String params, final Card host, CardState state, boolean intrinsic) { + StaticAbility st = new StaticAbility(params, state.getCard(), state); + st.setIntrinsic(intrinsic); + return st; + } + /** * Instantiates a new static ability. * @@ -325,8 +331,6 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone return StaticAbilityCantAttackBlock.applyCantAttackAbility(this, card, target); } else if (mode.equals("CantBlockBy") && target instanceof Card) { return StaticAbilityCantAttackBlock.applyCantBlockByAbility(this, card, (Card)target); - } else if (mode.equals("CantAttach")) { - return StaticAbilityCantAttach.applyCantAttachAbility(this, card, target); } else if (mode.equals("CanAttackIfHaste")) { return StaticAbilityCantAttackBlock.applyCanAttackHasteAbility(this, card, target); } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java index 7ad8ef7e0a6..c2593384496 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java @@ -2,29 +2,52 @@ package forge.game.staticability; import forge.game.GameEntity; import forge.game.card.Card; +import forge.game.zone.ZoneType; public class StaticAbilityCantAttach { - public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target) { - if (!stAb.matchesValidParam("ValidCard", card)) { - return false; - } + static String MODE = "CantAttach"; - if (!stAb.matchesValidParam("Target", target)) { - return false; - } + public static boolean cantAttach(final GameEntity target, final Card card, boolean checkSBA) { + // CantTarget static abilities + for (final Card ca : target.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.getParam("Mode").equals(MODE) || stAb.isSuppressed() || !stAb.checkConditions()) { + continue; + } - if (stAb.hasParam("ValidCardToTarget")) { - if (!(target instanceof Card)) { - return false; - } - Card tcard = (Card) target; + if (applyCantAttachAbility(stAb, card, target, checkSBA)) { + return true; + } + } + } + return false; + } - if (!stAb.matchesValid(card, stAb.getParam("ValidCardToTarget").split(","), tcard)) { - return false; - } - } + public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target, boolean checkSBA) { + if (!stAb.matchesValidParam("ValidCard", card)) { + return false; + } - return true; + if (!stAb.matchesValidParam("Target", target)) { + return false; + } + + if (stAb.hasParam("ValidCardToTarget")) { + if (!(target instanceof Card)) { + return false; + } + Card tcard = (Card) target; + + if (!stAb.matchesValid(card, stAb.getParam("ValidCardToTarget").split(","), tcard)) { + return false; + } + } + + if (checkSBA && stAb.matchesValidParam("Exceptions", card)) { + return false; + } + + return true; } } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java index f9512018a0f..6088154aeb7 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -875,9 +875,7 @@ public final class StaticAbilityContinuous { s = TextUtil.fastReplace(s, "ConvertedManaCost", costcmc); } - StaticAbility stat = new StaticAbility(s, affectedCard, stAb.getCardState()); - stat.setIntrinsic(false); - addedStaticAbility.add(stat); + addedStaticAbility.add(StaticAbility.create(s, affectedCard, stAb.getCardState(), false)); } } diff --git a/forge-game/src/main/java/forge/game/zone/ZoneType.java b/forge-game/src/main/java/forge/game/zone/ZoneType.java index 7a1ae6f227e..f8a434dd17c 100644 --- a/forge-game/src/main/java/forge/game/zone/ZoneType.java +++ b/forge-game/src/main/java/forge/game/zone/ZoneType.java @@ -28,7 +28,7 @@ public enum ZoneType { Subgame(true, "lblSubgameZone"), None(true, "lblNoneZone"); - public static final List STATIC_ABILITIES_SOURCE_ZONES = Arrays.asList(Battlefield, Graveyard, Exile, Command/*, Hand*/); + public static final List STATIC_ABILITIES_SOURCE_ZONES = Arrays.asList(Battlefield, Graveyard, Exile, Command, Stack/*, Hand*/); private final boolean holdsHiddenInfo; private final String zoneName; diff --git a/forge-gui/res/cardsfolder/d/dream_leash.txt b/forge-gui/res/cardsfolder/d/dream_leash.txt index bd368ace861..0dbcbe58831 100644 --- a/forge-gui/res/cardsfolder/d/dream_leash.txt +++ b/forge-gui/res/cardsfolder/d/dream_leash.txt @@ -1,9 +1,8 @@ Name:Dream Leash ManaCost:3 U U Types:Enchantment Aura -Text:You can't choose an untapped creature as this spell's target as you cast it. K:Enchant permanent -K:SpellCantTarget:Permanent.untapped +S:Mode$ CantTarget | EffectZone$ Stack | ValidSource$ Spell.Self | ValidCard$ Permanent.untapped | Description$ You can't choose an untapped permanent as this spell's target as you cast it. A:SP$ Attach | Cost$ 3 U U | ValidTgts$ Permanent | AILogic$ GainControl S:Mode$ Continuous | Affected$ Card.EnchantedBy | GainControl$ You | Description$ You control enchanted permanent. Oracle:Enchant permanent\nYou can't choose an untapped permanent as this spell's target as you cast it.\nYou control enchanted permanent. diff --git a/forge-gui/res/cardsfolder/e/enthralling_hold.txt b/forge-gui/res/cardsfolder/e/enthralling_hold.txt index 568a333a7ff..666dc8c2dfc 100755 --- a/forge-gui/res/cardsfolder/e/enthralling_hold.txt +++ b/forge-gui/res/cardsfolder/e/enthralling_hold.txt @@ -1,9 +1,8 @@ Name:Enthralling Hold ManaCost:3 U U Types:Enchantment Aura -Text:You can't choose an untapped creature as this spell's target as you cast it. K:Enchant creature -K:SpellCantTarget:Creature.untapped +S:Mode$ CantTarget | EffectZone$ Stack | ValidSource$ Spell.Self | ValidCard$ Creature.untapped | Description$ You can't choose an untapped creature as this spell's target as you cast it. A:SP$ Attach | Cost$ 3 U U | ValidTgts$ Creature | AILogic$ GainControl S:Mode$ Continuous | Affected$ Card.EnchantedBy | GainControl$ You | Description$ You control enchanted creature. Oracle:Enchant creature\nYou can't choose an untapped creature as this spell's target as you cast it.\nYou control enchanted creature. From 535ea437c1f623a6e8d9157b8cce49c658d492cb Mon Sep 17 00:00:00 2001 From: Bug Hunter Date: Mon, 3 Jan 2022 10:03:33 +0000 Subject: [PATCH 3/7] Card fixes --- .../java/forge/game/ability/SpellAbilityEffect.java | 2 +- .../game/ability/effects/ReplaceCounterEffect.java | 3 --- .../main/java/forge/game/card/CardFactoryUtil.java | 2 +- .../src/main/java/forge/game/player/Player.java | 13 +++++++------ .../res/cardsfolder/g/gisa_glorious_resurrector.txt | 2 +- forge-gui/res/cardsfolder/h/hardened_scales.txt | 2 +- forge-gui/res/cardsfolder/r/rite_of_flame.txt | 2 +- forge-gui/res/cardsfolder/s/submerged_boneyard.txt | 2 +- ...in_dean_of_the_vein_lisette_dean_of_the_root.txt | 2 +- forge-gui/res/cardsfolder/v/void_maw.txt | 2 +- .../res/cardsfolder/z/zabaz_the_glimmerwasp.txt | 2 +- 11 files changed, 16 insertions(+), 18 deletions(-) diff --git a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java index c8232876235..e7e0d2c2537 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -519,7 +519,7 @@ public abstract class SpellAbilityEffect { String valid = sa.getParamOrDefault("ReplaceDyingValid", "Card.IsRemembered"); - String repeffstr = "Event$ Moved | ValidCard$ " + valid + + String repeffstr = "Event$ Moved | ValidLKI$ " + valid + "| Origin$ Battlefield | Destination$ Graveyard " + "| Description$ If that permanent would die this turn, exile it instead."; String effect = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Battlefield | Destination$ " + zone; diff --git a/forge-game/src/main/java/forge/game/ability/effects/ReplaceCounterEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ReplaceCounterEffect.java index 0d7812f2635..af1684b8d9c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ReplaceCounterEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ReplaceCounterEffect.java @@ -24,7 +24,6 @@ public class ReplaceCounterEffect extends SpellAbilityEffect { @Override public void resolve(SpellAbility sa) { - final Card card = sa.getHostCard(); // outside of Replacement Effect, unwanted result @@ -64,8 +63,6 @@ public class ReplaceCounterEffect extends SpellAbilityEffect { counterTable.get(p).put(e.getKey(), value); } } - - } else { for (Map.Entry, Map> e : counterTable.entrySet()) { if (!sa.matchesValidParam("ValidSource", e.getKey().orNull())) { diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 56fd67c5de7..9bee4a88830 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -2968,7 +2968,7 @@ public class CardFactoryUtil { final String manacost = k[1]; StringBuilder sb = new StringBuilder(); - sb.append("AB$ PutCounter| Cost$ ").append(manacost); + sb.append("AB$ PutCounter | Cost$ ").append(manacost); sb.append(" | PrecostDesc$ Level Up | CostDesc$ ").append(ManaCostParser.parse(manacost)); sb.append(" | SorcerySpeed$ True | LevelUp$ True | CounterNum$ 1 | CounterType$ LEVEL"); if (card.hasSVar("maxLevel")) { diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 49cc9115457..c3c502f756c 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -1935,12 +1935,7 @@ public class Player extends GameEntity implements Comparable { return getOutcome().lossState != null; } - // Rule 704.5a - If a player has 0 or less life, he or she loses the game. - final boolean hasNoLife = getLife() <= 0; - if (hasNoLife && !cantLoseForZeroOrLessLife()) { - return loseConditionMet(GameLossReason.LifeReachedZero, null); - } - + // check this first because of Lich's Mirror (704.7) // Rule 704.5b - If a player attempted to draw a card from a library with no cards in it // since the last time state-based actions were checked, he or she loses the game. if (triedToDrawFromEmptyLibrary) { @@ -1951,6 +1946,12 @@ public class Player extends GameEntity implements Comparable { } } + // Rule 704.5a - If a player has 0 or less life, he or she loses the game. + final boolean hasNoLife = getLife() <= 0; + if (hasNoLife && !cantLoseForZeroOrLessLife()) { + return loseConditionMet(GameLossReason.LifeReachedZero, null); + } + // Rule 704.5c - If a player has ten or more poison counters, he or she loses the game. if (getCounters(CounterEnumType.POISON) >= 10) { return loseConditionMet(GameLossReason.Poisoned, null); diff --git a/forge-gui/res/cardsfolder/g/gisa_glorious_resurrector.txt b/forge-gui/res/cardsfolder/g/gisa_glorious_resurrector.txt index 7cbfd04b480..a87488d62fa 100644 --- a/forge-gui/res/cardsfolder/g/gisa_glorious_resurrector.txt +++ b/forge-gui/res/cardsfolder/g/gisa_glorious_resurrector.txt @@ -2,7 +2,7 @@ Name:Gisa, Glorious Resurrector ManaCost:2 B B Types:Legendary Creature Human Wizard PT:4/4 -R:Event$ Moved | ActiveZones$ Battlefield | CheckSelfLKIZone$ True | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.OppCtrl | ReplaceWith$ Exile | Description$ If a creature an opponent controls would die, exile it instead. +R:Event$ Moved | ActiveZones$ Battlefield | CheckSelfLKIZone$ True | Origin$ Battlefield | Destination$ Graveyard | ValidLKI$ Creature.OppCtrl | ReplaceWith$ Exile | Description$ If a creature an opponent controls would die, exile it instead. SVar:Exile:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Exile | Defined$ ReplacedCard T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | IsPresent$ Card.ExiledWithSource | PresentZone$ Exile | Execute$ TrigChange | TriggerDescription$ At the beginning of your upkeep, put all creature cards exiled with CARDNAME onto the battlefield under your control. They gain decayed. (A creature with decayed can't block. When it attacks, sacrifice it at end of combat.) SVar:TrigChange:DB$ ChangeZoneAll | ChangeType$ Card.ExiledWithSource | Origin$ Exile | Destination$ Battlefield | GainControl$ True | RememberChanged$ True | SubAbility$ DBPump diff --git a/forge-gui/res/cardsfolder/h/hardened_scales.txt b/forge-gui/res/cardsfolder/h/hardened_scales.txt index 3ea28653134..3d307b4429b 100644 --- a/forge-gui/res/cardsfolder/h/hardened_scales.txt +++ b/forge-gui/res/cardsfolder/h/hardened_scales.txt @@ -1,7 +1,7 @@ Name:Hardened Scales ManaCost:G Types:Enchantment -R:Event$ AddCounter | ActiveZones$ Battlefield | ValidCard$ Creature.YouCtrl+InZoneBattlefield | ValidCounterType$ P1P1 | ReplaceWith$ AddOneMoreCounters | Description$ If one or more +1/+1 counters would be put on a creature you control, that many plus one +1/+1 counters are put on it instead. +R:Event$ AddCounter | ActiveZones$ Battlefield | ValidCard$ Creature.YouCtrl+inZoneBattlefield | ValidCounterType$ P1P1 | ReplaceWith$ AddOneMoreCounters | Description$ If one or more +1/+1 counters would be put on a creature you control, that many plus one +1/+1 counters are put on it instead. SVar:AddOneMoreCounters:DB$ ReplaceCounter | ValidCounterType$ P1P1 | ChooseCounter$ True | Amount$ X SVar:X:ReplaceCount$CounterNum/Plus.1 AI:RemoveDeck:Random diff --git a/forge-gui/res/cardsfolder/r/rite_of_flame.txt b/forge-gui/res/cardsfolder/r/rite_of_flame.txt index 5a3f50ab7bf..45429884657 100644 --- a/forge-gui/res/cardsfolder/r/rite_of_flame.txt +++ b/forge-gui/res/cardsfolder/r/rite_of_flame.txt @@ -1,7 +1,7 @@ Name:Rite of Flame ManaCost:R Types:Sorcery -A:SP$ Mana | Produced$ R | Amount$ 2 | AILogic$ ManaRitual | AINoRecursiveCheck$ True | SpellDescription$ Add {R}{R}, then add {R} for each card named CARDNAME in each graveyard. +A:SP$ Mana | Produced$ R | Amount$ 2 | AILogic$ ManaRitual | AINoRecursiveCheck$ True | SubAbility$ SubMana | SpellDescription$ Add {R}{R}, then add {R} for each card named CARDNAME in each graveyard. SVar:SubMana:DB$ Mana | Produced$ R | Amount$ X SVar:X:Count$NamedInAllYards.Rite of Flame DeckHints:Name$Rite of Flame diff --git a/forge-gui/res/cardsfolder/s/submerged_boneyard.txt b/forge-gui/res/cardsfolder/s/submerged_boneyard.txt index 4ceb6d17f53..81d289643eb 100644 --- a/forge-gui/res/cardsfolder/s/submerged_boneyard.txt +++ b/forge-gui/res/cardsfolder/s/submerged_boneyard.txt @@ -2,5 +2,5 @@ Name:Submerged Boneyard ManaCost:no cost Types:Land K:CARDNAME enters the battlefield tapped. -A:AB$ Mana | Cost$ T | Produced$ Combo U | SpellDescription$ Add {U} or {B}. +A:AB$ Mana | Cost$ T | Produced$ Combo U B | SpellDescription$ Add {U} or {B}. Oracle:Submerged Boneyard enters the battlefield tapped.\n{T}: Add {U} or {B}. diff --git a/forge-gui/res/cardsfolder/v/valentin_dean_of_the_vein_lisette_dean_of_the_root.txt b/forge-gui/res/cardsfolder/v/valentin_dean_of_the_vein_lisette_dean_of_the_root.txt index a27337e8c2e..b1fb3f614c7 100644 --- a/forge-gui/res/cardsfolder/v/valentin_dean_of_the_vein_lisette_dean_of_the_root.txt +++ b/forge-gui/res/cardsfolder/v/valentin_dean_of_the_vein_lisette_dean_of_the_root.txt @@ -4,7 +4,7 @@ Types:Legendary Creature Vampire Warlock PT:1/1 K:Menace K:Lifelink -R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Creature+nonToken+OppOwn | ReplaceWith$ Exile | Description$ If a nontoken creature an opponent controls would die, exile it instead. When you do, you may pay {2}. If you do, create a 1/1 black and green Pest creature token with "When this creature dies, you gain 1 life." +R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | Destination$ Graveyard | ValidLKI$ Card.Creature+nonToken+OppOwn | ReplaceWith$ Exile | Description$ If a nontoken creature an opponent controls would die, exile it instead. When you do, you may pay {2}. If you do, create a 1/1 black and green Pest creature token with "When this creature dies, you gain 1 life." SVar:Exile:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Exile | Defined$ ReplacedCard | SubAbility$ DBImmediateTrigger SVar:DBImmediateTrigger:DB$ ImmediateTrigger | Execute$ TrigToken | TriggerDescription$ If a nontoken creature an opponent controls would die, exile it instead. When you do, you may pay {2}. If you do, create a 1/1 black and green Pest creature token with "When this creature dies, you gain 1 life." SVar:TrigToken:AB$ Token | Cost$ 2 | TokenAmount$ 1 | TokenScript$ bg_1_1_pest_lifegain | TokenOwner$ You diff --git a/forge-gui/res/cardsfolder/v/void_maw.txt b/forge-gui/res/cardsfolder/v/void_maw.txt index a03a01e23eb..aa24541cdc2 100644 --- a/forge-gui/res/cardsfolder/v/void_maw.txt +++ b/forge-gui/res/cardsfolder/v/void_maw.txt @@ -3,7 +3,7 @@ ManaCost:4 B B Types:Creature Horror PT:4/5 K:Trample -R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.Other | ReplaceWith$ Exile | Description$ If another creature would die, exile it instead. +R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | Destination$ Graveyard | ValidLKI$ Creature.Other | ReplaceWith$ Exile | Description$ If another creature would die, exile it instead. SVar:Exile:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Exile | Defined$ ReplacedCard | SubAbility$ DBRemember | RememberChanged$ True SVar:DBRemember:DB$ Pump | ConditionDefined$ Remembered | ConditionPresent$ Card.inZoneExile | ConditionCompare$ GE1 T:Mode$ ChangesZone | Origin$ Exile | Destination$ Any | Static$ True | ValidCard$ Card.IsRemembered+ExiledWithSource | Execute$ DBForget diff --git a/forge-gui/res/cardsfolder/z/zabaz_the_glimmerwasp.txt b/forge-gui/res/cardsfolder/z/zabaz_the_glimmerwasp.txt index d6a311ca3f5..39dceaef8ad 100644 --- a/forge-gui/res/cardsfolder/z/zabaz_the_glimmerwasp.txt +++ b/forge-gui/res/cardsfolder/z/zabaz_the_glimmerwasp.txt @@ -3,7 +3,7 @@ ManaCost:1 Types:Legendary Artifact Creature Insect PT:0/0 K:Modular:1 -R:Event$ AddCounter | ActiveZones$ Battlefield | ValidCause$ Triggered.Modular | ValidCard$ Creature.YouCtrl+InZoneBattlefield | ValidCounterType$ P1P1 | ReplaceWith$ AddOneMoreCounter | Description$ If a modular triggered ability would put one or more +1/+1 counters on a creature you control, that many plus one +1/+1 counters are put on it instead. +R:Event$ AddCounter | ActiveZones$ Battlefield | ValidCause$ Triggered.Modular | ValidCard$ Creature.YouCtrl+inZoneBattlefield | ValidCounterType$ P1P1 | ReplaceWith$ AddOneMoreCounter | Description$ If a modular triggered ability would put one or more +1/+1 counters on a creature you control, that many plus one +1/+1 counters are put on it instead. SVar:AddOneMoreCounter:DB$ ReplaceCounter | ValidCounterType$ P1P1 | ChooseCounter$ True | Amount$ X SVar:X:ReplaceCount$CounterNum/Plus.1 A:AB$ Destroy | Cost$ R | ValidTgts$ Artifact.YouCtrl | TgtPrompt$ Choose target artifact you control | SpellDescription$ Destroy target artifact you control. From aa66eee6de69d942b47e8a53123c048f8d0581cc Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Mon, 3 Jan 2022 15:02:11 +0100 Subject: [PATCH 4/7] Haunt fixes --- .../forge/ai/ability/CopyPermanentAi.java | 2 +- .../game/ability/effects/CounterEffect.java | 26 +++++++++---------- .../game/ability/effects/HauntEffect.java | 10 +++---- .../java/forge/game/card/CardFactoryUtil.java | 7 +++-- .../game/trigger/TriggerChangesZone.java | 6 +++++ .../main/java/forge/game/zone/MagicStack.java | 6 ++--- 6 files changed, 31 insertions(+), 26 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java index c18d777bc9a..f74fa8115ed 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java @@ -187,7 +187,7 @@ public class CopyPermanentAi extends SpellAbilityAi { } if (choice == null) { // can't find anything left - if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) { + if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { sa.resetTargets(); return false; } else { diff --git a/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java index cd7993acd90..4efbf6744cd 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java @@ -298,7 +298,8 @@ public class CounterEffect extends SpellAbilityEffect { private static void removeFromStack(final SpellAbility tgtSA, final SpellAbility srcSA, final SpellAbilityStackInstance si, CardZoneTable triggerList) { final Game game = tgtSA.getActivatingPlayer().getGame(); Card movedCard = null; - final Zone originZone = tgtSA.getHostCard().getZone(); + final Card c = tgtSA.getHostCard(); + final Zone originZone = c.getZone(); // Run any applicable replacement effects. final Map repParams = AbilityKey.mapFromAffected(tgtSA.getHostCard()); @@ -310,8 +311,8 @@ public class CounterEffect extends SpellAbilityEffect { game.getStack().remove(si); // if the target card on stack was a spell with Bestow, then unbestow it - if (tgtSA.getHostCard() != null && tgtSA.getHostCard().isBestowed()) { - tgtSA.getHostCard().unanimateBestow(true); + if (c.isBestowed()) { + c.unanimateBestow(true); } Map params = AbilityKey.newMap(); @@ -327,33 +328,32 @@ public class CounterEffect extends SpellAbilityEffect { // For Ability-targeted counterspells - do not move it anywhere, // even if Destination$ is specified. } else if (destination.equals("Graveyard")) { - movedCard = game.getAction().moveToGraveyard(tgtSA.getHostCard(), srcSA, params); + movedCard = game.getAction().moveToGraveyard(c, srcSA, params); } else if (destination.equals("Exile")) { - movedCard = game.getAction().exile(tgtSA.getHostCard(), srcSA, params); + movedCard = game.getAction().exile(c, srcSA, params); } else if (destination.equals("TopOfLibrary")) { - movedCard = game.getAction().moveToLibrary(tgtSA.getHostCard(), srcSA, params); + movedCard = game.getAction().moveToLibrary(c, srcSA, params); } else if (destination.equals("Hand")) { - movedCard = game.getAction().moveToHand(tgtSA.getHostCard(), srcSA, params); + movedCard = game.getAction().moveToHand(c, srcSA, params); } else if (destination.equals("Battlefield")) { if (tgtSA instanceof SpellPermanent) { - Card c = tgtSA.getHostCard(); c.setController(srcSA.getActivatingPlayer(), 0); movedCard = game.getAction().moveToPlay(c, srcSA.getActivatingPlayer(), srcSA, params); } else { - movedCard = game.getAction().moveToPlay(tgtSA.getHostCard(), srcSA.getActivatingPlayer(), srcSA, params); + movedCard = game.getAction().moveToPlay(c, srcSA.getActivatingPlayer(), srcSA, params); movedCard.setController(srcSA.getActivatingPlayer(), 0); } } else if (destination.equals("BottomOfLibrary")) { - movedCard = game.getAction().moveToBottomOfLibrary(tgtSA.getHostCard(), srcSA, params); + movedCard = game.getAction().moveToBottomOfLibrary(c, srcSA, params); } else if (destination.equals("ShuffleIntoLibrary")) { - movedCard = game.getAction().moveToBottomOfLibrary(tgtSA.getHostCard(), srcSA, params); - tgtSA.getHostCard().getController().shuffle(srcSA); + movedCard = game.getAction().moveToBottomOfLibrary(c, srcSA, params); + c.getController().shuffle(srcSA); } else { throw new IllegalArgumentException("AbilityFactory_CounterMagic: Invalid Destination argument for card " + srcSA.getHostCard().getName()); } // Run triggers - final Map runParams = AbilityKey.mapFromCard(tgtSA.getHostCard()); + final Map runParams = AbilityKey.mapFromCard(c); runParams.put(AbilityKey.Player, tgtSA.getActivatingPlayer()); runParams.put(AbilityKey.Cause, srcSA.getHostCard()); runParams.put(AbilityKey.CounteredSA, tgtSA); diff --git a/forge-game/src/main/java/forge/game/ability/effects/HauntEffect.java b/forge-game/src/main/java/forge/game/ability/effects/HauntEffect.java index 230d1d12f48..c912877686c 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/HauntEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/HauntEffect.java @@ -9,13 +9,13 @@ public class HauntEffect extends SpellAbilityEffect { @Override public void resolve(SpellAbility sa) { - Card card = sa.getHostCard(); - final Game game = card.getGame(); - card = game.getCardState(card, null); + Card host = sa.getHostCard(); + final Game game = host.getGame(); + Card card = game.getCardState(host, null); if (card == null) { return; - } else if (sa.usesTargeting() && !card.isToken()) { - // haunt target but only if card is no token + } else if (sa.usesTargeting() && !card.isToken() && host.equalsWithTimestamp(card)) { + // haunt target but only if card is no token and still in grave final Card copy = game.getAction().exile(card, sa); sa.getTargets().getFirstTargetedCard().addHauntedBy(copy); } else if (!sa.usesTargeting() && card.getHaunting() != null) { diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 9bee4a88830..dc8a17e7239 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -1293,13 +1293,12 @@ public class CardFactoryUtil { triggers.add(haunterETB); } - // First, create trigger that runs when the haunter goes to the - // graveyard + // First, create trigger that runs when the haunter goes to the graveyard final StringBuilder sbHaunter = new StringBuilder(); sbHaunter.append("Mode$ ChangesZone | Origin$ "); - sbHaunter.append(card.isCreature() ? "Battlefield" : "Stack"); + sbHaunter.append(card.isCreature() ? "Battlefield" : "Stack | ResolvedCard$ True"); sbHaunter.append(" | Destination$ Graveyard | ValidCard$ Card.Self"); - sbHaunter.append(" | Static$ True | Secondary$ True | TriggerDescription$ Blank"); + sbHaunter.append(" | Secondary$ True | TriggerDescription$ " + inst.getReminderText()); final Trigger haunterDies = TriggerHandler.parseTrigger(sbHaunter.toString(), card, intrinsic); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java b/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java index c599bf2d919..d830ec9033c 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java @@ -130,6 +130,12 @@ public class TriggerChangesZone extends Trigger { } } + if (hasParam("ResolvedCard")) { + if (!runParams.containsKey(AbilityKey.Fizzle)) { + return false; + } + } + // Check number of lands ETB this turn on triggered card's controller if (hasParam("CheckOnTriggeredCard")) { final String[] condition = getParam("CheckOnTriggeredCard").split(" ", 2); diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index 50b148ccf1c..f80d241cec7 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -253,7 +253,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable Date: Mon, 3 Jan 2022 16:26:36 +0100 Subject: [PATCH 5/7] GoadAi: improve targeting for triggers --- .../main/java/forge/ai/AiBlockController.java | 6 ++- .../main/java/forge/ai/ability/GoadAi.java | 37 ++++++++++++++++++- .../main/java/forge/game/card/CardView.java | 2 +- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index 63f7b7cbac2..4716723a622 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -1318,7 +1318,6 @@ public class AiBlockController { } int evalAtk = ComputerUtilCard.evaluateCreature(attacker, true, false); - int evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false); boolean atkEmbalm = (attacker.hasStartOfKeyword("Embalm") || attacker.hasStartOfKeyword("Eternalize")) && !attacker.isToken(); boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken(); @@ -1327,10 +1326,13 @@ public class AiBlockController { chance = Math.max(0, chance - chanceModForEmbalm); } - if (blocker.isFaceDown() && !checkingOther && blocker.getState(CardStateName.Original).getType().isCreature()) { + int evalBlk; + if (blocker.isFaceDown() && blocker.getView().canFaceDownBeShownTo(ai.getView(), false) && blocker.getState(CardStateName.Original).getType().isCreature()) { // if the blocker is a face-down creature (e.g. cast via Morph, Manifest), evaluate it // in relation to the original state, not to the Morph state evalBlk = ComputerUtilCard.evaluateCreature(Card.fromPaperCard(blocker.getPaperCard(), ai), false, true); + } else { + evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false); } int chanceToSavePW = chanceToTradeDownToSaveWalker > 0 && evalAtk + 1 < evalBlk ? chanceToTradeDownToSaveWalker : chanceToTradeToSaveWalker; boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower(); diff --git a/forge-ai/src/main/java/forge/ai/ability/GoadAi.java b/forge-ai/src/main/java/forge/ai/ability/GoadAi.java index cf8c8de79fe..f4c63e06b8f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/GoadAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/GoadAi.java @@ -30,7 +30,7 @@ public class GoadAi extends SpellAbilityAi { if (list.isEmpty()) return false; - if (game.getPlayers().size() >= 2) { + if (game.getPlayers().size() > 2) { // use this part only in multiplayer CardCollection betterList = CardLists.filter(list, new Predicate() { @Override @@ -79,10 +79,43 @@ public class GoadAi extends SpellAbilityAi { // AI does not find a good creature to goad. // because if it would goad a creature it would attack AI. - // AI might not have enough infomation to block it + // AI might not have enough information to block it return false; } return true; } + @Override + protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + if (checkApiLogic(ai, sa)) { + return true; + } + if (!mandatory) { + return false; + } + if (sa.usesTargeting()) { + if (sa.getTargetRestrictions().canTgtPlayer()) { + for (Player opp : ai.getOpponents()) { + if (sa.canTarget(opp)) { + sa.getTargets().add(opp); + return true; + } + } + if (sa.canTarget(ai)) { + sa.getTargets().add(ai); + return true; + } + } else { + List list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); + + if (list.isEmpty()) + return false; + + sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(list)); + return true; + } + } + return false; + } + } diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index c175fb7f829..42258ba0b12 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -562,7 +562,7 @@ public class CardView extends GameEntityView { }); } - private boolean canFaceDownBeShownTo(final PlayerView viewer, boolean skip) { + public boolean canFaceDownBeShownTo(final PlayerView viewer, boolean skip) { if (!isFaceDown()) { return true; } From a111ea1617882fef833488fbf76119c0354c4090 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Mon, 3 Jan 2022 16:59:18 +0100 Subject: [PATCH 6/7] Fix missing trigger --- .../src/main/java/forge/ai/ability/ReplaceDamageAi.java | 2 +- forge-ai/src/main/java/forge/ai/ability/RevealAi.java | 4 ++-- forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java | 6 +++--- forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java | 5 ++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ability/ReplaceDamageAi.java b/forge-ai/src/main/java/forge/ai/ability/ReplaceDamageAi.java index 10319c4bd51..daa3a870c08 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ReplaceDamageAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ReplaceDamageAi.java @@ -18,7 +18,7 @@ public class ReplaceDamageAi extends SpellAbilityAi { if (c.hasSVar("MustBeBlocked")) { return c; } - // TODO check if target can receive counters + // TODO check if target can receive counters + sort these to the front if that can prevent loss if (c.hasKeyword(Keyword.INFECT)) { return c; } diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java index 158c8e85d90..1d56d76361b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java @@ -18,7 +18,7 @@ public class RevealAi extends RevealAiBase { @Override protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { // we can reuse this function here... - final boolean bFlag = revealHandTargetAI(ai, sa/* , true, false */); + final boolean bFlag = revealHandTargetAI(ai, sa, false); if (!bFlag) { return false; @@ -80,7 +80,7 @@ public class RevealAi extends RevealAiBase { } - if (!revealHandTargetAI(ai, sa/*, false, mandatory*/)) { + if (!revealHandTargetAI(ai, sa, mandatory)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java b/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java index 44fb8acfeed..13da2ac6a57 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java @@ -15,7 +15,7 @@ import forge.game.zone.ZoneType; public abstract class RevealAiBase extends SpellAbilityAi { - protected boolean revealHandTargetAI(final Player ai, final SpellAbility sa) { + protected boolean revealHandTargetAI(final Player ai, final SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { // ability is targeted sa.resetTargets(); @@ -29,7 +29,7 @@ public abstract class RevealAiBase extends SpellAbilityAi { Player p = Collections.max(opps, PlayerPredicates.compareByZoneSize(ZoneType.Hand)); - if (p.getCardsIn(ZoneType.Hand).isEmpty()) { + if (!mandatory && p.getCardsIn(ZoneType.Hand).isEmpty()) { return false; } sa.getTargets().add(p); @@ -45,7 +45,7 @@ public abstract class RevealAiBase extends SpellAbilityAi { */ @Override public boolean chkAIDrawback(SpellAbility sa, Player ai) { - revealHandTargetAI(ai, sa); + revealHandTargetAI(ai, sa, false); return true; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java b/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java index b2484c99f30..077589ce3cf 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java @@ -12,7 +12,7 @@ public class RevealHandAi extends RevealAiBase { */ @Override protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { - final boolean bFlag = revealHandTargetAI(ai, sa/*, true, false*/); + final boolean bFlag = revealHandTargetAI(ai, sa, false); if (!bFlag) { return false; @@ -29,8 +29,7 @@ public class RevealHandAi extends RevealAiBase { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - - return revealHandTargetAI(ai, sa/*, false, mandatory*/); + return revealHandTargetAI(ai, sa, mandatory); } } From e996849fa7af62135b7e0dd8870fd6bdd0bbc8ff Mon Sep 17 00:00:00 2001 From: Bug Hunter Date: Tue, 4 Jan 2022 08:16:33 +0000 Subject: [PATCH 7/7] Update forge-gui/res/cardsfolder/e/eternal_dominion.txt --- forge-gui/res/cardsfolder/e/eternal_dominion.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui/res/cardsfolder/e/eternal_dominion.txt b/forge-gui/res/cardsfolder/e/eternal_dominion.txt index 2ea50d105b0..ce46add8efe 100644 --- a/forge-gui/res/cardsfolder/e/eternal_dominion.txt +++ b/forge-gui/res/cardsfolder/e/eternal_dominion.txt @@ -2,7 +2,7 @@ Name:Eternal Dominion ManaCost:7 U U U Types:Sorcery K:Epic -A:SP$ ChangeZone | Cost$ 7 U U U | Origin$ Library | Destination$ Battlefield | ValidTgts$ Opponent | ChangeType$ Artifact,Creature,Enchantment,Land | ChangeNum$ 1 | GainControl$ True | IsCurse$ True | StackDescription$ SpellDescription | SpellDescription$ Search target opponent's library for a creature card and put that card onto the battlefield under your control. Then that player shuffles their library. +A:SP$ ChangeZone | Cost$ 7 U U U | Origin$ Library | Destination$ Battlefield | ValidTgts$ Opponent | ChangeType$ Artifact,Creature,Enchantment,Land | ChangeNum$ 1 | GainControl$ True | IsCurse$ True | StackDescription$ SpellDescription | SpellDescription$ Search target opponent's library for an artifact, creature, enchantment, or land card. Put that card onto the battlefield under your control. Then that player shuffles. #TODO: Tutoring in general can be improved to make this card work better for the AI. Currently the AI will grab the most expensive targets in the opponent's library (which is not necessarily a bad thing in itself, but not always optimal). AI:RemoveDeck:Random SVar:Picture:http://www.wizards.com/global/images/magic/general/eternal_dominion.jpg