forked from External/mage
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:
parent
72e30f1574
commit
bea33c7493
29 changed files with 458 additions and 338 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UUID> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
* this ability.
|
||||
|
|
|
|||
|
|
@ -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<String, Object> 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<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
|
||||
public Effects getEffects() {
|
||||
return getModes().getMode().getEffects();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,12 +57,7 @@ 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;
|
||||
int amount = ((int) CardUtil.getSourceCostsTag(game, source, "X", 0)) * multiplier;
|
||||
if (amount > 0) {
|
||||
Counter counterToAdd = counter.copy();
|
||||
counterToAdd.add(amount - counter.getCount());
|
||||
|
|
@ -72,8 +65,6 @@ public class EntersBattlefieldWithXCountersEffect extends OneShotEffect {
|
|||
permanent.addCounters(counterToAdd, source.getControllerId(), source, game, appliedEffects);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Hint> {
|
||||
|
||||
// It's a constant hint for cards/permanents (e.g. visible all the time)
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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<UUID>, Serializable {
|
||||
public interface Cards extends Set<UUID>, Serializable, Copyable<Cards> {
|
||||
|
||||
/**
|
||||
* Add the passed in card to the set if it's not null.
|
||||
|
|
|
|||
|
|
@ -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<Counter> {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Counter.class);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, Counter> implements Serializable {
|
||||
public class Counters extends HashMap<String, Counter> implements Serializable, Copyable<Counters> {
|
||||
|
||||
public Counters() {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<E> extends Serializable {
|
||||
public interface Filter<E> extends Serializable, Copyable<Filter<E>> {
|
||||
|
||||
enum ComparisonScope {
|
||||
Any, All
|
||||
|
|
|
|||
|
|
@ -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<Game> {
|
|||
Map<UUID, Permanent> getPermanentsEntering();
|
||||
|
||||
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)
|
||||
Card getCard(UUID cardId);
|
||||
|
|
|
|||
|
|
@ -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<UUID, Card> entry : game.gameCards.entrySet()) {
|
||||
this.gameCards.put(entry.getKey(), entry.getValue().copy());
|
||||
}
|
||||
for (Entry<UUID, MeldCard> 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<Zone, Map<UUID, MageObject>> entry : game.lki.entrySet()) {
|
||||
Map<UUID, MageObject> lkiMap = new HashMap<>();
|
||||
for (Entry<UUID, MageObject> entryMap : entry.getValue().entrySet()) {
|
||||
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()));
|
||||
}
|
||||
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<UUID, Permanent> entry : game.permanentsEntering.entrySet()) {
|
||||
this.permanentsEntering.put(entry.getKey(), entry.getValue().copy());
|
||||
}
|
||||
for (Entry<UUID, Counters> 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<MageObjectReference, Map<String, Object>> getPermanentCostsTags() {
|
||||
return state.getPermanentCostsTags();
|
||||
}
|
||||
@Override
|
||||
public void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source){
|
||||
state.storePermanentCostsTags(permanentMOR, source);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
private Map<UUID, Zone> zones = new HashMap<>();
|
||||
private List<GameEvent> simultaneousEvents = new ArrayList<>();
|
||||
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, Integer> zoneChangeCounter = 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.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<String, Object> 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<UUID, CardState> entry : state.cardState.entrySet()) {
|
||||
cardState.put(entry.getKey(), entry.getValue().copy());
|
||||
}
|
||||
for (Map.Entry<UUID, MageObjectAttribute> 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<GameState> {
|
|||
gameOver = false;
|
||||
specialActions.clear();
|
||||
cardState.clear();
|
||||
permanentCostsTags.clear();
|
||||
combat.clear();
|
||||
turnMods.clear();
|
||||
watchers.clear();
|
||||
|
|
@ -280,6 +265,7 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
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<GameState> {
|
|||
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) {
|
||||
this.watchers.add(watcher);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MageObjectAttribute> {
|
||||
|
||||
protected final ObjectColor color;
|
||||
protected final SubTypes subtype;
|
||||
|
|
|
|||
|
|
@ -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<String, List<UUID>> 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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
public AbilityType getAbilityType() {
|
||||
return ability.getAbilityType();
|
||||
|
|
|
|||
|
|
@ -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<Step> {
|
||||
|
||||
private final PhaseStep type;
|
||||
private final boolean hasPriority;
|
||||
|
|
|
|||
|
|
@ -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<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) {
|
||||
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> 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)
|
||||
* <p>
|
||||
|
|
|
|||
|
|
@ -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<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));
|
||||
}
|
||||
field.set(watcher, CardUtil.deepCopyObject(field.get(this)));
|
||||
}
|
||||
}
|
||||
return watcher;
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ import java.util.UUID;
|
|||
public class ManaSpentToCastWatcher extends Watcher {
|
||||
|
||||
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() {
|
||||
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<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
|
||||
public void reset() {
|
||||
super.reset();
|
||||
manaMap.clear();
|
||||
xValueMap.clear();
|
||||
// xValueMapLong.clear(); // must keep until game end, so don't clear between turns
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue