From 3d634b6dac7326263fa4ae31cf53f4df81a8acc0 Mon Sep 17 00:00:00 2001 From: tool4ever Date: Mon, 4 Jul 2022 15:27:44 +0200 Subject: [PATCH] Global DamageHistory (#622) * Global DamageHistory * Add new cards * Improve Ogre Enforcer * Clean up * Minor fixes --- .../src/main/java/forge/ai/GameState.java | 4 +- .../java/forge/ai/simulation/GameCopier.java | 9 ++ forge-game/src/main/java/forge/game/Game.java | 51 ++++++++- .../src/main/java/forge/game/GameAction.java | 36 ++---- .../main/java/forge/game/GameActionUtil.java | 1 - .../src/main/java/forge/game/GameEntity.java | 29 +++++ .../forge/game/ability/AbilityFactory.java | 4 +- .../java/forge/game/ability/AbilityUtils.java | 46 ++++---- .../game/ability/effects/CounterEffect.java | 5 +- .../game/ability/effects/PlayEffect.java | 1 + .../src/main/java/forge/game/card/Card.java | 103 ++++-------------- .../forge/game/card/CardDamageHistory.java | 98 +++++++---------- .../java/forge/game/card/CardProperty.java | 91 +++++++++------- .../main/java/forge/game/card/CardUtil.java | 3 +- .../main/java/forge/game/player/Player.java | 90 +++------------ .../forge/game/player/PlayerProperty.java | 96 +++++++--------- .../game/spellability/AbilityManaPart.java | 4 +- .../spellability/SpellAbilityRestriction.java | 8 +- .../game/trigger/TriggerChangesZone.java | 2 +- .../forge/game/trigger/TriggerDamageDone.java | 2 +- .../cardsfolder/a/admiral_beckett_brass.txt | 2 +- .../res/cardsfolder/b/blazing_sunsteel.txt | 2 +- forge-gui/res/cardsfolder/b/blinding_beam.txt | 2 +- .../res/cardsfolder/b/bloodcrazed_goblin.txt | 2 +- .../res/cardsfolder/d/discordant_spirit.txt | 2 +- .../res/cardsfolder/g/gales_redirection.txt | 4 +- forge-gui/res/cardsfolder/g/glen_elendra.txt | 2 +- .../cardsfolder/g/grothama_all_devouring.txt | 2 +- .../res/cardsfolder/h/hope_of_ghirapur.txt | 2 +- forge-gui/res/cardsfolder/i/inferno_trap.txt | 4 +- ...umano_faces_kakkazan_etching_of_kumano.txt | 2 +- .../res/cardsfolder/k/kumanos_blessing.txt | 2 +- .../res/cardsfolder/l/loxodon_lifechanter.txt | 2 +- .../cardsfolder/n/nothing_can_stop_me_now.txt | 2 +- .../res/cardsfolder/n/notorious_throng.txt | 2 +- forge-gui/res/cardsfolder/r/runesword.txt | 2 +- forge-gui/res/cardsfolder/s/simulacrum.txt | 2 +- .../res/cardsfolder/s/skarrgan_firebird.txt | 2 +- .../res/cardsfolder/s/skirk_alarmist.txt | 2 +- .../res/cardsfolder/t/touch_of_moonglove.txt | 2 +- .../u/unscythe_killer_of_kings.txt | 2 +- .../cardsfolder/upcoming/dragon_cultist.txt | 9 ++ .../cardsfolder/upcoming/gnoll_war_band.txt | 9 ++ .../cardsfolder/upcoming/indulge_excess.txt | 22 ++++ forge-gui/res/cardsfolder/w/war_elemental.txt | 2 +- forge-gui/res/cardsfolder/w/wicked_akuba.txt | 2 +- 46 files changed, 359 insertions(+), 414 deletions(-) create mode 100644 forge-gui/res/cardsfolder/upcoming/dragon_cultist.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/gnoll_war_band.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/indulge_excess.txt diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index b7336ca66e8..40c0d4127ec 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -206,7 +206,7 @@ public abstract class GameState { cardsReferencedByID.add(card.getExiledWith()); } if (zone == ZoneType.Battlefield) { - if (!card.getAttachedCards().isEmpty()) { + if (card.hasCardAttachments()) { // Remember the ID of cards that have attachments cardsReferencedByID.add(card); } @@ -375,7 +375,7 @@ public abstract class GameState { newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ",")); } - if (!c.getMergedCards().isEmpty()) { + if (c.hasMergedCard()) { List mergedCardNames = new ArrayList<>(); for (Card merged : c.getMergedCards()) { if (c.getTopMergedCard() == merged) { diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java index 4e4b73fdb58..0ab07f2b306 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java @@ -91,6 +91,7 @@ public class GameCopier { newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn()); newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn()); newPlayer.setLifeGainedThisTurn(origPlayer.getLifeGainedThisTurn()); + // TODO creatureAttackedThisTurn for (Mana m : origPlayer.getManaPool()) { newPlayer.getManaPool().addMana(m, false); } @@ -207,6 +208,13 @@ public class GameCopier { private void copyGameState(Game newGame) { newGame.setAge(origGame.getAge()); + + // TODO countersAddedThisTurn + + if (origGame.getMonarch() != null) { + newGame.setMonarch(playerMap.get(origGame.getMonarch())); + } + for (ZoneType zone : ZONES) { for (Card card : origGame.getCardsIn(zone)) { addCard(newGame, zone, card); @@ -300,6 +308,7 @@ public class GameCopier { newCard.setPTCharacterDefiningTable(c.getSetPTCharacterDefiningTable()); newCard.setPTBoost(c.getPTBoostTable()); + // TODO copy by map newCard.setDamage(c.getDamage()); newCard.setChangedCardColors(c.getChangedCardColorsTable()); diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 2c0e6295e0c..a8fda242d85 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -47,6 +48,7 @@ import forge.game.ability.AbilityKey; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; +import forge.game.card.CardDamageHistory; import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CardUtil; @@ -78,6 +80,7 @@ import forge.trackable.Tracker; import forge.util.Aggregates; import forge.util.MyRandom; import forge.util.Visitor; +import forge.util.collect.FCollection; /** * Represents the state of a single game, a new instance is created for each game. @@ -119,6 +122,9 @@ public class Game { private Table>> countersAddedThisTurn = HashBasedTable.create(); + private FCollection globalDamageHistory = new FCollection<>(); + private IdentityHashMap, Pair> damageThisTurnLKI = new IdentityHashMap<>(); + private Map topLibsCast = Maps.newHashMap(); private Map facedownWhileCasting = Maps.newHashMap(); @@ -251,7 +257,7 @@ public class Game { if (c == null) { return null; } - return changeZoneLKIInfo.containsKey(c.getId()) ? changeZoneLKIInfo.get(c.getId()) : c; + return changeZoneLKIInfo.getOrDefault(c.getId(), c); } public final void clearChangeZoneLKIInfo() { changeZoneLKIInfo.clear(); @@ -1088,6 +1094,7 @@ public class Game { public void onCleanupPhase() { clearCounterAddedThisTurn(); + clearGlobalDamageHistory(); // some cards need this info updated even after a player lost, so don't skip them for (Player player : getRegisteredPlayers()) { player.onCleanupPhase(); @@ -1129,6 +1136,48 @@ public class Game { countersAddedThisTurn.clear(); } + /** + * Gets the damage instances done this turn. + * @param isCombat if true only combat damage matters, pass null for both + * @param anyIsEnough if true returns early once result has an entry + * @param validSourceCard + * @param validTargetEntity + * @param source + * @param sourceController + * @param ctb + * @return List for each source + */ + public List getDamageDoneThisTurn(Boolean isCombat, boolean anyIsEnough, String validSourceCard, String validTargetEntity, Card source, Player sourceController, CardTraitBase ctb) { + final List dmgList = Lists.newArrayList(); + for (CardDamageHistory cdh : globalDamageHistory) { + int dmg = cdh.getDamageDoneThisTurn(isCombat, anyIsEnough, validSourceCard, validTargetEntity, source, sourceController, ctb); + if (dmg == 0) { + continue; + } + + dmgList.add(dmg); + + if (anyIsEnough) { + break; + } + } + + return dmgList; + } + + public void addGlobalDamageHistory(CardDamageHistory cdh, Pair dmg, Card source, GameEntity target) { + globalDamageHistory.add(cdh); + damageThisTurnLKI.put(dmg, Pair.of(source, target)); + } + public void clearGlobalDamageHistory() { + globalDamageHistory.clear(); + damageThisTurnLKI.clear(); + } + + public Pair getDamageLKI(Pair dmg) { + return damageThisTurnLKI.get(dmg); + } + public Card getTopLibForPlayer(Player P) { return topLibsCast.get(P); } diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 3ed875b4cde..d889c460c84 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -27,7 +27,6 @@ import java.util.Set; import forge.util.*; import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Predicate; import com.google.common.collect.ComparisonChain; @@ -1323,33 +1322,13 @@ public class GameAction { noRegCreats.add(c); checkAgain = true; } else if (c.hasKeyword("CARDNAME can't be destroyed by lethal damage unless lethal damage dealt by a single source is marked on it.")) { - // merge entries with same source - List dmgList = Lists.newArrayList(); - List> remainingDamaged = Lists.newArrayList(c.getReceivedDamageFromThisTurn()); - while (!remainingDamaged.isEmpty()) { - Pair damaged = remainingDamaged.get(0); - int sum = damaged.getRight(); - remainingDamaged.remove(damaged); - for (Pair other : Lists.newArrayList(remainingDamaged)) { - if (other.getLeft().equalsWithTimestamp(damaged.getLeft())) { - sum += other.getRight(); - // once it got counted keep it out - remainingDamaged.remove(other); - } - } - dmgList.add(sum); - } - - for (final Integer dmg : dmgList) { - if (c.getLethal() <= dmg.intValue() || c.hasBeenDealtDeathtouchDamage()) { - if (desCreats == null) { - desCreats = new CardCollection(); - } - desCreats.add(c); - c.setHasBeenDealtDeathtouchDamage(false); - checkAgain = true; - break; + if (c.getLethal() <= c.getMaxDamageFromSource() || c.hasBeenDealtDeathtouchDamage()) { + if (desCreats == null) { + desCreats = new CardCollection(); } + desCreats.add(c); + c.setHasBeenDealtDeathtouchDamage(false); + checkAgain = true; } } // Rule 704.5g - Destroy due to lethal damage @@ -2364,6 +2343,7 @@ public class GameAction { game.getReplacementHandler().runReplaceDamage(isCombat, damageMap, preventMap, counterTable, cause); Map lethalDamage = Maps.newHashMap(); + Map lkiCache = Maps.newHashMap(); // Actually deal damage according to replaced damage map for (Map.Entry> et : damageMap.rowMap().entrySet()) { @@ -2390,6 +2370,8 @@ public class GameAction { e.setValue(Integer.valueOf(e.getKey().addDamageAfterPrevention(e.getValue(), sourceLKI, isCombat, counterTable))); sum += e.getValue(); + + sourceLKI.getDamageHistory().registerDamage(e.getValue(), isCombat, sourceLKI, e.getKey(), lkiCache); } if (sum > 0 && sourceLKI.hasKeyword(Keyword.LIFELINK)) { diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java index 4711d9fb34f..f7574571163 100644 --- a/forge-game/src/main/java/forge/game/GameActionUtil.java +++ b/forge-game/src/main/java/forge/game/GameActionUtil.java @@ -210,7 +210,6 @@ public final class GameActionUtil { final Cost escapeCost = new Cost(k[1], true); final SpellAbility newSA = sa.copyWithManaCostReplaced(activator, escapeCost); - newSA.setActivatingPlayer(activator); newSA.putParam("PrecostDesc", "Escape—"); newSA.putParam("CostDesc", escapeCost.toString()); diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java index 531bc6e6ecf..f7ba73ef30b 100644 --- a/forge-game/src/main/java/forge/game/GameEntity.java +++ b/forge-game/src/main/java/forge/game/GameEntity.java @@ -17,9 +17,13 @@ */ package forge.game; +import java.util.List; import java.util.Map; +import org.apache.commons.lang3.tuple.Pair; + import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.game.ability.AbilityUtils; @@ -46,6 +50,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { private String name = ""; protected CardCollection attachedCards = new CardCollection(); protected Map counters = Maps.newHashMap(); + protected List> damageReceivedThisTurn = Lists.newArrayList(); protected GameEntity(int id0) { id = id0; @@ -330,6 +335,30 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table); } + public void receiveDamage(Pair dmg) { + damageReceivedThisTurn.add(dmg); + } + + public final int getAssignedDamage() { + return getAssignedDamage(null, null); + } + public final int getAssignedCombatDamage() { + return getAssignedDamage(true, null); + } + public final int getAssignedDamage(Boolean isCombat, final Card source) { + int num = 0; + for (Pair dmg : damageReceivedThisTurn) { + if (isCombat != null && dmg.getRight() != isCombat) { + continue; + } + if (source != null && !getGame().getDamageLKI(dmg).getLeft().equalsWithTimestamp(source)) { + continue; + } + num += dmg.getLeft(); + } + return num; + } + @Override public final boolean equals(Object o) { if (o == null) { return false; } diff --git a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java index cfb843b4e3f..a34a1019d38 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java @@ -327,8 +327,8 @@ public final class AbilityFactory { } private static final TargetRestrictions readTarget(Map mapParams) { - final String min = mapParams.containsKey("TargetMin") ? mapParams.get("TargetMin") : "1"; - final String max = mapParams.containsKey("TargetMax") ? mapParams.get("TargetMax") : "1"; + final String min = mapParams.getOrDefault("TargetMin", "1"); + final String max = mapParams.getOrDefault("TargetMax", "1"); // TgtPrompt should only be needed for more complicated ValidTgts String tgtWhat = mapParams.get("ValidTgts"); diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 18190a19b1c..79bfd9181f8 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -14,6 +14,7 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; @@ -2030,7 +2031,7 @@ public class AbilityUtils { return doXMath(c.getTotalDamageDoneBy(), expr, c, ctb); } if (sq[0].equals("TotalDamageReceivedThisTurn")) { - return doXMath(c.getTotalDamageReceivedThisTurn(), expr, c, ctb); + return doXMath(c.getAssignedDamage(), expr, c, ctb); } if (sq[0].contains("CardPower")) { @@ -2091,13 +2092,6 @@ public class AbilityUtils { return doXMath(c.getTimesMutated(), expr, c, ctb); } - if (sq[0].startsWith("DamageDoneByPlayerThisTurn")) { - int sum = 0; - for (Player p : getDefinedPlayers(c, sq[1], ctb)) { - sum += c.getReceivedDamageByPlayerThisTurn(p); - } - return doXMath(sum, expr, c, ctb); - } if (sq[0].equals("RegeneratedThisTurn")) { return doXMath(c.getRegeneratedThisTurn(), expr, c, ctb); } @@ -2359,20 +2353,28 @@ public class AbilityUtils { return doXMath(player.getOpponentsTotalPoisonCounters(), expr, c, ctb); } - if (sq[0].equals("YourDamageThisTurn")) { - return doXMath(player.getAssignedDamage(), expr, c, ctb); - } - if (sq[0].equals("TotalOppDamageThisTurn")) { - return doXMath(player.getOpponentsAssignedDamage(), expr, c, ctb); - } if (sq[0].equals("MaxOppDamageThisTurn")) { return doXMath(player.getMaxOpponentAssignedDamage(), expr, c, ctb); } - if (sq[0].startsWith("YourDamageSourcesThisTurn")) { - Iterable allSrc = player.getAssignedDamageSources(); - String restriction = sq[0].split(" ")[1]; - return doXMath(CardLists.getValidCardCount(allSrc, restriction, player, c, ctb), expr, c, ctb); + if (sq[0].contains("TotalDamageThisTurn")) { + String[] props = l[0].split(" "); + int sum = 0; + for (Pair p : c.getDamageReceivedThisTurn()) { + if (game.getDamageLKI(p).getLeft().isValid(props[1], player, c, ctb)) { + sum += p.getLeft(); + } + } + return doXMath(sum, expr, c, ctb); + } + + if (sq[0].contains("DamageThisTurn")) { + String[] props = l[0].split(" "); + Boolean isCombat = null; + if (sq[0].contains("CombatDamage")) { + isCombat = true; + } + return doXMath(game.getDamageDoneThisTurn(isCombat, false, props[1], props[2], c, player, ctb).size(), expr, c, ctb); } if (sq[0].equals("YourTurns")) { @@ -3325,12 +3327,6 @@ public class AbilityUtils { totDmg += p.getAssignedDamage(); } return doXMath(totDmg, m, source, ctb); - } else if (sq[0].contains("LifeLostThisTurn")) { - int totDmg = 0; - for (Player p : players) { - totDmg += p.getLifeLostThisTurn(); - } - return doXMath(totDmg, m, source, ctb); } if (players.size() > 0) { @@ -3387,7 +3383,7 @@ public class AbilityUtils { if (value.contains("DomainPlayer")) { int n = 0; - final CardCollectionView someCards = player.getCardsIn(ZoneType.Battlefield); + final CardCollectionView someCards = player.getLandsInPlay(); final List basic = MagicColor.Constant.BASIC_LANDS; for (int i = 0; i < basic.size(); i++) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java index 4efbf6744cd..c0fe67d0440 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java @@ -123,6 +123,7 @@ public class CounterEffect extends SpellAbilityEffect { for (final SpellAbility tgtSA : sas) { final Card tgtSACard = tgtSA.getHostCard(); // should remember even that spell cannot be countered, e.g. Dovescape + // TODO use LKI in case the spell gets countered before (else X amounts would be missing) if (sa.hasParam("RememberCounteredCMC")) { sa.getHostCard().addRemembered(Integer.valueOf(tgtSACard.getCMC())); } @@ -152,9 +153,7 @@ public class CounterEffect extends SpellAbilityEffect { } if (sa.hasParam("RememberCountered")) { - if (sa.getParam("RememberCountered").equals("True")) { - sa.getHostCard().addRemembered(tgtSACard); - } + sa.getHostCard().addRemembered(tgtSACard); } if (sa.hasParam("RememberSplicedOntoCounteredSpell")) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java index e7d280ae7e2..2778b62f446 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java @@ -285,6 +285,7 @@ public class PlayEffect extends SpellAbilityEffect { } } + // TODO if cost isn't replaced should include alternative ones // get basic spells (no flashback, etc.) List sas = AbilityUtils.getBasicSpellsFromPlayEffect(tgtCard, controller); if (sa.hasParam("ValidSA")) { diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 7b2390b7a00..801e02076dc 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -180,9 +180,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private final Set rememberedObjects = Sets.newLinkedHashSet(); private Map flipResult; - private List> receivedDamageFromThisTurn = Lists.newArrayList(); - private Map receivedDamageFromPlayerThisTurn = Maps.newHashMap(); - private final Map assignedDamageMap = Maps.newTreeMap(); private boolean isCommander = false; @@ -257,7 +254,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private String oracleText = ""; - private int damage; + private Map damage = Maps.newHashMap(); private boolean hasBeenDealtDeathtouchDamage; // regeneration @@ -951,7 +948,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } devouredCards.add(c); } - public final void clearDevoured() { devouredCards = null; } @@ -982,7 +978,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { delvedCards = null; } - public final CardCollectionView getConvoked() { return CardCollection.getView(convokedCards); } @@ -5292,62 +5287,15 @@ public class Card extends GameEntity implements Comparable, IHasSVars { damageHistory = history; } - public final List> getReceivedDamageFromThisTurn() { - return receivedDamageFromThisTurn; + public List> getDamageReceivedThisTurn() { + return damageReceivedThisTurn; } - public final void setReceivedDamageFromThisTurn(final List> receivedDamageList) { - receivedDamageFromThisTurn = Lists.newArrayList(receivedDamageList); - } - public final Map getReceivedDamageFromPlayerThisTurn() { - return receivedDamageFromPlayerThisTurn; - } - public final void setReceivedDamageFromPlayerThisTurn(final Map receivedDamageMap) { - receivedDamageFromPlayerThisTurn = Maps.newHashMap(receivedDamageMap); - } - - public int getReceivedDamageByPlayerThisTurn(final Player p) { - if (receivedDamageFromPlayerThisTurn.containsKey(p)) { - return receivedDamageFromPlayerThisTurn.get(p); - } - return 0; - } - - public final void addReceivedDamageFromThisTurn(Card c, final int damage) { - int currentDamage = 0; - // because Aegar cares about the past state we need to keep all LKI instances - receivedDamageFromThisTurn.add(Pair.of(c.isLKI() ? c : CardUtil.getLKICopy(c), damage)); - - Player p = c.getController(); - if (p != null) { - if (receivedDamageFromPlayerThisTurn.containsKey(p)) { - currentDamage = receivedDamageFromPlayerThisTurn.get(p); - } - receivedDamageFromPlayerThisTurn.put(p, damage+currentDamage); - } - } - public final void resetReceivedDamageFromThisTurn() { - receivedDamageFromThisTurn.clear(); - receivedDamageFromPlayerThisTurn.clear(); - } - - public final int getTotalDamageReceivedThisTurn() { - int total = 0; - for (Integer i : receivedDamageFromPlayerThisTurn.values()) { - total += i; - } - return total; + public void setDamageReceivedThisTurn(List> dmg) { + damageReceivedThisTurn.addAll(dmg); } public final boolean hasDealtDamageToOpponentThisTurn() { - for (final GameEntity e : getDamageHistory().getThisTurnDamaged().keySet()) { - if (e instanceof Player) { - final Player p = (Player) e; - if (getController().isOpponentOf(p)) { - return true; - } - } - } - return false; + return getDamageHistory().getDamageDoneThisTurn(null, true, null, "Player.Opponent", this, getController(), null) > 0; } /** @@ -5356,11 +5304,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { * @return the damage done to player p this turn */ public final int getTotalDamageDoneBy() { - int sum = 0; - for (final GameEntity e : getDamageHistory().getThisTurnDamaged().keySet()) { - sum += getDamageHistory().getThisTurnDamaged().get(e); - } - return sum; + return getDamageHistory().getDamageDoneThisTurn(null, false, null, null, this, getController(), null); } // this is the amount of damage a creature needs to receive before it dies @@ -5383,15 +5327,26 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } public final int getDamage() { - return damage; + int sum = 0; + for (int i : damage.values()) { + sum += i; + } + return sum; } public final void setDamage(int damage0) { - if (damage == damage0) { return; } - damage = damage0; + if (getDamage() == damage0) { return; } + damage.clear(); + if (damage0 != 0) { + damage.put(0, damage0); + } view.updateDamage(this); getGame().fireEvent(new GameEventCardStatsChanged(this)); } + public int getMaxDamageFromSource() { + return damage.isEmpty() ? 0 : Collections.max(damage.values()); + } + public final boolean hasBeenDealtDeathtouchDamage() { return hasBeenDealtDeathtouchDamage; } @@ -5542,14 +5497,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { // Run replacement effects getGame().getReplacementHandler().run(ReplacementType.DealtDamage, AbilityKey.mapFromAffected(this)); - addReceivedDamageFromThisTurn(source, damageIn); - source.getDamageHistory().registerDamage(this, damageIn); - if (isCombat) { - source.getDamageHistory().registerCombatDamage(this, damageIn); - } else { - getDamageHistory().setHasBeenDealtNonCombatDamageThisTurn(true); - } - // Run triggers Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.DamageSource, source); @@ -5576,7 +5523,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars { damageType = DamageType.M1M1Counters; } else { // 120.3e - damage += damageIn; + int old = damage.getOrDefault(Objects.hash(source.getId(), source.getTimestamp()), 0); + damage.put(Objects.hash(source.getId(), source.getTimestamp()), old + damageIn); view.updateDamage(this); } @@ -5714,7 +5662,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } return false; } - public final void setForetold(final boolean foretold) { this.foretold = foretold; } @@ -5732,7 +5679,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public final void setForetoldThisTurn(final boolean foretoldThisTurn) { this.foretoldThisTurn = foretoldThisTurn; } - public void resetForetoldThisTurn() { foretoldThisTurn = false; } @@ -5803,7 +5749,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public final long getBestowTimestamp() { return bestowTimestamp; } - public final void setBestowTimestamp(final long t) { bestowTimestamp = t; } @@ -6206,7 +6151,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { setDamage(0); } setHasBeenDealtDeathtouchDamage(false); - resetReceivedDamageFromThisTurn(); setRegeneratedThisTurn(0); resetShield(); setBecameTargetThisTurn(false); @@ -6214,6 +6158,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { clearMustBlockCards(); getDamageHistory().setCreatureAttackedLastTurnOf(turn, getDamageHistory().getCreatureAttacksThisTurn() > 0); getDamageHistory().newTurn(); + damageReceivedThisTurn.clear(); clearBlockedByThisTurn(); clearBlockedThisTurn(); resetMayPlayTurn(); diff --git a/forge-game/src/main/java/forge/game/card/CardDamageHistory.java b/forge-game/src/main/java/forge/game/card/CardDamageHistory.java index 9337293bedc..105bb992bc8 100644 --- a/forge-game/src/main/java/forge/game/card/CardDamageHistory.java +++ b/forge-game/src/main/java/forge/game/card/CardDamageHistory.java @@ -4,9 +4,11 @@ package forge.game.card; import java.util.List; import java.util.Map; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; +import org.apache.commons.lang3.tuple.Pair; +import com.google.common.collect.Lists; + +import forge.game.CardTraitBase; import forge.game.GameEntity; import forge.game.player.Player; import forge.game.zone.ZoneType; @@ -22,7 +24,6 @@ public class CardDamageHistory { private boolean creatureBlockedThisCombat = false; private boolean creatureGotBlockedThisCombat = false; - boolean hasdealtDamagetoAny = false; private List attackedThisTurn = Lists.newArrayList(); private final List creatureAttackedLastTurnOf = Lists.newArrayList(); @@ -30,14 +31,13 @@ public class CardDamageHistory { private final List NotBlockedSinceLastUpkeepOf = Lists.newArrayList(); private final List NotBeenBlockedSinceLastUpkeepOf = Lists.newArrayList(); + private List> damageDoneThisTurn = Lists.newArrayList(); + // only needed for Glen Elendra (Plane) private final List damagedThisCombat = Lists.newArrayList(); // only needed for The Fallen private final FCollection damagedThisGame = new FCollection<>(); - - private final Map damagedThisTurn = Maps.newHashMap(); - private final Map damagedThisTurnInCombat = Maps.newHashMap(); - private boolean receivedNonCombatDamageThisTurn = false; + boolean hasdealtDamagetoAny = false; public final boolean getHasdealtDamagetoAny() { return hasdealtDamagetoAny; @@ -225,38 +225,9 @@ public class CardDamageHistory { return this.creatureGotBlockedThisCombat; } - /** - *

- * Getter for the field receivedNonCombatDamageThisTurn. - *

- * - * @return a boolean. - */ - public boolean hasBeenDealtNonCombatDamageThisTurn() { - return this.receivedNonCombatDamageThisTurn; - } - - /** - *

- * Setter for the field receivedNonCombatDamageThisTurn. - *

- * - * @param b - * a boolean. - */ - public void setHasBeenDealtNonCombatDamageThisTurn(boolean b) { - this.receivedNonCombatDamageThisTurn = b; - } - public final List getThisCombatDamaged() { return damagedThisCombat; } - public final Map getThisTurnDamaged() { - return damagedThisTurn; - } - public final Map getThisTurnCombatDamaged() { - return damagedThisTurnInCombat; - } public final FCollection getThisGameDamaged() { return damagedThisGame; } @@ -264,38 +235,44 @@ public class CardDamageHistory { * TODO: Write javadoc for this method. * @param player */ - public void registerCombatDamage(GameEntity entity, int amount) { - int old = 0; - if (entity instanceof Player) { - damagedThisCombat.add((Player) entity); - } - old = 0; - if (damagedThisTurnInCombat.containsKey(entity)) { - old = damagedThisTurnInCombat.get(entity); - } - damagedThisTurnInCombat.put(entity, old + amount); + public void registerDamage(int damage, boolean isCombat, Card sourceLKI, GameEntity target, Map lkiCache) { + damagedThisGame.add(target); hasdealtDamagetoAny = true; + if (isCombat && target instanceof Player) { + damagedThisCombat.add((Player) target); + } + Pair dmg = Pair.of(damage, isCombat); + damageDoneThisTurn.add(dmg); + target.receiveDamage(dmg); + sourceLKI.getGame().addGlobalDamageHistory(this, dmg, sourceLKI.isLKI() ? sourceLKI : CardUtil.getLKICopy(sourceLKI, lkiCache), CardUtil.getLKICopy(target, lkiCache)); } - /** - * TODO: Write javadoc for this method. - * @param player - */ - public void registerDamage(GameEntity entity, int amount) { - int old = 0; - if (damagedThisTurn.containsKey(entity)) { - old = damagedThisTurn.get(entity); + public int getDamageDoneThisTurn(Boolean isCombat, boolean anyIsEnough, String validSourceCard, String validTargetEntity, Card source, Player sourceController, CardTraitBase ctb) { + int sum = 0; + for (Pair damage : damageDoneThisTurn) { + Pair sourceToTarget = sourceController.getGame().getDamageLKI(damage); + + if (isCombat != null && damage.getRight() != isCombat) { + continue; + } + if (validSourceCard != null && !sourceToTarget.getLeft().isValid(validSourceCard.split(","), sourceController, source == null ? sourceToTarget.getLeft() : source, ctb)) { + continue; + } + if (validTargetEntity != null && !sourceToTarget.getRight().isValid(validTargetEntity.split(","), sourceController, source, ctb)) { + continue; + } + sum += damage.getLeft(); + if (anyIsEnough) { + break; + } } - damagedThisTurn.put(entity, old + amount); - damagedThisGame.add(entity); - hasdealtDamagetoAny = true; + return sum; } public void newTurn() { - damagedThisCombat.clear(); - damagedThisTurnInCombat.clear(); - damagedThisTurn.clear(); attackedThisTurn.clear(); + damagedThisCombat.clear(); + damageDoneThisTurn.clear(); // if card already LTB we can safely dereference (allows quite a few objects to be cleaned up earlier for bigger boardstates) CardCollection toRemove = new CardCollection(); @@ -307,7 +284,6 @@ public class CardDamageHistory { } } damagedThisGame.removeAll(toRemove); - setHasBeenDealtNonCombatDamageThisTurn(false); } public void endCombat() { diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index 058c45b80c7..b4398108a2a 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -608,35 +608,6 @@ public class CardProperty { if ((card.getCloneOrigin() == null) || !card.getCloneOrigin().equals(source)) { return false; } - } else if (property.startsWith("DamagedBy")) { - List damaged = Lists.newArrayList(); - for (Pair pair : card.getReceivedDamageFromThisTurn()) { - damaged.add(pair.getLeft()); - } - if (property.endsWith("Source") || property.equals("DamagedBy")) { - if (!damaged.contains(source)) { - return false; - } - } else { - String prop = property.substring("DamagedBy".length()); - boolean found = Iterables.any(damaged, CardPredicates.restriction(prop, sourceController, source, spellAbility)); - - if (!found) { - for (Card d : AbilityUtils.getDefinedCards(source, prop, spellAbility)) { - if (damaged.contains(d)) { - found = true; - break; - } - } - } - if (!found) { - return false; - } - } - } else if (property.startsWith("Damaged")) { - if (!card.getDamageHistory().getThisTurnDamaged().containsKey(source)) { - return false; - } } else if (property.startsWith("SharesCMCWith")) { if (property.equals("SharesCMCWith")) { if (!card.sharesCMCWith(source)) { @@ -1133,38 +1104,78 @@ public class CardProperty { && !card.getDamageHistory().hasBlockedSinceLastUpkeepOf(sourceController)) { return false; } + } else if (property.startsWith("DamagedBy")) { + String prop = property.substring("DamagedBy".length()); + CardCollection def = null; + if (prop.startsWith(" ")) { + def = AbilityUtils.getDefinedCards(source, prop.substring(1), spellAbility); + } + boolean found = false; + for (Pair p : card.getDamageReceivedThisTurn()) { + Card dmgSource = game.getDamageLKI(p).getLeft(); + if (def != null) { + for (Card c : def) { + if (dmgSource.equalsWithTimestamp(c)) { + found = true; + } + } + } + else if (prop.isEmpty() && dmgSource.equalsWithTimestamp(source)) { + found = true; + } else if (dmgSource.isValid(prop.split(","), sourceController, source, spellAbility)) { + found = true; + } + if (found) { + break; + } + } + if (!found) { + return false; + } + } else if (property.startsWith("Damaged")) { + for (Pair p : source.getDamageReceivedThisTurn()) { + boolean found = false; + if (game.getDamageLKI(p).getLeft().equalsWithTimestamp(card)) { + found = true; + break; + } + if (!found) { + return false; + } + } + } else if (property.startsWith("dealtCombatDamageThisCombat")) { + if (card.getDamageHistory().getThisCombatDamaged().isEmpty()) { + return false; + } } else if (property.startsWith("dealtDamageToYouThisTurn")) { - if (!card.getDamageHistory().getThisTurnDamaged().containsKey(sourceController)) { + if (card.getDamageHistory().getDamageDoneThisTurn(null, true, null, "You", card, sourceController, spellAbility) == 0) { return false; } } else if (property.startsWith("dealtDamageToOppThisTurn")) { if (!card.hasDealtDamageToOpponentThisTurn()) { return false; } - //dealtCombatDamageThisCombat , dealtCombatDamageThisTurn , and notDealt versions } else if (property.startsWith("dealtCombatDamage") || property.startsWith("notDealtCombatDamage")) { - final String[] v = property.split(" ")[1].split(","); - final Iterable list = property.contains("ThisCombat") ? - Lists.newArrayList(card.getDamageHistory().getThisCombatDamaged()) : - card.getDamageHistory().getThisTurnCombatDamaged().keySet(); - boolean found = Iterables.any(list, GameObjectPredicates.restriction(v, sourceController, source, spellAbility)); + final String v = property.split(" ")[1]; + boolean found = card.getDamageHistory().getDamageDoneThisTurn(true, true, null, v, card, sourceController, spellAbility) > 0; + if (found == property.startsWith("not")) { return false; } } else if (property.startsWith("controllerWasDealtCombatDamageByThisTurn")) { - if (!source.getDamageHistory().getThisTurnCombatDamaged().containsKey(controller)) { + if (source.getDamageHistory().getDamageDoneThisTurn(true, true, null, "You", card, controller, spellAbility) == 0) { return false; } } else if (property.startsWith("controllerWasDealtDamageByThisTurn")) { - if (!source.getDamageHistory().getThisTurnDamaged().containsKey(controller)) { + if (source.getDamageHistory().getDamageDoneThisTurn(null, true, null, "You", card, controller, spellAbility) == 0) { return false; } } else if (property.startsWith("wasDealtDamageThisTurn")) { - if (card.getReceivedDamageFromPlayerThisTurn().isEmpty()) { + if (card.getAssignedDamage() == 0) { return false; } } else if (property.equals("wasDealtNonCombatDamageThisTurn")) { - if (!card.getDamageHistory().hasBeenDealtNonCombatDamageThisTurn()) { + if (card.getAssignedDamage(false, null) == 0) { return false; } } else if (property.startsWith("wasDealtDamageByThisGame")) { diff --git a/forge-game/src/main/java/forge/game/card/CardUtil.java b/forge-game/src/main/java/forge/game/card/CardUtil.java index abd1fe3a069..7699689d798 100644 --- a/forge-game/src/main/java/forge/game/card/CardUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardUtil.java @@ -250,9 +250,8 @@ public final class CardUtil { newCopy.setColor(in.getColor().getColor()); newCopy.setPhasedOut(in.isPhasedOut()); - newCopy.setReceivedDamageFromThisTurn(in.getReceivedDamageFromThisTurn()); - newCopy.setReceivedDamageFromPlayerThisTurn(in.getReceivedDamageFromPlayerThisTurn()); newCopy.setDamageHistory(in.getDamageHistory()); + newCopy.setDamageReceivedThisTurn(in.getDamageReceivedThisTurn()); for (Card c : in.getBlockedThisTurn()) { newCopy.addBlockedThisTurn(c); } diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 6689bcdd50f..3e3a90e74f0 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -147,8 +147,6 @@ public class Player extends GameEntity implements Comparable { private int life = 20; private int startingLife = 20; private int lifeStartedThisTurnWith = startingLife; - private final Map assignedDamage = Maps.newHashMap(); - private final Map assignedCombatDamage = Maps.newHashMap(); private int spellsCastThisTurn; private int spellsCastThisGame; private int spellsCastLastTurn; @@ -219,7 +217,6 @@ public class Player extends GameEntity implements Comparable { private Map maingameCardsMap = Maps.newHashMap(); private CardCollection currentPlanes = new CardCollection(); - private Set prowl = Sets.newHashSet(); private PlayerStatistics stats = new PlayerStatistics(); private PlayerController controller; @@ -704,19 +701,6 @@ public class Player extends GameEntity implements Comparable { } } - int old = assignedDamage.containsKey(source) ? assignedDamage.get(source) : 0; - assignedDamage.put(source, old + amount); - source.getDamageHistory().registerDamage(this, amount); - - if (isCombat) { - old = assignedCombatDamage.containsKey(source) ? assignedCombatDamage.get(source) : 0; - assignedCombatDamage.put(source, old + amount); - for (final String type : source.getType().getCreatureTypes()) { - source.getController().addProwlType(type); - } - source.getDamageHistory().registerCombatDamage(this, amount); - } - // Run triggers final Map runParams = AbilityKey.newMap(); runParams.put(AbilityKey.DamageSource, source); @@ -830,49 +814,6 @@ public class Player extends GameEntity implements Comparable { simultaneousDamage = 0; } - public final void clearAssignedDamage() { - assignedDamage.clear(); - assignedCombatDamage.clear(); - } - - public final int getAssignedDamage() { - int num = 0; - for (final Integer value : assignedDamage.values()) { - num += value; - } - return num; - } - - public final int getAssignedCombatDamage() { - int num = 0; - for (final Integer value : assignedCombatDamage.values()) { - num += value; - } - return num; - } - - public final Iterable getAssignedDamageSources() { - return assignedDamage.keySet(); - } - - public final int getAssignedDamage(final Card c) { - return assignedDamage.get(c); - } - - public final int getAssignedDamage(final String type) { - final Map valueMap = Maps.newHashMap(); - for (final Card c : assignedDamage.keySet()) { - if (c.getType().hasStringType(type)) { - valueMap.put(c, assignedDamage.get(c)); - } - } - int num = 0; - for (final Integer value : valueMap.values()) { - num += value; - } - return num; - } - /** * Get the total damage assigned to this player's opponents this turn. */ @@ -2082,8 +2023,8 @@ public class Player extends GameEntity implements Comparable { } public final boolean hasBloodthirst() { - for (Player p : game.getRegisteredPlayers()) { - if (p.isOpponentOf(this) && p.getAssignedDamage() > 0) { + for (Player p : getRegisteredOpponents()) { + if (p.getAssignedDamage() > 0) { return true; } } @@ -2102,14 +2043,12 @@ public class Player extends GameEntity implements Comparable { return lost; } - public final boolean hasProwl(final String type) { - return prowl.contains(type); - } - public final void addProwlType(final String type) { - prowl.add(type); - } - public final void resetProwl() { - prowl.clear(); + public final boolean hasProwl(final Set types) { + StringBuilder sb = new StringBuilder(); + for (String type : types) { + sb.append("Card.YouCtrl+").append(type).append(","); + } + return !game.getDamageDoneThisTurn(true, true, sb.toString(), "Player", null, this, null).isEmpty(); } public final void setLibrarySearched(final int l) { @@ -2150,7 +2089,7 @@ public class Player extends GameEntity implements Comparable { } } else if (incR[0].equals("EnchantedController")) { final GameEntity enchanted = source.getEntityAttachedTo(); - if ((enchanted == null) || !(enchanted instanceof Card)) { + if (enchanted == null || !(enchanted instanceof Card)) { return false; } final Card enchantedCard = (Card) enchanted; @@ -2239,10 +2178,6 @@ public class Player extends GameEntity implements Comparable { investigatedThisTurn = 0; } - public final List getSacrificedThisTurn() { - return sacrificedThisTurn; - } - public final void addSacrificedThisTurn(final Card c, final SpellAbility source) { // Play the Sacrifice sound game.fireEvent(new GameEventCardSacrificed()); @@ -2260,6 +2195,9 @@ public class Player extends GameEntity implements Comparable { game.getTriggerHandler().runTrigger(TriggerType.Sacrificed, runParams, false); } + public final List getSacrificedThisTurn() { + return sacrificedThisTurn; + } public final void resetSacrificedThisTurn() { sacrificedThisTurn.clear(); } @@ -2452,10 +2390,8 @@ public class Player extends GameEntity implements Comparable { resetCycledThisTurn(); resetEquippedThisTurn(); resetSacrificedThisTurn(); - clearAssignedDamage(); resetVenturedThisTurn(); setRevolt(false); - resetProwl(); setSpellsCastLastTurn(getSpellsCastThisTurn()); resetSpellsCastThisTurn(); setLifeLostLastTurn(getLifeLostThisTurn()); @@ -2467,6 +2403,8 @@ public class Player extends GameEntity implements Comparable { setLibrarySearched(0); setNumManaConversion(0); + damageReceivedThisTurn.clear(); + // set last turn nr if (game.getPhaseHandler().isPlayerTurn(this)) { setAttackedPlayersMyLastTurn(attackedPlayersThisTurn); diff --git a/forge-game/src/main/java/forge/game/player/PlayerProperty.java b/forge-game/src/main/java/forge/game/player/PlayerProperty.java index f9b591391c7..887af2cd51c 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerProperty.java +++ b/forge-game/src/main/java/forge/game/player/PlayerProperty.java @@ -1,6 +1,7 @@ package forge.game.player; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import forge.game.CardTraitBase; @@ -86,81 +87,66 @@ public class PlayerProperty { if (!player.hasBlessing()) { return false; } + } else if (property.startsWith("damageDoneSingleSource")) { + String props = property.split(" ")[1]; + List sourceDmg = game.getDamageDoneThisTurn(null, false, "Card.YouCtrl", null, source, sourceController, spellAbility); + int maxDmg = sourceDmg.isEmpty() ? 0 : Collections.max(sourceDmg); + if (!Expressions.compare(maxDmg, props.substring(0, 2), AbilityUtils.calculateAmount(source, props.substring(2), spellAbility))) { + return false; + } } else if (property.startsWith("wasDealtCombatDamageThisCombatBy ")) { String v = property.split(" ")[1]; - - int count = 1; - if (v.contains("_AtLeast")) { - count = Integer.parseInt(v.substring(v.indexOf("_AtLeast") + 8)); - v = v.substring(0, v.indexOf("_AtLeast")).replace("Valid:", "Valid "); - } + boolean found = true; final List cards = AbilityUtils.getDefinedCards(source, v, spellAbility); - int found = 0; for (final Card card : cards) { if (card.getDamageHistory().getThisCombatDamaged().contains(player)) { - found++; + found = true; } } - if (found < count) { + if (!found) { return false; } } else if (property.startsWith("wasDealtDamageThisGameBy ")) { String v = property.split(" ")[1]; - - int count = 1; - if (v.contains("_AtLeast")) { - count = Integer.parseInt(v.substring(v.indexOf("_AtLeast") + 8)); - v = TextUtil.fastReplace(v.substring(0, v.indexOf("_AtLeast")), "Valid:", "Valid "); - } + boolean found = true; final List cards = AbilityUtils.getDefinedCards(source, v, spellAbility); - int found = 0; for (final Card card : cards) { if (card.getDamageHistory().getThisGameDamaged().contains(player)) { - found++; + found = true; } } - if (found < count) { + if (!found) { return false; } - } else if (property.startsWith("wasDealtDamageThisTurnBy ")) { - String v = property.split(" ")[1]; - int count = 1; - - if (v.contains("_AtLeast")) { - count = Integer.parseInt(v.substring(v.indexOf("_AtLeast") + 8)); - v = TextUtil.fastReplace(v.substring(0, v.indexOf("_AtLeast")), "Valid:", "Valid "); + } else if (property.startsWith("wasDealt")) { + boolean found = false; + String validCard = null; + Boolean combat = null; + if (property.contains("CombatDamage")) { + combat = true; } + if (property.contains("ThisTurnBySource")) { + found = source.getDamageHistory().getDamageDoneThisTurn(combat, validCard == null, validCard, "You", source, player, spellAbility) > 0; + } else { + String comp = "GE"; + int right = 1; + int numValid = 0; - final List cards = AbilityUtils.getDefinedCards(source, v, spellAbility); - int found = 0; - for (final Card card : cards) { - if (card.getDamageHistory().getThisTurnDamaged().containsKey(player)) { - found++; + if (property.contains("ThisTurnBy")) { + String[] props = property.split(" "); + validCard = props[1]; + if (props.length > 2) { + comp = props[2].substring(0, 2); + right = AbilityUtils.calculateAmount(source, props[2].substring(2), spellAbility); + } } - } - if (found < count) { - return false; - } - } else if (property.startsWith("wasDealtCombatDamageThisTurnBy ")) { - String v = property.split(" ")[1]; - int count = 1; - if (v.contains("_AtLeast")) { - count = Integer.parseInt(v.substring(v.indexOf("_AtLeast") + 8)); - v = TextUtil.fastReplace(v.substring(0, v.indexOf("_AtLeast")), "Valid:", "Valid "); + numValid = game.getDamageDoneThisTurn(combat, validCard == null, validCard, "You", source, player, spellAbility).size(); + found = Expressions.compare(numValid, comp, right); } - - final List cards = AbilityUtils.getDefinedCards(source, v, spellAbility); - - int found = 0; - for (final Card card : cards) { - if (card.getDamageHistory().getThisTurnCombatDamaged().containsKey(player)) { - found++; - } - } - if (found < count) { + if (!found) { return false; } } else if (property.equals("attackedBySourceThisCombat")) { @@ -171,18 +157,10 @@ public class PlayerProperty { if (!source.getDamageHistory().hasAttackedThisTurn(player)) { return false; } - } else if (property.equals("wasDealtDamageThisTurn")) { - if (player.getAssignedDamage() == 0) { - return false; - } - } else if (property.equals("Defending")) { + } else if (property.equals("Defending")) { if (!game.getCombat().getAttackersAndDefenders().values().contains(player)) { return false; } - } else if (property.equals("wasDealtCombatDamageThisTurn")) { - if (player.getAssignedCombatDamage() == 0) { - return false; - } } else if (property.equals("LostLifeThisTurn")) { if (player.getLifeLostThisTurn() <= 0) { return false; diff --git a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java index c767b4e36c9..2261081fc6d 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java @@ -87,8 +87,8 @@ public class AbilityManaPart implements java.io.Serializable { public AbilityManaPart(final Card sourceCard, final Map params) { this.sourceCard = sourceCard; - origProduced = params.containsKey("Produced") ? params.get("Produced") : "1"; - this.manaRestrictions = params.containsKey("RestrictValid") ? params.get("RestrictValid") : ""; + origProduced = params.getOrDefault("Produced", "1"); + this.manaRestrictions = params.getOrDefault("RestrictValid", ""); this.cannotCounterSpell = params.get("AddsNoCounter"); this.addsKeywords = params.get("AddsKeywords"); this.addsKeywordsType = params.get("AddsKeywordsType"); diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java index 99e347d2e02..8f72ba69673 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java @@ -434,13 +434,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { } } if (sa.isProwl()) { - boolean prowlFlag = false; - for (final String type : c.getType().getCreatureTypes()) { - if (activator.hasProwl(type)) { - prowlFlag = true; - } - } - if (!prowlFlag) { + if (!activator.hasProwl(c.getType().getCreatureTypes())) { return false; } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java b/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java index 627f763531d..d1a2c9ac8e7 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java @@ -166,7 +166,7 @@ public class TriggerChangesZone extends Trigger { // need to check the ChangeZone LKI copy for damage, otherwise it'll return 0 for a new object in the new zone Card lkiCard = card.getGame().getChangeZoneLKIInfo(card); - final boolean expr = Expressions.compare(lkiCard.getTotalDamageReceivedThisTurn(), cond, rightSide); + final boolean expr = Expressions.compare(lkiCard.getAssignedDamage(), cond, rightSide); if (!expr) { return false; } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDone.java b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDone.java index 3df9d4ffc39..55061cbe6f0 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerDamageDone.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerDamageDone.java @@ -101,7 +101,7 @@ public class TriggerDamageDone extends Trigger { if (target instanceof Player) { final Player trigTgt = (Player) target; - if (!Expressions.compare(trigTgt.getAssignedDamage(source), operator, operand)) { + if (!Expressions.compare(trigTgt.getAssignedDamage(null, source), operator, operand)) { return false; } } else { diff --git a/forge-gui/res/cardsfolder/a/admiral_beckett_brass.txt b/forge-gui/res/cardsfolder/a/admiral_beckett_brass.txt index 59bb5e60214..f17d1c259b7 100644 --- a/forge-gui/res/cardsfolder/a/admiral_beckett_brass.txt +++ b/forge-gui/res/cardsfolder/a/admiral_beckett_brass.txt @@ -4,6 +4,6 @@ Types:Legendary Creature Human Pirate PT:3/3 S:Mode$ Continuous | Affected$ Creature.Pirate+Other+YouCtrl | AddPower$ 1 | AddToughness$ 1 | Description$ Other Pirates you control get +1/+1. T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigGainCtrl | TriggerDescription$ At the beginning of your end step, gain control of target nonland permanent controlled by a player who was dealt combat damage by three or more Pirates this turn. -SVar:TrigGainCtrl:DB$ GainControl | ValidTgts$ Permanent.nonLand+ControlledBy Player.wasDealtCombatDamageThisTurnBy Valid:Creature.Pirate_AtLeast3 | TgtPrompt$ Select target creature controlled by a player who was dealt damage by three or more Pirates this turn | SpellDescription$ Gain control of target nonland permanent controlled by a player who was dealt combat damage by three or more Pirates this turn. +SVar:TrigGainCtrl:DB$ GainControl | ValidTgts$ Permanent.nonLand+ControlledBy Player.wasDealtCombatDamageThisTurnBy Creature.Pirate GE3 | TgtPrompt$ Select target creature controlled by a player who was dealt damage by three or more Pirates this turn | SpellDescription$ Gain control of target nonland permanent controlled by a player who was dealt combat damage by three or more Pirates this turn. DeckHints:Type$Pirate Oracle:Other Pirates you control get +1/+1.\nAt the beginning of your end step, gain control of target nonland permanent controlled by a player who was dealt combat damage by three or more Pirates this turn. diff --git a/forge-gui/res/cardsfolder/b/blazing_sunsteel.txt b/forge-gui/res/cardsfolder/b/blazing_sunsteel.txt index 7bdc9153c9b..f8f5350af9c 100644 --- a/forge-gui/res/cardsfolder/b/blazing_sunsteel.txt +++ b/forge-gui/res/cardsfolder/b/blazing_sunsteel.txt @@ -4,7 +4,7 @@ Types:Artifact Equipment S:Mode$ Continuous | Affected$ Creature.EquippedBy | AddPower$ Y | Description$ Equipped creature gets +1/+0 for each opponent you have. SVar:Y:PlayerCountOpponents$Amount T:Mode$ DamageDoneOnce | Execute$ TrigDamage | ValidTarget$ Creature.EquippedBy | TriggerZones$ Battlefield | TriggerDescription$ Whenever equipped creature is dealt damage, it deals that much damage to any target. -SVar:TrigDamage:DB$ DealDamage | NumDmg$ X | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target +SVar:TrigDamage:DB$ DealDamage | NumDmg$ X | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target | DamageSource$ TriggeredTargetLKICopy SVar:X:TriggerCount$DamageAmount K:Equip:4 Oracle:Equipped creature gets +1/+0 for each opponent you have.\nWhenever equipped creature is dealt damage, it deals that much damage to any target.\nEquip {4} diff --git a/forge-gui/res/cardsfolder/b/blinding_beam.txt b/forge-gui/res/cardsfolder/b/blinding_beam.txt index e1006f83348..ed426a6ec83 100644 --- a/forge-gui/res/cardsfolder/b/blinding_beam.txt +++ b/forge-gui/res/cardsfolder/b/blinding_beam.txt @@ -5,7 +5,7 @@ K:Entwine:1 A:SP$ Charm | Cost$ 2 W | Choices$ DBTap,DBEffect | CharmNum$ 1 SVar:DBTap:DB$ Tap | ValidTgts$ Creature | TargetMin$ 2 | TargetMax$ 2 | TgtPrompt$ Select two target creatures | SpellDescription$ Tap two target creatures. SVar:DBEffect:DB$ Effect | ValidTgts$ Player | TgtPrompt$ Select target player | IsCurse$ True | StaticAbilities$ DontUntap | Triggers$ RestoreSight | RememberObjects$ Targeted | Duration$ Permanent | SpellDescription$ Creatures don't untap during target player's next untap step. -SVar:DontUntap:Mode$ Continuous | ValidPlayer$ Player.IsRemembered | EffectZone$ Command | AffectedZone$ Battlefield | Affected$ Creature | AddHiddenKeyword$ This card doesn't untap. +SVar:DontUntap:Mode$ Continuous | EffectZone$ Command | AffectedZone$ Battlefield | Affected$ Creature.RememberedPlayerCtrl | AddHiddenKeyword$ This card doesn't untap. SVar:RestoreSight:Mode$ Phase | Phase$ Untap | ValidPlayer$ Player.IsRemembered | TriggerZones$ Command | Execute$ ExileEffect | Static$ True SVar:ExileEffect:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile AI:RemoveDeck:All diff --git a/forge-gui/res/cardsfolder/b/bloodcrazed_goblin.txt b/forge-gui/res/cardsfolder/b/bloodcrazed_goblin.txt index 9e93e010db3..23bf1b34831 100644 --- a/forge-gui/res/cardsfolder/b/bloodcrazed_goblin.txt +++ b/forge-gui/res/cardsfolder/b/bloodcrazed_goblin.txt @@ -3,5 +3,5 @@ ManaCost:R Types:Creature Goblin Berserker PT:2/2 S:Mode$ Continuous | Affected$ Card.Self | AddKeyword$ CARDNAME can't attack. | CheckSVar$ X | SVarCompare$ LT1 | Description$ CARDNAME can't attack unless an opponent has been dealt damage this turn. -SVar:X:Count$TotalOppDamageThisTurn +SVar:X:PlayerCountPropertyYou$DamageToOppsThisTurn Oracle:Bloodcrazed Goblin can't attack unless an opponent has been dealt damage this turn. diff --git a/forge-gui/res/cardsfolder/d/discordant_spirit.txt b/forge-gui/res/cardsfolder/d/discordant_spirit.txt index d91bc57573f..b0c03b19749 100644 --- a/forge-gui/res/cardsfolder/d/discordant_spirit.txt +++ b/forge-gui/res/cardsfolder/d/discordant_spirit.txt @@ -6,5 +6,5 @@ T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ Player.Opponent | TriggerZones SVar:GhostCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ X T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ GhostClear | TriggerDescription$ At the beginning of your end step, remove all +1/+1 counters from CARDNAME. SVar:GhostClear:DB$ RemoveCounter | CounterType$ P1P1 | CounterNum$ All -SVar:X:Count$YourDamageThisTurn +SVar:X:PlayerCountPropertyYou$DamageThisTurn Oracle:At the beginning of each end step, if it's an opponent's turn, put a +1/+1 counter on Discordant Spirit for each 1 damage dealt to you this turn.\nAt the beginning of your end step, remove all +1/+1 counters from Discordant Spirit. diff --git a/forge-gui/res/cardsfolder/g/gales_redirection.txt b/forge-gui/res/cardsfolder/g/gales_redirection.txt index 5386ec8ba23..bd90f32dd46 100644 --- a/forge-gui/res/cardsfolder/g/gales_redirection.txt +++ b/forge-gui/res/cardsfolder/g/gales_redirection.txt @@ -5,8 +5,8 @@ A:SP$ ChangeZone | ValidTgts$ Card | TgtZone$ Stack | Origin$ Stack | Fizzle$ Tr SVar:DBRoll:DB$ RollDice | Sides$ 20 | Modifier$ Y | ResultSubAbilities$ 1-14:DBMayPlay,Else:DBMayPlayWithoutCost | StackDescription$ SpellDescription | SpellDescription$ Roll a d20 and add that spell's mana value. SVar:DBMayPlay:DB$ Effect | StaticAbilities$ STPlay | RememberObjects$ Remembered | Duration$ Permanent | ExileOnMoved$ Exile | SubAbility$ DBCleanup | SpellDescription$ 1—14 VERT You may cast that card for as long as it remains exiled, and you may spend mana as though it were mana of any color to cast it. SVar:DBMayPlayWithoutCost:DB$ Effect | StaticAbilities$ STPlayWithoutCost | RememberObjects$ Remembered | Duration$ Permanent | ExileOnMoved$ Exile | SubAbility$ DBCleanup | SpellDescription$ 15+ VERT You may cast that card without paying its mana cost for as long as it remains exiled. -SVar:STPlay:Mode$ Continuous | MayPlay$ True | MayPlayIgnoreColor$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may cast that card for as long as it remains exiled, and you may spend mana as though it were mana of any color to cast it. -SVar:STPlayWithoutCost:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may cast that card without paying its mana cost for as long as it remains exiled. +SVar:STPlay:Mode$ Continuous | MayPlay$ True | MayPlayIgnoreColor$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand | AffectedZone$ Exile | Description$ You may cast that card for as long as it remains exiled, and you may spend mana as though it were mana of any color to cast it. +SVar:STPlayWithoutCost:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand | AffectedZone$ Exile | Description$ You may cast that card without paying its mana cost for as long as it remains exiled. SVar:Y:RememberedLKI$CardManaCost SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True AI:RemoveDeck:All diff --git a/forge-gui/res/cardsfolder/g/glen_elendra.txt b/forge-gui/res/cardsfolder/g/glen_elendra.txt index 55c2617f398..f4c0254ef80 100644 --- a/forge-gui/res/cardsfolder/g/glen_elendra.txt +++ b/forge-gui/res/cardsfolder/g/glen_elendra.txt @@ -2,7 +2,7 @@ Name:Glen Elendra ManaCost:no cost Types:Plane Lorwyn T:Mode$ Phase | Phase$ EndCombat | ValidPlayer$ You | TriggerZones$ Command | OptionalDecider$ You | Execute$ TrigExchange | TriggerDescription$ At end of combat, you may exchange control of target creature you control that dealt combat damage to a player this combat and target creature that player controls. -SVar:TrigExchange:DB$ Pump | ValidTgts$ Creature.YouCtrl+dealtCombatDamageThisCombat Player | TgtPrompt$ Select target creature you control that dealt combat damage to a player | SubAbility$ DBExchange +SVar:TrigExchange:DB$ Pump | ValidTgts$ Creature.YouCtrl+dealtCombatDamageThisCombat | TgtPrompt$ Select target creature you control that dealt combat damage to a player | SubAbility$ DBExchange SVar:DBExchange:DB$ ExchangeControl | Defined$ ParentTarget | ValidTgts$ Creature.ControlledBy Player.wasDealtCombatDamageThisCombatBy ParentTarget | TgtPrompt$ Select target creature that player controls. T:Mode$ PlanarDice | Result$ Chaos | TriggerZones$ Command | Execute$ RolledChaos | TriggerDescription$ Whenever you roll {CHAOS}, gain control of target creature you own. SVar:RolledChaos:DB$ GainControl | ValidTgts$ Creature.YouOwn | TgtPrompt$ Select target creature you own to gain control of diff --git a/forge-gui/res/cardsfolder/g/grothama_all_devouring.txt b/forge-gui/res/cardsfolder/g/grothama_all_devouring.txt index 29f7ae03ed3..96c6f1854f4 100644 --- a/forge-gui/res/cardsfolder/g/grothama_all_devouring.txt +++ b/forge-gui/res/cardsfolder/g/grothama_all_devouring.txt @@ -8,6 +8,6 @@ SVar:GrothamaFight:DB$ Fight | Defined$ TriggeredAttackerLKICopy | ExtraDefined$ T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ TrigRepeat | TriggerDescription$ When CARDNAME leaves the battlefield, each player draws cards equal to the amount of damage dealt to Grothama this turn by sources they controlled. SVar:TrigRepeat:DB$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ TrigDraw SVar:TrigDraw:DB$ Draw | Defined$ Remembered | NumCards$ X -SVar:X:TriggeredCard$DamageDoneByPlayerThisTurn.Remembered +SVar:X:TriggeredCard$TotalDamageThisTurn Card.RememberedPlayerCtrl SVar:HasAttackEffect:TRUE Oracle:Other creatures have "Whenever this creature attacks, you may have it fight Grothama, All-Devouring."\nWhen Grothama leaves the battlefield, each player draws cards equal to the amount of damage dealt to Grothama this turn by sources they controlled. diff --git a/forge-gui/res/cardsfolder/h/hope_of_ghirapur.txt b/forge-gui/res/cardsfolder/h/hope_of_ghirapur.txt index 18929ce60bc..9c2836da5f3 100644 --- a/forge-gui/res/cardsfolder/h/hope_of_ghirapur.txt +++ b/forge-gui/res/cardsfolder/h/hope_of_ghirapur.txt @@ -3,6 +3,6 @@ ManaCost:1 Types:Legendary Artifact Creature Thopter PT:1/1 K:Flying -A:AB$ Effect | Cost$ Sac<1/CARDNAME> | ValidTgts$ Player.wasDealtCombatDamageThisTurnBy Self | Name$ Hope of Ghirapur Effect | StaticAbilities$ STCantBeCast | RememberObjects$ Targeted | Duration$ UntilYourNextTurn | SpellDescription$ Until your next turn, target player who was dealt combat damage by Hope of Ghirapur can't cast noncreature spells. +A:AB$ Effect | Cost$ Sac<1/CARDNAME> | ValidTgts$ Player.wasDealtCombatDamageThisTurnBySource | Name$ Hope of Ghirapur Effect | StaticAbilities$ STCantBeCast | RememberObjects$ Targeted | Duration$ UntilYourNextTurn | SpellDescription$ Until your next turn, target player who was dealt combat damage by Hope of Ghirapur can't cast noncreature spells. SVar:STCantBeCast:Mode$ CantBeCast | EffectZone$ Command | ValidCard$ Card.nonCreature | Caster$ Player.IsRemembered | Description$ Until your next turn, target player who was dealt combat damage by Hope of Ghirapur this turn can't cast noncreature spells. Oracle:Flying\nSacrifice Hope of Ghirapur: Until your next turn, target player who was dealt combat damage by Hope of Ghirapur this turn can't cast noncreature spells. diff --git a/forge-gui/res/cardsfolder/i/inferno_trap.txt b/forge-gui/res/cardsfolder/i/inferno_trap.txt index f170117162a..002e2bed393 100644 --- a/forge-gui/res/cardsfolder/i/inferno_trap.txt +++ b/forge-gui/res/cardsfolder/i/inferno_trap.txt @@ -1,8 +1,8 @@ Name:Inferno Trap ManaCost:3 R Types:Instant Trap -SVar:AltCost:Cost$ R | CheckSVar$ CreaturesAttacked | SVarCompare$ GE2 | Description$ If you've been dealt damage by two or more creatures this turn, you may pay {R} rather than pay this spell's mana cost. +SVar:AltCost:Cost$ R | CheckSVar$ CreaturesDmg | SVarCompare$ GE2 | Description$ If you've been dealt damage by two or more creatures this turn, you may pay {R} rather than pay this spell's mana cost. A:SP$ DealDamage | Cost$ 3 R | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumDmg$ 4 | SpellDescription$ CARDNAME deals 4 damage to target creature. -SVar:CreaturesAttacked:Count$YourDamageSourcesThisTurn Creature +SVar:CreaturesDmg:Count$NumDamageThisTurn Creature You AI:RemoveDeck:All Oracle:If you've been dealt damage by two or more creatures this turn, you may pay {R} rather than pay this spell's mana cost.\nInferno Trap deals 4 damage to target creature. diff --git a/forge-gui/res/cardsfolder/k/kumano_faces_kakkazan_etching_of_kumano.txt b/forge-gui/res/cardsfolder/k/kumano_faces_kakkazan_etching_of_kumano.txt index 8b84bf7276c..db5a149d522 100644 --- a/forge-gui/res/cardsfolder/k/kumano_faces_kakkazan_etching_of_kumano.txt +++ b/forge-gui/res/cardsfolder/k/kumano_faces_kakkazan_etching_of_kumano.txt @@ -25,6 +25,6 @@ Colors:red Types:Enchantment Creature Human Shaman PT:2/2 K:Haste -R:Event$ Moved | ValidLKI$ Creature.DamagedByCard.YouCtrl,Creature.DamagedByEmblem.YouCtrl | Destination$ Graveyard | ReplaceWith$ DBExile | ActiveZones$ Battlefield | Description$ If a creature dealt damage this turn by a source you controlled would die, exile it instead. +R:Event$ Moved | ValidLKI$ Creature.DamagedByCard.YouCtrl,Emblem.YouCtrl | Destination$ Graveyard | ReplaceWith$ DBExile | ActiveZones$ Battlefield | Description$ If a creature dealt damage this turn by a source you controlled would die, exile it instead. SVar:DBExile:DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Battlefield | Destination$ Exile Oracle:Haste\nIf a creature dealt damage this turn by a source you controlled would die, exile it instead. diff --git a/forge-gui/res/cardsfolder/k/kumanos_blessing.txt b/forge-gui/res/cardsfolder/k/kumanos_blessing.txt index 0a00052704b..694e9a2add7 100644 --- a/forge-gui/res/cardsfolder/k/kumanos_blessing.txt +++ b/forge-gui/res/cardsfolder/k/kumanos_blessing.txt @@ -4,7 +4,7 @@ Types:Enchantment Aura K:Flash K:Enchant creature A:SP$ Attach | Cost$ 2 R | ValidTgts$ Creature | AILogic$ Pump -R:Event$ Moved | ValidLKI$ Creature.DamagedByEnchanted | Destination$ Graveyard | ActiveZones$ Battlefield | ReplaceWith$ DBExile | Description$ If a creature dealt damage by enchanted creature this turn would die, exile it instead. +R:Event$ Moved | ValidLKI$ Creature.DamagedBy Enchanted | Destination$ Graveyard | ActiveZones$ Battlefield | ReplaceWith$ DBExile | Description$ If a creature dealt damage by enchanted creature this turn would die, exile it instead. SVar:DBExile:DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Battlefield | Destination$ Exile AI:RemoveDeck:Random SVar:NonStackingAttachEffect:True diff --git a/forge-gui/res/cardsfolder/l/loxodon_lifechanter.txt b/forge-gui/res/cardsfolder/l/loxodon_lifechanter.txt index 705631e0df3..d2fc646edba 100644 --- a/forge-gui/res/cardsfolder/l/loxodon_lifechanter.txt +++ b/forge-gui/res/cardsfolder/l/loxodon_lifechanter.txt @@ -3,7 +3,7 @@ ManaCost:5 W Types:Creature Elephant Cleric PT:4/6 T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigLife | OptionalDecider$ You | TriggerDescription$ When CARDNAME enters the battlefield, you may have your life total become the total toughness of creatures you control. -SVar:TrigLife:DB$ SetLife | Defined$ You | LifeAmount$ Y | SubAbility$ DBCleanup +SVar:TrigLife:DB$ SetLife | Defined$ You | LifeAmount$ Y SVar:Y:Count$Valid Creature.YouCtrl$SumToughness A:AB$ Pump | Cost$ 5 W | NumAtt$ X | NumDef$ X | SpellDescription$ CARDNAME gets +X/+X until end of turn, where X is your life total. SVar:X:Count$YourLifeTotal diff --git a/forge-gui/res/cardsfolder/n/nothing_can_stop_me_now.txt b/forge-gui/res/cardsfolder/n/nothing_can_stop_me_now.txt index b8e0046aa6d..19c4cca2f6b 100644 --- a/forge-gui/res/cardsfolder/n/nothing_can_stop_me_now.txt +++ b/forge-gui/res/cardsfolder/n/nothing_can_stop_me_now.txt @@ -6,5 +6,5 @@ R:Event$ DamageDone | ActiveZones$ Command | ValidSource$ Card.OppCtrl,Emblem.Op SVar:DBReplace:DB$ ReplaceDamage | Amount$ 1 T:Mode$ Phase | Phase$ End of Turn | CheckSVar$ X | SVarCompare$ GE5 | TriggerZones$ Command | Execute$ Abandon | TriggerDescription$ At the beginning of each end step, if you've been dealt 5 or more damage this turn, abandon this scheme. SVar:Abandon:AB$ Abandon | Cost$ 0 -SVar:X:Count$YourDamageThisTurn +SVar:X:PlayerCountPropertyYou$DamageThisTurn Oracle:(An ongoing scheme remains face up until it's abandoned.)\nIf a source an opponent controls would deal damage to you, prevent 1 of that damage.\nAt the beginning of each end step, if you've been dealt 5 or more damage this turn, abandon this scheme. diff --git a/forge-gui/res/cardsfolder/n/notorious_throng.txt b/forge-gui/res/cardsfolder/n/notorious_throng.txt index 227c90761ce..da2a0930a3b 100644 --- a/forge-gui/res/cardsfolder/n/notorious_throng.txt +++ b/forge-gui/res/cardsfolder/n/notorious_throng.txt @@ -4,6 +4,6 @@ Types:Tribal Sorcery Rogue K:Prowl:5 U A:SP$ Token | Cost$ 3 U | TokenAmount$ X | TokenScript$ b_1_1_faerie_rogue_flying | TokenOwner$ You | SubAbility$ DBTakeTurn | SpellDescription$ Create X 1/1 black Faerie Rogue creature tokens with flying, where X is the damage dealt to your opponents this turn. If CARDNAME's prowl cost was paid, take an extra turn after this one. SVar:DBTakeTurn:DB$ AddTurn | NumTurns$ 1 | ConditionDefined$ Self | ConditionPresent$ Card.prowled -SVar:X:Count$TotalOppDamageThisTurn +SVar:X:PlayerCountPropertyYou$DamageToOppsThisTurn DeckNeeds:Type$Rogue Oracle:Prowl {5}{U} (You may cast this for its prowl cost if you dealt combat damage to a player this turn with a Rogue.)\nCreate X 1/1 black Faerie Rogue creature tokens with flying, where X is the damage dealt to your opponents this turn. If this spell's prowl cost was paid, take an extra turn after this one. diff --git a/forge-gui/res/cardsfolder/r/runesword.txt b/forge-gui/res/cardsfolder/r/runesword.txt index 6d588d4b594..017f9c53a96 100644 --- a/forge-gui/res/cardsfolder/r/runesword.txt +++ b/forge-gui/res/cardsfolder/r/runesword.txt @@ -8,6 +8,6 @@ SVar:RuneswordSac:DB$ SacrificeAll | Defined$ Imprinted | SubAbility$ ExileEffec SVar:ExileEffect:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile SVar:TrigNoregen:Mode$ DamageDone | ValidSource$ Card.IsRemembered | ValidTarget$ Creature | Execute$ PumpNogen | Static$ True | TriggerDescription$ If the creature deals damage to a creature this turn, the creature dealt damage can't be regenerated this turn. SVar:PumpNogen:DB$ Pump | KW$ HIDDEN CARDNAME can't be regenerated. | Defined$ TriggeredTarget -SVar:RuneswordRep:Event$ Moved | ValidLKI$ Creature.DamagedByRemembered | Destination$ Graveyard | ReplaceWith$ RuneswordExile | Description$ If a creature dealt damage by CARDNAME this turn would die, exile it instead. +SVar:RuneswordRep:Event$ Moved | ValidLKI$ Creature.DamagedBy Remembered | Destination$ Graveyard | ReplaceWith$ RuneswordExile | Description$ If a creature dealt damage by CARDNAME this turn would die, exile it instead. SVar:RuneswordExile:DB$ ChangeZone | Defined$ ReplacedCard | Origin$ Battlefield | Destination$ Exile Oracle:{3}, {T}: Target attacking creature gets +2/+0 until end of turn. When that creature leaves the battlefield this turn, sacrifice Runesword. If the creature deals damage to a creature this turn, the creature dealt damage can't be regenerated this turn. If a creature dealt damage by the targeted creature would die this turn, exile that creature instead. diff --git a/forge-gui/res/cardsfolder/s/simulacrum.txt b/forge-gui/res/cardsfolder/s/simulacrum.txt index c7c3b7c9850..ca9060d1cc4 100644 --- a/forge-gui/res/cardsfolder/s/simulacrum.txt +++ b/forge-gui/res/cardsfolder/s/simulacrum.txt @@ -3,6 +3,6 @@ ManaCost:1 B Types:Instant A:SP$ GainLife | Cost$ 1 B | Defined$ You | LifeAmount$ X | SubAbility$ Dmg | SpellDescription$ You gain life equal to the damage dealt to you this turn. CARDNAME deals damage to target creature you control equal to the damage dealt to you this turn. SVar:Dmg:DB$ DealDamage | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | NumDmg$ X -SVar:X:Count$YourDamageThisTurn +SVar:X:PlayerCountPropertyYou$DamageThisTurn AI:RemoveDeck:All Oracle:You gain life equal to the damage dealt to you this turn. Simulacrum deals damage to target creature you control equal to the damage dealt to you this turn. diff --git a/forge-gui/res/cardsfolder/s/skarrgan_firebird.txt b/forge-gui/res/cardsfolder/s/skarrgan_firebird.txt index 2eb26dbf159..11b683a753c 100644 --- a/forge-gui/res/cardsfolder/s/skarrgan_firebird.txt +++ b/forge-gui/res/cardsfolder/s/skarrgan_firebird.txt @@ -5,5 +5,5 @@ PT:3/3 K:Bloodthirst:3 K:Flying A:AB$ ChangeZone | Cost$ R R R | Origin$ Graveyard | Destination$ Hand | ActivationZone$ Graveyard | CheckSVar$ OppDamaged | SVarCompare$ GE1 | SpellDescription$ Return CARDNAME from your graveyard to your hand. Activate only if an opponent was dealt damage this turn. -SVar:OppDamaged:Count$TotalOppDamageThisTurn +SVar:OppDamaged:PlayerCountPropertyYou$DamageToOppsThisTurn Oracle:Bloodthirst 3 (If an opponent was dealt damage this turn, this creature enters the battlefield with three +1/+1 counters on it.)\nFlying\n{R}{R}{R}: Return Skarrgan Firebird from your graveyard to your hand. Activate only if an opponent was dealt damage this turn. diff --git a/forge-gui/res/cardsfolder/s/skirk_alarmist.txt b/forge-gui/res/cardsfolder/s/skirk_alarmist.txt index ba869be08e9..c1bd2d5f393 100644 --- a/forge-gui/res/cardsfolder/s/skirk_alarmist.txt +++ b/forge-gui/res/cardsfolder/s/skirk_alarmist.txt @@ -3,7 +3,7 @@ ManaCost:1 R Types:Creature Human Wizard PT:1/2 K:Haste -A:AB$ SetState | Cost$ T | ValidTgts$ Creature.YouCtrl+faceDown | Mode$ TurnFace | SubAbility$ DBDelTrig | SpellDescription$ Turn target face-down creature you control face up. At the beginning of the next end step, sacrifice it. +A:AB$ SetState | Cost$ T | ValidTgts$ Creature.YouCtrl+faceDown | Mode$ TurnFace | SubAbility$ DBPump | SpellDescription$ Turn target face-down creature you control face up. At the beginning of the next end step, sacrifice it. SVar:DBPump:DB$ Pump | Defined$ Targeted | AtEOT$ Sacrifice AI:RemoveDeck:All Oracle:Haste\n{T}: Turn target face-down creature you control face up. At the beginning of the next end step, sacrifice it. diff --git a/forge-gui/res/cardsfolder/t/touch_of_moonglove.txt b/forge-gui/res/cardsfolder/t/touch_of_moonglove.txt index 7640f86d04a..016b2c55151 100644 --- a/forge-gui/res/cardsfolder/t/touch_of_moonglove.txt +++ b/forge-gui/res/cardsfolder/t/touch_of_moonglove.txt @@ -3,6 +3,6 @@ ManaCost:B Types:Instant A:SP$ Pump | Cost$ B | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ +1 | KW$ Deathtouch | RememberObjects$ Targeted | SubAbility$ DBMoonglove | SpellDescription$ Target creature you control gets +1/+0 and gains deathtouch until end of turn. Whenever a creature dealt damage by that creature dies this turn, its controller loses 2 life. (Any amount of damage a creature with deathtouch deals to a creature is enough to destroy it.) SVar:DBMoonglove:DB$ Effect | Triggers$ MoongloveTrigger | RememberObjects$ Remembered | StackDescription$ Whenever a creature dealt damage by that creature dies this turn, its controller loses 2 life. -SVar:MoongloveTrigger:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.DamagedByRemembered | TriggerZones$ Command | Execute$ TrigLoseLife | TriggerDescription$ Whenever a creature dealt damage by that creature dies this turn, its controller loses 2 life. +SVar:MoongloveTrigger:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.DamagedBy Remembered | TriggerZones$ Command | Execute$ TrigLoseLife | TriggerDescription$ Whenever a creature dealt damage by that creature dies this turn, its controller loses 2 life. SVar:TrigLoseLife:DB$ LoseLife | LifeAmount$ 2 | Defined$ TriggeredCardController Oracle:Target creature you control gets +1/+0 and gains deathtouch until end of turn. Whenever a creature dealt damage by that creature dies this turn, its controller loses 2 life. (Any amount of damage a creature with deathtouch deals to a creature is enough to destroy it.) diff --git a/forge-gui/res/cardsfolder/u/unscythe_killer_of_kings.txt b/forge-gui/res/cardsfolder/u/unscythe_killer_of_kings.txt index d58f6850bc9..df4df4ba31d 100644 --- a/forge-gui/res/cardsfolder/u/unscythe_killer_of_kings.txt +++ b/forge-gui/res/cardsfolder/u/unscythe_killer_of_kings.txt @@ -3,7 +3,7 @@ ManaCost:U B B R Types:Legendary Artifact Equipment K:Equip:2 S:Mode$ Continuous | Affected$ Creature.EquippedBy | AddPower$ 3 | AddToughness$ 3 | AddKeyword$ First Strike | Description$ Equipped creature gets +3/+3 and has first strike. -T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.DamagedByEquipped | Execute$ UnscytheTrigExile | TriggerZones$ Battlefield | OptionalDecider$ You | TriggerDescription$ Whenever a creature dealt damage by equipped creature this turn dies, you may exile that card. If you do, create a 2/2 black Zombie creature token. +T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.DamagedBy Equipped | Execute$ UnscytheTrigExile | TriggerZones$ Battlefield | OptionalDecider$ You | TriggerDescription$ Whenever a creature dealt damage by equipped creature this turn dies, you may exile that card. If you do, create a 2/2 black Zombie creature token. SVar:UnscytheTrigExile:DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Exile | RememberChanged$ True | SubAbility$ UnscytheDBToken SVar:UnscytheDBToken:DB$ Token | TokenOwner$ You | TokenAmount$ 1 | TokenScript$ b_2_2_zombie | ConditionDefined$ Remembered | ConditionPresent$ Card | SubAbility$ UnscytheDBCleanup SVar:UnscytheDBCleanup:DB$ Cleanup | ClearRemembered$ True diff --git a/forge-gui/res/cardsfolder/upcoming/dragon_cultist.txt b/forge-gui/res/cardsfolder/upcoming/dragon_cultist.txt new file mode 100644 index 00000000000..e46bd1c639d --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/dragon_cultist.txt @@ -0,0 +1,9 @@ +Name:Dragon Cultist +ManaCost:4 R +Types:Legendary Enchantment Background +S:Mode$ Continuous | Affected$ Creature.IsCommander+YouOwn | AddTrigger$ TrigDragon | Description$ Commander creatures you own have "At the beginning of your end step, if a source you controlled dealt 5 or more damage this turn, create a 4/4 red Dragon creature token with flying." +SVar:TrigDragon:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | CheckDefinedPlayer$ You.damageDoneSingleSource GE5 | Execute$ DBDragon | TriggerDescription$ At the beginning of your end step, if a source you controlled dealt 5 or more damage this turn, create a 4/4 red Dragon creature token with flying. +SVar:DBDragon:DB$ Token | TokenScript$ r_4_4_dragon_flying +DeckHas:Ability$Token & Type$Dragon +AI:RemoveDeck:NonCommander +Oracle:Commander creatures you own have "At the beginning of your end step, if a source you controlled dealt 5 or more damage this turn, create a 4/4 red Dragon creature token with flying." diff --git a/forge-gui/res/cardsfolder/upcoming/gnoll_war_band.txt b/forge-gui/res/cardsfolder/upcoming/gnoll_war_band.txt new file mode 100644 index 00000000000..ca21162fc94 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/gnoll_war_band.txt @@ -0,0 +1,9 @@ +Name:Gnoll War Band +ManaCost:5 R +Types:Creature Gnoll +PT:5/5 +S:Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Amount$ X | EffectZone$ All | Description$ This spell costs {1} less to cast for each opponent who was dealt damage this turn. +SVar:X:PlayerCountOpponents$HasPropertywasDealtDamageThisTurn +K:Menace +K:Myriad +Oracle:This spell costs {1} less to cast for each opponent who was dealt damage this turn.\nMenace\nMyriad diff --git a/forge-gui/res/cardsfolder/upcoming/indulge_excess.txt b/forge-gui/res/cardsfolder/upcoming/indulge_excess.txt new file mode 100644 index 00000000000..2358538d66a --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/indulge_excess.txt @@ -0,0 +1,22 @@ +Name:Indulge +ManaCost:2 R +Types:Sorcery +A:SP$ Effect | CounterType$ P1P1 | CounterNum$ 3 | Triggers$ TriggerAttacks | AddSVar$ AE | SpellDescription$ Whenever a creature you control attacks this turn, create a 1/1 green and white Citizen creature token that's tapped and attacking. +SVar:TriggerAttacks:Mode$ Attacks | ValidCard$ Creature.YouCtrl | TriggerZones$ Battlefield | Execute$ TrigToken | TriggerDescription$ Whenever a creature you control attacks this turn, create a 1/1 green and white Citizen creature token that's tapped and attacking. +SVar:TrigToken:DB$ Token | TokenScript$ gw_1_1_citizen | TokenTapped$ True | TokenAttacking$ True +SVar:AE:SVar:HasAttackEffect:TRUE +DeckHas:Ability$Token & Type$Citizen +AlternateMode:Split +Oracle:Whenever a creature you control attacks this turn, create a 1/1 green and white Citizen creature token that's tapped and attacking. + +ALTERNATE + +Name:Excess +ManaCost:1 R +Types:Sorcery +K:Aftermath +A:SP$ Token | TokenAmount$ X | TokenScript$ c_a_treasure_sac | SpellDescription$ Create a Treasure token for each creature you controlled that dealt combat damage to a player this turn. +SVar:X:Count$NumCombatDamageThisTurn Creature.YouCtrl Player +DeckHas:Ability$Token|Sacrifice|Graveyard & Type$Artifact|Treasure +SVar:NeedsToPlayVar:X GE3 +Oracle:Aftermath (Cast this spell only from your graveyard. Then exile it.)\nCreate a Treasure token for each creature you controlled that dealt combat damage to a player this turn. diff --git a/forge-gui/res/cardsfolder/w/war_elemental.txt b/forge-gui/res/cardsfolder/w/war_elemental.txt index 5a840ab8e8b..509d4f84f54 100644 --- a/forge-gui/res/cardsfolder/w/war_elemental.txt +++ b/forge-gui/res/cardsfolder/w/war_elemental.txt @@ -4,7 +4,7 @@ Types:Creature Elemental PT:1/1 T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigSac | TriggerDescription$ When CARDNAME enters the battlefield, sacrifice it unless an opponent was dealt damage this turn. SVar:TrigSac:DB$ Sacrifice | Defined$ Self | ConditionCheckSVar$ WarElementalX | ConditionSVarCompare$ EQ0 -SVar:WarElementalX:Count$TotalOppDamageThisTurn +SVar:WarElementalX:PlayerCountPropertyYou$DamageToOppsThisTurn T:Mode$ DamageDoneOnce | ValidSource$ Card | ValidTarget$ Opponent | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever an opponent is dealt damage, put that many +1/+1 counters on CARDNAME. SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ WarElementalY SVar:WarElementalY:TriggerCount$DamageAmount diff --git a/forge-gui/res/cardsfolder/w/wicked_akuba.txt b/forge-gui/res/cardsfolder/w/wicked_akuba.txt index 66af74d7b07..4f3783e1e35 100644 --- a/forge-gui/res/cardsfolder/w/wicked_akuba.txt +++ b/forge-gui/res/cardsfolder/w/wicked_akuba.txt @@ -2,5 +2,5 @@ Name:Wicked Akuba ManaCost:B B Types:Creature Spirit PT:2/2 -A:AB$ LoseLife | Cost$ B | ValidTgts$ Player.wasDealtDamageThisTurnBy Self | TgtPrompt$ Select target player that was dealt damage this turn | LifeAmount$ 1 | SpellDescription$ Target player dealt damage by CARDNAME this turn loses 1 life. +A:AB$ LoseLife | Cost$ B | ValidTgts$ Player.wasDealtDamageThisTurnBySource | TgtPrompt$ Select target player that was dealt damage this turn | LifeAmount$ 1 | SpellDescription$ Target player dealt damage by CARDNAME this turn loses 1 life. Oracle:{B}: Target player dealt damage by Wicked Akuba this turn loses 1 life.