diff --git a/Mage.Sets/src/mage/cards/b/Biophagus.java b/Mage.Sets/src/mage/cards/b/Biophagus.java index ab333cdb4e4..a42dfb1ca57 100644 --- a/Mage.Sets/src/mage/cards/b/Biophagus.java +++ b/Mage.Sets/src/mage/cards/b/Biophagus.java @@ -36,7 +36,7 @@ public final class Biophagus extends CardImpl { Ability ability = new AnyColorManaAbility(new TapSourceCost(), true).withFlavorWord("Genomic Enhancement"); ability.getEffects().get(0).setText("Add one mana of any color. If this mana is spent to cast a creature spell, " + "that creature enters the battlefield with an additional +1/+1 counter on it."); - this.addAbility(ability, new BiophagusWatcher(ability)); + this.addAbility(ability, new BiophagusWatcher(ability.getId())); } private Biophagus(final Biophagus card) { @@ -51,11 +51,11 @@ public final class Biophagus extends CardImpl { class BiophagusWatcher extends Watcher { - private final Ability source; + private final UUID sourceAbilityID; - BiophagusWatcher(Ability source) { + BiophagusWatcher(UUID sourceAbilityID) { super(WatcherScope.CARD); - this.source = source; + this.sourceAbilityID = sourceAbilityID; } @Override @@ -68,7 +68,8 @@ class BiophagusWatcher extends Watcher { && event.getFlag()) { if (target instanceof Spell) { game.getState().addEffect(new BiophagusEntersBattlefieldEffect( - new MageObjectReference(((Spell) target).getSourceId(), target.getZoneChangeCounter(game), game)), source); + new MageObjectReference(((Spell) target).getSourceId(), target.getZoneChangeCounter(game), game)), + game.getAbility(sourceAbilityID, this.getSourceId()).orElse(null)); //null will cause an immediate crash } } } diff --git a/Mage.Sets/src/mage/cards/g/GuildmagesForum.java b/Mage.Sets/src/mage/cards/g/GuildmagesForum.java index 0e85f773e04..0504a925027 100644 --- a/Mage.Sets/src/mage/cards/g/GuildmagesForum.java +++ b/Mage.Sets/src/mage/cards/g/GuildmagesForum.java @@ -39,7 +39,7 @@ public final class GuildmagesForum extends CardImpl { Ability ability = new AnyColorManaAbility(new GenericManaCost(1), true); ability.getEffects().get(0).setText("Add one mana of any color. If that mana is spent on a multicolored creature spell, that creature enters the battlefield with an additional +1/+1 counter on it"); ability.addCost(new TapSourceCost()); - this.addAbility(ability, new GuildmagesForumWatcher(ability)); + this.addAbility(ability, new GuildmagesForumWatcher(ability.getId())); } private GuildmagesForum(final GuildmagesForum card) { @@ -54,11 +54,11 @@ public final class GuildmagesForum extends CardImpl { class GuildmagesForumWatcher extends Watcher { - private final Ability source; + private final UUID sourceAbilityID; - GuildmagesForumWatcher(Ability source) { + GuildmagesForumWatcher(UUID sourceAbilityID) { super(WatcherScope.CARD); - this.source = source; + this.sourceAbilityID = sourceAbilityID; } @Override @@ -71,7 +71,8 @@ class GuildmagesForumWatcher extends Watcher { && event.getFlag()) { if (target instanceof Spell) { game.getState().addEffect(new GuildmagesForumEntersBattlefieldEffect( - new MageObjectReference(((Spell) target).getSourceId(), target.getZoneChangeCounter(game), game)), source); + new MageObjectReference(((Spell) target).getSourceId(), target.getZoneChangeCounter(game), game)), + game.getAbility(sourceAbilityID, this.getSourceId()).orElse(null)); //null will cause an immediate crash } } } diff --git a/Mage.Sets/src/mage/cards/h/HallOfTheBanditLord.java b/Mage.Sets/src/mage/cards/h/HallOfTheBanditLord.java index 7f12b0b92a0..2ef0ebce41f 100644 --- a/Mage.Sets/src/mage/cards/h/HallOfTheBanditLord.java +++ b/Mage.Sets/src/mage/cards/h/HallOfTheBanditLord.java @@ -2,6 +2,7 @@ package mage.cards.h; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import mage.MageObject; import mage.Mana; @@ -45,7 +46,7 @@ public final class HallOfTheBanditLord extends CardImpl { effect.setText("Add {C}. If that mana is spent on a creature spell, it gains haste"); Ability ability = new SimpleManaAbility(Zone.BATTLEFIELD, effect, new TapSourceCost()); ability.addCost(new PayLifeCost(3)); - this.addAbility(ability, new HallOfTheBanditLordWatcher(ability)); + this.addAbility(ability, new HallOfTheBanditLordWatcher(ability.getId())); } private HallOfTheBanditLord(final HallOfTheBanditLord card) { @@ -60,12 +61,12 @@ public final class HallOfTheBanditLord extends CardImpl { class HallOfTheBanditLordWatcher extends Watcher { - private final Ability source; + private final UUID sourceAbilityID; private final List creatures = new ArrayList<>(); - HallOfTheBanditLordWatcher(Ability source) { + HallOfTheBanditLordWatcher(UUID sourceAbilityID) { super(WatcherScope.CARD); - this.source = source; + this.sourceAbilityID = sourceAbilityID; } @Override @@ -99,7 +100,7 @@ class HallOfTheBanditLordWatcher extends Watcher { if (creatures.contains(event.getSourceId())) { ContinuousEffect effect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.Custom); effect.setTargetPointer(new FixedTarget(event.getSourceId(), game)); - game.addEffect(effect, source); + game.addEffect(effect, game.getAbility(sourceAbilityID, this.getSourceId()).orElse(null)); //null will cause an immediate crash creatures.remove(event.getSourceId()); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java index a37845af21c..b332c8f80bd 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java @@ -10,6 +10,8 @@ import mage.cards.repository.CardRepository; import mage.constants.PhaseStep; import mage.constants.Zone; import mage.counters.CounterType; +import mage.game.permanent.PermanentCard; +import mage.game.permanent.PermanentToken; import mage.util.CardUtil; import org.junit.Assert; import org.junit.Ignore; @@ -571,10 +573,10 @@ public class CopySpellTest extends CardTestPlayerBase { } @Test - public void test_CopiedSpellsHasntETB() { + public void test_CopiedSpellsETBCounters() { // testing: // - x in copied creature spell (copy x) - // - copied spells enters as tokens and it hasn't ETB, see rules below + // - copied spells enters as tokens and correctly ETB, see rules below // 0/0 // Capricopian enters the battlefield with X +1/+1 counters on it. @@ -616,36 +618,34 @@ public class CopySpellTest extends CardTestPlayerBase { activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Grenzo, Dungeon Warden", "Grenzo, Dungeon Warden"); - // ETB triggers will not trigger here due not normal cast. From rules: - // - The token that a resolving copy of a spell becomes isn’t said to have been “created.” (2021-04-16) - // - A nontoken permanent “enters the battlefield” when it’s moved onto the battlefield from another zone. - // A token “enters the battlefield” when it’s created. See rules 403.3, 603.6a, 603.6d, and 614.12. - // - // So both copies enters without counters: - // - Capricopian copy must die - // - Grenzo, Dungeon Warden must have default PT + // 608.3f If the object that’s resolving is a copy of a permanent spell, it will become a token permanent + // as it is put onto the battlefield in any of the steps above. + // 111.12. A copy of a permanent spell becomes a token as it resolves. The token has the characteristics of + // the spell that became that token. The token is not “created” for the purposes of any replacement effects + // or triggered abilities that refer to creating a token. + // The tokens must enter with counters waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); - checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian", 1); // copy dies + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian", 2); checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden", 2); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); - // counters checks + // counters checks, have to check if it's a card or a token since token copies have isCopy()=false int originalCounters = currentGame.getBattlefield().getAllActivePermanents().stream() .filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) - .filter(p -> !p.isCopy()) + .filter(p -> p instanceof PermanentCard) .mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1)) .sum(); int copyCounters = currentGame.getBattlefield().getAllActivePermanents().stream() .filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) - .filter(p -> p.isCopy()) + .filter(p -> p instanceof PermanentToken) .mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1)) .sum(); Assert.assertEquals("original grenzo must have 2x counters", 2, originalCounters); - Assert.assertEquals("copied grenzo must have 0x counters", 0, copyCounters); + Assert.assertEquals("copied grenzo must have 2x counters", 2, copyCounters); } @Test @@ -748,7 +748,6 @@ public class CopySpellTest extends CardTestPlayerBase { * Thieving Skydiver is kicked and then copied, but the copied version does not let you gain control of anything. */ @Test - @Ignore public void copySpellWithKicker() { // When Thieving Skydiver enters the battlefield, if it was kicked, gain control of target artifact with mana value X or less. // If that artifact is an Equipment, attach it to Thieving Skydiver. @@ -758,7 +757,8 @@ public class CopySpellTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Island", 3); // Original price, + 1 kicker, + 1 for Double Major addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); - addCard(Zone.BATTLEFIELD, playerB, "Sol Ring", 2); + addCard(Zone.BATTLEFIELD, playerB, "Sol Ring", 1); + addCard(Zone.BATTLEFIELD, playerB, "Expedition Map", 1); setStrictChooseMode(true); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thieving Skydiver"); @@ -766,14 +766,16 @@ public class CopySpellTest extends CardTestPlayerBase { setChoice(playerA, "X=1"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Thieving Skydiver", "Thieving Skydiver"); addTarget(playerA, "Sol Ring"); // Choice for copy - addTarget(playerA, "Sol Ring"); // Choice for original + addTarget(playerA, "Expedition Map"); // Choice for original setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - assertPermanentCount(playerA, "Sol Ring", 2); // 1 taken by original, one by copy + assertPermanentCount(playerA, "Sol Ring", 1); + assertPermanentCount(playerA, "Expedition Map", 1); assertPermanentCount(playerB, "Sol Ring", 0); + assertPermanentCount(playerB, "Expedition Map", 0); } private void abilitySourceMustBeSame(Card card, String infoPrefix) { diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/CardIconsTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/CardIconsTest.java index 543e7e9ab45..7a93e3942ab 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/CardIconsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/CardIconsTest.java @@ -65,7 +65,7 @@ public class CardIconsTest extends CardTestPlayerBase { } @Test - public void test_CostX_Copies() { + public void test_CostX_StackCopy() { // Grenzo, Dungeon Warden enters the battlefield with X +1/+1 counters on it. addCard(Zone.HAND, playerA, "Grenzo, Dungeon Warden", 1);// {X}{B}{R} addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); @@ -144,6 +144,67 @@ public class CardIconsTest extends CardTestPlayerBase { .orElse(null); Assert.assertNotNull("copied card must be in battlefield", copiedCardView); Assert.assertEquals("copied must have x cost card icons", 1, copiedCardView.getCardIcons().size()); + Assert.assertEquals("copied x cost text", "x=2", copiedCardView.getCardIcons().get(0).getText()); + }); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_CostX_TokenCopy() { + //Legend Rule doesn't apply + addCard(Zone.BATTLEFIELD, playerA, "Mirror Gallery", 1); + // Grenzo, Dungeon Warden enters the battlefield with X +1/+1 counters on it. + addCard(Zone.HAND, playerA, "Grenzo, Dungeon Warden", 1);// {X}{B}{R} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + // Create a token that's a copy of target creature you control. + // should not copy the X value of the Grenzo + addCard(Zone.HAND, playerA, "Quasiduplicate", 1); // {1}{U}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + + // cast Grenzo + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 2); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 1); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden"); + setChoice(playerA, "X=2"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // cast Quasiduplicate + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Quasiduplicate", "Grenzo, Dungeon Warden"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden", 2); + + // battlefield (card and copied card as token) + runCode("card icons in battlefield (cloned)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + GameView gameView = getGameView(player); + PlayerView playerView = gameView.getPlayers().get(0); + Assert.assertEquals("player", player.getName(), playerView.getName()); + // original + CardView originalCardView = playerView.getBattlefield().values() + .stream() + .filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) + .filter(p -> !p.isToken()) + .findFirst() + .orElse(null); + Assert.assertNotNull("original card must be in battlefield", originalCardView); + Assert.assertEquals("original must have x cost card icons", 1, originalCardView.getCardIcons().size()); + Assert.assertEquals("original x cost text", "x=2", originalCardView.getCardIcons().get(0).getText()); + // + CardView copiedCardView = playerView.getBattlefield().values() + .stream() + .filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) + .filter(p -> p.isToken()) + .findFirst() + .orElse(null); + Assert.assertNotNull("copied card must be in battlefield", copiedCardView); + Assert.assertEquals("copied must have x cost card icons", 1, copiedCardView.getCardIcons().size()); Assert.assertEquals("copied x cost text", "x=0", copiedCardView.getCardIcons().get(0).getText()); }); diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index 6bbf5e83b97..c36be304506 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -26,6 +26,7 @@ import mage.watchers.Watcher; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; /** @@ -157,6 +158,26 @@ public interface Ability extends Controllable, Serializable { void addManaCostsToPay(ManaCost manaCost); + /** + * Gets a map of the cost tags (set while casting/activating) of this ability, can be null if no tags have been set yet + * does NOT return the source permanent's tags + * + * @return The map of tags and corresponding objects + */ + Map getCostsTagMap(); + + /** + * Set tag to the value, initializes this ability's tags map if it is null + */ + void setCostsTag(String tag, Object value); + /** + * Returns the value of the tag or defaultValue if the tag is not found in this ability's tag map + * does NOT check the source permanent's tags, use CardUtil.getSourceCostsTag for that + * + * @return The given tag value (or the default if not found) + */ + Object getCostsTagOrDefault(String tag, Object defaultValue); + /** * Retrieves the effects that are put into the place by the resolution of * this ability. diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 29d3722227a..817f03f9436 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -83,6 +83,7 @@ public abstract class AbilityImpl implements Ability { protected MageIdentifier identifier = MageIdentifier.Default; // used to identify specific ability (e.g. to match with corresponding watcher) protected String appendToRule = null; protected int sourcePermanentTransformCount = 0; + private Map costsTagMap = null; protected AbilityImpl(AbilityType abilityType, Zone zone) { this.id = UUID.randomUUID(); @@ -107,16 +108,9 @@ public abstract class AbilityImpl implements Ability { this.manaCosts = ability.manaCosts.copy(); this.manaCostsToPay = ability.manaCostsToPay.copy(); this.costs = ability.costs.copy(); - for (Watcher watcher : ability.getWatchers()) { - watchers.add(watcher.copy()); - } + this.watchers = CardUtil.deepCopyObject(ability.getWatchers()); - if (ability.subAbilities != null) { - this.subAbilities = new ArrayList<>(); - for (Ability subAbility : ability.subAbilities) { - subAbilities.add(subAbility.copy()); - } - } + this.subAbilities = CardUtil.deepCopyObject(ability.subAbilities); this.modes = ability.getModes().copy(); this.ruleAtTheTop = ability.ruleAtTheTop; this.ruleVisible = ability.ruleVisible; @@ -129,17 +123,14 @@ public abstract class AbilityImpl implements Ability { this.canFizzle = ability.canFizzle; this.targetAdjuster = ability.targetAdjuster; this.costAdjuster = ability.costAdjuster; - for (Hint hint : ability.getHints()) { - this.hints.add(hint.copy()); - } - for (CardIcon icon : ability.getIcons()) { - this.icons.add(icon.copy()); - } + this.hints = CardUtil.deepCopyObject(ability.getHints()); + this.icons = CardUtil.deepCopyObject(ability.getIcons()); this.customOutcome = ability.customOutcome; this.identifier = ability.identifier; this.activated = ability.activated; this.appendToRule = ability.appendToRule; this.sourcePermanentTransformCount = ability.sourcePermanentTransformCount; + this.costsTagMap = CardUtil.deepCopyObject(ability.costsTagMap); } @Override @@ -527,6 +518,7 @@ public abstract class AbilityImpl implements Ability { ((Cost) variableCost).setPaid(); String message = controller.getLogName() + " announces a value of " + xValue + " (" + variableCost.getActionText() + ')'; announceString.append(message); + setCostsTag("X",xValue); } } return announceString.toString(); @@ -631,6 +623,7 @@ public abstract class AbilityImpl implements Ability { } addManaCostsToPay(new ManaCostsImpl<>(manaString.toString())); getManaCostsToPay().setX(xValue * xValueMultiplier, amountMana); + setCostsTag("X",xValue * xValueMultiplier); } variableManaCost.setPaid(); } @@ -713,6 +706,28 @@ public abstract class AbilityImpl implements Ability { return manaCostsToPay; } + /** + * Accessed to see what was optional/variable costs were paid + * + * @return + */ + @Override + public Map getCostsTagMap() { + return costsTagMap; + } + public void setCostsTag(String tag, Object value){ + if (costsTagMap == null){ + costsTagMap = new HashMap<>(); + } + costsTagMap.put(tag, value); + } + public Object getCostsTagOrDefault(String tag, Object defaultValue){ + if (costsTagMap != null && costsTagMap.containsKey(tag)){ + return costsTagMap.get(tag); + } + return defaultValue; + } + @Override public Effects getEffects() { return getModes().getMode().getEffects(); diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetKickerXValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetKickerXValue.java index e4ab0453cc0..cc63b4a614b 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetKickerXValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetKickerXValue.java @@ -1,12 +1,10 @@ package mage.abilities.dynamicvalue.common; import mage.abilities.Ability; -import mage.abilities.costs.OptionalAdditionalCostImpl; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; -import mage.abilities.keyword.KickerAbility; import mage.game.Game; -import mage.game.stack.Spell; +import mage.util.CardUtil; /** @@ -19,35 +17,9 @@ public enum GetKickerXValue implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - // calcs only kicker with X values - - // kicker adds additional costs to spell ability - // only one X value per card possible - // kicker can be calls multiple times (use getKickedCounter) - - int countX = 0; - Spell spell = game.getSpellOrLKIStack(sourceAbility.getSourceId()); - if (spell != null && spell.getSpellAbility() != null) { - int xValue = spell.getSpellAbility().getManaCostsToPay().getX(); - for (Ability ability : spell.getAbilities()) { - if (ability instanceof KickerAbility) { - - // search that kicker used X value - KickerAbility kickerAbility = (KickerAbility) ability; - boolean haveVarCost = kickerAbility.getKickerCosts() - .stream() - .anyMatch(varCost -> !((OptionalAdditionalCostImpl) varCost).getVariableCosts().isEmpty()); - - if (haveVarCost) { - int kickedCount = ((KickerAbility) ability).getKickedCounter(game, sourceAbility); - if (kickedCount > 0) { - countX += kickedCount * xValue; - } - } - } - } - } - return countX; + // Currently identical logic to the Manacost X value + // which should be fine since you can only have one X at a time + return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0); } @Override diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetXLoyaltyValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetXLoyaltyValue.java index e72a3472832..19240bedfbb 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetXLoyaltyValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetXLoyaltyValue.java @@ -1,11 +1,10 @@ package mage.abilities.dynamicvalue.common; import mage.abilities.Ability; -import mage.abilities.costs.Cost; -import mage.abilities.costs.common.PayVariableLoyaltyCost; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.game.Game; +import mage.util.CardUtil; /** * @author TheElk801 @@ -15,12 +14,7 @@ public enum GetXLoyaltyValue implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - for (Cost cost : sourceAbility.getCosts()) { - if (cost instanceof PayVariableLoyaltyCost) { - return ((PayVariableLoyaltyCost) cost).getAmount(); - } - } - return 0; + return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0); } @Override diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetXValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetXValue.java index 72d19416cda..e0854eaa7da 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetXValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GetXValue.java @@ -1,10 +1,10 @@ package mage.abilities.dynamicvalue.common; import mage.abilities.Ability; -import mage.abilities.costs.VariableCost; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.game.Game; +import mage.util.CardUtil; /** * @author BetaSteward_at_googlemail.com @@ -14,12 +14,7 @@ public enum GetXValue implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - return sourceAbility - .getCosts() - .getVariableCosts() - .stream() - .mapToInt(VariableCost::getAmount) - .sum(); + return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0); } @Override diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ManacostVariableValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ManacostVariableValue.java index fb22062caed..26c50d8b697 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ManacostVariableValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ManacostVariableValue.java @@ -4,10 +4,11 @@ import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.game.Game; -import mage.watchers.common.ManaSpentToCastWatcher; +import mage.util.CardUtil; public enum ManacostVariableValue implements DynamicValue { - + //TODO: all three of these variants plus GetXValue, GetKickerXValue, and GetXLoyaltyValue use the same logic + // and should be consolidated into a single instance REGULAR, // if you need X on cast/activate (in stack) - reset each turn ETB, // if you need X after ETB (in battlefield) - reset each turn END_GAME; // if you need X until end game - keep data forever @@ -15,18 +16,7 @@ public enum ManacostVariableValue implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - if (this == REGULAR) { - return sourceAbility.getManaCostsToPay().getX(); - } - ManaSpentToCastWatcher watcher = game.getState().getWatcher(ManaSpentToCastWatcher.class); - if (watcher != null) { - if (this == END_GAME) { - return watcher.getLastXValue(sourceAbility, true); - } else { - return watcher.getLastXValue(sourceAbility, false); - } - } - return 0; + return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0); } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/common/EntersBattlefieldWithXCountersEffect.java b/Mage/src/main/java/mage/abilities/effects/common/EntersBattlefieldWithXCountersEffect.java index 0b7730027d1..4e74068c5cc 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/EntersBattlefieldWithXCountersEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/EntersBattlefieldWithXCountersEffect.java @@ -1,8 +1,6 @@ package mage.abilities.effects.common; import mage.abilities.Ability; -import mage.abilities.SpellAbility; -import mage.abilities.effects.EntersBattlefieldEffect; import mage.abilities.effects.OneShotEffect; import mage.constants.AbilityType; import mage.constants.Outcome; @@ -59,19 +57,12 @@ public class EntersBattlefieldWithXCountersEffect extends OneShotEffect { } } if (permanent != null) { - SpellAbility spellAbility = (SpellAbility) getValue(EntersBattlefieldEffect.SOURCE_CAST_SPELL_ABILITY); - if (spellAbility != null - && spellAbility.getSourceId().equals(source.getSourceId()) - && permanent.getZoneChangeCounter(game) == spellAbility.getSourceObjectZoneChangeCounter()) { - if (spellAbility.getSourceId().equals(source.getSourceId())) { // put into play by normal cast - int amount = spellAbility.getManaCostsToPay().getX() * this.multiplier; - if (amount > 0) { - Counter counterToAdd = counter.copy(); - counterToAdd.add(amount - counter.getCount()); - List appliedEffects = (ArrayList) this.getValue("appliedEffects"); - permanent.addCounters(counterToAdd, source.getControllerId(), source, game, appliedEffects); - } - } + int amount = ((int) CardUtil.getSourceCostsTag(game, source, "X", 0)) * multiplier; + if (amount > 0) { + Counter counterToAdd = counter.copy(); + counterToAdd.add(amount - counter.getCount()); + List appliedEffects = (ArrayList) this.getValue("appliedEffects"); + permanent.addCounters(counterToAdd, source.getControllerId(), source, game, appliedEffects); } } return true; diff --git a/Mage/src/main/java/mage/abilities/hint/Hint.java b/Mage/src/main/java/mage/abilities/hint/Hint.java index cf39f69cc8b..2a18db0c2af 100644 --- a/Mage/src/main/java/mage/abilities/hint/Hint.java +++ b/Mage/src/main/java/mage/abilities/hint/Hint.java @@ -2,13 +2,14 @@ package mage.abilities.hint; import mage.abilities.Ability; import mage.game.Game; +import mage.util.Copyable; import java.io.Serializable; /** * @author JayDi85 */ -public interface Hint extends Serializable { +public interface Hint extends Serializable, Copyable { // It's a constant hint for cards/permanents (e.g. visible all the time) // diff --git a/Mage/src/main/java/mage/cards/Cards.java b/Mage/src/main/java/mage/cards/Cards.java index 878a0838c84..2dde930b91e 100644 --- a/Mage/src/main/java/mage/cards/Cards.java +++ b/Mage/src/main/java/mage/cards/Cards.java @@ -4,6 +4,7 @@ import mage.abilities.Ability; import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.Game; +import mage.util.Copyable; import java.io.Serializable; import java.util.Collection; @@ -11,7 +12,7 @@ import java.util.List; import java.util.Set; import java.util.UUID; -public interface Cards extends Set, Serializable { +public interface Cards extends Set, Serializable, Copyable { /** * Add the passed in card to the set if it's not null. diff --git a/Mage/src/main/java/mage/counters/Counter.java b/Mage/src/main/java/mage/counters/Counter.java index d58d781e10e..de42b834f57 100644 --- a/Mage/src/main/java/mage/counters/Counter.java +++ b/Mage/src/main/java/mage/counters/Counter.java @@ -1,6 +1,7 @@ package mage.counters; import mage.util.CardUtil; +import mage.util.Copyable; import java.io.Serializable; @@ -9,7 +10,7 @@ import org.apache.log4j.Logger; /** * @author BetaSteward_at_googlemail.com */ -public class Counter implements Serializable { +public class Counter implements Serializable, Copyable { private static final Logger logger = Logger.getLogger(Counter.class); diff --git a/Mage/src/main/java/mage/counters/Counters.java b/Mage/src/main/java/mage/counters/Counters.java index 5fbc966c312..89c569d6da6 100644 --- a/Mage/src/main/java/mage/counters/Counters.java +++ b/Mage/src/main/java/mage/counters/Counters.java @@ -1,6 +1,8 @@ package mage.counters; +import mage.util.Copyable; + import java.io.Serializable; import java.util.HashMap; import java.util.List; @@ -10,7 +12,7 @@ import java.util.stream.Collectors; /** * @author BetaSteward_at_googlemail.com */ -public class Counters extends HashMap implements Serializable { +public class Counters extends HashMap implements Serializable, Copyable { public Counters() { } diff --git a/Mage/src/main/java/mage/filter/Filter.java b/Mage/src/main/java/mage/filter/Filter.java index 50d0cceeeae..8ed5e6dd831 100644 --- a/Mage/src/main/java/mage/filter/Filter.java +++ b/Mage/src/main/java/mage/filter/Filter.java @@ -2,6 +2,7 @@ package mage.filter; import mage.filter.predicate.Predicate; import mage.game.Game; +import mage.util.Copyable; import java.io.Serializable; import java.util.List; @@ -11,7 +12,7 @@ import java.util.List; * @author BetaSteward_at_googlemail.com * @author North */ -public interface Filter extends Serializable { +public interface Filter extends Serializable, Copyable> { enum ComparisonScope { Any, All diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 414b2f2b7ae..2eaa7e37ca5 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -2,6 +2,7 @@ package mage.game; import mage.MageItem; import mage.MageObject; +import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.ActivatedAbility; import mage.abilities.DelayedTriggeredAbility; @@ -118,6 +119,12 @@ public interface Game extends MageItem, Serializable, Copyable { Map getPermanentsEntering(); Map> getLKI(); + Map> getPermanentCostsTags(); + + /** + * Take the source's Costs Tags and store it for later access through the MOR. + */ + void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source); // Result must be checked for null. Possible errors search pattern: (\S*) = game.getCard.+\n(?!.+\1 != null) Card getCard(UUID cardId); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index b36ca501bd8..fc091a6b8d4 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -2,6 +2,7 @@ package mage.game; import mage.MageException; import mage.MageObject; +import mage.MageObjectReference; import mage.abilities.*; import mage.abilities.common.AttachableToRestrictedAbility; import mage.abilities.common.CantHaveMoreThanAmountCountersSourceAbility; @@ -184,48 +185,16 @@ public abstract class GameImpl implements Game { //this.tableEventSource = game.tableEventSource; // client-server part, not need on copy/simulations //this.playerQueryEventSource = game.playerQueryEventSource; // client-server part, not need on copy/simulations - for (Entry entry : game.gameCards.entrySet()) { - this.gameCards.put(entry.getKey(), entry.getValue().copy()); - } - for (Entry entry : game.meldCards.entrySet()) { - this.meldCards.put(entry.getKey(), entry.getValue().copy()); - } + this.gameCards = CardUtil.deepCopyObject(game.gameCards); + this.meldCards = CardUtil.deepCopyObject(game.meldCards); - // lki - for (Entry> entry : game.lki.entrySet()) { - Map lkiMap = new HashMap<>(); - for (Entry entryMap : entry.getValue().entrySet()) { - lkiMap.put(entryMap.getKey(), entryMap.getValue().copy()); - } - this.lki.put(entry.getKey(), lkiMap); - } - // lkiCardState - for (Entry> entry : game.lkiCardState.entrySet()) { - Map lkiMap = new HashMap<>(); - for (Entry entryMap : entry.getValue().entrySet()) { - lkiMap.put(entryMap.getKey(), entryMap.getValue().copy()); - } - this.lkiCardState.put(entry.getKey(), lkiMap); - } - // lkiExtended - for (Entry> entry : game.lkiExtended.entrySet()) { - Map lkiMap = new HashMap<>(); - for (Entry entryMap : entry.getValue().entrySet()) { - lkiMap.put(entryMap.getKey(), entryMap.getValue().copy()); - } - this.lkiExtended.put(entry.getKey(), lkiMap); - } - // lkiShortLiving - for (Entry> entry : game.lkiShortLiving.entrySet()) { - this.lkiShortLiving.put(entry.getKey(), new HashSet<>(entry.getValue())); - } + this.lki = CardUtil.deepCopyObject(game.lki); + this.lkiCardState = CardUtil.deepCopyObject(game.lkiCardState); + this.lkiExtended = CardUtil.deepCopyObject(game.lkiExtended); + this.lkiShortLiving = CardUtil.deepCopyObject(game.lkiShortLiving); - for (Entry entry : game.permanentsEntering.entrySet()) { - this.permanentsEntering.put(entry.getKey(), entry.getValue().copy()); - } - for (Entry entry : game.enterWithCounters.entrySet()) { - this.enterWithCounters.put(entry.getKey(), entry.getValue().copy()); - } + this.permanentsEntering = CardUtil.deepCopyObject(game.permanentsEntering); + this.enterWithCounters = CardUtil.deepCopyObject(game.enterWithCounters); this.state = game.state.copy(); // client-server part, not need on copy/simulations: @@ -1451,6 +1420,10 @@ public abstract class GameImpl implements Game { player.endOfTurn(this); } state.resetWatchers(); + // Could be done any time as long as the stack is empty + // Tags are stored in the game state as a spell resolves into a permanent + // and must be kept while any abilities with that permanent as a source could resolve + state.cleanupPermanentCostsTags(this); } protected UUID pickChoosingPlayer() { @@ -3560,6 +3533,15 @@ public abstract class GameImpl implements Game { return lki; } + @Override + public Map> getPermanentCostsTags() { + return state.getPermanentCostsTags(); + } + @Override + public void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source){ + state.storePermanentCostsTags(permanentMOR, source); + } + @Override public void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard, List command) { // fake test ability for triggers and events diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 312aeb36009..9d467e965f9 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -101,6 +101,7 @@ public class GameState implements Serializable, Copyable { private Map zones = new HashMap<>(); private List simultaneousEvents = new ArrayList<>(); private Map cardState = new HashMap<>(); + private Map> permanentCostsTags = new HashMap<>(); // Permanent reference -> map of (tag -> values) describing how the permanent's spell was cast private Map mageObjectAttribute = new HashMap<>(); private Map zoneChangeCounter = new HashMap<>(); private Map copiedCards = new HashMap<>(); @@ -162,36 +163,19 @@ public class GameState implements Serializable, Copyable { this.stepNum = state.stepNum; this.extraTurnId = state.extraTurnId; this.effects = state.effects.copy(); - for (TriggeredAbility trigger : state.triggered) { - this.triggered.add(trigger.copy()); - } + this.triggered = CardUtil.deepCopyObject(state.triggered); this.triggers = state.triggers.copy(); this.delayed = state.delayed.copy(); this.specialActions = state.specialActions.copy(); this.combat = state.combat.copy(); this.turnMods = state.turnMods.copy(); this.watchers = state.watchers.copy(); - for (Map.Entry entry : state.values.entrySet()) { - if (entry.getValue() instanceof HashSet) { - this.values.put(entry.getKey(), ((HashSet) entry.getValue()).clone()); - } else if (entry.getValue() instanceof EnumSet) { - this.values.put(entry.getKey(), ((EnumSet) entry.getValue()).clone()); - } else if (entry.getValue() instanceof HashMap) { - this.values.put(entry.getKey(), ((HashMap) entry.getValue()).clone()); - } else if (entry.getValue() instanceof List) { - this.values.put(entry.getKey(), ((List) entry.getValue()).stream().collect(Collectors.toList())); - } else { - this.values.put(entry.getKey(), entry.getValue()); - } - } + this.values = CardUtil.deepCopyObject(state.values); this.zones.putAll(state.zones); this.simultaneousEvents.addAll(state.simultaneousEvents); - for (Map.Entry entry : state.cardState.entrySet()) { - cardState.put(entry.getKey(), entry.getValue().copy()); - } - for (Map.Entry entry : state.mageObjectAttribute.entrySet()) { - mageObjectAttribute.put(entry.getKey(), entry.getValue().copy()); - } + this.cardState = CardUtil.deepCopyObject(state.cardState); + this.permanentCostsTags = CardUtil.deepCopyObject(state.permanentCostsTags); + this.mageObjectAttribute = CardUtil.deepCopyObject(state.mageObjectAttribute); this.zoneChangeCounter.putAll(state.zoneChangeCounter); this.copiedCards.putAll(state.copiedCards); this.permanentOrderNumber = state.permanentOrderNumber; @@ -231,6 +215,7 @@ public class GameState implements Serializable, Copyable { gameOver = false; specialActions.clear(); cardState.clear(); + permanentCostsTags.clear(); combat.clear(); turnMods.clear(); watchers.clear(); @@ -280,6 +265,7 @@ public class GameState implements Serializable, Copyable { this.zones = state.zones; this.simultaneousEvents = state.simultaneousEvents; this.cardState = state.cardState; + this.permanentCostsTags = state.permanentCostsTags; this.mageObjectAttribute = state.mageObjectAttribute; this.zoneChangeCounter = state.zoneChangeCounter; this.copiedCards = state.copiedCards; @@ -1369,6 +1355,29 @@ public class GameState implements Serializable, Copyable { return mageObjectAtt; } + public Map> getPermanentCostsTags() { + return permanentCostsTags; + } + + /** + * Store the tags of source ability using the MOR as a reference + */ + void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source){ + if (source.getCostsTagMap() != null) { + permanentCostsTags.put(permanentMOR, CardUtil.deepCopyObject(source.getCostsTagMap())); + } + } + + /** + * Removes the cost tags if the corresponding permanent is no longer on the battlefield. + * Only use if the stack is empty and nothing can refer to them anymore (such as at EOT, the current behavior) + */ + public void cleanupPermanentCostsTags(Game game){ + getPermanentCostsTags().entrySet().removeIf(entry -> + !(entry.getKey().zoneCounterIsCurrent(game)) + ); + } + public void addWatcher(Watcher watcher) { this.watchers.add(watcher); } diff --git a/Mage/src/main/java/mage/game/MageObjectAttribute.java b/Mage/src/main/java/mage/game/MageObjectAttribute.java index 24bfe51c44b..61a5662772d 100644 --- a/Mage/src/main/java/mage/game/MageObjectAttribute.java +++ b/Mage/src/main/java/mage/game/MageObjectAttribute.java @@ -4,6 +4,7 @@ import mage.MageObject; import mage.ObjectColor; import mage.constants.CardType; import mage.constants.SuperType; +import mage.util.Copyable; import mage.util.SubTypes; import java.io.Serializable; @@ -16,7 +17,7 @@ import java.util.List; * * @author LevelX2 */ -public class MageObjectAttribute implements Serializable { +public class MageObjectAttribute implements Serializable, Copyable { protected final ObjectColor color; protected final SubTypes subtype; diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 821096c34ff..bf8dcab5649 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -144,13 +144,8 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.maxBlocks = permanent.maxBlocks; this.deathtouched = permanent.deathtouched; this.markedLifelink = permanent.markedLifelink; - - for (Map.Entry> entry : permanent.connectedCards.entrySet()) { - this.connectedCards.put(entry.getKey(), new ArrayList<>(entry.getValue())); - } - if (permanent.dealtDamageByThisTurn != null) { - dealtDamageByThisTurn = new HashSet<>(permanent.dealtDamageByThisTurn); - } + this.connectedCards = CardUtil.deepCopyObject(permanent.connectedCards); + this.dealtDamageByThisTurn = CardUtil.deepCopyObject(permanent.dealtDamageByThisTurn); if (permanent.markedDamage != null) { markedDamage = new ArrayList<>(); for (MarkedDamageInfo mdi : permanent.markedDamage) { diff --git a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java index 990e303b9b0..16966f1f938 100644 --- a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java +++ b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java @@ -1,9 +1,6 @@ package mage.game.permanent.token; -import mage.MageInt; -import mage.MageObject; -import mage.MageObjectImpl; -import mage.ObjectColor; +import mage.*; import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.abilities.effects.Effect; @@ -316,6 +313,11 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { // tokens zcc must simulate card's zcc to keep copied card/spell settings // (example: etb's kicker ability of copied creature spell, see tests with Deathforge Shaman) newPermanent.updateZoneChangeCounter(game, emptyEvent); + + if (source != null) { + MageObjectReference mor = new MageObjectReference(newPermanent.getId(),newPermanent.getZoneChangeCounter(game)-1,game); + game.storePermanentCostsTags(mor, source); + } } // check ETB effects diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index a05b1f73f12..4fdd346c32b 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -1,9 +1,6 @@ package mage.game.stack; -import mage.MageInt; -import mage.MageObject; -import mage.Mana; -import mage.ObjectColor; +import mage.*; import mage.abilities.*; import mage.abilities.costs.mana.ActivationManaAbilityStep; import mage.abilities.costs.mana.ManaCost; @@ -336,6 +333,8 @@ public class Spell extends StackObjectImpl implements Card { } } else { permId = card.getId(); + MageObjectReference mor = new MageObjectReference(getSpellAbility()); + game.storePermanentCostsTags(mor, getSpellAbility()); flag = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null); } if (flag) { @@ -374,6 +373,8 @@ public class Spell extends StackObjectImpl implements Card { } // Aura has no legal target and its a bestow enchantment -> Add it to battlefield as creature if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) { + MageObjectReference mor = new MageObjectReference(getSpellAbility()); + game.storePermanentCostsTags(mor, getSpellAbility()); if (controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null)) { Permanent permanent = game.getPermanent(card.getId()); if (permanent instanceof PermanentCard) { @@ -397,6 +398,8 @@ public class Spell extends StackObjectImpl implements Card { token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, null, false); return true; } else { + MageObjectReference mor = new MageObjectReference(getSpellAbility()); + game.storePermanentCostsTags(mor, getSpellAbility()); return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null); } } diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index 0e95af451a0..bc148ae320c 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -33,10 +33,7 @@ import mage.util.SubTypes; import mage.util.functions.StackObjectCopyApplier; import mage.watchers.Watcher; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; +import java.util.*; /** * @author BetaSteward_at_googlemail.com @@ -402,7 +399,18 @@ public class StackAbility extends StackObjectImpl implements Ability { public void addManaCostsToPay(ManaCost manaCost) { // Do nothing } - + @Override + public Map getCostsTagMap() { + return ability.getCostsTagMap(); + } + @Override + public void setCostsTag(String tag, Object value){ + ability.setCostsTag(tag, value); + } + @Override + public Object getCostsTagOrDefault(String tag, Object defaultValue){ + return ability.getCostsTagOrDefault(tag, defaultValue); + } @Override public AbilityType getAbilityType() { return ability.getAbilityType(); diff --git a/Mage/src/main/java/mage/game/turn/Step.java b/Mage/src/main/java/mage/game/turn/Step.java index 97580fc09c1..ce94f2862ea 100644 --- a/Mage/src/main/java/mage/game/turn/Step.java +++ b/Mage/src/main/java/mage/game/turn/Step.java @@ -8,6 +8,7 @@ import mage.constants.PhaseStep; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.GameEvent.EventType; +import mage.util.Copyable; /** * Game's step @@ -17,7 +18,7 @@ import mage.game.events.GameEvent.EventType; * * @author BetaSteward_at_googlemail.com */ -public abstract class Step implements Serializable { +public abstract class Step implements Serializable, Copyable { private final PhaseStep type; private final boolean hasPriority; diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 56d634fdc4a..8ce419b47dc 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1,10 +1,7 @@ package mage.util; import com.google.common.collect.ImmutableList; -import mage.ApprovingObject; -import mage.MageIdentifier; -import mage.MageObject; -import mage.Mana; +import mage.*; import mage.abilities.*; import mage.abilities.condition.Condition; import mage.abilities.costs.Cost; @@ -42,9 +39,11 @@ import mage.game.permanent.token.Token; import mage.game.stack.Spell; import mage.game.stack.StackObject; import mage.players.Player; +import mage.players.PlayerList; import mage.target.Target; import mage.target.TargetCard; import mage.target.targetpointer.FixedTarget; +import mage.watchers.Watcher; import org.apache.log4j.Logger; import java.io.UnsupportedEncodingException; @@ -1648,6 +1647,75 @@ public final class CardUtil { } return zcc; } + /** + * Create a MageObjectReference of the ability's source + * Subtract 1 zcc if not on the stack, referencing when it was on the stack if it's a resolved permanent. + * works in any moment (even before source ability activated) + * + * @param game + * @param ability + * @return MageObjectReference to the ability's source stack moment + */ + public static MageObjectReference getSourceStackMomentReference(Game game, Ability ability) { + // Squad/Kicker activates in STACK zone so all zcc must be from "stack moment" + // Use cases: + // * resolving spell have same zcc (example: check kicker status in sorcery/instant); + // * copied spell have same zcc as source spell (see Spell.copySpell and zcc sync); + // * creature/token from resolved spell have +1 zcc after moved to battlefield (example: check kicker status in ETB triggers/effects); + + // find object info from the source ability (it can be a permanent or a spell on stack, on the moment of trigger/resolve) + MageObject sourceObject = ability.getSourceObject(game); + Zone sourceObjectZone = game.getState().getZone(sourceObject.getId()); + int zcc = CardUtil.getActualSourceObjectZoneChangeCounter(game, ability); + // find "stack moment" zcc: + // * permanent cards enters from STACK to BATTLEFIELD (+1 zcc) + // * permanent tokens enters from OUTSIDE to BATTLEFIELD (+1 zcc, see prepare code in TokenImpl.putOntoBattlefieldHelper) + // * spells and copied spells resolves on STACK (zcc not changes) + if (sourceObjectZone != Zone.STACK) { + --zcc; + } + return new MageObjectReference(ability.getSourceId(), zcc, game); + } + + //Use the two other functions below to access the tags, this is just the shared logic for them + private static Map getCostsTags(Game game, Ability source) { + Map costTags; + costTags = source.getCostsTagMap(); + if (costTags == null && source.getSourcePermanentOrLKI(game) != null) { + costTags = game.getPermanentCostsTags().get(CardUtil.getSourceStackMomentReference(game, source)); + } + return costTags; + } + /** + * Check if a specific tag exists in the cost tags of either the source ability, or the permanent source of the ability. + * Works in any moment (even before source ability activated) + * + * @param game + * @param source + * @param tag The tag's string identifier to look up + * @return if the tag was found + */ + public static boolean checkSourceCostsTagExists(Game game, Ability source, String tag) { + Map costTags = getCostsTags(game, source); + return costTags != null && costTags.containsKey(tag); + } + /** + * Find a specific tag in the cost tags of either the source ability, or the permanent source of the ability. + * Works in any moment (even before source ability activated) + * + * @param game + * @param source + * @param tag The tag's string identifier to look up + * @param defaultValue A default value to return if the tag is not found + * @return The object stored by the tag if found, the default if not + */ + public static Object getSourceCostsTag(Game game, Ability source, String tag, Object defaultValue){ + Map costTags = getCostsTags(game, source); + if (costTags != null) { + return costTags.getOrDefault(tag, defaultValue); + } + return defaultValue; + } public static String addCostVerb(String text) { if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) { @@ -1656,6 +1724,117 @@ public final class CardUtil { return "pay " + text; } + private static boolean isImmutableObject(Object o){ + return o == null + || o instanceof Number || o instanceof Boolean || o instanceof String + || o instanceof MageObjectReference || o instanceof UUID + || o instanceof Enum; + } + public static T deepCopyObject(T value){ + if (isImmutableObject(value)) { + return value; + } else if (value instanceof Copyable) { + return (T) ((Copyable) value).copy(); + } else if (value instanceof Watcher) { + return (T) ((Watcher) value).copy(); + } else if (value instanceof Ability) { + return (T) ((Ability) value).copy(); + } else if (value instanceof PlayerList) { + return (T) ((PlayerList) value).copy(); + } else if (value instanceof EnumSet) { + return (T) ((EnumSet) value).clone(); + } else if (value instanceof EnumMap) { + return (T) deepCopyEnumMap((EnumMap) value); + } else if (value instanceof LinkedHashSet) { + return (T) deepCopyLinkedHashSet((LinkedHashSet) value); + } else if (value instanceof LinkedHashMap) { + return (T) deepCopyLinkedHashMap((LinkedHashMap) value); + } else if (value instanceof TreeSet) { + return (T) deepCopyTreeSet((TreeSet) value); + } else if (value instanceof HashSet) { + return (T) deepCopyHashSet((HashSet) value); + } else if (value instanceof HashMap) { + return (T) deepCopyHashMap((HashMap) value); + } else if (value instanceof List) { + return (T) deepCopyList((List) value); + } else if (value instanceof AbstractMap.SimpleImmutableEntry){ //Used by Leonin Arbiter, Vessel Of The All Consuming Wanderer as a generic Pair class + AbstractMap.SimpleImmutableEntry entryValue = (AbstractMap.SimpleImmutableEntry) value; + return (T) new AbstractMap.SimpleImmutableEntry(deepCopyObject(entryValue.getKey()),deepCopyObject(entryValue.getValue())); + } else { + throw new IllegalStateException("Unhandled object " + value.getClass().getSimpleName() + " during deep copy, must add explicit handling of all Object types"); + } + } + private static > TreeSet deepCopyTreeSet(TreeSet original) { + if (original.getClass() != TreeSet.class) { + throw new IllegalStateException("Unhandled TreeSet type " + original.getClass().getSimpleName() + " in deep copy"); + } + TreeSet newSet = new TreeSet<>(); + for (T value : original){ + newSet.add((T) deepCopyObject(value)); + } + return newSet; + } + private static HashSet deepCopyHashSet(Set original) { + if (original.getClass() != HashSet.class) { + throw new IllegalStateException("Unhandled HashSet type " + original.getClass().getSimpleName() + " in deep copy"); + } + HashSet newSet = new HashSet<>(original.size()); + for (T value : original){ + newSet.add((T) deepCopyObject(value)); + } + return newSet; + } + private static LinkedHashSet deepCopyLinkedHashSet(LinkedHashSet original) { + if (original.getClass() != LinkedHashSet.class) { + throw new IllegalStateException("Unhandled LinkedHashSet type " + original.getClass().getSimpleName() + " in deep copy"); + } + LinkedHashSet newSet = new LinkedHashSet<>(original.size()); + for (T value : original){ + newSet.add((T) deepCopyObject(value)); + } + return newSet; + } + private static List deepCopyList(List original) { //always returns an ArrayList + if (original.getClass() != ArrayList.class) { + throw new IllegalStateException("Unhandled List type " + original.getClass().getSimpleName() + " in deep copy"); + } + ArrayList newList = new ArrayList<>(original.size()); + for (T value : original){ + newList.add((T) deepCopyObject(value)); + } + return newList; + } + private static HashMap deepCopyHashMap(Map original) { + if (original.getClass() != HashMap.class) { + throw new IllegalStateException("Unhandled HashMap type " + original.getClass().getSimpleName() + " in deep copy"); + } + HashMap newMap = new HashMap<>(original.size()); + for (Map.Entry entry : original.entrySet()) { + newMap.put((K) deepCopyObject(entry.getKey()), (V) deepCopyObject(entry.getValue())); + } + return newMap; + } + private static LinkedHashMap deepCopyLinkedHashMap(Map original) { + if (original.getClass() != LinkedHashMap.class) { + throw new IllegalStateException("Unhandled LinkedHashMap type " + original.getClass().getSimpleName() + " in deep copy"); + } + LinkedHashMap newMap = new LinkedHashMap<>(original.size()); + for (Map.Entry entry : original.entrySet()) { + newMap.put((K) deepCopyObject(entry.getKey()), (V) deepCopyObject(entry.getValue())); + } + return newMap; + } + private static , V> EnumMap deepCopyEnumMap(Map original) { + if (original.getClass() != EnumMap.class) { + throw new IllegalStateException("Unhandled EnumMap type " + original.getClass().getSimpleName() + " in deep copy"); + } + EnumMap newMap = new EnumMap<>(original); + for (Map.Entry entry : newMap.entrySet()) { + entry.setValue((V) deepCopyObject(entry.getValue())); + } + return newMap; + } + /** * Collect all possible object's parts (example: all sides in mdf/split cards) *

diff --git a/Mage/src/main/java/mage/watchers/Watcher.java b/Mage/src/main/java/mage/watchers/Watcher.java index b8879e2b82a..f13f37b5dad 100644 --- a/Mage/src/main/java/mage/watchers/Watcher.java +++ b/Mage/src/main/java/mage/watchers/Watcher.java @@ -1,11 +1,9 @@ package mage.watchers; -import mage.cards.Cards; import mage.constants.WatcherScope; import mage.game.Game; import mage.game.events.GameEvent; -import mage.players.PlayerList; -import mage.util.Copyable; +import mage.util.CardUtil; import org.apache.log4j.Logger; import java.io.Serializable; @@ -114,96 +112,7 @@ public abstract class Watcher implements Serializable { for (Field field : allFields) { if (!Modifier.isStatic(field.getModifiers())) { field.setAccessible(true); - - if (field.getType() == Set.class) { - // Set - ((Set) field.get(watcher)).clear(); - ((Set) field.get(watcher)).addAll((Set) field.get(this)); - } else if (field.getType() == Map.class || field.getType() == HashMap.class) { - // Map - ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType(); - Type valueType = parameterizedType.getActualTypeArguments()[1]; - if (valueType.getTypeName().contains("SortedSet")) { - // Map> - Map> source = (Map>) field.get(this); - Map> target = (Map>) field.get(watcher); - target.clear(); - for (Map.Entry> e : source.entrySet()) { - Set set = new TreeSet<>(); - set.addAll(e.getValue()); - target.put(e.getKey(), set); - } - } else if (valueType.getTypeName().contains("Set")) { - // Map> - Map> source = (Map>) field.get(this); - Map> target = (Map>) field.get(watcher); - target.clear(); - for (Map.Entry> e : source.entrySet()) { - Set set = new HashSet<>(); - set.addAll(e.getValue()); - target.put(e.getKey(), set); - } - } else if (valueType.getTypeName().contains("PlayerList")) { - // Map - Map source = (Map) field.get(this); - Map target = (Map) field.get(watcher); - target.clear(); - for (Map.Entry e : source.entrySet()) { - PlayerList list = e.getValue().copy(); - target.put(e.getKey(), list); - } - } else if (valueType.getTypeName().endsWith("Cards")) { - // Map - Map source = (Map) field.get(this); - Map target = (Map) field.get(watcher); - target.clear(); - for (Map.Entry e : source.entrySet()) { - Cards list = e.getValue().copy(); - target.put(e.getKey(), list); - } - } else if (valueType instanceof Class && Arrays.stream(((Class) valueType).getInterfaces()).anyMatch(c -> c.equals(Copyable.class))) { - // Map - Map source = (Map) field.get(this); - Map target = (Map) field.get(watcher); - target.clear(); - for (Map.Entry e : source.entrySet()) { - Copyable object = (Copyable) e.getValue().copy(); - target.put(e.getKey(), object); - } - } else if (valueType.getTypeName().contains("List")) { - // Map> - Map> source = (Map>) field.get(this); - Map> target = (Map>) field.get(watcher); - target.clear(); - for (Map.Entry> e : source.entrySet()) { - List list = new ArrayList<>(); - list.addAll(e.getValue()); - target.put(e.getKey(), list); - } - } else if (valueType.getTypeName().contains("Map")) { - // Map> - Map> source = (Map>) field.get(this); - Map> target = (Map>) field.get(watcher); - target.clear(); - for (Map.Entry> e : source.entrySet()) { - Map map = new HashMap<>(); - map.putAll(e.getValue()); - target.put(e.getKey(), map); - } - } else { - // Map - // TODO: add additional tests to find unsupported watcher data - - ((Map) field.get(watcher)).putAll((Map) field.get(this)); - } - } else if (field.getType() == List.class) { - // List - ((List) field.get(watcher)).clear(); - ((List) field.get(watcher)).addAll((List) field.get(this)); - } else { - // Object - field.set(watcher, field.get(this)); - } + field.set(watcher, CardUtil.deepCopyObject(field.get(this))); } } return watcher; diff --git a/Mage/src/main/java/mage/watchers/common/ManaSpentToCastWatcher.java b/Mage/src/main/java/mage/watchers/common/ManaSpentToCastWatcher.java index 4b194eaf8de..85de7e05964 100644 --- a/Mage/src/main/java/mage/watchers/common/ManaSpentToCastWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/ManaSpentToCastWatcher.java @@ -25,8 +25,6 @@ import java.util.UUID; public class ManaSpentToCastWatcher extends Watcher { private final Map manaMap = new HashMap<>(); - private final Map xValueMap = new HashMap<>(); - private final Map xValueMapLong = new HashMap<>(); // do not reset, keep until game end public ManaSpentToCastWatcher() { super(WatcherScope.GAME); @@ -40,15 +38,11 @@ public class ManaSpentToCastWatcher extends Watcher { Spell spell = (Spell) game.getObject(event.getTargetId()); if (spell != null) { manaMap.put(spell.getSourceId(), spell.getSpellAbility().getManaCostsToPay().getUsedManaToPay()); - xValueMap.put(spell.getSourceId(), spell.getSpellAbility().getManaCostsToPay().getX()); - xValueMapLong.put(spell.getSourceId(), spell.getSpellAbility().getManaCostsToPay().getX()); } return; case ZONE_CHANGE: if (((ZoneChangeEvent) event).getFromZone() == Zone.BATTLEFIELD) { manaMap.remove(event.getTargetId()); - xValueMap.remove(event.getTargetId()); - xValueMapLong.remove(event.getTargetId()); } } } @@ -57,29 +51,9 @@ public class ManaSpentToCastWatcher extends Watcher { return manaMap.getOrDefault(sourceId, null); } - /** - * Return X value for casted spell or permanents - * - * @param source - * @param useLongSource - use X value that keeps until end of game (for info only) - * @return - */ - public int getLastXValue(Ability source, boolean useLongSource) { - Map xSource = useLongSource ? this.xValueMapLong : this.xValueMap; - if (xSource.containsKey(source.getSourceId())) { - // cast normal way - return xSource.get(source.getSourceId()); - } else { - // put to battlefield without cast (example: copied spell must keep announced X) - return source.getManaCostsToPay().getX(); - } - } - @Override public void reset() { super.reset(); manaMap.clear(); - xValueMap.clear(); - // xValueMapLong.clear(); // must keep until game end, so don't clear between turns } }