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 evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false);
boolean atkEmbalm = (attacker.hasStartOfKeyword("Embalm") || attacker.hasStartOfKeyword("Eternalize")) && !attacker.isToken();
boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken();
@@ -1327,10 +1326,13 @@ public class AiBlockController {
chance = Math.max(0, chance - chanceModForEmbalm);
}
if (blocker.isFaceDown() && !checkingOther && blocker.getState(CardStateName.Original).getType().isCreature()) {
int evalBlk;
if (blocker.isFaceDown() && blocker.getView().canFaceDownBeShownTo(ai.getView(), false) && blocker.getState(CardStateName.Original).getType().isCreature()) {
// if the blocker is a face-down creature (e.g. cast via Morph, Manifest), evaluate it
// in relation to the original state, not to the Morph state
evalBlk = ComputerUtilCard.evaluateCreature(Card.fromPaperCard(blocker.getPaperCard(), ai), false, true);
} else {
evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false);
}
int chanceToSavePW = chanceToTradeDownToSaveWalker > 0 && evalAtk + 1 < evalBlk ? chanceToTradeDownToSaveWalker : chanceToTradeToSaveWalker;
boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower();

View File

@@ -125,15 +125,8 @@ public class AttachAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) {
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
final ZoneType origin = ZoneType.listValueOf(effectExile.getParam("Origin")).get(0);
final TargetRestrictions exile_tgt = effectExile.getTargetRestrictions();
final CardCollection list = CardLists.getValidCards(ai.getGame().getCardsIn(origin), exile_tgt.getValidTgts(), ai, source, effectExile);
final CardCollection targets = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF));
}
});
final CardCollection targets = CardLists.filter(CardUtil.getValidCardsToTarget(exile_tgt, effectExile), CardPredicates.canBeAttached(source));
return !targets.isEmpty();
}
@@ -1325,8 +1318,7 @@ public class AttachAi extends SpellAbilityAi {
// Is a SA that moves target attachment
if ("MoveTgtAura".equals(sa.getParam("AILogic"))) {
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);
list = CardLists.filter(list, Predicates.not(CardPredicates.isProtectedFrom(attachSource)));
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(tgt, sa));
list = CardLists.filter(list, Predicates.or(CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()), new Predicate<Card>() {
@Override
public boolean apply(final Card card) {
@@ -1336,7 +1328,7 @@ public class AttachAi extends SpellAbilityAi {
return !list.isEmpty() ? ComputerUtilCard.getBestAI(list) : null;
} else if ("Unenchanted".equals(sa.getParam("AILogic"))) {
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(tgt, sa));
CardCollection preferred = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card card) {
@@ -1361,18 +1353,7 @@ public class AttachAi extends SpellAbilityAi {
if (tgt == null) {
list = AbilityUtils.getDefinedCards(attachSource, sa.getParam("Defined"), sa);
} else {
list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);
list = CardLists.filter(list, CardPredicates.canBeAttached(attachSource));
// TODO If Attaching without casting, don't need to actually target.
// I believe this is the only case where mandatory will be true, so just
// check that when starting that work
// But we shouldn't attach to things with Protection
if (!mandatory) {
list = CardLists.getTargetableCards(list, sa);
} else {
list = CardLists.filter(list, Predicates.not(CardPredicates.isProtectedFrom(attachSource)));
}
list = CardLists.filter(CardUtil.getValidCardsToTarget(tgt, sa), CardPredicates.canBeAttached(attachSource));
}
if (list.isEmpty()) {

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ public class GoadAi extends SpellAbilityAi {
if (list.isEmpty())
return false;
if (game.getPlayers().size() >= 2) {
if (game.getPlayers().size() > 2) {
// use this part only in multiplayer
CardCollection betterList = CardLists.filter(list, new Predicate<Card>() {
@Override
@@ -79,10 +79,43 @@ public class GoadAi extends SpellAbilityAi {
// AI does not find a good creature to goad.
// because if it would goad a creature it would attack AI.
// AI might not have enough infomation to block it
// AI might not have enough information to block it
return false;
}
return true;
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (checkApiLogic(ai, sa)) {
return true;
}
if (!mandatory) {
return false;
}
if (sa.usesTargeting()) {
if (sa.getTargetRestrictions().canTgtPlayer()) {
for (Player opp : ai.getOpponents()) {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
return true;
}
}
if (sa.canTarget(ai)) {
sa.getTargets().add(ai);
return true;
}
} else {
List<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")) {
return c;
}
// TODO check if target can receive counters
// TODO check if target can receive counters + sort these to the front if that can prevent loss
if (c.hasKeyword(Keyword.INFECT)) {
return c;
}

View File

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

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttach;
import forge.game.zone.ZoneType;
public abstract class GameEntity extends GameObject implements IIdentifiable {
@@ -222,15 +222,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return false;
}
// CantTarget static abilities
for (final Card ca : getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (stAb.applyAbility("CantAttach", attach, this)) {
return false;
}
}
}
// check for rules
if (attach.isAura() && !canBeEnchantedBy(attach)) {
return false;
}
@@ -241,8 +233,13 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return false;
}
// check for can't attach static
if (StaticAbilityCantAttach.cantAttach(this, attach, checkSBA)) {
return false;
}
// true for all
return !hasProtectionFrom(attach, checkSBA);
return true;
}
protected boolean canBeEquippedBy(final Card aura) {
@@ -260,6 +257,8 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
}
protected boolean canBeEnchantedBy(final Card aura) {
// TODO need to check for multiple Enchant Keywords
SpellAbility sa = aura.getFirstAttachSpell();
TargetRestrictions tgt = null;
if (sa != null) {
@@ -269,11 +268,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return !((tgt != null) && !isValid(tgt.getValidTgts(), aura.getController(), aura, sa));
}
public boolean hasProtectionFrom(final Card source) {
return hasProtectionFrom(source, false);
}
public abstract boolean hasProtectionFrom(final Card source, final boolean checkSBA);
// Counters!
public boolean hasCounters() {
return !counters.isEmpty();

View File

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

View File

@@ -127,7 +127,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
// itself a static ability)
final List<StaticAbility> addedStaticAbilities = Lists.newArrayList();
for (final String s : stAbs) {
addedStaticAbilities.add(new StaticAbility(AbilityUtils.getSVar(sa, s), c, sa.getCardState()));
addedStaticAbilities.add(StaticAbility.create(AbilityUtils.getSVar(sa, s), c, sa.getCardState(), false));
}
final GameCommand unanimate = new GameCommand() {

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

View File

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

View File

@@ -24,7 +24,6 @@ public class ReplaceCounterEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
// outside of Replacement Effect, unwanted result
@@ -64,8 +63,6 @@ public class ReplaceCounterEffect extends SpellAbilityEffect {
counterTable.get(p).put(e.getKey(), value);
}
}
} else {
for (Map.Entry<Optional<Player>, Map<CounterType, Integer>> e : counterTable.entrySet()) {
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) {
String keyword = inst.getOriginal();
try {
if (keyword.startsWith("SpellCantTarget")) {
continue;
}
if (keyword.startsWith("CantBeCounteredBy")) {
final String[] p = keyword.split(":");
sbLong.append(p[2]).append("\r\n");
@@ -4596,8 +4593,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final StaticAbility addStaticAbility(final String s) {
if (!s.trim().isEmpty()) {
final StaticAbility stAb = new StaticAbility(s, this, currentState);
stAb.setIntrinsic(true);
final StaticAbility stAb = StaticAbility.create(s, this, currentState, true);
currentState.addStaticAbility(stAb);
return stAb;
}
@@ -5693,103 +5689,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public Card getMeldedWith() { return meldedWith; }
public void setMeldedWith(Card meldedWith) { this.meldedWith = meldedWith; }
public boolean hasProtectionFrom(final Card source) {
return hasProtectionFrom(source, false, false);
}
@Override
public boolean hasProtectionFrom(final Card source, final boolean checkSBA) {
return hasProtectionFrom(source, checkSBA, false);
}
public boolean hasProtectionFrom(final Card source, final boolean checkSBA, final boolean damageSource) {
if (source == null) {
return false;
}
if (isImmutable()) {
return true;
}
// Protection only works on the Battlefield
if (!isInPlay()) {
return false;
}
final boolean colorlessDamage = damageSource && source.hasKeyword("Colorless Damage Source");
for (final KeywordInterface inst : getKeywords(Keyword.PROTECTION)) {
String kw = inst.getOriginal();
if (kw.equals("Protection from white")) {
if (source.isWhite() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from blue")) {
if (source.isBlue() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from black")) {
if (source.isBlack() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from red")) {
if (source.isRed() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from green")) {
if (source.isGreen() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from all colors")) {
if (!source.isColorless() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from colorless")) {
if (source.isColorless() || colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from everything")) {
return true;
} else if (kw.startsWith("Protection:")) { // uses isValid; Protection:characteristic:desc:exception
final String[] kws = kw.split(":");
String characteristic = kws[1];
if (characteristic.startsWith("Player")) {
// TODO need to handle that better in CardProperty
if (source.getController().isValid(characteristic.split(","), getController(), this, null)) {
return true;
}
} else {
// if damageSource then it does only check damage color..
if (damageSource) {
if (characteristic.endsWith("White") || characteristic.endsWith("Blue")
|| characteristic.endsWith("Black") || characteristic.endsWith("Red")
|| characteristic.endsWith("Green") || characteristic.endsWith("Colorless")
|| characteristic.endsWith("MonoColor") || characteristic.endsWith("MultiColor")) {
characteristic += "Source";
}
}
final String[] characteristics = characteristic.split(",");
final String[] exceptions = kws.length > 3 ? kws[3].split(",") : null; // check "This effect cannot remove sth"
if (source.isValid(characteristics, getController(), this, null)
&& (!checkSBA || exceptions == null || !source.isValid(exceptions, getController(), this, null))) {
return true;
}
}
} else if (kw.startsWith("Protection from opponent of ")) {
final String playerName = kw.substring("Protection from opponent of ".length());
if (source.getController().isOpponentOf(playerName)) {
return true;
}
} else if (kw.startsWith("Protection from ")) {
final String protectType = CardType.getSingularType(kw.substring("Protection from ".length()));
if (source.getType().hasStringType(protectType)) {
return true;
}
}
}
return false;
}
public String getProtectionKey() {
String protectKey = "";
boolean pR = false; boolean pG = false; boolean pB = false; boolean pU = false; boolean pW = false;
@@ -5933,30 +5832,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return true;
}
if (hasProtectionFrom(sa.getHostCard())) {
return false;
}
if (isPhasedOut()) {
return false;
}
final Card source = sa.getHostCard();
if (sa.isSpell()) {
// TODO replace with Static Ability
for (KeywordInterface inst : source.getKeywords()) {
String kw = inst.getOriginal();
if (!kw.startsWith("SpellCantTarget")) {
continue;
}
final String[] k = kw.split(":");
final String[] restrictions = k[1].split(",");
if (isValid(restrictions, source.getController(), source, null)) {
return false;
}
}
}
return true;
}

View File

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

View File

@@ -677,13 +677,13 @@ public class CardFactoryUtil {
}
public static String getProtectionValid(final String kw, final boolean damage) {
String validSource = "Card."; // TODO extend for Emblem too
String validSource = "";
if (kw.startsWith("Protection:")) {
final String[] kws = kw.split(":");
String characteristic = kws[1];
if (characteristic.startsWith("Player")) {
validSource += "ControlledBy " + characteristic;
validSource = "ControlledBy " + characteristic;
} else {
if (damage && (characteristic.endsWith("White") || characteristic.endsWith("Blue")
|| characteristic.endsWith("Black") || characteristic.endsWith("Red")
@@ -691,35 +691,38 @@ public class CardFactoryUtil {
|| characteristic.endsWith("MonoColor") || characteristic.endsWith("MultiColor"))) {
characteristic += "Source";
}
validSource = characteristic;
return characteristic;
}
} else if (kw.startsWith("Protection from ")) {
String protectType = kw.substring("Protection from ".length());
if (protectType.equals("white")) {
validSource += "White" + (damage ? "Source" : "");
validSource = "White" + (damage ? "Source" : "");
} else if (protectType.equals("blue")) {
validSource += "Blue" + (damage ? "Source" : "");
validSource = "Blue" + (damage ? "Source" : "");
} else if (protectType.equals("black")) {
validSource += "Black" + (damage ? "Source" : "");
validSource = "Black" + (damage ? "Source" : "");
} else if (protectType.equals("red")) {
validSource += "Red" + (damage ? "Source" : "");
validSource = "Red" + (damage ? "Source" : "");
} else if (protectType.equals("green")) {
validSource += "Green" + (damage ? "Source" : "");
validSource = "Green" + (damage ? "Source" : "");
} else if (protectType.equals("colorless")) {
validSource += "Colorless" + (damage ? "Source" : "");
validSource = "Colorless" + (damage ? "Source" : "");
} else if (protectType.equals("all colors")) {
validSource += "nonColorless" + (damage ? "Source" : "");
validSource = "nonColorless" + (damage ? "Source" : "");
} else if (protectType.equals("everything")) {
validSource = "";
return "";
} else if (protectType.startsWith("opponent of ")) {
final String playerName = protectType.substring("opponent of ".length());
validSource += "ControlledBy Player.OpponentOf PlayerNamed_" + playerName;
validSource = "ControlledBy Player.OpponentOf PlayerNamed_" + playerName;
} else {
validSource = CardType.getSingularType(protectType);
}
}
if (validSource.isEmpty()) {
return validSource;
}
return "Card." + validSource + ",Emblem." + validSource;
}
public static ReplacementEffect makeEtbCounter(final String kw, final CardState card, final boolean intrinsic) {
String parse = kw;
@@ -1290,13 +1293,12 @@ public class CardFactoryUtil {
triggers.add(haunterETB);
}
// First, create trigger that runs when the haunter goes to the
// graveyard
// First, create trigger that runs when the haunter goes to the graveyard
final StringBuilder sbHaunter = new StringBuilder();
sbHaunter.append("Mode$ ChangesZone | Origin$ ");
sbHaunter.append(card.isCreature() ? "Battlefield" : "Stack");
sbHaunter.append(card.isCreature() ? "Battlefield" : "Stack | ResolvedCard$ True");
sbHaunter.append(" | Destination$ Graveyard | ValidCard$ Card.Self");
sbHaunter.append(" | Static$ True | Secondary$ True | TriggerDescription$ Blank");
sbHaunter.append(" | Secondary$ True | TriggerDescription$ " + inst.getReminderText());
final Trigger haunterDies = TriggerHandler.parseTrigger(sbHaunter.toString(), card, intrinsic);
@@ -2965,7 +2967,7 @@ public class CardFactoryUtil {
final String manacost = k[1];
StringBuilder sb = new StringBuilder();
sb.append("AB$ PutCounter| Cost$ ").append(manacost);
sb.append("AB$ PutCounter | Cost$ ").append(manacost);
sb.append(" | PrecostDesc$ Level Up | CostDesc$ ").append(ManaCostParser.parse(manacost));
sb.append(" | SorcerySpeed$ True | LevelUp$ True | CounterNum$ 1 | CounterType$ LEVEL");
if (card.hasSVar("maxLevel")) {
@@ -3398,8 +3400,6 @@ public class CardFactoryUtil {
public static void addStaticAbility(final KeywordInterface inst, final CardState state, final boolean intrinsic) {
String keyword = inst.getOriginal();
String effect = null;
Map<String, String> svars = Maps.newHashMap();
if (keyword.startsWith("Affinity")) {
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("| Description$ Affinity for ").append(desc);
sb.append(" (").append(inst.getReminderText()).append(")");
effect = sb.toString();
String effect = sb.toString();
svars.put("AffinityX", "Count$Valid " + t + ".YouCtrl");
StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic);
st.setSVar("AffinityX", "Count$Valid " + t + ".YouCtrl");
inst.addStaticAbility(st);
} else if (keyword.equals("Changeling")) {
effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self" +
String effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self" +
" | CharacteristicDefining$ True | AddAllCreatureTypes$ True | Secondary$ True" +
" | Description$ Changeling (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Cipher")) {
StringBuilder sb = new StringBuilder();
sb.append("Mode$ Continuous | EffectZone$ Exile | Affected$ Card.EncodedWithSource");
sb.append(" | AddTrigger$ CipherTrigger");
sb.append(" | Description$ Cipher (").append(inst.getReminderText()).append(")");
effect = sb.toString();
String effect = sb.toString();
sb = new StringBuilder();
@@ -3445,8 +3449,12 @@ public class CardFactoryUtil {
String ab = "DB$ Play | Defined$ OriginalHost | WithoutManaCost$ True | CopyCard$ True";
svars.put("CipherTrigger", trig);
svars.put("PlayEncoded", ab);
StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic);
st.setSVar("CipherTrigger", trig);
st.setSVar("PlayEncoded", ab);
inst.addStaticAbility(st);
} else if (keyword.startsWith("Class")) {
final String[] k = keyword.split(":");
final String level = k[1];
@@ -3475,24 +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) {
effect += " | Description$ " + desc.toString();
}
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Dash")) {
effect = "Mode$ Continuous | Affected$ Card.Self+dashed | AddKeyword$ Haste";
String effect = "Mode$ Continuous | Affected$ Card.Self+dashed | AddKeyword$ Haste";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Daybound")) {
effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Daybound | Secondary$ True | Description$ This permanent can't be transformed except by its daybound ability.";
String effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Daybound | Secondary$ True | Description$ This permanent can't be transformed except by its daybound ability.";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Decayed")) {
effect = "Mode$ Continuous | Affected$ Card.Self | AddHiddenKeyword$ CARDNAME can't block. | " +
String effect = "Mode$ Continuous | Affected$ Card.Self | AddHiddenKeyword$ CARDNAME can't block. | " +
"Secondary$ True";
svars.put("SacrificeEndCombat", "True");
StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic);
st.setSVar("SacrificeEndCombat", "True");
inst.addStaticAbility(st);
} else if (keyword.equals("Defender")) {
effect = "Mode$ CantAttack | ValidCard$ Card.Self | DefenderKeyword$ True | Secondary$ True";
String effect = "Mode$ CantAttack | ValidCard$ Card.Self | DefenderKeyword$ True | Secondary$ True";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Devoid")) {
effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self" +
String effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self" +
" | CharacteristicDefining$ True | SetColor$ Colorless | Secondary$ True" +
" | Description$ Devoid (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Escalate")) {
final String[] k = keyword.split(":");
final String manacost = k[1];
@@ -3506,15 +3522,18 @@ public class CardFactoryUtil {
}
sb.append(cost.toSimpleString());
effect = "Mode$ RaiseCost | ValidCard$ Card.Self | Type$ Spell | Secondary$ True"
String effect = "Mode$ RaiseCost | ValidCard$ Card.Self | Type$ Spell | Secondary$ True"
+ " | Amount$ Escalate | Cost$ "+ manacost +" | EffectZone$ All"
+ " | Description$ " + sb.toString() + " (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Fear")) {
effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+nonBlack | Secondary$ True " +
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+nonBlack | Secondary$ True " +
" | Description$ Fear ( " + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Flying")) {
effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutFlying+withoutReach | Secondary$ True " +
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutFlying+withoutReach | Secondary$ True " +
" | Description$ Flying ( " + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Hexproof")) {
final StringBuilder sbDesc = new StringBuilder("Hexproof");
final StringBuilder sbValid = new StringBuilder();
@@ -3526,55 +3545,84 @@ public class CardFactoryUtil {
sbValid.append("| ValidSource$ ").append(k[1]);
}
effect = "Mode$ CantTarget | Hexproof$ True | ValidCard$ Card.Self | Secondary$ True"
String effect = "Mode$ CantTarget | Hexproof$ True | ValidCard$ Card.Self | Secondary$ True"
+ sbValid.toString() + " | Activator$ Opponent | Description$ "
+ sbDesc.toString() + " (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Horsemanship")) {
effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutHorsemanship | Secondary$ True " +
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.withoutHorsemanship | Secondary$ True " +
" | Description$ Horsemanship ( " + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Intimidate")) {
effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+notSharesColorWith | Secondary$ True " +
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.nonArtifact+notSharesColorWith | Secondary$ True " +
" | Description$ Intimidate ( " + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Nightbound")) {
effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Nightbound | Secondary$ True | Description$ This permanent can't be transformed except by its nightbound ability.";
String effect = "Mode$ CantTransform | ValidCard$ Creature.Self | ExceptCause$ SpellAbility.Nightbound | Secondary$ True | Description$ This permanent can't be transformed except by its nightbound ability.";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.startsWith("Protection")) {
String valid = getProtectionValid(keyword, false);
effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self ";
// Block
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Secondary$ True ";
String desc = "Protection ( " + inst.getReminderText() + ")";
if (!valid.isEmpty()) {
effect += "| ValidBlocker$ " + valid;
}
effect += " | Secondary$ True | Description$ Protection ( " + inst.getReminderText() + ")";
effect += " | Description$ " + desc;
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
// Target
effect = "Mode$ CantTarget | Protection$ True | ValidCard$ Card.Self | Secondary$ True ";
if (!valid.isEmpty()) {
effect += "| ValidSource$ " + valid;
}
effect += " | Description$ " + desc;
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
// Attach
effect = "Mode$ CantAttach | Protection$ True | Target$ Card.Self | Secondary$ True ";
if (!valid.isEmpty()) {
effect += "| ValidCard$ " + valid;
}
// This effect doesn't remove something
if (keyword.startsWith("Protection:")) {
final String[] kws = keyword.split(":");
if (kws.length > 3) {
effect += "| Exceptions$ " + kws[3];
}
}
effect += " | Description$ " + desc;
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Shroud")) {
effect = "Mode$ CantTarget | Shroud$ True | ValidCard$ Card.Self | Secondary$ True"
String effect = "Mode$ CantTarget | Shroud$ True | ValidCard$ Card.Self | Secondary$ True"
+ " | Description$ Shroud (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Skulk")) {
effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.powerGTX | Secondary$ True " +
String effect = "Mode$ CantBlockBy | ValidAttacker$ Creature.Self | ValidBlocker$ Creature.powerGTX | Secondary$ True " +
" | Description$ Skulk ( " + inst.getReminderText() + ")";
svars.put("X", "Count$CardPower");
StaticAbility st = StaticAbility.create(effect, state.getCard(), state, intrinsic);
st.setSVar("X", "Count$CardPower");
inst.addStaticAbility(st);
} else if (keyword.startsWith("Strive")) {
final String[] k = keyword.split(":");
final String manacost = k[1];
effect = "Mode$ RaiseCost | ValidCard$ Card.Self | Type$ Spell | Amount$ Strive | Cost$ "+ manacost +" | EffectZone$ All" +
String effect = "Mode$ RaiseCost | ValidCard$ Card.Self | Type$ Spell | Amount$ Strive | Cost$ "+ manacost +" | EffectZone$ All" +
" | Description$ Strive - " + inst.getReminderText();
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Unleash")) {
effect = "Mode$ Continuous | Affected$ Card.Self+counters_GE1_P1P1 | AddHiddenKeyword$ CARDNAME can't block.";
String effect = "Mode$ Continuous | Affected$ Card.Self+counters_GE1_P1P1 | AddHiddenKeyword$ CARDNAME can't block.";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("Undaunted")) {
effect = "Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Secondary$ True"
String effect = "Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Secondary$ True"
+ "| Amount$ Undaunted | EffectZone$ All | Description$ Undaunted (" + inst.getReminderText() + ")";
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
} else if (keyword.equals("MayFlashSac")) {
effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self | Secondary$ True | MayPlay$ True"
String effect = "Mode$ Continuous | EffectZone$ All | Affected$ Card.Self | Secondary$ True | MayPlay$ True"
+ " | MayPlayNotSorcerySpeed$ True | MayPlayWithFlash$ True | MayPlayText$ Sacrifice at the next cleanup step"
+ " | AffectedZone$ Exile,Graveyard,Hand,Library,Stack | Description$ " + inst.getReminderText();
}
if (effect != null) {
StaticAbility st = new StaticAbility(effect, state.getCard(), state);
st.setIntrinsic(intrinsic);
for (Map.Entry<String, String> e : svars.entrySet()) {
st.setSVar(e.getKey(), e.getValue());
}
inst.addStaticAbility(st);
inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic));
}
}

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) {
return new Predicate<Card>() {
@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()) {
return true;
}

View File

@@ -529,8 +529,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.LifeAmount, lifeGain);
runParams.put(AbilityKey.Source, source);
runParams.put(AbilityKey.SourceSA, sa);
@@ -602,8 +601,7 @@ public class Player extends GameEntity implements Comparable<Player> {
lifeLostThisTurn += toLose;
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.LifeAmount, toLose);
runParams.put(AbilityKey.FirstTime, firstLost);
game.getTriggerHandler().runTrigger(TriggerType.LifeLost, runParams, false);
@@ -630,8 +628,7 @@ public class Player extends GameEntity implements Comparable<Player> {
loseLife(lifePayment, false, false);
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.LifeAmount, lifePayment);
game.getTriggerHandler().runTrigger(TriggerType.PayLife, runParams, false);
@@ -1097,71 +1094,8 @@ public class Player extends GameEntity implements Comparable<Player> {
return false;
}
return !hasProtectionFrom(sa.getHostCard());
}
public boolean hasProtectionFromDamage(final Card source) {
return hasProtectionFrom(source, false, true);
}
@Override
public boolean hasProtectionFrom(final Card source, final boolean checkSBA) {
return hasProtectionFrom(source, checkSBA, false);
}
public boolean hasProtectionFrom(final Card source, final boolean checkSBA, final boolean damageSource) {
final boolean colorlessDamage = damageSource && source.hasKeyword("Colorless Damage Source");
for (KeywordInterface ki : keywords) {
String kw = ki.getOriginal();
if (kw.startsWith("Protection")) {
if (kw.startsWith("Protection:")) { // uses isValid
final String characteristic = kw.split(":")[1];
if (characteristic.startsWith("Player")) {
// Protection:PlayerUID
if (source.getController().isValid(characteristic, this, null, null)) {
return true;
}
} else {
final String[] characteristics = characteristic.split(",");
if (source.isValid(characteristics, this, null, null)) {
return true;
}
}
} else if (kw.equals("Protection from white")) {
if (source.isWhite() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from blue")) {
if (source.isBlue() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from black")) {
if (source.isBlack() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from red")) {
if (source.isRed() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from green")) {
if (source.isGreen() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from all colors")) {
if (!source.isColorless() && !colorlessDamage) {
return true;
}
} else if (kw.equals("Protection from everything")) {
return true;
} else if (kw.startsWith("Protection from ")) {
final String protectType = CardType.getSingularType(kw.substring("Protection from ".length()));
if (source.getType().hasStringType(protectType)) {
return true;
}
}
}
}
return false;
}
public void surveil(int num, SpellAbility cause, CardZoneTable table) {
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
@@ -1212,8 +1146,7 @@ public class Player extends GameEntity implements Comparable<Player> {
getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave));
surveilThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.NumThisTurn, surveilThisTurn);
getGame().getTriggerHandler().runTrigger(TriggerType.Surveil, runParams, false);
}
@@ -1329,7 +1262,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
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
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
runParams.put(AbilityKey.Card, c);
runParams.put(AbilityKey.Number, numDrawnThisTurn);
runParams.put(AbilityKey.Player, this);
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();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Card, c);
runParams.put(AbilityKey.Cause, cause);
runParams.put(AbilityKey.IsMadness, discardMadness);
@@ -1564,8 +1495,7 @@ public class Player extends GameEntity implements Comparable<Player> {
public final void addTokensCreatedThisTurn(Card token) {
numTokenCreatedThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Num, numTokenCreatedThisTurn);
runParams.put(AbilityKey.Card, token);
game.getTriggerHandler().runTrigger(TriggerType.TokenCreated, runParams, false);
@@ -1581,8 +1511,7 @@ public class Player extends GameEntity implements Comparable<Player> {
public final void addForetoldThisTurn() {
numForetoldThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Num, numForetoldThisTurn);
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));
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Source, sa);
game.getTriggerHandler().runTrigger(TriggerType.Shuffled, runParams, false);
@@ -2007,12 +1935,7 @@ public class Player extends GameEntity implements Comparable<Player> {
return getOutcome().lossState != null;
}
// Rule 704.5a - If a player has 0 or less life, he or she loses the game.
final boolean hasNoLife = getLife() <= 0;
if (hasNoLife && !cantLoseForZeroOrLessLife()) {
return loseConditionMet(GameLossReason.LifeReachedZero, null);
}
// check this first because of Lich's Mirror (704.7)
// Rule 704.5b - If a player attempted to draw a card from a library with no cards in it
// since the last time state-based actions were checked, he or she loses the game.
if (triedToDrawFromEmptyLibrary) {
@@ -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.
if (getCounters(CounterEnumType.POISON) >= 10) {
return loseConditionMet(GameLossReason.Poisoned, null);
@@ -2244,8 +2173,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
public final void addInvestigatedThisTurn() {
investigatedThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Num, investigatedThisTurn);
game.getTriggerHandler().runTrigger(TriggerType.Investigated, runParams, false);
}
@@ -2265,10 +2193,9 @@ public class Player extends GameEntity implements Comparable<Player> {
sacrificedThisTurn.add(cpy);
// 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)
runParams.put(AbilityKey.Card, cpy);
runParams.put(AbilityKey.Player, this);
runParams.put(AbilityKey.Cause, source);
runParams.put(AbilityKey.CostStack, game.costPaymentStack);
runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek());
@@ -3513,8 +3440,7 @@ public class Player extends GameEntity implements Comparable<Player> {
// Change this if something would make multiple player learn at the same time
// Discard Trigger outside Effect
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, this);
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Cards, new CardCollection(c));
runParams.put(AbilityKey.Cause, sa);
runParams.put(AbilityKey.FirstTime, firstDiscard);

View File

@@ -11,7 +11,7 @@ public class PlayerFactoryUtil {
public static void addStaticAbility(final KeywordInterface inst, final Player player) {
String keyword = inst.getOriginal();
String effect = null;
if (keyword.startsWith("Hexproof")) {
final StringBuilder sbDesc = new StringBuilder("Hexproof");
final StringBuilder sbValid = new StringBuilder();
@@ -23,18 +23,40 @@ public class PlayerFactoryUtil {
sbValid.append("| ValidSource$ ").append(k[1]);
}
effect = "Mode$ CantTarget | Hexproof$ True | ValidPlayer$ Player.You | Secondary$ True "
String effect = "Mode$ CantTarget | Hexproof$ True | ValidPlayer$ Player.You | Secondary$ True "
+ sbValid.toString() + " | Activator$ Opponent | EffectZone$ Command | Description$ "
+ sbDesc.toString() + " (" + inst.getReminderText() + ")";
} else if (keyword.equals("Shroud")) {
effect = "Mode$ CantTarget | Shroud$ True | ValidPlayer$ Player.You | Secondary$ True "
+ "| EffectZone$ Command | Description$ Shroud (" + inst.getReminderText() + ")";
}
if (effect != null) {
final Card card = player.getKeywordCard();
StaticAbility st = new StaticAbility(effect, card, card.getCurrentState());
st.setIntrinsic(false);
inst.addStaticAbility(st);
inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false));
} else if (keyword.equals("Shroud")) {
String effect = "Mode$ CantTarget | Shroud$ True | ValidPlayer$ Player.You | Secondary$ True "
+ "| EffectZone$ Command | Description$ Shroud (" + inst.getReminderText() + ")";
final Card card = player.getKeywordCard();
inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false));
} else if (keyword.startsWith("Protection")) {
String valid = CardFactoryUtil.getProtectionValid(keyword, false);
String effect = "Mode$ CantTarget | Protection$ True | ValidCard$ Player.You | Secondary$ True ";
if (!valid.isEmpty()) {
effect += "| ValidSource$ " + valid;
}
final Card card = player.getKeywordCard();
inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false));
// Attach
effect = "Mode$ CantAttach | Protection$ True | Target$ Player.You | Secondary$ True ";
if (!valid.isEmpty()) {
effect += "| ValidCard$ " + valid;
}
// This effect doesn't remove something
if (keyword.startsWith("Protection:")) {
final String[] kws = keyword.split(":");
if (kws.length > 3) {
effect += "| Exceptions$ " + kws[3];
}
}
inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false));
}
}

View File

@@ -504,7 +504,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
public boolean isCycling() {
return this.isAlternativeCost(AlternativeCost.Cycling);
return isAlternativeCost(AlternativeCost.Cycling);
}
public boolean isBoast() {
@@ -515,6 +515,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return this.hasParam("Ninjutsu");
}
public boolean isEpic() {
AbilitySub sub = this.getSubAbility();
while (sub != null && !sub.hasParam("Epic")) {
sub = sub.getSubAbility();
}
return sub != null && sub.hasParam("Epic");
}
// If this is not null, then ability was made in a factory
public ApiType getApi() {
return api;
@@ -777,7 +785,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public void resetOnceResolved() {
//resetPaidHash(); // FIXME: if uncommented, breaks Dragon Presence, e.g. Orator of Ojutai + revealing a Dragon from hand.
// Is it truly necessary at this point? The paid hash seems to be reset on all SA instance operations.
// Epic spell keeps original targets
if (!isEpic()) {
resetTargets();
}
resetTriggeringObjects();
resetTriggerRemembered();

View File

@@ -234,6 +234,12 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
this(parseParams(params, host), host, state);
}
public static StaticAbility create(final String params, final Card host, CardState state, boolean intrinsic) {
StaticAbility st = new StaticAbility(params, state.getCard(), state);
st.setIntrinsic(intrinsic);
return st;
}
/**
* Instantiates a new static ability.
*
@@ -325,8 +331,6 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
return StaticAbilityCantAttackBlock.applyCantAttackAbility(this, card, target);
} else if (mode.equals("CantBlockBy") && target instanceof Card) {
return StaticAbilityCantAttackBlock.applyCantBlockByAbility(this, card, (Card)target);
} else if (mode.equals("CantAttach")) {
return StaticAbilityCantAttach.applyCantAttachAbility(this, card, target);
} else if (mode.equals("CanAttackIfHaste")) {
return StaticAbilityCantAttackBlock.applyCanAttackHasteAbility(this, card, target);
}

View File

@@ -2,10 +2,29 @@ package forge.game.staticability;
import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.zone.ZoneType;
public class StaticAbilityCantAttach {
public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target) {
static String MODE = "CantAttach";
public static boolean cantAttach(final GameEntity target, final Card card, boolean checkSBA) {
// CantTarget static abilities
for (final Card ca : target.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.getParam("Mode").equals(MODE) || stAb.isSuppressed() || !stAb.checkConditions()) {
continue;
}
if (applyCantAttachAbility(stAb, card, target, checkSBA)) {
return true;
}
}
}
return false;
}
public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target, boolean checkSBA) {
if (!stAb.matchesValidParam("ValidCard", card)) {
return false;
}
@@ -25,6 +44,10 @@ public class StaticAbilityCantAttach {
}
}
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);
}
StaticAbility stat = new StaticAbility(s, affectedCard, stAb.getCardState());
stat.setIntrinsic(false);
addedStaticAbility.add(stat);
addedStaticAbility.add(StaticAbility.create(s, affectedCard, stAb.getCardState(), false));
}
}

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
if (hasParam("CheckOnTriggeredCard")) {
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();
System.err.println(str + sp.getAllTargetChoices());
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) {
return true;
}
if (!sa.isTargetNumberValid()) {
return false;
}
return hasLegalTargeting(sa.getSubAbility(), source);
return hasLegalTargeting(sa.getSubAbility());
}
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"),
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 String zoneName;

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ Name:Eternal Dominion
ManaCost:7 U U U
Types:Sorcery
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).
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.)

View File

@@ -2,7 +2,7 @@ Name:Gisa, Glorious Resurrector
ManaCost:2 B B
Types:Legendary Creature Human Wizard
PT:4/4
R:Event$ Moved | ActiveZones$ Battlefield | CheckSelfLKIZone$ True | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.OppCtrl | ReplaceWith$ Exile | Description$ If a creature an opponent controls would die, exile it instead.
R:Event$ Moved | ActiveZones$ Battlefield | CheckSelfLKIZone$ True | Origin$ Battlefield | Destination$ Graveyard | ValidLKI$ Creature.OppCtrl | ReplaceWith$ Exile | Description$ If a creature an opponent controls would die, exile it instead.
SVar:Exile:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Exile | Defined$ ReplacedCard
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | IsPresent$ Card.ExiledWithSource | PresentZone$ Exile | Execute$ TrigChange | TriggerDescription$ At the beginning of your upkeep, put all creature cards exiled with CARDNAME onto the battlefield under your control. They gain decayed. (A creature with decayed can't block. When it attacks, sacrifice it at end of combat.)
SVar:TrigChange:DB$ ChangeZoneAll | ChangeType$ Card.ExiledWithSource | Origin$ Exile | Destination$ Battlefield | GainControl$ True | RememberChanged$ True | SubAbility$ DBPump

View File

@@ -1,7 +1,7 @@
Name:Hardened Scales
ManaCost:G
Types:Enchantment
R:Event$ AddCounter | ActiveZones$ Battlefield | ValidCard$ Creature.YouCtrl+InZoneBattlefield | ValidCounterType$ P1P1 | ReplaceWith$ AddOneMoreCounters | Description$ If one or more +1/+1 counters would be put on a creature you control, that many plus one +1/+1 counters are put on it instead.
R:Event$ AddCounter | ActiveZones$ Battlefield | ValidCard$ Creature.YouCtrl+inZoneBattlefield | ValidCounterType$ P1P1 | ReplaceWith$ AddOneMoreCounters | Description$ If one or more +1/+1 counters would be put on a creature you control, that many plus one +1/+1 counters are put on it instead.
SVar:AddOneMoreCounters:DB$ ReplaceCounter | ValidCounterType$ P1P1 | ChooseCounter$ True | Amount$ X
SVar:X:ReplaceCount$CounterNum/Plus.1
AI:RemoveDeck:Random

View File

@@ -1,7 +1,7 @@
Name:Rite of Flame
ManaCost:R
Types:Sorcery
A:SP$ Mana | Produced$ R | Amount$ 2 | AILogic$ ManaRitual | AINoRecursiveCheck$ True | SpellDescription$ Add {R}{R}, then add {R} for each card named CARDNAME in each graveyard.
A:SP$ Mana | Produced$ R | Amount$ 2 | AILogic$ ManaRitual | AINoRecursiveCheck$ True | SubAbility$ SubMana | SpellDescription$ Add {R}{R}, then add {R} for each card named CARDNAME in each graveyard.
SVar:SubMana:DB$ Mana | Produced$ R | Amount$ X
SVar:X:Count$NamedInAllYards.Rite of Flame
DeckHints:Name$Rite of Flame

View File

@@ -2,5 +2,5 @@ Name:Submerged Boneyard
ManaCost:no cost
Types:Land
K:CARDNAME enters the battlefield tapped.
A:AB$ Mana | Cost$ T | Produced$ Combo U | SpellDescription$ Add {U} or {B}.
A:AB$ Mana | Cost$ T | Produced$ Combo U B | SpellDescription$ Add {U} or {B}.
Oracle:Submerged Boneyard enters the battlefield tapped.\n{T}: Add {U} or {B}.

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Vampire Warlock
PT:1/1
K:Menace
K:Lifelink
R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Creature+nonToken+OppOwn | ReplaceWith$ Exile | Description$ If a nontoken creature an opponent controls would die, exile it instead. When you do, you may pay {2}. If you do, create a 1/1 black and green Pest creature token with "When this creature dies, you gain 1 life."
R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | Destination$ Graveyard | ValidLKI$ Card.Creature+nonToken+OppOwn | ReplaceWith$ Exile | Description$ If a nontoken creature an opponent controls would die, exile it instead. When you do, you may pay {2}. If you do, create a 1/1 black and green Pest creature token with "When this creature dies, you gain 1 life."
SVar:Exile:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Exile | Defined$ ReplacedCard | SubAbility$ DBImmediateTrigger
SVar:DBImmediateTrigger:DB$ ImmediateTrigger | Execute$ TrigToken | TriggerDescription$ If a nontoken creature an opponent controls would die, exile it instead. When you do, you may pay {2}. If you do, create a 1/1 black and green Pest creature token with "When this creature dies, you gain 1 life."
SVar:TrigToken:AB$ Token | Cost$ 2 | TokenAmount$ 1 | TokenScript$ bg_1_1_pest_lifegain | TokenOwner$ You

View File

@@ -3,7 +3,7 @@ ManaCost:4 B B
Types:Creature Horror
PT:4/5
K:Trample
R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.Other | ReplaceWith$ Exile | Description$ If another creature would die, exile it instead.
R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | Destination$ Graveyard | ValidLKI$ Creature.Other | ReplaceWith$ Exile | Description$ If another creature would die, exile it instead.
SVar:Exile:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Exile | Defined$ ReplacedCard | SubAbility$ DBRemember | RememberChanged$ True
SVar:DBRemember:DB$ Pump | ConditionDefined$ Remembered | ConditionPresent$ Card.inZoneExile | ConditionCompare$ GE1
T:Mode$ ChangesZone | Origin$ Exile | Destination$ Any | Static$ True | ValidCard$ Card.IsRemembered+ExiledWithSource | Execute$ DBForget

View File

@@ -3,7 +3,7 @@ ManaCost:1
Types:Legendary Artifact Creature Insect
PT:0/0
K:Modular:1
R:Event$ AddCounter | ActiveZones$ Battlefield | ValidCause$ Triggered.Modular | ValidCard$ Creature.YouCtrl+InZoneBattlefield | ValidCounterType$ P1P1 | ReplaceWith$ AddOneMoreCounter | Description$ If a modular triggered ability would put one or more +1/+1 counters on a creature you control, that many plus one +1/+1 counters are put on it instead.
R:Event$ AddCounter | ActiveZones$ Battlefield | ValidCause$ Triggered.Modular | ValidCard$ Creature.YouCtrl+inZoneBattlefield | ValidCounterType$ P1P1 | ReplaceWith$ AddOneMoreCounter | Description$ If a modular triggered ability would put one or more +1/+1 counters on a creature you control, that many plus one +1/+1 counters are put on it instead.
SVar:AddOneMoreCounter:DB$ ReplaceCounter | ValidCounterType$ P1P1 | ChooseCounter$ True | Amount$ X
SVar:X:ReplaceCount$CounterNum/Plus.1
A:AB$ Destroy | Cost$ R | ValidTgts$ Artifact.YouCtrl | TgtPrompt$ Choose target artifact you control | SpellDescription$ Destroy target artifact you control.