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 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 that’s resolving is a copy of a permanent spell, it will become a token permanent
|
||||||
// - The token that a resolving copy of a spell becomes isn’t 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 it’s 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 it’s 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) {
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue