Costs Tag Tracking part 2: Tag system and X values, reworked deep copy code (#11406)

* Implement Costs Tag Map system

* Use Costs Tag Map system to store X value for spells, abilities, and resolving permanents

* Store Bestow without target's tags
Change functions for getting tags and storing the tags of a new permanent

* Create and use deep copy function in CardUtil, add Copyable<T> to many classes

* Fix Hall Of the Bandit Lord infinite loop

* Add additional comments

* Don't store null/empty costs tags maps (saves memory)

* Fix two more Watchers with Ability variable

* Add check for exact collection types during deep copy

* Use generics instead of pure type erasure during deep copy

* convert more code to using deep copy helper, everything use Object copier, add EnumMap

* fix documentation

* Don't need the separate null checks anymore (handled in deepCopyObject)

* Minor cleanup
This commit is contained in:
ssk97 2023-11-16 11:12:32 -08:00 committed by GitHub
parent 72e30f1574
commit bea33c7493
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 458 additions and 338 deletions

View file

@ -36,7 +36,7 @@ public final class Biophagus extends CardImpl {
Ability ability = new AnyColorManaAbility(new TapSourceCost(), true).withFlavorWord("Genomic Enhancement"); 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, " + 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."); "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) { private Biophagus(final Biophagus card) {
@ -51,11 +51,11 @@ public final class Biophagus extends CardImpl {
class BiophagusWatcher extends Watcher { class BiophagusWatcher extends Watcher {
private final Ability source; private final UUID sourceAbilityID;
BiophagusWatcher(Ability source) { BiophagusWatcher(UUID sourceAbilityID) {
super(WatcherScope.CARD); super(WatcherScope.CARD);
this.source = source; this.sourceAbilityID = sourceAbilityID;
} }
@Override @Override
@ -68,7 +68,8 @@ class BiophagusWatcher extends Watcher {
&& event.getFlag()) { && event.getFlag()) {
if (target instanceof Spell) { if (target instanceof Spell) {
game.getState().addEffect(new BiophagusEntersBattlefieldEffect( 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
} }
} }
} }

View file

@ -39,7 +39,7 @@ public final class GuildmagesForum extends CardImpl {
Ability ability = new AnyColorManaAbility(new GenericManaCost(1), true); 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.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()); ability.addCost(new TapSourceCost());
this.addAbility(ability, new GuildmagesForumWatcher(ability)); this.addAbility(ability, new GuildmagesForumWatcher(ability.getId()));
} }
private GuildmagesForum(final GuildmagesForum card) { private GuildmagesForum(final GuildmagesForum card) {
@ -54,11 +54,11 @@ public final class GuildmagesForum extends CardImpl {
class GuildmagesForumWatcher extends Watcher { class GuildmagesForumWatcher extends Watcher {
private final Ability source; private final UUID sourceAbilityID;
GuildmagesForumWatcher(Ability source) { GuildmagesForumWatcher(UUID sourceAbilityID) {
super(WatcherScope.CARD); super(WatcherScope.CARD);
this.source = source; this.sourceAbilityID = sourceAbilityID;
} }
@Override @Override
@ -71,7 +71,8 @@ class GuildmagesForumWatcher extends Watcher {
&& event.getFlag()) { && event.getFlag()) {
if (target instanceof Spell) { if (target instanceof Spell) {
game.getState().addEffect(new GuildmagesForumEntersBattlefieldEffect( 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
} }
} }
} }

View file

@ -2,6 +2,7 @@ package mage.cards.h;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import mage.MageObject; import mage.MageObject;
import mage.Mana; 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"); 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 ability = new SimpleManaAbility(Zone.BATTLEFIELD, effect, new TapSourceCost());
ability.addCost(new PayLifeCost(3)); ability.addCost(new PayLifeCost(3));
this.addAbility(ability, new HallOfTheBanditLordWatcher(ability)); this.addAbility(ability, new HallOfTheBanditLordWatcher(ability.getId()));
} }
private HallOfTheBanditLord(final HallOfTheBanditLord card) { private HallOfTheBanditLord(final HallOfTheBanditLord card) {
@ -60,12 +61,12 @@ public final class HallOfTheBanditLord extends CardImpl {
class HallOfTheBanditLordWatcher extends Watcher { class HallOfTheBanditLordWatcher extends Watcher {
private final Ability source; private final UUID sourceAbilityID;
private final List<UUID> creatures = new ArrayList<>(); private final List<UUID> creatures = new ArrayList<>();
HallOfTheBanditLordWatcher(Ability source) { HallOfTheBanditLordWatcher(UUID sourceAbilityID) {
super(WatcherScope.CARD); super(WatcherScope.CARD);
this.source = source; this.sourceAbilityID = sourceAbilityID;
} }
@Override @Override
@ -99,7 +100,7 @@ class HallOfTheBanditLordWatcher extends Watcher {
if (creatures.contains(event.getSourceId())) { if (creatures.contains(event.getSourceId())) {
ContinuousEffect effect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.Custom); ContinuousEffect effect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.Custom);
effect.setTargetPointer(new FixedTarget(event.getSourceId(), game)); 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()); creatures.remove(event.getSourceId());
} }
} }

View file

@ -10,6 +10,8 @@ import mage.cards.repository.CardRepository;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.PermanentToken;
import mage.util.CardUtil; import mage.util.CardUtil;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Ignore; import org.junit.Ignore;
@ -571,10 +573,10 @@ public class CopySpellTest extends CardTestPlayerBase {
} }
@Test @Test
public void test_CopiedSpellsHasntETB() { public void test_CopiedSpellsETBCounters() {
// testing: // testing:
// - x in copied creature spell (copy x) // - 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 // 0/0
// Capricopian enters the battlefield with X +1/+1 counters on it. // 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); activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Grenzo, Dungeon Warden", "Grenzo, Dungeon Warden"); 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: // 608.3f If the object thats resolving is a copy of a permanent spell, it will become a token permanent
// - The token that a resolving copy of a spell becomes isnt said to have been created. (2021-04-16) // as it is put onto the battlefield in any of the steps above.
// - A nontoken permanent enters the battlefield when its moved onto the battlefield from another zone. // 111.12. A copy of a permanent spell becomes a token as it resolves. The token has the characteristics of
// A token enters the battlefield when its created. See rules 403.3, 603.6a, 603.6d, and 614.12. // 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.
// So both copies enters without counters: // The tokens must enter with counters
// - Capricopian copy must die
// - Grenzo, Dungeon Warden must have default PT
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); 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); checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden", 2);
setStrictChooseMode(true); setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); 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() int originalCounters = currentGame.getBattlefield().getAllActivePermanents().stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) .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)) .mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1))
.sum(); .sum();
int copyCounters = currentGame.getBattlefield().getAllActivePermanents().stream() int copyCounters = currentGame.getBattlefield().getAllActivePermanents().stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden")) .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)) .mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1))
.sum(); .sum();
Assert.assertEquals("original grenzo must have 2x counters", 2, originalCounters); 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 @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. * Thieving Skydiver is kicked and then copied, but the copied version does not let you gain control of anything.
*/ */
@Test @Test
@Ignore
public void copySpellWithKicker() { public void copySpellWithKicker() {
// When Thieving Skydiver enters the battlefield, if it was kicked, gain control of target artifact with mana value X or less. // 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. // 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, "Island", 3); // Original price, + 1 kicker, + 1 for Double Major
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); 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); setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thieving Skydiver"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thieving Skydiver");
@ -766,14 +766,16 @@ public class CopySpellTest extends CardTestPlayerBase {
setChoice(playerA, "X=1"); setChoice(playerA, "X=1");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Thieving Skydiver", "Thieving Skydiver"); 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 copy
addTarget(playerA, "Sol Ring"); // Choice for original addTarget(playerA, "Expedition Map"); // Choice for original
setStopAt(1, PhaseStep.BEGIN_COMBAT); setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute(); 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, "Sol Ring", 0);
assertPermanentCount(playerB, "Expedition Map", 0);
} }
private void abilitySourceMustBeSame(Card card, String infoPrefix) { private void abilitySourceMustBeSame(Card card, String infoPrefix) {

View file

@ -65,7 +65,7 @@ public class CardIconsTest extends CardTestPlayerBase {
} }
@Test @Test
public void test_CostX_Copies() { public void test_CostX_StackCopy() {
// Grenzo, Dungeon Warden enters the battlefield with X +1/+1 counters on it. // 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.HAND, playerA, "Grenzo, Dungeon Warden", 1);// {X}{B}{R}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
@ -144,6 +144,67 @@ public class CardIconsTest extends CardTestPlayerBase {
.orElse(null); .orElse(null);
Assert.assertNotNull("copied card must be in battlefield", copiedCardView); 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 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()); Assert.assertEquals("copied x cost text", "x=0", copiedCardView.getCardIcons().get(0).getText());
}); });

