mirror of
https://github.com/magefree/mage.git
synced 2025-12-21 19:11:59 -08:00
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
|
|
@ -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());
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue