Merge branch 'master' into 'typo'

# Conflicts:
#   forge-gui/res/cardsfolder/e/eternal_dominion.txt
This commit is contained in:
Tim Mocny
2022-01-05 00:16:56 +00:00
40 changed files with 335 additions and 428 deletions

View File

@@ -1318,7 +1318,6 @@ public class AiBlockController {
} }
int evalAtk = ComputerUtilCard.evaluateCreature(attacker, true, false); 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 atkEmbalm = (attacker.hasStartOfKeyword("Embalm") || attacker.hasStartOfKeyword("Eternalize")) && !attacker.isToken();
boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken(); boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken();
@@ -1327,10 +1326,13 @@ public class AiBlockController {
chance = Math.max(0, chance - chanceModForEmbalm); 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 // 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 // in relation to the original state, not to the Morph state
evalBlk = ComputerUtilCard.evaluateCreature(Card.fromPaperCard(blocker.getPaperCard(), ai), false, true); 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; int chanceToSavePW = chanceToTradeDownToSaveWalker > 0 && evalAtk + 1 < evalBlk ? chanceToTradeDownToSaveWalker : chanceToTradeToSaveWalker;
boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower(); boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower();

View File

@@ -125,15 +125,8 @@ public class AttachAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) {
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source); 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 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(CardUtil.getValidCardsToTarget(exile_tgt, effectExile), CardPredicates.canBeAttached(source));
final CardCollection targets = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF));
}
});
return !targets.isEmpty(); return !targets.isEmpty();
} }
@@ -1325,8 +1318,7 @@ public class AttachAi extends SpellAbilityAi {
// Is a SA that moves target attachment // Is a SA that moves target attachment
if ("MoveTgtAura".equals(sa.getParam("AILogic"))) { if ("MoveTgtAura".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));
list = CardLists.filter(list, Predicates.not(CardPredicates.isProtectedFrom(attachSource)));
list = CardLists.filter(list, Predicates.or(CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()), new Predicate<Card>() { list = CardLists.filter(list, Predicates.or(CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()), new Predicate<Card>() {
@Override @Override
public boolean apply(final Card card) { public boolean apply(final Card card) {
@@ -1336,7 +1328,7 @@ public class AttachAi extends SpellAbilityAi {
return !list.isEmpty() ? ComputerUtilCard.getBestAI(list) : null; return !list.isEmpty() ? ComputerUtilCard.getBestAI(list) : null;
} else if ("Unenchanted".equals(sa.getParam("AILogic"))) { } 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<Card>() { CardCollection preferred = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card card) { public boolean apply(final Card card) {
@@ -1361,18 +1353,7 @@ public class AttachAi extends SpellAbilityAi {
if (tgt == null) { if (tgt == null) {
list = AbilityUtils.getDefinedCards(attachSource, sa.getParam("Defined"), sa); list = AbilityUtils.getDefinedCards(attachSource, sa.getParam("Defined"), sa);
} else { } else {
list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa); list = CardLists.filter(CardUtil.getValidCardsToTarget(tgt, sa), CardPredicates.canBeAttached(attachSource));
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)));
}
} }
if (list.isEmpty()) { if (list.isEmpty()) {

View File

@@ -187,7 +187,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
} }
if (choice == null) { // can't find anything left if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) { if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
sa.resetTargets(); sa.resetTargets();
return false; return false;
} else { } else {

View File

@@ -598,9 +598,9 @@ public class DamageDealAi extends DamageAiBase {
lastTgt = humanCreature; lastTgt = humanCreature;
dmg -= assignedDamage; dmg -= assignedDamage;
} }
if (!source.hasProtectionFrom(humanCreature)) { // protection is already checked by target above
dmgTaken += humanCreature.getNetPower(); dmgTaken += humanCreature.getNetPower();
}
if (dmg == 0) { if (dmg == 0) {
return true; return true;
} }

View File

@@ -1,21 +1,18 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.List; import java.util.List;
import java.util.Map;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -263,15 +260,13 @@ public class FightAi extends SpellAbilityAi {
for (Trigger t : aiCreature.getTriggers()) { for (Trigger t : aiCreature.getTriggers()) {
if (t.getMode() == TriggerType.SpellCast) { if (t.getMode() == TriggerType.SpellCast) {
SpellAbility sa = t.ensureAbility(); SpellAbility sa = t.ensureAbility();
final Map<String, String> params = t.getMapParams();
if (sa == null) { if (sa == null) {
continue; continue;
} }
if (ApiType.PutCounter.equals(sa.getApi())) { if (ApiType.PutCounter.equals(sa.getApi())) {
if ("Card.Self".equals(params.get("TargetsValid")) && "You".equals(params.get("ValidActivatingPlayer"))) { if ("Card.Self".equals(t.getParam("TargetsValid")) && "You".equals(t.getParam("ValidActivatingPlayer"))) {
SpellAbility heroic = AbilityFactory.getAbility(aiCreature.getSVar(params.get("Execute")),aiCreature); if ("Self".equals(sa.getParam("Defined")) && "P1P1".equals(sa.getParam("CounterType"))) {
if ("Self".equals(heroic.getParam("Defined")) && "P1P1".equals(heroic.getParam("CounterType"))) { return AbilityUtils.calculateAmount(aiCreature, sa.getParam("CounterNum"), sa);
return AbilityUtils.calculateAmount(aiCreature, heroic.getParam("CounterNum"), heroic);
} }
break; break;
} }
@@ -299,12 +294,14 @@ public class FightAi extends SpellAbilityAi {
if (opponent.getSVar("Targeting").equals("Dies")) { if (opponent.getSVar("Targeting").equals("Dies")) {
return true; return true;
} }
if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed() || opponent.getShieldCount() > 0 // the damage prediction is later
|| ComputerUtil.canRegenerate(opponent.getController(), opponent)) { int damage = fighter.getNetPower() + pumpAttack;
if (damage <= 0 || opponent.getShieldCount() > 0 || ComputerUtil.canRegenerate(opponent.getController(), opponent)) {
return false; return false;
} }
if (fighter.hasKeyword(Keyword.DEATHTOUCH) // try to predict the damage that fighter would deal to opponent
|| ComputerUtilCombat.getDamageToKill(opponent, true) <= fighter.getNetPower() + pumpAttack) { // this should also handle if the opponents creature can be destroyed or not
if (ComputerUtilCombat.getEnoughDamageToKill(opponent, damage, fighter, false) <= damage) {
return true; return true;
} }
return false; return false;

View File

@@ -30,7 +30,7 @@ public class GoadAi extends SpellAbilityAi {
if (list.isEmpty()) if (list.isEmpty())
return false; return false;
if (game.getPlayers().size() >= 2) { if (game.getPlayers().size() > 2) {
// use this part only in multiplayer // use this part only in multiplayer
CardCollection betterList = CardLists.filter(list, new Predicate<Card>() { CardCollection betterList = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
@@ -79,10 +79,43 @@ public class GoadAi extends SpellAbilityAi {
// AI does not find a good creature to goad. // AI does not find a good creature to goad.
// because if it would goad a creature it would attack AI. // 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 false;
} }
return true; 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<Card> list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty())
return false;
sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(list));
return true;
}
}
return false;
}
} }

View File

@@ -18,7 +18,7 @@ public class ReplaceDamageAi extends SpellAbilityAi {
if (c.hasSVar("MustBeBlocked")) { if (c.hasSVar("MustBeBlocked")) {
return c; 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)) { if (c.hasKeyword(Keyword.INFECT)) {
return c; return c;
} }

View File

@@ -18,7 +18,7 @@ public class RevealAi extends RevealAiBase {
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
// we can reuse this function here... // we can reuse this function here...
final boolean bFlag = revealHandTargetAI(ai, sa/* , true, false */); final boolean bFlag = revealHandTargetAI(ai, sa, false);
if (!bFlag) { if (!bFlag) {
return false; return false;
@@ -80,7 +80,7 @@ public class RevealAi extends RevealAiBase {
} }
if (!revealHandTargetAI(ai, sa/*, false, mandatory*/)) { if (!revealHandTargetAI(ai, sa, mandatory)) {
return false; return false;
} }

View File

@@ -15,7 +15,7 @@ import forge.game.zone.ZoneType;
public abstract class RevealAiBase extends SpellAbilityAi { 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()) { if (sa.usesTargeting()) {
// ability is targeted // ability is targeted
sa.resetTargets(); sa.resetTargets();
@@ -29,7 +29,7 @@ public abstract class RevealAiBase extends SpellAbilityAi {
Player p = Collections.max(opps, PlayerPredicates.compareByZoneSize(ZoneType.Hand)); Player p = Collections.max(opps, PlayerPredicates.compareByZoneSize(ZoneType.Hand));
if (p.getCardsIn(ZoneType.Hand).isEmpty()) { if (!mandatory && p.getCardsIn(ZoneType.Hand).isEmpty()) {
return false; return false;
} }
sa.getTargets().add(p); sa.getTargets().add(p);
@@ -45,7 +45,7 @@ public abstract class RevealAiBase extends SpellAbilityAi {
*/ */
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) { public boolean chkAIDrawback(SpellAbility sa, Player ai) {
revealHandTargetAI(ai, sa); revealHandTargetAI(ai, sa, false);
return true; return true;
} }
} }

View File

@@ -12,7 +12,7 @@ public class RevealHandAi extends RevealAiBase {
*/ */
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { 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) { if (!bFlag) {
return false; return false;
@@ -29,8 +29,7 @@ public class RevealHandAi extends RevealAiBase {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return revealHandTargetAI(ai, sa, mandatory);
return revealHandTargetAI(ai, sa/*, false, mandatory*/);
} }
} }

View File

@@ -39,7 +39,7 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbilityCantAttach;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
public abstract class GameEntity extends GameObject implements IIdentifiable { public abstract class GameEntity extends GameObject implements IIdentifiable {
@@ -222,15 +222,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return false; return false;
} }
// CantTarget static abilities // check for rules
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;
}
}
}
if (attach.isAura() && !canBeEnchantedBy(attach)) { if (attach.isAura() && !canBeEnchantedBy(attach)) {
return false; return false;
} }
@@ -241,8 +233,13 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return false; return false;
} }
// check for can't attach static
if (StaticAbilityCantAttach.cantAttach(this, attach, checkSBA)) {
return false;
}
// true for all // true for all
return !hasProtectionFrom(attach, checkSBA); return true;
} }
protected boolean canBeEquippedBy(final Card aura) { protected boolean canBeEquippedBy(final Card aura) {
@@ -260,6 +257,8 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
} }
protected boolean canBeEnchantedBy(final Card aura) { protected boolean canBeEnchantedBy(final Card aura) {
// TODO need to check for multiple Enchant Keywords
SpellAbility sa = aura.getFirstAttachSpell(); SpellAbility sa = aura.getFirstAttachSpell();
TargetRestrictions tgt = null; TargetRestrictions tgt = null;
if (sa != 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)); 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! // Counters!
public boolean hasCounters() { public boolean hasCounters() {
return !counters.isEmpty(); return !counters.isEmpty();

View File

@@ -519,7 +519,7 @@ public abstract class SpellAbilityEffect {
String valid = sa.getParamOrDefault("ReplaceDyingValid", "Card.IsRemembered"); String valid = sa.getParamOrDefault("ReplaceDyingValid", "Card.IsRemembered");
String repeffstr = "Event$ Moved | ValidCard$ " + valid + String repeffstr = "Event$ Moved | ValidLKI$ " + valid +
"| Origin$ Battlefield | Destination$ Graveyard " + "| Origin$ Battlefield | Destination$ Graveyard " +
"| Description$ If that permanent would die this turn, exile it instead."; "| Description$ If that permanent would die this turn, exile it instead.";
String effect = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Battlefield | Destination$ " + zone; String effect = "DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Battlefield | Destination$ " + zone;

View File

@@ -127,7 +127,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
// itself a static ability) // itself a static ability)
final List<StaticAbility> addedStaticAbilities = Lists.newArrayList(); final List<StaticAbility> addedStaticAbilities = Lists.newArrayList();
for (final String s : stAbs) { 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() { final GameCommand unanimate = new GameCommand() {

View File

@@ -298,7 +298,8 @@ public class CounterEffect extends SpellAbilityEffect {
private static void removeFromStack(final SpellAbility tgtSA, final SpellAbility srcSA, final SpellAbilityStackInstance si, CardZoneTable triggerList) { private static void removeFromStack(final SpellAbility tgtSA, final SpellAbility srcSA, final SpellAbilityStackInstance si, CardZoneTable triggerList) {
final Game game = tgtSA.getActivatingPlayer().getGame(); final Game game = tgtSA.getActivatingPlayer().getGame();
Card movedCard = null; Card movedCard = null;
final Zone originZone = tgtSA.getHostCard().getZone(); final Card c = tgtSA.getHostCard();
final Zone originZone = c.getZone();
// Run any applicable replacement effects. // Run any applicable replacement effects.
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(tgtSA.getHostCard()); final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(tgtSA.getHostCard());
@@ -310,8 +311,8 @@ public class CounterEffect extends SpellAbilityEffect {
game.getStack().remove(si); game.getStack().remove(si);
// if the target card on stack was a spell with Bestow, then unbestow it // if the target card on stack was a spell with Bestow, then unbestow it
if (tgtSA.getHostCard() != null && tgtSA.getHostCard().isBestowed()) { if (c.isBestowed()) {
tgtSA.getHostCard().unanimateBestow(true); c.unanimateBestow(true);
} }
Map<AbilityKey, Object> params = AbilityKey.newMap(); Map<AbilityKey, Object> params = AbilityKey.newMap();
@@ -327,33 +328,32 @@ public class CounterEffect extends SpellAbilityEffect {
// For Ability-targeted counterspells - do not move it anywhere, // For Ability-targeted counterspells - do not move it anywhere,
// even if Destination$ is specified. // even if Destination$ is specified.
} else if (destination.equals("Graveyard")) { } 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")) { } 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")) { } 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")) { } 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")) { } else if (destination.equals("Battlefield")) {
if (tgtSA instanceof SpellPermanent) { if (tgtSA instanceof SpellPermanent) {
Card c = tgtSA.getHostCard();
c.setController(srcSA.getActivatingPlayer(), 0); c.setController(srcSA.getActivatingPlayer(), 0);
movedCard = game.getAction().moveToPlay(c, srcSA.getActivatingPlayer(), srcSA, params); movedCard = game.getAction().moveToPlay(c, srcSA.getActivatingPlayer(), srcSA, params);
} else { } 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); movedCard.setController(srcSA.getActivatingPlayer(), 0);
} }
} else if (destination.equals("BottomOfLibrary")) { } 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")) { } else if (destination.equals("ShuffleIntoLibrary")) {
movedCard = game.getAction().moveToBottomOfLibrary(tgtSA.getHostCard(), srcSA, params); movedCard = game.getAction().moveToBottomOfLibrary(c, srcSA, params);
tgtSA.getHostCard().getController().shuffle(srcSA); c.getController().shuffle(srcSA);
} else { } else {
throw new IllegalArgumentException("AbilityFactory_CounterMagic: Invalid Destination argument for card " throw new IllegalArgumentException("AbilityFactory_CounterMagic: Invalid Destination argument for card "
+ srcSA.getHostCard().getName()); + srcSA.getHostCard().getName());
} }
// Run triggers // Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(tgtSA.getHostCard()); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c);
runParams.put(AbilityKey.Player, tgtSA.getActivatingPlayer()); runParams.put(AbilityKey.Player, tgtSA.getActivatingPlayer());
runParams.put(AbilityKey.Cause, srcSA.getHostCard()); runParams.put(AbilityKey.Cause, srcSA.getHostCard());
runParams.put(AbilityKey.CounteredSA, tgtSA); runParams.put(AbilityKey.CounteredSA, tgtSA);

View File

@@ -9,13 +9,13 @@ public class HauntEffect extends SpellAbilityEffect {
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
Card card = sa.getHostCard(); Card host = sa.getHostCard();
final Game game = card.getGame(); final Game game = host.getGame();
card = game.getCardState(card, null); Card card = game.getCardState(host, null);
if (card == null) { if (card == null) {
return; return;
} else if (sa.usesTargeting() && !card.isToken()) { } else if (sa.usesTargeting() && !card.isToken() && host.equalsWithTimestamp(card)) {
// haunt target but only if card is no token // haunt target but only if card is no token and still in grave
final Card copy = game.getAction().exile(card, sa); final Card copy = game.getAction().exile(card, sa);
sa.getTargets().getFirstTargetedCard().addHauntedBy(copy); sa.getTargets().getFirstTargetedCard().addHauntedBy(copy);
} else if (!sa.usesTargeting() && card.getHaunting() != null) { } else if (!sa.usesTargeting() && card.getHaunting() != null) {

View File

@@ -24,7 +24,6 @@ public class ReplaceCounterEffect extends SpellAbilityEffect {
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard(); final Card card = sa.getHostCard();
// outside of Replacement Effect, unwanted result // outside of Replacement Effect, unwanted result
@@ -64,8 +63,6 @@ public class ReplaceCounterEffect extends SpellAbilityEffect {
counterTable.get(p).put(e.getKey(), value); counterTable.get(p).put(e.getKey(), value);
} }
} }
} else { } else {
for (Map.Entry<Optional<Player>, Map<CounterType, Integer>> e : counterTable.entrySet()) { for (Map.Entry<Optional<Player>, Map<CounterType, Integer>> e : counterTable.entrySet()) {
if (!sa.matchesValidParam("ValidSource", e.getKey().orNull())) { if (!sa.matchesValidParam("ValidSource", e.getKey().orNull())) {

View File

@@ -1912,9 +1912,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
for (KeywordInterface inst : keywords) { for (KeywordInterface inst : keywords) {
String keyword = inst.getOriginal(); String keyword = inst.getOriginal();
try { try {
if (keyword.startsWith("SpellCantTarget")) {
continue;
}
if (keyword.startsWith("CantBeCounteredBy")) { if (keyword.startsWith("CantBeCounteredBy")) {
final String[] p = keyword.split(":"); final String[] p = keyword.split(":");
sbLong.append(p[2]).append("\r\n"); sbLong.append(p[2]).append("\r\n");
@@ -4596,8 +4593,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
public final StaticAbility addStaticAbility(final String s) { public final StaticAbility addStaticAbility(final String s) {
if (!s.trim().isEmpty()) { if (!s.trim().isEmpty()) {
final StaticAbility stAb = new StaticAbility(s, this, currentState); final StaticAbility stAb = StaticAbility.create(s, this, currentState, true);
stAb.setIntrinsic(true);
currentState.addStaticAbility(stAb); currentState.addStaticAbility(stAb);
return stAb; return stAb;
} }
@@ -5693,103 +5689,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public Card getMeldedWith() { return meldedWith; } public Card getMeldedWith() { return meldedWith; }
public void setMeldedWith(Card meldedWith) { this.meldedWith = 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() { public String getProtectionKey() {
String protectKey = ""; String protectKey = "";
boolean pR = false; boolean pG = false; boolean pB = false; boolean pU = false; boolean pW = false; 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<Card>, IHasSVars {
return true; return true;
} }
if (hasProtectionFrom(sa.getHostCard())) {
return false;
}
if (isPhasedOut()) { if (isPhasedOut()) {
return false; 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; return true;
} }

View File

@@ -745,9 +745,7 @@ public class CardFactory {
for (final String s : str.split(",")) { for (final String s : str.split(",")) {
if (origSVars.containsKey(s)) { if (origSVars.containsKey(s)) {
final String actualStatic = origSVars.get(s); final String actualStatic = origSVars.get(s);
final StaticAbility grantedStatic = new StaticAbility(actualStatic, out, sa.getCardState()); state.addStaticAbility(StaticAbility.create(actualStatic, out, sa.getCardState(), true));
grantedStatic.setIntrinsic(true);
state.addStaticAbility(grantedStatic);
} }
} }
} }

View File

@@ -677,13 +677,13 @@ public class CardFactoryUtil {
} }
public static String getProtectionValid(final String kw, final boolean damage) { public static String getProtectionValid(final String kw, final boolean damage) {
String validSource = "Card."; // TODO extend for Emblem too String validSource = "";
if (kw.startsWith("Protection:")) { if (kw.startsWith("Protection:")) {
final String[] kws = kw.split(":"); final String[] kws = kw.split(":");
String characteristic = kws[1]; String characteristic = kws[1];
if (characteristic.startsWith("Player")) { if (characteristic.startsWith("Player")) {
validSource += "ControlledBy " + characteristic; validSource = "ControlledBy " + characteristic;
} else { } else {
if (damage && (characteristic.endsWith("White") || characteristic.endsWith("Blue") if (damage && (characteristic.endsWith("White") || characteristic.endsWith("Blue")
|| characteristic.endsWith("Black") || characteristic.endsWith("Red") || characteristic.endsWith("Black") || characteristic.endsWith("Red")
@@ -691,34 +691,37 @@ public class CardFactoryUtil {
|| characteristic.endsWith("MonoColor") || characteristic.endsWith("MultiColor"))) { || characteristic.endsWith("MonoColor") || characteristic.endsWith("MultiColor"))) {
characteristic += "Source"; characteristic += "Source";
} }
validSource = characteristic; return characteristic;
} }
} else if (kw.startsWith("Protection from ")) { } else if (kw.startsWith("Protection from ")) {
String protectType = kw.substring("Protection from ".length()); String protectType = kw.substring("Protection from ".length());
if (protectType.equals("white")) { if (protectType.equals("white")) {
validSource += "White" + (damage ? "Source" : ""); validSource = "White" + (damage ? "Source" : "");
} else if (protectType.equals("blue")) { } else if (protectType.equals("blue")) {
validSource += "Blue" + (damage ? "Source" : ""); validSource = "Blue" + (damage ? "Source" : "");
} else if (protectType.equals("black")) { } else if (protectType.equals("black")) {
validSource += "Black" + (damage ? "Source" : ""); validSource = "Black" + (damage ? "Source" : "");
} else if (protectType.equals("red")) { } else if (protectType.equals("red")) {
validSource += "Red" + (damage ? "Source" : ""); validSource = "Red" + (damage ? "Source" : "");
} else if (protectType.equals("green")) { } else if (protectType.equals("green")) {
validSource += "Green" + (damage ? "Source" : ""); validSource = "Green" + (damage ? "Source" : "");
} else if (protectType.equals("colorless")) { } else if (protectType.equals("colorless")) {
validSource += "Colorless" + (damage ? "Source" : ""); validSource = "Colorless" + (damage ? "Source" : "");
} else if (protectType.equals("all colors")) { } else if (protectType.equals("all colors")) {
validSource += "nonColorless" + (damage ? "Source" : ""); validSource = "nonColorless" + (damage ? "Source" : "");
} else if (protectType.equals("everything")) { } else if (protectType.equals("everything")) {
validSource = ""; return "";
} else if (protectType.startsWith("opponent of ")) { } else if (protectType.startsWith("opponent of ")) {
final String playerName = protectType.substring("opponent of ".length()); final String playerName = protectType.substring("opponent of ".length());
validSource += "ControlledBy Player.OpponentOf PlayerNamed_" + playerName; validSource = "ControlledBy Player.OpponentOf PlayerNamed_" + playerName;
} else { } else {
validSource = CardType.getSingularType(protectType); 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) { public static ReplacementEffect makeEtbCounter(final String kw, final CardState card, final boolean intrinsic) {
@@ -1290,13 +1293,12 @@ public class CardFactoryUtil {
triggers.add(haunterETB); triggers.add(haunterETB);
} }
// First, create trigger that runs when the haunter goes to the // First, create trigger that runs when the haunter goes to the graveyard
// graveyard
final StringBuilder sbHaunter = new StringBuilder(); final StringBuilder sbHaunter = new StringBuilder();
sbHaunter.append("Mode$ ChangesZone | Origin$ "); 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(" | 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); final Trigger haunterDies = TriggerHandler.parseTrigger(sbHaunter.toString(), card, intrinsic);
@@ -2965,7 +2967,7 @@ public class CardFactoryUtil {
final String manacost = k[1]; final String manacost = k[1];
StringBuilder sb = new StringBuilder(); 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(" | PrecostDesc$ Level Up | CostDesc$ ").append(ManaCostParser.parse(manacost));
sb.append(" | SorcerySpeed$ True | LevelUp$ True | CounterNum$ 1 | CounterType$ LEVEL"); sb.append(" | SorcerySpeed$ True | LevelUp$ True | CounterNum$ 1 | CounterType$ LEVEL");
if (card.hasSVar("maxLevel")) { if (card.hasSVar("maxLevel")) {
@@ -3398,8 +3400,6 @@ public class CardFactoryUtil {
public static void addStaticAbility(final KeywordInterface inst, final CardState state, final boolean intrinsic) { public static void addStaticAbility(final KeywordInterface inst, final CardState state, final boolean intrinsic) {
String keyword = inst.getOriginal(); String keyword = inst.getOriginal();
String effect = null;
Map<String, String> svars = Maps.newHashMap();
if (keyword.startsWith("Affinity")) { if (keyword.startsWith("Affinity")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
@@ -3419,20 +3419,24 @@ public class CardFactoryUtil {
sb.append("Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Amount$ AffinityX | EffectZone$ All"); sb.append("Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Amount$ AffinityX | EffectZone$ All");
sb.append("| Description$ Affinity for ").append(desc); sb.append("| Description$ Affinity for ").append(desc);
sb.append(" (").append(inst.getReminderText()).append(")"); 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")) { } 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" + " | CharacteristicDefining$ True | AddAllCreatureTypes$ True | Secondary$ True" +
" | Description$ Changeling (" + inst.getReminderText() + ")"; " | Description$ Changeling (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Cipher")) { } else if (keyword.equals("Cipher")) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("Mode$ Continuous | EffectZone$ Exile | Affected$ Card.EncodedWithSource"); sb.append("Mode$ Continuous | EffectZone$ Exile | Affected$ Card.EncodedWithSource");
sb.append(" | AddTrigger$ CipherTrigger"); sb.append(" | AddTrigger$ CipherTrigger");
sb.append(" | Description$ Cipher (").append(inst.getReminderText()).append(")"); sb.append(" | Description$ Cipher (").append(inst.getReminderText()).append(")");
effect = sb.toString(); String effect = sb.toString();
sb = new StringBuilder(); sb = new StringBuilder();
@@ -3445,8 +3449,12 @@ public class CardFactoryUtil {
String ab = "DB$ Play | Defined$ OriginalHost | WithoutManaCost$ True | CopyCard$ True"; String ab = "DB$ Play | Defined$ OriginalHost | WithoutManaCost$ True | CopyCard$ True";
svars.put("CipherTrigger", trig); StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic);
svars.put("PlayEncoded", ab);
st.setSVar("CipherTrigger", trig);
st.setSVar("PlayEncoded", ab);
inst.addStaticAbility(st);
} else if (keyword.startsWith("Class")) { } else if (keyword.startsWith("Class")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final String level = k[1]; final String level = k[1];
@@ -3475,24 +3483,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) { if (descAdded) {
effect += " | Description$ " + desc.toString(); effect += " | Description$ " + desc.toString();
} }
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Dash")) { } 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")) { } 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")) { } 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"; "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")) { } 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")) { } 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" + " | CharacteristicDefining$ True | SetColor$ Colorless | Secondary$ True" +
" | Description$ Devoid (" + inst.getReminderText() + ")"; " | Description$ Devoid (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Escalate")) { } else if (keyword.startsWith("Escalate")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final String manacost = k[1]; final String manacost = k[1];
@@ -3506,15 +3522,18 @@ public class CardFactoryUtil {
} }
sb.append(cost.toSimpleString()); 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" + " | Amount$ Escalate | Cost$ "+ manacost +" | EffectZone$ All"
+ " | Description$ " + sb.toString() + " (" + inst.getReminderText() + ")"; + " | Description$ " + sb.toString() + " (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Fear")) { } 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() + ")"; " | Description$ Fear ( " + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Flying")) { } 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() + ")"; " | Description$ Flying ( " + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Hexproof")) { } else if (keyword.startsWith("Hexproof")) {
final StringBuilder sbDesc = new StringBuilder("Hexproof"); final StringBuilder sbDesc = new StringBuilder("Hexproof");
final StringBuilder sbValid = new StringBuilder(); final StringBuilder sbValid = new StringBuilder();
@@ -3526,55 +3545,84 @@ public class CardFactoryUtil {
sbValid.append("| ValidSource$ ").append(k[1]); 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$ " + sbValid.toString() + " | Activator$ Opponent | Description$ "
+ sbDesc.toString() + " (" + inst.getReminderText() + ")"; + sbDesc.toString() + " (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Horsemanship")) { } 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() + ")"; " | Description$ Horsemanship ( " + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Intimidate")) { } 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() + ")"; " | Description$ Intimidate ( " + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Nightbound")) { } 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")) { } else if (keyword.startsWith("Protection")) {
String valid = getProtectionValid(keyword, false); 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()) { if (!valid.isEmpty()) {
effect += "| ValidBlocker$ " + valid; 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")) { } 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() + ")"; + " | Description$ Shroud (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Skulk")) { } 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() + ")"; " | 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")) { } else if (keyword.startsWith("Strive")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final String manacost = k[1]; 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(); " | Description$ Strive - " + inst.getReminderText();
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Unleash")) { } 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")) { } 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() + ")"; + "| Amount$ Undaunted | EffectZone$ All | Description$ Undaunted (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("MayFlashSac")) { } 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" + " | MayPlayNotSorcerySpeed$ True | MayPlayWithFlash$ True | MayPlayText$ Sacrifice at the next cleanup step"
+ " | AffectedZone$ Exile,Graveyard,Hand,Library,Stack | Description$ " + inst.getReminderText(); + " | AffectedZone$ Exile,Graveyard,Hand,Library,Stack | Description$ " + inst.getReminderText();
} inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
if (effect != null) {
StaticAbility st = new StaticAbility(effect, state.getCard(), state);
st.setIntrinsic(intrinsic);
for (Map.Entry<String, String> e : svars.entrySet()) {
st.setSVar(e.getKey(), e.getValue());
}
inst.addStaticAbility(st);
} }
} }

View File

@@ -224,15 +224,6 @@ public final class CardPredicates {
}; };
} }
public static final Predicate<Card> isProtectedFrom(final Card source) {
return new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.hasProtectionFrom(source);
}
};
}
public static final Predicate<Card> restriction(final String[] restrictions, final Player sourceController, final Card source, final CardTraitBase spellAbility) { public static final Predicate<Card> restriction(final String[] restrictions, final Player sourceController, final Card source, final CardTraitBase spellAbility) {
return new Predicate<Card>() { return new Predicate<Card>() {
@Override @Override

View File

@@ -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()) { if (!isFaceDown()) {
return true; return true;
} }

View File

@@ -529,8 +529,7 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
// Run triggers // Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.LifeAmount, lifeGain); runParams.put(AbilityKey.LifeAmount, lifeGain);
runParams.put(AbilityKey.Source, source); runParams.put(AbilityKey.Source, source);
runParams.put(AbilityKey.SourceSA, sa); runParams.put(AbilityKey.SourceSA, sa);
@@ -602,8 +601,7 @@ public class Player extends GameEntity implements Comparable<Player> {
lifeLostThisTurn += toLose; lifeLostThisTurn += toLose;
// Run triggers // Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.LifeAmount, toLose); runParams.put(AbilityKey.LifeAmount, toLose);
runParams.put(AbilityKey.FirstTime, firstLost); runParams.put(AbilityKey.FirstTime, firstLost);
game.getTriggerHandler().runTrigger(TriggerType.LifeLost, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.LifeLost, runParams, false);
@@ -630,8 +628,7 @@ public class Player extends GameEntity implements Comparable<Player> {
loseLife(lifePayment, false, false); loseLife(lifePayment, false, false);
// Run triggers // Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.LifeAmount, lifePayment); runParams.put(AbilityKey.LifeAmount, lifePayment);
game.getTriggerHandler().runTrigger(TriggerType.PayLife, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.PayLife, runParams, false);
@@ -1097,70 +1094,7 @@ public class Player extends GameEntity implements Comparable<Player> {
return false; return false;
} }
return !hasProtectionFrom(sa.getHostCard()); return true;
}
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;
} }
public void surveil(int num, SpellAbility cause, CardZoneTable table) { public void surveil(int num, SpellAbility cause, CardZoneTable table) {
@@ -1212,8 +1146,7 @@ public class Player extends GameEntity implements Comparable<Player> {
getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave)); getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave));
surveilThisTurn++; surveilThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.NumThisTurn, surveilThisTurn); runParams.put(AbilityKey.NumThisTurn, surveilThisTurn);
getGame().getTriggerHandler().runTrigger(TriggerType.Surveil, runParams, false); getGame().getTriggerHandler().runTrigger(TriggerType.Surveil, runParams, false);
} }
@@ -1329,7 +1262,7 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
view.updateNumDrawnThisTurn(this); view.updateNumDrawnThisTurn(this);
final Map<AbilityKey, Object> runParams = Maps.newHashMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
// CR 121.8 card was drawn as part of another sa (e.g. paying with Chromantic Sphere), hide it temporarily // 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()) { if (game.getTopLibForPlayer(this) != null && getPaidForSA() != null && cause != null && getPaidForSA() != cause.getRootAbility()) {
@@ -1341,7 +1274,6 @@ public class Player extends GameEntity implements Comparable<Player> {
// Run triggers // Run triggers
runParams.put(AbilityKey.Card, c); runParams.put(AbilityKey.Card, c);
runParams.put(AbilityKey.Number, numDrawnThisTurn); runParams.put(AbilityKey.Number, numDrawnThisTurn);
runParams.put(AbilityKey.Player, this);
game.getTriggerHandler().runTrigger(TriggerType.Drawn, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.Drawn, runParams, false);
} }
} }
@@ -1545,8 +1477,7 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
} }
} }
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.Card, c); runParams.put(AbilityKey.Card, c);
runParams.put(AbilityKey.Cause, cause); runParams.put(AbilityKey.Cause, cause);
runParams.put(AbilityKey.IsMadness, discardMadness); runParams.put(AbilityKey.IsMadness, discardMadness);
@@ -1564,8 +1495,7 @@ public class Player extends GameEntity implements Comparable<Player> {
public final void addTokensCreatedThisTurn(Card token) { public final void addTokensCreatedThisTurn(Card token) {
numTokenCreatedThisTurn++; numTokenCreatedThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.Num, numTokenCreatedThisTurn); runParams.put(AbilityKey.Num, numTokenCreatedThisTurn);
runParams.put(AbilityKey.Card, token); runParams.put(AbilityKey.Card, token);
game.getTriggerHandler().runTrigger(TriggerType.TokenCreated, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.TokenCreated, runParams, false);
@@ -1581,8 +1511,7 @@ public class Player extends GameEntity implements Comparable<Player> {
public final void addForetoldThisTurn() { public final void addForetoldThisTurn() {
numForetoldThisTurn++; numForetoldThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.Num, numForetoldThisTurn); runParams.put(AbilityKey.Num, numForetoldThisTurn);
game.getTriggerHandler().runTrigger(TriggerType.Foretell, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.Foretell, runParams, false);
} }
@@ -1717,8 +1646,7 @@ public class Player extends GameEntity implements Comparable<Player> {
getZone(ZoneType.Library).setCards(getController().cheatShuffle(list)); getZone(ZoneType.Library).setCards(getController().cheatShuffle(list));
// Run triggers // Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.Source, sa); runParams.put(AbilityKey.Source, sa);
game.getTriggerHandler().runTrigger(TriggerType.Shuffled, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.Shuffled, runParams, false);
@@ -2007,12 +1935,7 @@ public class Player extends GameEntity implements Comparable<Player> {
return getOutcome().lossState != null; return getOutcome().lossState != null;
} }
// Rule 704.5a - If a player has 0 or less life, he or she loses the game. // check this first because of Lich's Mirror (704.7)
final boolean hasNoLife = getLife() <= 0;
if (hasNoLife && !cantLoseForZeroOrLessLife()) {
return loseConditionMet(GameLossReason.LifeReachedZero, null);
}
// Rule 704.5b - If a player attempted to draw a card from a library with no cards in it // 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. // since the last time state-based actions were checked, he or she loses the game.
if (triedToDrawFromEmptyLibrary) { if (triedToDrawFromEmptyLibrary) {
@@ -2023,6 +1946,12 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
} }
// 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. // Rule 704.5c - If a player has ten or more poison counters, he or she loses the game.
if (getCounters(CounterEnumType.POISON) >= 10) { if (getCounters(CounterEnumType.POISON) >= 10) {
return loseConditionMet(GameLossReason.Poisoned, null); return loseConditionMet(GameLossReason.Poisoned, null);
@@ -2244,8 +2173,7 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
public final void addInvestigatedThisTurn() { public final void addInvestigatedThisTurn() {
investigatedThisTurn++; investigatedThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.Num, investigatedThisTurn); runParams.put(AbilityKey.Num, investigatedThisTurn);
game.getTriggerHandler().runTrigger(TriggerType.Investigated, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.Investigated, runParams, false);
} }
@@ -2265,10 +2193,9 @@ public class Player extends GameEntity implements Comparable<Player> {
sacrificedThisTurn.add(cpy); sacrificedThisTurn.add(cpy);
// Run triggers // Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> 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) // 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.Card, cpy);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.Cause, source); runParams.put(AbilityKey.Cause, source);
runParams.put(AbilityKey.CostStack, game.costPaymentStack); runParams.put(AbilityKey.CostStack, game.costPaymentStack);
runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek()); runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek());
@@ -3513,8 +3440,7 @@ public class Player extends GameEntity implements Comparable<Player> {
// Change this if something would make multiple player learn at the same time // Change this if something would make multiple player learn at the same time
// Discard Trigger outside Effect // Discard Trigger outside Effect
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.Cards, new CardCollection(c)); runParams.put(AbilityKey.Cards, new CardCollection(c));
runParams.put(AbilityKey.Cause, sa); runParams.put(AbilityKey.Cause, sa);
runParams.put(AbilityKey.FirstTime, firstDiscard); runParams.put(AbilityKey.FirstTime, firstDiscard);

View File

@@ -11,7 +11,7 @@ public class PlayerFactoryUtil {
public static void addStaticAbility(final KeywordInterface inst, final Player player) { public static void addStaticAbility(final KeywordInterface inst, final Player player) {
String keyword = inst.getOriginal(); String keyword = inst.getOriginal();
String effect = null;
if (keyword.startsWith("Hexproof")) { if (keyword.startsWith("Hexproof")) {
final StringBuilder sbDesc = new StringBuilder("Hexproof"); final StringBuilder sbDesc = new StringBuilder("Hexproof");
final StringBuilder sbValid = new StringBuilder(); final StringBuilder sbValid = new StringBuilder();
@@ -23,18 +23,40 @@ public class PlayerFactoryUtil {
sbValid.append("| ValidSource$ ").append(k[1]); 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$ " + sbValid.toString() + " | Activator$ Opponent | EffectZone$ Command | Description$ "
+ sbDesc.toString() + " (" + inst.getReminderText() + ")"; + 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(); final Card card = player.getKeywordCard();
StaticAbility st = new StaticAbility(effect, card, card.getCurrentState()); inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false));
st.setIntrinsic(false); } else if (keyword.equals("Shroud")) {
inst.addStaticAbility(st); 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));
} }
} }

View File

@@ -504,7 +504,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
} }
public boolean isCycling() { public boolean isCycling() {
return this.isAlternativeCost(AlternativeCost.Cycling); return isAlternativeCost(AlternativeCost.Cycling);
} }
public boolean isBoast() { public boolean isBoast() {
@@ -515,6 +515,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return this.hasParam("Ninjutsu"); 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 // If this is not null, then ability was made in a factory
public ApiType getApi() { public ApiType getApi() {
return api; return api;
@@ -777,7 +785,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public void resetOnceResolved() { public void resetOnceResolved() {
//resetPaidHash(); // FIXME: if uncommented, breaks Dragon Presence, e.g. Orator of Ojutai + revealing a Dragon from hand. //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. // 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(); resetTriggeringObjects();
resetTriggerRemembered(); resetTriggerRemembered();

View File

@@ -234,6 +234,12 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
this(parseParams(params, host), host, state); 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. * Instantiates a new static ability.
* *
@@ -325,8 +331,6 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
return StaticAbilityCantAttackBlock.applyCantAttackAbility(this, card, target); return StaticAbilityCantAttackBlock.applyCantAttackAbility(this, card, target);
} else if (mode.equals("CantBlockBy") && target instanceof Card) { } else if (mode.equals("CantBlockBy") && target instanceof Card) {
return StaticAbilityCantAttackBlock.applyCantBlockByAbility(this, card, (Card)target); return StaticAbilityCantAttackBlock.applyCantBlockByAbility(this, card, (Card)target);
} else if (mode.equals("CantAttach")) {
return StaticAbilityCantAttach.applyCantAttachAbility(this, card, target);
} else if (mode.equals("CanAttackIfHaste")) { } else if (mode.equals("CanAttackIfHaste")) {
return StaticAbilityCantAttackBlock.applyCanAttackHasteAbility(this, card, target); return StaticAbilityCantAttackBlock.applyCanAttackHasteAbility(this, card, target);
} }

View File

@@ -2,29 +2,52 @@ package forge.game.staticability;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.zone.ZoneType;
public class StaticAbilityCantAttach { public class StaticAbilityCantAttach {
public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target) { static String MODE = "CantAttach";
if (!stAb.matchesValidParam("ValidCard", card)) {
return false;
}
if (!stAb.matchesValidParam("Target", target)) { public static boolean cantAttach(final GameEntity target, final Card card, boolean checkSBA) {
return false; // 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 (applyCantAttachAbility(stAb, card, target, checkSBA)) {
if (!(target instanceof Card)) { return true;
return false; }
} }
Card tcard = (Card) target; }
return false;
}
if (!stAb.matchesValid(card, stAb.getParam("ValidCardToTarget").split(","), tcard)) { public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target, boolean checkSBA) {
return false; 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;
} }
} }

View File

@@ -875,9 +875,7 @@ public final class StaticAbilityContinuous {
s = TextUtil.fastReplace(s, "ConvertedManaCost", costcmc); s = TextUtil.fastReplace(s, "ConvertedManaCost", costcmc);
} }
StaticAbility stat = new StaticAbility(s, affectedCard, stAb.getCardState()); addedStaticAbility.add(StaticAbility.create(s, affectedCard, stAb.getCardState(), false));
stat.setIntrinsic(false);
addedStaticAbility.add(stat);
} }
} }