View file

@ -26,6 +26,7 @@ import mage.watchers.Watcher;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
@ -157,6 +158,26 @@ public interface Ability extends Controllable, Serializable {
void addManaCostsToPay(ManaCost manaCost); 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<String, Object> 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 * Retrieves the effects that are put into the place by the resolution of
* this ability. * this ability.

View file

@ -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 MageIdentifier identifier = MageIdentifier.Default; // used to identify specific ability (e.g. to match with corresponding watcher)
protected String appendToRule = null; protected String appendToRule = null;
protected int sourcePermanentTransformCount = 0; protected int sourcePermanentTransformCount = 0;
private Map<String, Object> costsTagMap = null;
protected AbilityImpl(AbilityType abilityType, Zone zone) { protected AbilityImpl(AbilityType abilityType, Zone zone) {
this.id = UUID.randomUUID(); this.id = UUID.randomUUID();
@ -107,16 +108,9 @@ public abstract class AbilityImpl implements Ability {
this.manaCosts = ability.manaCosts.copy(); this.manaCosts = ability.manaCosts.copy();
this.manaCostsToPay = ability.manaCostsToPay.copy(); this.manaCostsToPay = ability.manaCostsToPay.copy();
this.costs = ability.costs.copy(); this.costs = ability.costs.copy();
for (Watcher watcher : ability.getWatchers()) { this.watchers = CardUtil.deepCopyObject(ability.getWatchers());
watchers.add(watcher.copy());
}
if (ability.subAbilities != null) { this.subAbilities = CardUtil.deepCopyObject(ability.subAbilities);
this.subAbilities = new ArrayList<>();
for (Ability subAbility : ability.subAbilities) {
subAbilities.add(subAbility.copy());
}
}
this.modes = ability.getModes().copy(); this.modes = ability.getModes().copy();
this.ruleAtTheTop = ability.ruleAtTheTop; this.ruleAtTheTop = ability.ruleAtTheTop;
this.ruleVisible = ability.ruleVisible; this.ruleVisible = ability.ruleVisible;
@ -129,17 +123,14 @@ public abstract class AbilityImpl implements Ability {
this.canFizzle = ability.canFizzle; this.canFizzle = ability.canFizzle;
this.targetAdjuster = ability.targetAdjuster; this.targetAdjuster = ability.targetAdjuster;
this.costAdjuster = ability.costAdjuster; this.costAdjuster = ability.costAdjuster;
for (Hint hint : ability.getHints()) { this.hints = CardUtil.deepCopyObject(ability.getHints());
this.hints.add(hint.copy()); this.icons = CardUtil.deepCopyObject(ability.getIcons());
}
for (CardIcon icon : ability.getIcons()) {
this.icons.add(icon.copy());
}
this.customOutcome = ability.customOutcome; this.customOutcome = ability.customOutcome;
this.identifier = ability.identifier; this.identifier = ability.identifier;
this.activated = ability.activated; this.activated = ability.activated;
this.appendToRule = ability.appendToRule; this.appendToRule = ability.appendToRule;
this.sourcePermanentTransformCount = ability.sourcePermanentTransformCount; this.sourcePermanentTransformCount = ability.sourcePermanentTransformCount;
this.costsTagMap = CardUtil.deepCopyObject(ability.costsTagMap);
} }
@Override @Override
@ -527,6 +518,7 @@ public abstract class AbilityImpl implements Ability {
((Cost) variableCost).setPaid(); ((Cost) variableCost).setPaid();
String message = controller.getLogName() + " announces a value of " + xValue + " (" + variableCost.getActionText() + ')'; String message = controller.getLogName() + " announces a value of " + xValue + " (" + variableCost.getActionText() + ')';
announceString.append(message); announceString.append(message);
setCostsTag("X",xValue);
} }
} }
return announceString.toString(); return announceString.toString();
@ -631,6 +623,7 @@ public abstract class AbilityImpl implements Ability {
} }
addManaCostsToPay(new ManaCostsImpl<>(manaString.toString())); addManaCostsToPay(new ManaCostsImpl<>(manaString.toString()));
getManaCostsToPay().setX(xValue * xValueMultiplier, amountMana); getManaCostsToPay().setX(xValue * xValueMultiplier, amountMana);
setCostsTag("X",xValue * xValueMultiplier);
} }
variableManaCost.setPaid(); variableManaCost.setPaid();
} }
@ -713,6 +706,28 @@ public abstract class AbilityImpl implements Ability {
return manaCostsToPay; return manaCostsToPay;
} }
/**
* Accessed to see what was optional/variable costs were paid
*
* @return
*/
@Override
public Map<String, Object> 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 @Override
public Effects getEffects() { public Effects getEffects() {
return getModes().getMode().getEffects(); return getModes().getMode().getEffects();

View file

@ -1,12 +1,10 @@
package mage.abilities.dynamicvalue.common; package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.costs.OptionalAdditionalCostImpl;
import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.abilities.keyword.KickerAbility;
import mage.game.Game; import mage.game.Game;
import mage.game.stack.Spell; import mage.util.CardUtil;
/** /**
@ -19,35 +17,9 @@ public enum GetKickerXValue implements DynamicValue {
@Override @Override
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
// calcs only kicker with X values // Currently identical logic to the Manacost X value
// which should be fine since you can only have one X at a time
// kicker adds additional costs to spell ability return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
// 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;
} }
@Override @Override

View file

@ -1,11 +1,10 @@
package mage.abilities.dynamicvalue.common; package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.common.PayVariableLoyaltyCost;
import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
/** /**
* @author TheElk801 * @author TheElk801
@ -15,12 +14,7 @@ public enum GetXLoyaltyValue implements DynamicValue {
@Override @Override
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
for (Cost cost : sourceAbility.getCosts()) { return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
if (cost instanceof PayVariableLoyaltyCost) {
return ((PayVariableLoyaltyCost) cost).getAmount();
}
}
return 0;
} }
@Override @Override

View file

@ -1,10 +1,10 @@
package mage.abilities.dynamicvalue.common; package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.costs.VariableCost;
import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.game.Game; import mage.game.Game;
import mage.util.CardUtil;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
@ -14,12 +14,7 @@ public enum GetXValue implements DynamicValue {
@Override @Override
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
return sourceAbility return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
.getCosts()
.getVariableCosts()
.stream()
.mapToInt(VariableCost::getAmount)
.sum();
} }
@Override @Override

View file

@ -4,10 +4,11 @@ import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.game.Game; import mage.game.Game;
import mage.watchers.common.ManaSpentToCastWatcher; import mage.util.CardUtil;
public enum ManacostVariableValue implements DynamicValue { 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 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 ETB, // if you need X after ETB (in battlefield) - reset each turn
END_GAME; // if you need X until end game - keep data forever END_GAME; // if you need X until end game - keep data forever
@ -15,18 +16,7 @@ public enum ManacostVariableValue implements DynamicValue {
@Override @Override
public int calculate(Game game, Ability sourceAbility, Effect effect) { public int calculate(Game game, Ability sourceAbility, Effect effect) {
if (this == REGULAR) { return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
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;
} }
@Override @Override

View file

@ -1,8 +1,6 @@
package mage.abilities.effects.common; package mage.abilities.effects.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.effects.EntersBattlefieldEffect;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.constants.AbilityType; import mage.constants.AbilityType;
import mage.constants.Outcome; import mage.constants.Outcome;
@ -59,19 +57,12 @@ public class EntersBattlefieldWithXCountersEffect extends OneShotEffect {
} }
} }
if (permanent != null) { if (permanent != null) {
SpellAbility spellAbility = (SpellAbility) getValue(EntersBattlefieldEffect.SOURCE_CAST_SPELL_ABILITY); int amount = ((int) CardUtil.getSourceCostsTag(game, source, "X", 0)) * multiplier;
if (spellAbility != null if (amount > 0) {
&& spellAbility.getSourceId().equals(source.getSourceId()) Counter counterToAdd = counter.copy();
&& permanent.getZoneChangeCounter(game) == spellAbility.getSourceObjectZoneChangeCounter()) { counterToAdd.add(amount - counter.getCount());
if (spellAbility.getSourceId().equals(source.getSourceId())) { // put into play by normal cast List<UUID> appliedEffects = (ArrayList<UUID>) this.getValue("appliedEffects");
int amount = spellAbility.getManaCostsToPay().getX() * this.multiplier; permanent.addCounters(counterToAdd, source.getControllerId(), source, game, appliedEffects);
if (amount > 0) {
Counter counterToAdd = counter.copy();
counterToAdd.add(amount - counter.getCount());
List<UUID> appliedEffects = (ArrayList<UUID>) this.getValue("appliedEffects");
permanent.addCounters(counterToAdd, source.getControllerId(), source, game, appliedEffects);
}
}
} }
} }
return true; return true;

View file

@ -2,13 +2,14 @@ package mage.abilities.hint;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.game.Game; import mage.game.Game;
import mage.util.Copyable;
import java.io.Serializable; import java.io.Serializable;
/** /**
* @author JayDi85 * @author JayDi85
*/ */
public interface Hint extends Serializable { public interface Hint extends Serializable, Copyable<Hint> {
// It's a constant hint for cards/permanents (e.g. visible all the time) // It's a constant hint for cards/permanents (e.g. visible all the time)
// //

View file

@ -4,6 +4,7 @@ import mage.abilities.Ability;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.FilterCard; import mage.filter.FilterCard;
import mage.game.Game; import mage.game.Game;
import mage.util.Copyable;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
@ -11,7 +12,7 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
public interface Cards extends Set<UUID>, Serializable { public interface Cards extends Set<UUID>, Serializable, Copyable<Cards> {
/** /**
* Add the passed in card to the set if it's not null. * Add the passed in card to the set if it's not null.

View file

@ -1,6 +1,7 @@
package mage.counters; package mage.counters;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.Copyable;
import java.io.Serializable; import java.io.Serializable;
@ -9,7 +10,7 @@ import org.apache.log4j.Logger;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class Counter implements Serializable { public class Counter implements Serializable, Copyable<Counter> {
private static final Logger logger = Logger.getLogger(Counter.class); private static final Logger logger = Logger.getLogger(Counter.class);

View file

@ -1,6 +1,8 @@
package mage.counters; package mage.counters;
import mage.util.Copyable;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -10,7 +12,7 @@ import java.util.stream.Collectors;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class Counters extends HashMap<String, Counter> implements Serializable { public class Counters extends HashMap<String, Counter> implements Serializable, Copyable<Counters> {
public Counters() { public Counters() {
} }

View file

@ -2,6 +2,7 @@ package mage.filter;
import mage.filter.predicate.Predicate; import mage.filter.predicate.Predicate;
import mage.game.Game; import mage.game.Game;
import mage.util.Copyable;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
@ -11,7 +12,7 @@ import java.util.List;
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
* @author North * @author North
*/ */
public interface Filter<E> extends Serializable { public interface Filter<E> extends Serializable, Copyable<Filter<E>> {
enum ComparisonScope { enum ComparisonScope {
Any, All Any, All

View file

@ -2,6 +2,7 @@ package mage.game;
import mage.MageItem; import mage.MageItem;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.ActivatedAbility; import mage.abilities.ActivatedAbility;
import mage.abilities.DelayedTriggeredAbility; import mage.abilities.DelayedTriggeredAbility;
@ -118,6 +119,12 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
Map<UUID, Permanent> getPermanentsEntering(); Map<UUID, Permanent> getPermanentsEntering();
Map<Zone, Map<UUID, MageObject>> getLKI(); Map<Zone, Map<UUID, MageObject>> getLKI();
Map<MageObjectReference, Map<String, Object>> 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) // Result must be checked for null. Possible errors search pattern: (\S*) = game.getCard.+\n(?!.+\1 != null)
Card getCard(UUID cardId); Card getCard(UUID cardId);

View file

@ -2,6 +2,7 @@ package mage.game;
import mage.MageException; import mage.MageException;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.*; import mage.abilities.*;
import mage.abilities.common.AttachableToRestrictedAbility; import mage.abilities.common.AttachableToRestrictedAbility;
import mage.abilities.common.CantHaveMoreThanAmountCountersSourceAbility; 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.tableEventSource = game.tableEventSource; // client-server part, not need on copy/simulations
//this.playerQueryEventSource = game.playerQueryEventSource; // client-server part, not need on copy/simulations //this.playerQueryEventSource = game.playerQueryEventSource; // client-server part, not need on copy/simulations
for (Entry<UUID, Card> entry : game.gameCards.entrySet()) { this.gameCards = CardUtil.deepCopyObject(game.gameCards);
this.gameCards.put(entry.getKey(), entry.getValue().copy()); this.meldCards = CardUtil.deepCopyObject(game.meldCards);
}
for (Entry<UUID, MeldCard> entry : game.meldCards.entrySet()) {
this.meldCards.put(entry.getKey(), entry.getValue().copy());
}
// lki this.lki = CardUtil.deepCopyObject(game.lki);
for (Entry<Zone, Map<UUID, MageObject>> entry : game.lki.entrySet()) { this.lkiCardState = CardUtil.deepCopyObject(game.lkiCardState);
Map<UUID, MageObject> lkiMap = new HashMap<>(); this.lkiExtended = CardUtil.deepCopyObject(game.lkiExtended);
for (Entry<UUID, MageObject> entryMap : entry.getValue().entrySet()) { this.lkiShortLiving = CardUtil.deepCopyObject(game.lkiShortLiving);
lkiMap.put(entryMap.getKey(), entryMap.getValue().copy());
}
this.lki.put(entry.getKey(), lkiMap);
}
// lkiCardState
for (Entry<Zone, Map<UUID, CardState>> entry : game.lkiCardState.entrySet()) {
Map<UUID, CardState> lkiMap = new HashMap<>();
for (Entry<UUID, CardState> entryMap : entry.getValue().entrySet()) {
lkiMap.put(entryMap.getKey(), entryMap.getValue().copy());
}
this.lkiCardState.put(entry.getKey(), lkiMap);
}
// lkiExtended
for (Entry<UUID, Map<Integer, MageObject>> entry : game.lkiExtended.entrySet()) {
Map<Integer, MageObject> lkiMap = new HashMap<>();
for (Entry<Integer, MageObject> entryMap : entry.getValue().entrySet()) {
lkiMap.put(entryMap.getKey(), entryMap.getValue().copy());
}
this.lkiExtended.put(entry.getKey(), lkiMap);
}
// lkiShortLiving
for (Entry<Zone, Set<UUID>> entry : game.lkiShortLiving.entrySet()) {
this.lkiShortLiving.put(entry.getKey(), new HashSet<>(entry.getValue()));
}
for (Entry<UUID, Permanent> entry : game.permanentsEntering.entrySet()) { this.permanentsEntering = CardUtil.deepCopyObject(game.permanentsEntering);
this.permanentsEntering.put(entry.getKey(), entry.getValue().copy()); this.enterWithCounters = CardUtil.deepCopyObject(game.enterWithCounters);
}
for (Entry<UUID, Counters> entry : game.enterWithCounters.entrySet()) {
this.enterWithCounters.put(entry.getKey(), entry.getValue().copy());
}
this.state = game.state.copy(); this.state = game.state.copy();
// client-server part, not need on copy/simulations: // client-server part, not need on copy/simulations:
@ -1451,6 +1420,10 @@ public abstract class GameImpl implements Game {
player.endOfTurn(this); player.endOfTurn(this);
} }
state.resetWatchers(); 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() { protected UUID pickChoosingPlayer() {
@ -3560,6 +3533,15 @@ public abstract class GameImpl implements Game {
return lki; return lki;
} }
@Override
public Map<MageObjectReference, Map<String, Object>> getPermanentCostsTags() {
return state.getPermanentCostsTags();
}
@Override
public void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source){
state.storePermanentCostsTags(permanentMOR, source);
}
@Override @Override
public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command) { public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command) {
// fake test ability for triggers and events // fake test ability for triggers and events

View file

@ -101,6 +101,7 @@ public class GameState implements Serializable, Copyable<GameState> {
private Map<UUID, Zone> zones = new HashMap<>(); private Map<UUID, Zone> zones = new HashMap<>();
private List<GameEvent> simultaneousEvents = new ArrayList<>(); private List<GameEvent> simultaneousEvents = new ArrayList<>();
private Map<UUID, CardState> cardState = new HashMap<>(); private Map<UUID, CardState> cardState = new HashMap<>();
private Map<MageObjectReference, Map<String, Object>> permanentCostsTags = new HashMap<>(); // Permanent reference -> map of (tag -> values) describing how the permanent's spell was cast
private Map<UUID, MageObjectAttribute> mageObjectAttribute = new HashMap<>(); private Map<UUID, MageObjectAttribute> mageObjectAttribute = new HashMap<>();
private Map<UUID, Integer> zoneChangeCounter = new HashMap<>(); private Map<UUID, Integer> zoneChangeCounter = new HashMap<>();
private Map<UUID, Card> copiedCards = new HashMap<>(); private Map<UUID, Card> copiedCards = new HashMap<>();
@ -162,36 +163,19 @@ public class GameState implements Serializable, Copyable<GameState> {
this.stepNum = state.stepNum; this.stepNum = state.stepNum;
this.extraTurnId = state.extraTurnId; this.extraTurnId = state.extraTurnId;
this.effects = state.effects.copy(); this.effects = state.effects.copy();
for (TriggeredAbility trigger : state.triggered) { this.triggered = CardUtil.deepCopyObject(state.triggered);
this.triggered.add(trigger.copy());
}
this.triggers = state.triggers.copy(); this.triggers = state.triggers.copy();
this.delayed = state.delayed.copy(); this.delayed = state.delayed.copy();
this.specialActions = state.specialActions.copy(); this.specialActions = state.specialActions.copy();
this.combat = state.combat.copy(); this.combat = state.combat.copy();
this.turnMods = state.turnMods.copy(); this.turnMods = state.turnMods.copy();
this.watchers = state.watchers.copy(); this.watchers = state.watchers.copy();
for (Map.Entry<String, Object> entry : state.values.entrySet()) { this.values = CardUtil.deepCopyObject(state.values);
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.zones.putAll(state.zones); this.zones.putAll(state.zones);
this.simultaneousEvents.addAll(state.simultaneousEvents); this.simultaneousEvents.addAll(state.simultaneousEvents);
for (Map.Entry<UUID, CardState> entry : state.cardState.entrySet()) { this.cardState = CardUtil.deepCopyObject(state.cardState);
cardState.put(entry.getKey(), entry.getValue().copy()); this.permanentCostsTags = CardUtil.deepCopyObject(state.permanentCostsTags);
} this.mageObjectAttribute = CardUtil.deepCopyObject(state.mageObjectAttribute);
for (Map.Entry<UUID, MageObjectAttribute> entry : state.mageObjectAttribute.entrySet()) {
mageObjectAttribute.put(entry.getKey(), entry.getValue().copy());
}
this.zoneChangeCounter.putAll(state.zoneChangeCounter); this.zoneChangeCounter.putAll(state.zoneChangeCounter);
this.copiedCards.putAll(state.copiedCards); this.copiedCards.putAll(state.copiedCards);
this.permanentOrderNumber = state.permanentOrderNumber; this.permanentOrderNumber = state.permanentOrderNumber;
@ -231,6 +215,7 @@ public class GameState implements Serializable, Copyable<GameState> {
gameOver = false; gameOver = false;
specialActions.clear(); specialActions.clear();
cardState.clear(); cardState.clear();
permanentCostsTags.clear();
combat.clear(); combat.clear();
turnMods.clear(); turnMods.clear();
watchers.clear(); watchers.clear();
@ -280,6 +265,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.zones = state.zones; this.zones = state.zones;
this.simultaneousEvents = state.simultaneousEvents; this.simultaneousEvents = state.simultaneousEvents;
this.cardState = state.cardState; this.cardState = state.cardState;
this.permanentCostsTags = state.permanentCostsTags;
this.mageObjectAttribute = state.mageObjectAttribute; this.mageObjectAttribute = state.mageObjectAttribute;
this.zoneChangeCounter = state.zoneChangeCounter; this.zoneChangeCounter = state.zoneChangeCounter;
this.copiedCards = state.copiedCards; this.copiedCards = state.copiedCards;
@ -1369,6 +1355,29 @@ public class GameState implements Serializable, Copyable<GameState> {
return mageObjectAtt; return mageObjectAtt;
} }
public Map<MageObjectReference, Map<String, Object>> 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) { public void addWatcher(Watcher watcher) {
this.watchers.add(watcher); this.watchers.add(watcher);
} }

View file

@ -4,6 +4,7 @@ import mage.MageObject;
import mage.ObjectColor; import mage.ObjectColor;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SuperType; import mage.constants.SuperType;
import mage.util.Copyable;
import mage.util.SubTypes; import mage.util.SubTypes;
import java.io.Serializable; import java.io.Serializable;
@ -16,7 +17,7 @@ import java.util.List;
* *
* @author LevelX2 * @author LevelX2
*/ */
public class MageObjectAttribute implements Serializable { public class MageObjectAttribute implements Serializable, Copyable<MageObjectAttribute> {
protected final ObjectColor color; protected final ObjectColor color;
protected final SubTypes subtype; protected final SubTypes subtype;

View file

@ -144,13 +144,8 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.maxBlocks = permanent.maxBlocks; this.maxBlocks = permanent.maxBlocks;
this.deathtouched = permanent.deathtouched; this.deathtouched = permanent.deathtouched;
this.markedLifelink = permanent.markedLifelink; this.markedLifelink = permanent.markedLifelink;
this.connectedCards = CardUtil.deepCopyObject(permanent.connectedCards);
for (Map.Entry<String, List<UUID>> entry : permanent.connectedCards.entrySet()) { this.dealtDamageByThisTurn = CardUtil.deepCopyObject(permanent.dealtDamageByThisTurn);
this.connectedCards.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
if (permanent.dealtDamageByThisTurn != null) {
dealtDamageByThisTurn = new HashSet<>(permanent.dealtDamageByThisTurn);
}
if (permanent.markedDamage != null) { if (permanent.markedDamage != null) {
markedDamage = new ArrayList<>(); markedDamage = new ArrayList<>();
for (MarkedDamageInfo mdi : permanent.markedDamage) { for (MarkedDamageInfo mdi : permanent.markedDamage) {

View file

@ -1,9 +1,6 @@
package mage.game.permanent.token; package mage.game.permanent.token;
import mage.MageInt; import mage.*;
import mage.MageObject;
import mage.MageObjectImpl;
import mage.ObjectColor;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.effects.Effect; 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 // 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) // (example: etb's kicker ability of copied creature spell, see tests with Deathforge Shaman)
newPermanent.updateZoneChangeCounter(game, emptyEvent); 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 // check ETB effects

View file

@ -1,9 +1,6 @@
package mage.game.stack; package mage.game.stack;
import mage.MageInt; import mage.*;
import mage.MageObject;
import mage.Mana;
import mage.ObjectColor;
import mage.abilities.*; import mage.abilities.*;
import mage.abilities.costs.mana.ActivationManaAbilityStep; import mage.abilities.costs.mana.ActivationManaAbilityStep;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCost;
@ -336,6 +333,8 @@ public class Spell extends StackObjectImpl implements Card {
} }
} else { } else {
permId = card.getId(); permId = card.getId();
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
flag = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null); flag = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
} }
if (flag) { 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 // Aura has no legal target and its a bestow enchantment -> Add it to battlefield as creature
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) { 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)) { if (controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null)) {
Permanent permanent = game.getPermanent(card.getId()); Permanent permanent = game.getPermanent(card.getId());
if (permanent instanceof PermanentCard) { 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); token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, null, false);
return true; return true;
} else { } else {
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null); return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
} }
} }

View file

@ -33,10 +33,7 @@ import mage.util.SubTypes;
import mage.util.functions.StackObjectCopyApplier; import mage.util.functions.StackObjectCopyApplier;
import mage.watchers.Watcher; import mage.watchers.Watcher;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
@ -402,7 +399,18 @@ public class StackAbility extends StackObjectImpl implements Ability {
public void addManaCostsToPay(ManaCost manaCost) { public void addManaCostsToPay(ManaCost manaCost) {
// Do nothing // Do nothing
} }
@Override
public Map<String, Object> 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 @Override
public AbilityType getAbilityType() { public AbilityType getAbilityType() {
return ability.getAbilityType(); return ability.getAbilityType();

View file

@ -8,6 +8,7 @@ import mage.constants.PhaseStep;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType; import mage.game.events.GameEvent.EventType;
import mage.util.Copyable;
/** /**
* Game's step * Game's step
@ -17,7 +18,7 @@ import mage.game.events.GameEvent.EventType;
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public abstract class Step implements Serializable { public abstract class Step implements Serializable, Copyable<Step> {
private final PhaseStep type; private final PhaseStep type;
private final boolean hasPriority; private final boolean hasPriority;

View file

@ -1,10 +1,7 @@
package mage.util; package mage.util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import mage.ApprovingObject; import mage.*;
import mage.MageIdentifier;
import mage.MageObject;
import mage.Mana;
import mage.abilities.*; import mage.abilities.*;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
@ -42,9 +39,11 @@ import mage.game.permanent.token.Token;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject; import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import mage.players.PlayerList;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import mage.watchers.Watcher;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -1648,6 +1647,75 @@ public final class CardUtil {
} }
return zcc; 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<String, Object> getCostsTags(Game game, Ability source) {
Map<String, Object> 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<String, Object> 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<String, Object> costTags = getCostsTags(game, source);
if (costTags != null) {
return costTags.getOrDefault(tag, defaultValue);
}
return defaultValue;
}
public static String addCostVerb(String text) { public static String addCostVerb(String text) {
if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) { if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) {
@ -1656,6 +1724,117 @@ public final class CardUtil {
return "pay " + text; 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> T deepCopyObject(T value){
if (isImmutableObject(value)) {
return value;
} else if (value instanceof Copyable) {
return (T) ((Copyable<T>) 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 <T extends Comparable<T>> TreeSet<T> deepCopyTreeSet(TreeSet<T> original) {
if (original.getClass() != TreeSet.class) {
throw new IllegalStateException("Unhandled TreeSet type " + original.getClass().getSimpleName() + " in deep copy");
}
TreeSet<T> newSet = new TreeSet<>();
for (T value : original){
newSet.add((T) deepCopyObject(value));
}
return newSet;
}
private static <T> HashSet<T> deepCopyHashSet(Set<T> original) {
if (original.getClass() != HashSet.class) {
throw new IllegalStateException("Unhandled HashSet type " + original.getClass().getSimpleName() + " in deep copy");
}
HashSet<T> newSet = new HashSet<>(original.size());
for (T value : original){
newSet.add((T) deepCopyObject(value));
}
return newSet;
}
private static <T> LinkedHashSet<T> deepCopyLinkedHashSet(LinkedHashSet<T> original) {
if (original.getClass() != LinkedHashSet.class) {
throw new IllegalStateException("Unhandled LinkedHashSet type " + original.getClass().getSimpleName() + " in deep copy");
}
LinkedHashSet<T> newSet = new LinkedHashSet<>(original.size());
for (T value : original){
newSet.add((T) deepCopyObject(value));
}
return newSet;
}
private static <T> List<T> deepCopyList(List<T> original) { //always returns an ArrayList
if (original.getClass() != ArrayList.class) {
throw new IllegalStateException("Unhandled List type " + original.getClass().getSimpleName() + " in deep copy");
}
ArrayList<T> newList = new ArrayList<>(original.size());
for (T value : original){
newList.add((T) deepCopyObject(value));
}
return newList;
}
private static <K, V> HashMap<K, V> deepCopyHashMap(Map<K, V> original) {
if (original.getClass() != HashMap.class) {
throw new IllegalStateException("Unhandled HashMap type " + original.getClass().getSimpleName() + " in deep copy");
}
HashMap<K, V> newMap = new HashMap<>(original.size());
for (Map.Entry<K, V> entry : original.entrySet()) {
newMap.put((K) deepCopyObject(entry.getKey()), (V) deepCopyObject(entry.getValue()));
}
return newMap;
}
private static <K, V> LinkedHashMap<K, V> deepCopyLinkedHashMap(Map<K, V> original) {
if (original.getClass() != LinkedHashMap.class) {
throw new IllegalStateException("Unhandled LinkedHashMap type " + original.getClass().getSimpleName() + " in deep copy");
}
LinkedHashMap<K, V> newMap = new LinkedHashMap<>(original.size());
for (Map.Entry<K, V> entry : original.entrySet()) {
newMap.put((K) deepCopyObject(entry.getKey()), (V) deepCopyObject(entry.getValue()));
}
return newMap;
}
private static <K extends Enum<K>, V> EnumMap<K, V> deepCopyEnumMap(Map<K, V> original) {
if (original.getClass() != EnumMap.class) {
throw new IllegalStateException("Unhandled EnumMap type " + original.getClass().getSimpleName() + " in deep copy");
}
EnumMap<K, V> newMap = new EnumMap<>(original);
for (Map.Entry<K, V> entry : newMap.entrySet()) {
entry.setValue((V) deepCopyObject(entry.getValue()));
}
return newMap;
}
/** /**
* Collect all possible object's parts (example: all sides in mdf/split cards) * Collect all possible object's parts (example: all sides in mdf/split cards)
* <p> * <p>

View file

@ -1,11 +1,9 @@
package mage.watchers; package mage.watchers;
import mage.cards.Cards;
import mage.constants.WatcherScope; import mage.constants.WatcherScope;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.players.PlayerList; import mage.util.CardUtil;
import mage.util.Copyable;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable; import java.io.Serializable;
@ -114,96 +112,7 @@ public abstract class Watcher implements Serializable {
for (Field field : allFields) { for (Field field : allFields) {
if (!Modifier.isStatic(field.getModifiers())) { if (!Modifier.isStatic(field.getModifiers())) {
field.setAccessible(true); field.setAccessible(true);
field.set(watcher, CardUtil.deepCopyObject(field.get(this)));
if (field.getType() == Set.class) {
// Set<UUID, xxx>
((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<UUID, xxx>
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
Type valueType = parameterizedType.getActualTypeArguments()[1];
if (valueType.getTypeName().contains("SortedSet")) {
// Map<UUID, SortedSet<Object>>
Map<Object, Set<Object>> source = (Map<Object, Set<Object>>) field.get(this);
Map<Object, Set<Object>> target = (Map<Object, Set<Object>>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Set<Object>> e : source.entrySet()) {
Set<Object> set = new TreeSet<>();
set.addAll(e.getValue());
target.put(e.getKey(), set);
}
} else if (valueType.getTypeName().contains("Set")) {
// Map<UUID, Set<Object>>
Map<Object, Set<Object>> source = (Map<Object, Set<Object>>) field.get(this);
Map<Object, Set<Object>> target = (Map<Object, Set<Object>>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Set<Object>> e : source.entrySet()) {
Set<Object> set = new HashSet<>();
set.addAll(e.getValue());
target.put(e.getKey(), set);
}
} else if (valueType.getTypeName().contains("PlayerList")) {
// Map<UUID, PlayerList>
Map<Object, PlayerList> source = (Map<Object, PlayerList>) field.get(this);
Map<Object, PlayerList> target = (Map<Object, PlayerList>) field.get(watcher);
target.clear();
for (Map.Entry<Object, PlayerList> e : source.entrySet()) {
PlayerList list = e.getValue().copy();
target.put(e.getKey(), list);
}
} else if (valueType.getTypeName().endsWith("Cards")) {
// Map<UUID, Cards>
Map<Object, Cards> source = (Map<Object, Cards>) field.get(this);
Map<Object, Cards> target = (Map<Object, Cards>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Cards> 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<UUID, Copyable>
Map<Object, Copyable> source = (Map<Object, Copyable>) field.get(this);
Map<Object, Copyable> target = (Map<Object, Copyable>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Copyable> e : source.entrySet()) {
Copyable object = (Copyable) e.getValue().copy();
target.put(e.getKey(), object);
}
} else if (valueType.getTypeName().contains("List")) {
// Map<UUID, List<Object>>
Map<Object, List<Object>> source = (Map<Object, List<Object>>) field.get(this);
Map<Object, List<Object>> target = (Map<Object, List<Object>>) field.get(watcher);
target.clear();
for (Map.Entry<Object, List<Object>> e : source.entrySet()) {
List<Object> list = new ArrayList<>();
list.addAll(e.getValue());
target.put(e.getKey(), list);
}
} else if (valueType.getTypeName().contains("Map")) {
// Map<UUID, Map<UUID, Object>>
Map<Object, Map<Object, Object>> source = (Map<Object, Map<Object, Object>>) field.get(this);
Map<Object, Map<Object, Object>> target = (Map<Object, Map<Object, Object>>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Map<Object, Object>> e : source.entrySet()) {
Map<Object, Object> map = new HashMap<>();
map.putAll(e.getValue());
target.put(e.getKey(), map);
}
} else {
// Map<UUID, Object>
// 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<Object>
((List) field.get(watcher)).clear();
((List) field.get(watcher)).addAll((List) field.get(this));
} else {
// Object
field.set(watcher, field.get(this));
}
} }
} }
return watcher; return watcher;

View file

@ -25,8 +25,6 @@ import java.util.UUID;
public class ManaSpentToCastWatcher extends Watcher { public class ManaSpentToCastWatcher extends Watcher {
private final Map<UUID, Mana> manaMap = new HashMap<>(); private final Map<UUID, Mana> manaMap = new HashMap<>();
private final Map<UUID, Integer> xValueMap = new HashMap<>();
private final Map<UUID, Integer> xValueMapLong = new HashMap<>(); // do not reset, keep until game end
public ManaSpentToCastWatcher() { public ManaSpentToCastWatcher() {
super(WatcherScope.GAME); super(WatcherScope.GAME);
@ -40,15 +38,11 @@ public class ManaSpentToCastWatcher extends Watcher {
Spell spell = (Spell) game.getObject(event.getTargetId()); Spell spell = (Spell) game.getObject(event.getTargetId());
if (spell != null) { if (spell != null) {
manaMap.put(spell.getSourceId(), spell.getSpellAbility().getManaCostsToPay().getUsedManaToPay()); 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; return;
case ZONE_CHANGE: case ZONE_CHANGE:
if (((ZoneChangeEvent) event).getFromZone() == Zone.BATTLEFIELD) { if (((ZoneChangeEvent) event).getFromZone() == Zone.BATTLEFIELD) {
manaMap.remove(event.getTargetId()); 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 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<UUID, Integer> 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 @Override
public void reset() { public void reset() {
super.reset(); super.reset();
manaMap.clear(); manaMap.clear();
xValueMap.clear();
// xValueMapLong.clear(); // must keep until game end, so don't clear between turns
} }
} }