View File

@@ -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 // Check number of lands ETB this turn on triggered card's controller
if (hasParam("CheckOnTriggeredCard")) { if (hasParam("CheckOnTriggeredCard")) {
final String[] condition = getParam("CheckOnTriggeredCard").split(" ", 2); final String[] condition = getParam("CheckOnTriggeredCard").split(" ", 2);

View File

@@ -253,7 +253,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
} }
} }
if (!sp.isCopied() && !hasLegalTargeting(sp, source)) { if (!sp.isCopied() && !hasLegalTargeting(sp)) {
String str = source + " - [Couldn't add to stack, failed to target] - " + sp.getDescription(); String str = source + " - [Couldn't add to stack, failed to target] - " + sp.getDescription();
System.err.println(str + sp.getAllTargetChoices()); System.err.println(str + sp.getAllTargetChoices());
game.getGameLog().add(GameLogEntryType.STACK_ADD, str); game.getGameLog().add(GameLogEntryType.STACK_ADD, str);
@@ -604,14 +604,14 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
} }
} }
public final boolean hasLegalTargeting(final SpellAbility sa, final Card source) { public final boolean hasLegalTargeting(final SpellAbility sa) {
if (sa == null) { if (sa == null) {
return true; return true;
} }
if (!sa.isTargetNumberValid()) { if (!sa.isTargetNumberValid()) {
return false; return false;
} }
return hasLegalTargeting(sa.getSubAbility(), source); return hasLegalTargeting(sa.getSubAbility());
} }
private final boolean hasFizzled(final SpellAbility sa, final Card source, final Boolean parentFizzled) { private final boolean hasFizzled(final SpellAbility sa, final Card source, final Boolean parentFizzled) {

View File

@@ -28,7 +28,7 @@ public enum ZoneType {
Subgame(true, "lblSubgameZone"), Subgame(true, "lblSubgameZone"),
None(true, "lblNoneZone"); None(true, "lblNoneZone");
public static final List<ZoneType> STATIC_ABILITIES_SOURCE_ZONES = Arrays.asList(Battlefield, Graveyard, Exile, Command/*, Hand*/); public static final List<ZoneType> STATIC_ABILITIES_SOURCE_ZONES = Arrays.asList(Battlefield, Graveyard, Exile, Command, Stack/*, Hand*/);
private final boolean holdsHiddenInfo; private final boolean holdsHiddenInfo;
private final String zoneName; private final String zoneName;

View File

@@ -1,9 +1,8 @@
Name:Dream Leash Name:Dream Leash
ManaCost:3 U U ManaCost:3 U U
Types:Enchantment Aura Types:Enchantment Aura
Text:You can't choose an untapped creature as this spell's target as you cast it.
K:Enchant permanent 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 A:SP$ Attach | Cost$ 3 U U | ValidTgts$ Permanent | AILogic$ GainControl
S:Mode$ Continuous | Affected$ Card.EnchantedBy | GainControl$ You | Description$ You control enchanted permanent. 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. Oracle:Enchant permanent\nYou can't choose an untapped permanent as this spell's target as you cast it.\nYou control enchanted permanent.

View File

@@ -1,9 +1,8 @@
Name:Enthralling Hold Name:Enthralling Hold
ManaCost:3 U U ManaCost:3 U U
Types:Enchantment Aura Types:Enchantment Aura
Text:You can't choose an untapped creature as this spell's target as you cast it.
K:Enchant creature 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 A:SP$ Attach | Cost$ 3 U U | ValidTgts$ Creature | AILogic$ GainControl
S:Mode$ Continuous | Affected$ Card.EnchantedBy | GainControl$ You | Description$ You control enchanted creature. 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. Oracle:Enchant creature\nYou can't choose an untapped creature as this spell's target as you cast it.\nYou control enchanted creature.

View File

@@ -2,7 +2,7 @@ Name:Eternal Dominion
ManaCost:7 U U U ManaCost:7 U U U
Types:Sorcery Types:Sorcery
K:Epic K:Epic
A:SP$ ChangeZone | Origin$ Library | Destination$ Battlefield | ValidTgts$ Opponent | TgtPrompt$ Select target 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 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). #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 AI:RemoveDeck:Random
Oracle: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.\nEpic (For the rest of the game, you can't cast spells. At the beginning of each of your upkeeps, copy this spell except for its epic ability. You may choose a new target for the copy.) Oracle: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.\nEpic (For the rest of the game, you can't cast spells. At the beginning of each of your upkeeps, copy this spell except for its epic ability. You may choose a new target for the copy.)

View File

@@ -2,7 +2,7 @@ Name:Gisa, Glorious Resurrector
ManaCost:2 B B ManaCost:2 B B
Types:Legendary Creature Human Wizard Types:Legendary Creature Human Wizard
PT:4/4 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 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.) 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 SVar:TrigChange:DB$ ChangeZoneAll | ChangeType$ Card.ExiledWithSource | Origin$ Exile | Destination$ Battlefield | GainControl$ True | RememberChanged$ True | SubAbility$ DBPump

View File

@@ -1,7 +1,7 @@
Name:Hardened Scales Name:Hardened Scales
ManaCost:G ManaCost:G
Types:Enchantment 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:AddOneMoreCounters:DB$ ReplaceCounter | ValidCounterType$ P1P1 | ChooseCounter$ True | Amount$ X
SVar:X:ReplaceCount$CounterNum/Plus.1 SVar:X:ReplaceCount$CounterNum/Plus.1
AI:RemoveDeck:Random AI:RemoveDeck:Random

View File

@@ -1,7 +1,7 @@
Name:Rite of Flame Name:Rite of Flame
ManaCost:R ManaCost:R
Types:Sorcery 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:SubMana:DB$ Mana | Produced$ R | Amount$ X
SVar:X:Count$NamedInAllYards.Rite of Flame SVar:X:Count$NamedInAllYards.Rite of Flame
DeckHints:Name$Rite of Flame DeckHints:Name$Rite of Flame

View File

@@ -2,5 +2,5 @@ Name:Submerged Boneyard
ManaCost:no cost ManaCost:no cost
Types:Land Types:Land
K:CARDNAME enters the battlefield tapped. 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}. Oracle:Submerged Boneyard enters the battlefield tapped.\n{T}: Add {U} or {B}.

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Vampire Warlock
PT:1/1 PT:1/1
K:Menace K:Menace
K:Lifelink 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: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: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 SVar:TrigToken:AB$ Token | Cost$ 2 | TokenAmount$ 1 | TokenScript$ bg_1_1_pest_lifegain | TokenOwner$ You

View File

@@ -3,7 +3,7 @@ ManaCost:4 B B
Types:Creature Horror Types:Creature Horror
PT:4/5 PT:4/5
K:Trample 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: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 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 T:Mode$ ChangesZone | Origin$ Exile | Destination$ Any | Static$ True | ValidCard$ Card.IsRemembered+ExiledWithSource | Execute$ DBForget

View File

@@ -3,7 +3,7 @@ ManaCost:1
Types:Legendary Artifact Creature Insect Types:Legendary Artifact Creature Insect
PT:0/0 PT:0/0
K:Modular:1 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:AddOneMoreCounter:DB$ ReplaceCounter | ValidCounterType$ P1P1 | ChooseCounter$ True | Amount$ X
SVar:X:ReplaceCount$CounterNum/Plus.1 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. A:AB$ Destroy | Cost$ R | ValidTgts$ Artifact.YouCtrl | TgtPrompt$ Choose target artifact you control | SpellDescription$ Destroy target artifact you control.