* Copy spell - improved support, now all copied spells are independent (bug example: Seasons Past fizzled after copy resolve, see #7634, 10f8022043);

This commit is contained in:
Oleg Agafonov 2021-03-06 12:25:53 +04:00
parent 8704b9cb9b
commit b36f915d74
22 changed files with 537 additions and 179 deletions

View file

@ -64,12 +64,11 @@ class ApproachOfTheSecondSunEffect extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
Spell spell = game.getStack().getSpell(source.getSourceId()); Spell spell = game.getStack().getSpell(source.getSourceId(), false);
if (controller != null && spell != null) { if (controller != null && spell != null) {
ApproachOfTheSecondSunWatcher watcher ApproachOfTheSecondSunWatcher watcher
= game.getState().getWatcher(ApproachOfTheSecondSunWatcher.class); = game.getState().getWatcher(ApproachOfTheSecondSunWatcher.class);
if (watcher != null if (watcher != null
&& !spell.isCopy()
&& watcher.getApproachesCast(controller.getId()) > 1 && watcher.getApproachesCast(controller.getId()) > 1
&& spell.getFromZone() == Zone.HAND) { && spell.getFromZone() == Zone.HAND) {
// Win the game // Win the game
@ -79,10 +78,7 @@ class ApproachOfTheSecondSunEffect extends OneShotEffect {
controller.gainLife(7, game, source); controller.gainLife(7, game, source);
// Put this into the library as the 7th from the top // Put this into the library as the 7th from the top
if (spell.isCopy()) { Card spellCard = game.getStack().getSpell(source.getSourceId(), false).getCard();
return true;
}
Card spellCard = game.getStack().getSpell(source.getSourceId()).getCard();
if (spellCard != null) { if (spellCard != null) {
controller.putCardOnTopXOfLibrary(spellCard, game, source, 7, true); controller.putCardOnTopXOfLibrary(spellCard, game, source, 7, true);
} }

View file

@ -16,6 +16,7 @@ import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.permanent.ControllerIdPredicate; import mage.filter.predicate.permanent.ControllerIdPredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.CopiedStackObjectEvent; import mage.game.events.CopiedStackObjectEvent;
import mage.game.events.CopyStackObjectEvent;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
@ -166,7 +167,13 @@ class BeamsplitterMageEffect extends OneShotEffect {
if (creature == null) { if (creature == null) {
return false; return false;
} }
Spell copy = spell.copySpell(source.getControllerId(), game); // TODO: add support if multiple copies? See Twinning Staff
GameEvent gameEvent = new CopyStackObjectEvent(source, spell, source.getControllerId(), 1);
if (game.replaceEvent(gameEvent)) {
return false;
}
Spell copy = spell.copySpell(game, source, source.getControllerId());
copy.setZone(Zone.STACK, game);
game.getStack().push(copy); game.getStack().push(copy);
setTarget: setTarget:
for (UUID modeId : copy.getSpellAbility().getModes().getSelectedModes()) { for (UUID modeId : copy.getSpellAbility().getModes().getSelectedModes()) {

View file

@ -7,9 +7,12 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.game.events.CopiedStackObjectEvent; import mage.game.events.CopiedStackObjectEvent;
import mage.game.events.CopyStackObjectEvent;
import mage.game.events.GameEvent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
import mage.target.TargetSpell; import mage.target.TargetSpell;
@ -57,8 +60,14 @@ class ForkEffect extends OneShotEffect {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
Spell spell = game.getStack().getSpell(targetPointer.getFirst(game, source)); Spell spell = game.getStack().getSpell(targetPointer.getFirst(game, source));
if (spell != null && controller != null) { if (spell != null && controller != null) {
Spell copy = spell.copySpell(source.getControllerId(), game); // TODO: add support if multiple copies? See Twinning Staff
GameEvent gameEvent = new CopyStackObjectEvent(source, spell, source.getControllerId(), 1);
if (game.replaceEvent(gameEvent)) {
return false;
}
Spell copy = spell.copySpell(game, source, source.getControllerId());
copy.getColor(game).setRed(true); copy.getColor(game).setRed(true);
copy.setZone(Zone.STACK, game);
game.getStack().push(copy); game.getStack().push(copy);
copy.chooseNewTargets(game, controller.getId()); copy.chooseNewTargets(game, controller.getId());
game.fireEvent(new CopiedStackObjectEvent(spell, copy, source.getControllerId())); game.fireEvent(new CopiedStackObjectEvent(spell, copy, source.getControllerId()));

View file

@ -1,7 +1,5 @@
package mage.cards.g; package mage.cards.g;
import java.util.UUID;
import mage.ApprovingObject; import mage.ApprovingObject;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -13,20 +11,16 @@ import mage.abilities.effects.common.combat.CantBeBlockedByOneEffect;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.*;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.Zone;
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.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 java.util.UUID;
/** /**
*
* @author emerald000 * @author emerald000
*/ */
public final class Guile extends CardImpl { public final class Guile extends CardImpl {
@ -85,12 +79,12 @@ class GuileReplacementEffect extends ReplacementEffectImpl {
public boolean replaceEvent(GameEvent event, Ability source, Game game) { public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Spell spell = game.getStack().getSpell(event.getTargetId()); Spell spell = game.getStack().getSpell(event.getTargetId());
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (spell != null if (spell != null
&& controller != null) { && controller != null) {
controller.moveCards(spell, Zone.EXILED, source, game); controller.moveCards(spell, Zone.EXILED, source, game);
if (!spell.isCopy()) { if (!spell.isCopy()) { // copies doesn't exists in exile zone
Card spellCard = spell.getCard(); Card spellCard = spell.getCard();
if (spellCard != null if (spellCard != null
&& controller.chooseUse(Outcome.PlayForFree, "Play " + spellCard.getIdName() + " for free?", source, game)) { && controller.chooseUse(Outcome.PlayForFree, "Play " + spellCard.getIdName() + " for free?", source, game)) {
controller.playCard(spellCard, game, true, true, new ApprovingObject(source, game)); controller.playCard(spellCard, game, true, true, new ApprovingObject(source, game));
} }

View file

@ -1,4 +1,3 @@
package org.mage.test.cards.abilities.other; package org.mage.test.cards.abilities.other;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
@ -7,7 +6,6 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
/** /**
*
* @author BetaSteward * @author BetaSteward
*/ */
public class SoulfireGrandMasterTest extends CardTestPlayerBase { public class SoulfireGrandMasterTest extends CardTestPlayerBase {
@ -17,7 +15,6 @@ public class SoulfireGrandMasterTest extends CardTestPlayerBase {
* and sorcery spells you control have lifelink. {2}{U/R}{U/R}: The next * and sorcery spells you control have lifelink. {2}{U/R}{U/R}: The next
* time you cast an instant or sorcery spell from your hand this turn, put * time you cast an instant or sorcery spell from your hand this turn, put
* that card into your hand instead of into your graveyard as it resolves. * that card into your hand instead of into your graveyard as it resolves.
*
*/ */
@Test @Test
public void testSpellsGainLifelink() { public void testSpellsGainLifelink() {
@ -120,19 +117,23 @@ public class SoulfireGrandMasterTest extends CardTestPlayerBase {
/** /**
* Test copied instant spell gives also life * Test copied instant spell gives also life
*
*/ */
@Test @Test
public void testCopySpell() { public void test_CopiesMustHaveGainedLifelink() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1); addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.HAND, playerA, "Lightning Bolt"); addCard(Zone.HAND, playerA, "Lightning Bolt");
//
// Instant and sorcery spells you control have lifelink.
addCard(Zone.BATTLEFIELD, playerA, "Soulfire Grand Master", 1); addCard(Zone.BATTLEFIELD, playerA, "Soulfire Grand Master", 1);
//
// {2}{U}{R}: Copy target instant or sorcery spell you control. You may choose new targets for the copy. // {2}{U}{R}: Copy target instant or sorcery spell you control. You may choose new targets for the copy.
addCard(Zone.BATTLEFIELD, playerA, "Nivix Guildmage", 1); addCard(Zone.BATTLEFIELD, playerA, "Nivix Guildmage", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{U}{R}:"); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{U}{R}:");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
checkStackSize("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // 2x bolts
setStopAt(1, PhaseStep.BEGIN_COMBAT); setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute(); execute();
@ -143,12 +144,10 @@ public class SoulfireGrandMasterTest extends CardTestPlayerBase {
assertLife(playerB, 14); assertLife(playerB, 14);
assertLife(playerA, 26); assertLife(playerA, 26);
} }
/** /**
* Test damage of activated ability of a permanent does not gain lifelink * Test damage of activated ability of a permanent does not gain lifelink
*
*/ */
@Test @Test
public void testActivatedAbility() { public void testActivatedAbility() {
@ -235,7 +234,6 @@ public class SoulfireGrandMasterTest extends CardTestPlayerBase {
/** /**
* Check if second ability resolved, the next spell that is counterer won't * Check if second ability resolved, the next spell that is counterer won't
* go to hand back because it did not resolve * go to hand back because it did not resolve
*
*/ */
@Test @Test
public void testSoulfireCounteredSpellDontGoesBack() { public void testSoulfireCounteredSpellDontGoesBack() {
@ -269,7 +267,6 @@ public class SoulfireGrandMasterTest extends CardTestPlayerBase {
* caster life. It should as it has lifelink, and it's Deflecting Palm (an * caster life. It should as it has lifelink, and it's Deflecting Palm (an
* instant) dealing damage. I was playing against a human in Standard * instant) dealing damage. I was playing against a human in Standard
* Constructed. * Constructed.
*
*/ */
@Test @Test
public void testWithDeflectingPalm() { public void testWithDeflectingPalm() {

View file

@ -1,12 +1,23 @@
package org.mage.test.cards.copy; package org.mage.test.cards.copy;
import mage.abilities.MageSingleton;
import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.FlyingAbility;
import mage.cards.AdventureCard;
import mage.cards.Card;
import mage.cards.ModalDoubleFacesCard;
import mage.cards.SplitCard;
import mage.cards.repository.CardRepository;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.util.CardUtil;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.player.TestPlayer; import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
import java.util.Set;
import java.util.UUID;
/** /**
* @author LevelX2 * @author LevelX2
*/ */
@ -55,24 +66,31 @@ public class CopySpellTest extends CardTestPlayerBase {
// Target creature gets +3/+3 and gains flying until end of turn. // Target creature gets +3/+3 and gains flying until end of turn.
addCard(Zone.HAND, playerA, "Angelic Blessing", 1); // {2}{W} addCard(Zone.HAND, playerA, "Angelic Blessing", 1); // {2}{W}
addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); addCard(Zone.BATTLEFIELD, playerA, "Plains", 3);
//
// Whenever you cast an instant or sorcery spell that targets only Zada, Hedron Grinder,
// copy that spell for each other creature you control that the spell could target.
// Each copy targets a different one of those creatures.
addCard(Zone.BATTLEFIELD, playerA, "Zada, Hedron Grinder", 1); // 3/3
//
addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox", 1); // 2/4
addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); // 2/2
// Whenever you cast an instant or sorcery spell that targets only Zada, Hedron Grinder, copy that spell for each other creature you control that the spell could target. Each copy targets a different one of those creatures. // cast boost and copy it for another target (lion will not get boost cause can't be targeted)
addCard(Zone.BATTLEFIELD, playerA, "Zada, Hedron Grinder", 1);
addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox", 1);
addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angelic Blessing", "Zada, Hedron Grinder"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angelic Blessing", "Zada, Hedron Grinder");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT); setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute(); execute();
assertAllCommandsUsed();
assertGraveyardCount(playerA, "Angelic Blessing", 1); assertGraveyardCount(playerA, "Angelic Blessing", 1);
assertPowerToughness(playerA, "Pillarfield Ox", 5, 7); // original target
assertAbility(playerA, "Pillarfield Ox", FlyingAbility.getInstance(), true); assertPowerToughness(playerA, "Zada, Hedron Grinder", 3 + 3, 3 + 3);
assertPowerToughness(playerA, "Zada, Hedron Grinder", 6, 6);
assertAbility(playerA, "Zada, Hedron Grinder", FlyingAbility.getInstance(), true); assertAbility(playerA, "Zada, Hedron Grinder", FlyingAbility.getInstance(), true);
// copied target
assertPowerToughness(playerA, "Pillarfield Ox", 2 + 3, 4 + 3);
assertAbility(playerA, "Pillarfield Ox", FlyingAbility.getInstance(), true);
// can't target lion, so no boost
assertPowerToughness(playerB, "Silvercoat Lion", 2, 2); assertPowerToughness(playerB, "Silvercoat Lion", 2, 2);
assertAbility(playerB, "Silvercoat Lion", FlyingAbility.getInstance(), false); assertAbility(playerB, "Silvercoat Lion", FlyingAbility.getInstance(), false);
} }
@ -205,8 +223,8 @@ public class CopySpellTest extends CardTestPlayerBase {
assertPermanentCount(playerA, "Wolf", 1); // created from Silverfur ability assertPermanentCount(playerA, "Wolf", 1); // created from Silverfur ability
} }
*/ */
@Test @Test
public void ZadaHedronGrinderBoostWithCharm() { public void ZadaHedronGrinderBoostWithCharm() {
// Choose two - // Choose two -
@ -443,7 +461,7 @@ public class CopySpellTest extends CardTestPlayerBase {
setChoice(playerA, "Yes"); // use copy setChoice(playerA, "Yes"); // use copy
setChoice(playerA, "Imoti, Celebrant of Bounty"); // copy of imoti setChoice(playerA, "Imoti, Celebrant of Bounty"); // copy of imoti
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Imoti, Celebrant of Bounty", 2); checkPermanentCount("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Imoti, Celebrant of Bounty", 2);
// cast big spell and catch cascade 2x times (from two copies) // cast big spell and catch cascade 2x times (from two copies)
// possible bug: cascade activates only 1x times // possible bug: cascade activates only 1x times
@ -462,4 +480,173 @@ public class CopySpellTest extends CardTestPlayerBase {
assertLife(playerB, 20 - 3 * 2); // 2x bolts from 2x cascades assertLife(playerB, 20 - 3 * 2); // 2x bolts from 2x cascades
} }
@Test
public void test_CopiedSpellsMustUseIndependentCards() {
// possible bug: copied spell on stack depends on the original spell/card
// https://github.com/magefree/mage/issues/7634
// Return any number of cards with different converted mana costs from your graveyard to your hand.
// Put Seasons Past on the bottom of its owner's library.
addCard(Zone.HAND, playerA, "Seasons Past", 1); // {4}{G}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 6);
//
addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 5); // for return
//
// Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy.
addCard(Zone.HAND, playerA, "Fork", 1); // {R}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
// cast season and make copy of it on stack
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 6);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Seasons Past");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fork", "Seasons Past", "Cast Seasons Past");
checkStackSize("after copy cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // season + fork
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
checkStackSize("after copy resolve", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // season + copied season
checkStackObject("after copy resolve", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Seasons Past", 2);
// resolve copied season (possible bug: after copied resolve it will return an original card too, so original spell will be fizzled)
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA, true);
setChoice(playerA, "Grizzly Bears"); // return to hand
checkStackSize("after copied resolve", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1);
// resolve original season
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA, true);
setChoice(playerA, "Grizzly Bears"); // return to hand
checkStackSize("after original resolve", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 0);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
}
@Test
public void test_SimpleCopy_Card() {
Card sourceCard = CardRepository.instance.findCard("Grizzly Bears").getCard();
Card originalCard = CardRepository.instance.findCard("Grizzly Bears").getCard();
prepareZoneAndZCC(originalCard);
Card copiedCard = currentGame.copyCard(originalCard, null, playerA.getId());
// main
Assert.assertNotEquals("main - id must be different", originalCard.getId(), copiedCard.getId());
Assert.assertEquals("main - rules must be same", originalCard.getRules(), copiedCard.getRules());
abilitySourceMustBeSame(sourceCard, "main source");
abilitySourceMustBeSame(originalCard, "main original"); // original card can be broken after copyCard call
abilitySourceMustBeSame(copiedCard, "main copied");
//cardsMustHaveSameZoneAndZCC(originalCard, copiedCard, "main");
}
@Test
public void test_SimpleCopy_SplitCard() {
SplitCard sourceCard = (SplitCard) CardRepository.instance.findCard("Alive // Well").getCard();
SplitCard originalCard = (SplitCard) CardRepository.instance.findCard("Alive // Well").getCard();
prepareZoneAndZCC(originalCard);
SplitCard copiedCard = (SplitCard) currentGame.copyCard(originalCard, null, playerA.getId());
// main
Assert.assertNotEquals("main - id must be different", originalCard.getId(), copiedCard.getId());
Assert.assertEquals("main - rules must be same", originalCard.getRules(), copiedCard.getRules());
abilitySourceMustBeSame(sourceCard, "main source");
abilitySourceMustBeSame(originalCard, "main original");
abilitySourceMustBeSame(copiedCard, "main copied");
//cardsMustHaveSameZoneAndZCC(originalCard, copiedCard, "main");
// left
Assert.assertNotEquals("left - id must be different", originalCard.getLeftHalfCard().getId(), copiedCard.getLeftHalfCard().getId());
Assert.assertEquals("left - rules must be same", originalCard.getLeftHalfCard().getRules(), copiedCard.getLeftHalfCard().getRules());
Assert.assertEquals("left - parent ref", copiedCard.getLeftHalfCard().getParentCard().getId(), copiedCard.getId());
abilitySourceMustBeSame(originalCard.getLeftHalfCard(), "left original");
abilitySourceMustBeSame(copiedCard.getLeftHalfCard(), "left copied");
//cardsMustHaveSameZoneAndZCC(originalCard.getLeftHalfCard(), copiedCard.getLeftHalfCard(), "left");
// right
Assert.assertNotEquals("right - id must be different", originalCard.getRightHalfCard().getId(), copiedCard.getRightHalfCard().getId());
Assert.assertEquals("right - rules must be same", originalCard.getRightHalfCard().getRules(), copiedCard.getRightHalfCard().getRules());
Assert.assertEquals("right - parent ref", copiedCard.getRightHalfCard().getParentCard().getId(), copiedCard.getId());
abilitySourceMustBeSame(originalCard.getRightHalfCard(), "right original");
abilitySourceMustBeSame(copiedCard.getRightHalfCard(), "right copied");
//cardsMustHaveSameZoneAndZCC(originalCard.getRightHalfCard(), copiedCard.getRightHalfCard(), "right");
}
@Test
public void test_SimpleCopy_AdventureCard() {
AdventureCard sourceCard = (AdventureCard) CardRepository.instance.findCard("Animating Faerie").getCard();
AdventureCard originalCard = (AdventureCard) CardRepository.instance.findCard("Animating Faerie").getCard();
prepareZoneAndZCC(originalCard);
AdventureCard copiedCard = (AdventureCard) currentGame.copyCard(originalCard, null, playerA.getId());
// main
Assert.assertNotEquals("main - id must be different", originalCard.getId(), copiedCard.getId());
Assert.assertEquals("main - rules must be same", originalCard.getRules(), copiedCard.getRules());
abilitySourceMustBeSame(sourceCard, "main source");
abilitySourceMustBeSame(originalCard, "main original");
abilitySourceMustBeSame(copiedCard, "main copied");
//cardsMustHaveSameZoneAndZCC(originalCard, copiedCard, "main");
// right (spell)
Assert.assertNotEquals("right - id must be different", originalCard.getSpellCard().getId(), copiedCard.getSpellCard().getId());
Assert.assertEquals("right - rules must be same", originalCard.getSpellCard().getRules(), copiedCard.getSpellCard().getRules());
Assert.assertEquals("right - parent ref", copiedCard.getSpellCard().getParentCard().getId(), copiedCard.getId());
abilitySourceMustBeSame(originalCard.getSpellCard(), "right original");
abilitySourceMustBeSame(copiedCard.getSpellCard(), "right copied");
//cardsMustHaveSameZoneAndZCC(originalCard.getSpellCard(), copiedCard.getSpellCard(), "right");
}
@Test
public void test_SimpleCopy_MDFC() {
ModalDoubleFacesCard sourceCard = (ModalDoubleFacesCard) CardRepository.instance.findCard("Agadeem's Awakening").getCard();
ModalDoubleFacesCard originalCard = (ModalDoubleFacesCard) CardRepository.instance.findCard("Agadeem's Awakening").getCard();
prepareZoneAndZCC(originalCard);
ModalDoubleFacesCard copiedCard = (ModalDoubleFacesCard) currentGame.copyCard(originalCard, null, playerA.getId());
// main
Assert.assertNotEquals("main - id must be different", originalCard.getId(), copiedCard.getId());
Assert.assertEquals("main - rules must be same", originalCard.getRules(), copiedCard.getRules());
abilitySourceMustBeSame(sourceCard, "main source");
abilitySourceMustBeSame(originalCard, "main original");
abilitySourceMustBeSame(copiedCard, "main copied");
//cardsMustHaveSameZoneAndZCC(originalCard, copiedCard, "main");
// left
Assert.assertNotEquals("left - id must be different", originalCard.getLeftHalfCard().getId(), copiedCard.getLeftHalfCard().getId());
Assert.assertEquals("left - rules must be same", originalCard.getLeftHalfCard().getRules(), copiedCard.getLeftHalfCard().getRules());
Assert.assertEquals("left - parent ref", copiedCard.getLeftHalfCard().getParentCard().getId(), copiedCard.getId());
abilitySourceMustBeSame(originalCard.getLeftHalfCard(), "left original");
abilitySourceMustBeSame(copiedCard.getLeftHalfCard(), "left copied");
//cardsMustHaveSameZoneAndZCC(originalCard.getLeftHalfCard(), copiedCard.getLeftHalfCard(), "left");
// right
Assert.assertNotEquals("right - id must be different", originalCard.getRightHalfCard().getId(), copiedCard.getRightHalfCard().getId());
Assert.assertEquals("right - rules must be same", originalCard.getRightHalfCard().getRules(), copiedCard.getRightHalfCard().getRules());
Assert.assertEquals("right - parent ref", copiedCard.getRightHalfCard().getParentCard().getId(), copiedCard.getId());
abilitySourceMustBeSame(originalCard.getRightHalfCard(), "right original");
abilitySourceMustBeSame(copiedCard.getRightHalfCard(), "right copied");
//cardsMustHaveSameZoneAndZCC(originalCard.getRightHalfCard(), copiedCard.getRightHalfCard(), "right");
}
private void abilitySourceMustBeSame(Card card, String infoPrefix) {
Set<UUID> partIds = CardUtil.getObjectParts(card);
card.getAbilities(currentGame).forEach(ability -> {
// ability can refs to part or main card only
if (!partIds.contains(ability.getSourceId())) {
if (ability instanceof MageSingleton) {
// sourceId don't work with MageSingleton abilities
return;
}
Assert.fail(infoPrefix + " - " + "ability source must be same: " + ability.toString());
}
});
}
private void prepareZoneAndZCC(Card originalCard) {
// prepare custom zcc and zone for copy testing
originalCard.setZoneChangeCounter(5, currentGame);
originalCard.setZone(Zone.STACK, currentGame);
}
private void cardsMustHaveSameZoneAndZCC(Card originalCard, Card copiedCard, String infoPrefix) {
// zcc and zone are not copied, so you don't need it here yet
Zone zone1 = currentGame.getState().getZone(originalCard.getId());
Zone zone2 = currentGame.getState().getZone(copiedCard.getId());
int zcc1 = currentGame.getState().getZoneChangeCounter(originalCard.getId());
int zcc2 = currentGame.getState().getZoneChangeCounter(copiedCard.getId());
if (zone1 != zone2 || zcc1 != zcc2) {
Assert.fail(infoPrefix + " - " + "cards must have same zone and zcc: " + zcc1 + " - " + zone1 + " != " + zcc2 + " - " + zone2);
}
}
} }

View file

@ -88,6 +88,8 @@ public interface Ability extends Controllable, Serializable {
/** /**
* Gets the id of the object which put this ability in motion. * Gets the id of the object which put this ability in motion.
* *
* WARNING, MageSingleton abilities contains dirty data here, so you can't use sourceId with it
*
* @return The {@link java.util.UUID} of the object this ability is * @return The {@link java.util.UUID} of the object this ability is
* associated with. * associated with.
*/ */

View file

@ -13,11 +13,9 @@ import mage.constants.*;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil;
import java.util.HashSet; import java.util.*;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
@ -176,9 +174,17 @@ public class SpellAbility extends ActivatedAbilityImpl {
return new SpellAbility(this); return new SpellAbility(this);
} }
public SpellAbility copySpell() { public SpellAbility copySpell(Card originalCard, Card copiedCard) {
// all copied spells must have own copied card
Map<UUID, MageObject> mapOldToNew = CardUtil.getOriginalToCopiedPartsMap(originalCard, copiedCard);
if (!mapOldToNew.containsKey(this.getSourceId())) {
throw new IllegalStateException("Can't find source id after copy: " + originalCard.getName() + " -> " + copiedCard.getName());
}
UUID copiedSourceId = mapOldToNew.getOrDefault(this.getSourceId(), copiedCard).getId();
SpellAbility spell = new SpellAbility(this); SpellAbility spell = new SpellAbility(this);
spell.id = UUID.randomUUID(); spell.newId();
spell.setSourceId(copiedSourceId);
return spell; return spell;
} }

View file

@ -6,6 +6,8 @@ import mage.game.Game;
import mage.game.stack.Spell; import mage.game.stack.Spell;
/** /**
* Addendum If you cast this spell during your main phase, you get some boost
*
* @author LevelX2 * @author LevelX2
*/ */
@ -23,6 +25,6 @@ public enum AddendumCondition implements Condition {
return true; return true;
} }
Spell spell = game.getSpell(source.getSourceId()); Spell spell = game.getSpell(source.getSourceId());
return spell != null && !spell.isCopy(); return spell != null && !spell.isCopy(); // copies are not casted
} }
} }

View file

@ -80,8 +80,9 @@ public abstract class CopySpellForEachItCouldTargetEffect<T extends MageItem> ex
return false; return false;
} }
// generate copies for each possible target, but do not put it to stack (use must choose targets in custom order later) // TODO: add support if multiple copies? See Twinning Staff
Spell copy = spell.copySpell(source.getControllerId(), game); // generate copies for each possible target, but do not put it to stack (must choose targets in custom order later)
Spell copy = spell.copySpell(game, source, source.getControllerId());
modifyCopy(copy, game, source); modifyCopy(copy, game, source);
Target sampleTarget = targetsToBeChanged.iterator().next().getTarget(copy); Target sampleTarget = targetsToBeChanged.iterator().next().getTarget(copy);
sampleTarget.setNotTarget(true); sampleTarget.setNotTarget(true);
@ -93,7 +94,8 @@ public abstract class CopySpellForEachItCouldTargetEffect<T extends MageItem> ex
obj = game.getPlayer(objId); obj = game.getPlayer(objId);
} }
if (obj != null) { if (obj != null) {
copy = spell.copySpell(source.getControllerId(), game); // TODO: add support if multiple copies? See Twinning Staff
copy = spell.copySpell(game, source, source.getControllerId());
try { try {
modifyCopy(copy, (T) obj, game, source); modifyCopy(copy, (T) obj, game, source);
if (!filter.match((T) obj, source.getSourceId(), actingPlayer.getId(), game)) { if (!filter.match((T) obj, source.getSourceId(), actingPlayer.getId(), game)) {
@ -168,6 +170,8 @@ public abstract class CopySpellForEachItCouldTargetEffect<T extends MageItem> ex
for (UUID chosenId : chosenIds) { for (UUID chosenId : chosenIds) {
Spell chosenCopy = targetCopyMap.get(chosenId); Spell chosenCopy = targetCopyMap.get(chosenId);
if (chosenCopy != null) { if (chosenCopy != null) {
// COPY DONE, can put to stack
chosenCopy.setZone(Zone.STACK, game);
game.getStack().push(chosenCopy); game.getStack().push(chosenCopy);
game.fireEvent(new CopiedStackObjectEvent(spell, chosenCopy, source.getControllerId())); game.fireEvent(new CopiedStackObjectEvent(spell, chosenCopy, source.getControllerId()));
toDelete.add(chosenId); toDelete.add(chosenId);

View file

@ -55,7 +55,7 @@ public class EpicEffect extends OneShotEffect {
if (spell == null) { if (spell == null) {
return false; return false;
} }
spell = spell.copySpell(source.getControllerId(), game); spell = spell.copySpell(game, source, source.getControllerId()); // it's a fake copy, real copy with events in EpicPushEffect
// Remove Epic effect from the spell // Remove Epic effect from the spell
Effect epicEffect = null; Effect epicEffect = null;
for (Effect effect : spell.getSpellAbility().getEffects()) { for (Effect effect : spell.getSpellAbility().getEffects()) {

View file

@ -1,6 +1,5 @@
package mage.abilities.effects.common; package mage.abilities.effects.common;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.MageSingleton; import mage.abilities.MageSingleton;
import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.AsThoughEffectImpl;
@ -18,6 +17,8 @@ import mage.players.Player;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil; import mage.util.CardUtil;
import java.util.UUID;
/** /**
* @author phulin * @author phulin
*/ */
@ -48,7 +49,7 @@ public class ExileAdventureSpellEffect extends OneShotEffect implements MageSing
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
Spell spell = game.getStack().getSpell(source.getId()); Spell spell = game.getStack().getSpell(source.getId());
if (spell != null && !spell.isCopy()) { if (spell != null) {
Card spellCard = spell.getCard(); Card spellCard = spell.getCard();
if (spellCard instanceof AdventureCardSpell) { if (spellCard instanceof AdventureCardSpell) {
UUID exileId = adventureExileId(controller.getId(), game); UUID exileId = adventureExileId(controller.getId(), game);

View file

@ -36,7 +36,7 @@ public class ExileSpellEffect extends OneShotEffect implements MageSingleton {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
Spell spell = game.getStack().getSpell(source.getId()); Spell spell = game.getStack().getSpell(source.getId());
if (spell != null && !spell.isCopy()) { if (spell != null) {
Card spellCard = spell.getCard(); Card spellCard = spell.getCard();
if (spellCard != null) { if (spellCard != null) {
controller.moveCards(spellCard, Zone.EXILED, source, game); controller.moveCards(spellCard, Zone.EXILED, source, game);

View file

@ -11,7 +11,6 @@ import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
/** /**
*
* @author LevelX2 * @author LevelX2
*/ */
public class ReturnToLibrarySpellEffect extends OneShotEffect { public class ReturnToLibrarySpellEffect extends OneShotEffect {
@ -20,7 +19,7 @@ public class ReturnToLibrarySpellEffect extends OneShotEffect {
public ReturnToLibrarySpellEffect(boolean top) { public ReturnToLibrarySpellEffect(boolean top) {
super(Outcome.Neutral); super(Outcome.Neutral);
staticText = "Put {this} on "+ (top ? "top":"the bottom") + " of its owner's library"; staticText = "Put {this} on " + (top ? "top" : "the bottom") + " of its owner's library";
this.toTop = top; this.toTop = top;
} }
@ -34,7 +33,7 @@ public class ReturnToLibrarySpellEffect extends OneShotEffect {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
Spell spell = game.getStack().getSpell(source.getSourceId()); Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell != null && !spell.isCopy()) { if (spell != null) {
Card spellCard = spell.getCard(); Card spellCard = spell.getCard();
if (spellCard != null) { if (spellCard != null) {
controller.moveCardToLibraryWithInfo(spellCard, source, game, Zone.STACK, toTop, true); controller.moveCardToLibraryWithInfo(spellCard, source, game, Zone.STACK, toTop, true);

View file

@ -1,4 +1,3 @@
package mage.abilities.effects.common; package mage.abilities.effects.common;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -11,7 +10,6 @@ import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
/** /**
*
* @author nantuko * @author nantuko
*/ */
public class ShuffleSpellEffect extends OneShotEffect implements MageSingleton { public class ShuffleSpellEffect extends OneShotEffect implements MageSingleton {
@ -34,7 +32,7 @@ public class ShuffleSpellEffect extends OneShotEffect implements MageSingleton {
// We have to use the spell id because in case of copied spells, the sourceId can be multiple times on the stack // We have to use the spell id because in case of copied spells, the sourceId can be multiple times on the stack
Spell spell = game.getStack().getSpell(source.getId()); Spell spell = game.getStack().getSpell(source.getId());
if (spell != null) { if (spell != null) {
if (controller.moveCards(spell, Zone.LIBRARY, source, game) && !spell.isCopy()) { if (controller.moveCards(spell, Zone.LIBRARY, source, game)) {
Player owner = game.getPlayer(spell.getCard().getOwnerId()); Player owner = game.getPlayer(spell.getCard().getOwnerId());
if (owner != null) { if (owner != null) {
owner.shuffleLibrary(source, game); owner.shuffleLibrary(source, game);

View file

@ -18,7 +18,7 @@ import java.util.UUID;
public abstract class AdventureCard extends CardImpl { public abstract class AdventureCard extends CardImpl {
/* The adventure spell card, i.e. Swift End. */ /* The adventure spell card, i.e. Swift End. */
protected Card spellCard; protected AdventureCardSpell spellCard;
public AdventureCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String adventureName, String costsSpell) { public AdventureCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String adventureName, String costsSpell) {
super(ownerId, setInfo, types, costs); super(ownerId, setInfo, types, costs);
@ -28,13 +28,19 @@ public abstract class AdventureCard extends CardImpl {
public AdventureCard(AdventureCard card) { public AdventureCard(AdventureCard card) {
super(card); super(card);
this.spellCard = card.getSpellCard().copy(); this.spellCard = card.getSpellCard().copy();
((AdventureCardSpell) this.spellCard).setParentCard(this); this.spellCard.setParentCard(this);
} }
public Card getSpellCard() { public AdventureCardSpell getSpellCard() {
return spellCard; return spellCard;
} }
public void setParts(AdventureCardSpell cardSpell) {
// for card copy only - set new parts
this.spellCard = cardSpell;
cardSpell.setParentCard(this);
}
@Override @Override
public void assignNewId() { public void assignNewId() {
super.assignNewId(); super.assignNewId();

View file

@ -55,6 +55,14 @@ public abstract class ModalDoubleFacesCard extends CardImpl {
return (ModalDoubleFacesCardHalf) rightHalfCard; return (ModalDoubleFacesCardHalf) rightHalfCard;
} }
public void setParts(ModalDoubleFacesCardHalf leftHalfCard, ModalDoubleFacesCardHalf rightHalfCard) {
// for card copy only - set new parts
this.leftHalfCard = leftHalfCard;
leftHalfCard.setParentCard(this);
this.rightHalfCard = rightHalfCard;
rightHalfCard.setParentCard(this);
}
@Override @Override
public void assignNewId() { public void assignNewId() {
super.assignNewId(); super.assignNewId();

View file

@ -42,6 +42,14 @@ public abstract class SplitCard extends CardImpl {
((SplitCardHalf) rightHalfCard).setParentCard(this); ((SplitCardHalf) rightHalfCard).setParentCard(this);
} }
public void setParts(SplitCardHalf leftHalfCard, SplitCardHalf rightHalfCard) {
// for card copy only - set new parts
this.leftHalfCard = leftHalfCard;
leftHalfCard.setParentCard(this);
this.rightHalfCard = rightHalfCard;
rightHalfCard.setParentCard(this);
}
public SplitCardHalf getLeftHalfCard() { public SplitCardHalf getLeftHalfCard() {
return (SplitCardHalf) leftHalfCard; return (SplitCardHalf) leftHalfCard;
} }

View file

@ -74,6 +74,7 @@ import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.stream.Collectors;
public abstract class GameImpl implements Game, Serializable { public abstract class GameImpl implements Game, Serializable {
@ -552,7 +553,7 @@ public abstract class GameImpl implements Game, Serializable {
// copied cards removes, but delayed triggered possible from it, see https://github.com/magefree/mage/issues/5437 // copied cards removes, but delayed triggered possible from it, see https://github.com/magefree/mage/issues/5437
// TODO: remove that workround after LKI rework, see GameState.copyCard // TODO: remove that workround after LKI rework, see GameState.copyCard
if (card == null) { if (card == null) {
card = (Card) state.getValue(GameState.COPIED_FROM_CARD_KEY + cardId.toString()); card = (Card) state.getValue(GameState.COPIED_CARD_KEY + cardId.toString());
} }
return card; return card;
} }
@ -1421,7 +1422,7 @@ public abstract class GameImpl implements Game, Serializable {
errorContinueCounter++; errorContinueCounter++;
continue; continue;
} else { } else {
throw new MageException("Error in testclass"); throw new MageException("Error in unit tests");
} }
} finally { } finally {
setCheckPlayableState(false); setCheckPlayableState(false);
@ -1734,7 +1735,7 @@ public abstract class GameImpl implements Game, Serializable {
@Override @Override
public Card copyCard(Card cardToCopy, Ability source, UUID newController) { public Card copyCard(Card cardToCopy, Ability source, UUID newController) {
return state.copyCard(cardToCopy, source, this); return state.copyCard(cardToCopy, newController, this);
} }
/** /**
@ -1947,48 +1948,67 @@ public abstract class GameImpl implements Game, Serializable {
somethingHappened = true; somethingHappened = true;
} }
// 704.5e If a copy of a spell is in a zone other than the stack, it ceases to exist. If a copy of a card is in any zone other than the stack or the battlefield, it ceases to exist. // 704.5e
// (Isochron Scepter) 12/1/2004: If you don't want to cast the copy, you can choose not to; the copy ceases to exist the next time state-based actions are checked. // If a copy of a spell is in a zone other than the stack, it ceases to exist.
Iterator<Card> copiedCards = this.getState().getCopiedCards().iterator(); // If a copy of a card is in any zone other than the stack or the battlefield, it ceases to exist.
while (copiedCards.hasNext()) { // (Isochron Scepter) 12/1/2004: If you don't want to cast the copy, you can choose not to; the copy ceases
Card card = copiedCards.next(); // to exist the next time state-based actions are checked.
if (card instanceof SplitCardHalf || card instanceof AdventureCardSpell || card instanceof ModalDoubleFacesCardHalf) { //
continue; // only the main card is moves, not the halves (cause halfes is not copied - it uses original card -- TODO: need to fix (bugs with same card copy)? // Copied cards can be stored in GameState.copiedCards or in game state value (until LKI rework)
Set<Card> allCopiedCards = new HashSet<>();
allCopiedCards.addAll(this.getState().getCopiedCards());
Map<String, Object> stateSavedCopiedCards = this.getState().getValues(GameState.COPIED_CARD_KEY);
allCopiedCards.addAll(stateSavedCopiedCards.values()
.stream()
.map(object -> (Card) object)
.filter(Objects::nonNull)
.collect(Collectors.toList())
);
for (Card copiedCard : allCopiedCards) {
// 1. Zone must be checked from main card only cause mdf parts can have different zones
// (one side on battlefield, another side on outsize)
// 2. Copied card creates in OUTSIDE zone and put to stack manually in the same code,
// so no SBA calls before real zone change (you will see here only unused cards like Isochron Scepter)
// (Isochron Scepter) 12/1/2004: If you don't want to cast the copy, you can choose not to; the copy ceases
// to exist the next time state-based actions are checked.
Zone zone = state.getZone(copiedCard.getMainCard().getId());
if (zone == Zone.BATTLEFIELD || zone == Zone.STACK) {
continue;
} }
Zone zone = state.getZone(card.getId()); // TODO: remember LKI of copied cards here after LKI rework
if (zone != Zone.BATTLEFIELD && zone != Zone.STACK) { switch (zone) {
// TODO: remember LKI of copied cards here after LKI rework case GRAVEYARD:
switch (zone) { for (Player player : getPlayers().values()) {
case GRAVEYARD: if (player.getGraveyard().contains(copiedCard.getId())) {
for (Player player : getPlayers().values()) { player.getGraveyard().remove(copiedCard);
if (player.getGraveyard().contains(card.getId())) { break;
player.getGraveyard().remove(card);
break;
}
} }
break; }
case HAND: break;
for (Player player : getPlayers().values()) { case HAND:
if (player.getHand().contains(card.getId())) { for (Player player : getPlayers().values()) {
player.getHand().remove(card); if (player.getHand().contains(copiedCard.getId())) {
break; player.getHand().remove(copiedCard);
} break;
} }
break; }
case LIBRARY: break;
for (Player player : getPlayers().values()) { case LIBRARY:
if (player.getLibrary().getCard(card.getId(), this) != null) { for (Player player : getPlayers().values()) {
player.getLibrary().remove(card.getId(), this); if (player.getLibrary().getCard(copiedCard.getId(), this) != null) {
break; player.getLibrary().remove(copiedCard.getId(), this);
} break;
} }
break; }
case EXILED: break;
getExile().removeCard(card, this); case EXILED:
break; getExile().removeCard(copiedCard, this);
} break;
copiedCards.remove();
} }
// remove copied card info
this.getState().getCopiedCards().remove(copiedCard);
this.getState().removeValue(GameState.COPIED_CARD_KEY + copiedCard.getId().toString());
} }
List<Permanent> legendary = new ArrayList<>(); List<Permanent> legendary = new ArrayList<>();

View file

@ -6,10 +6,7 @@ import mage.abilities.*;
import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.ContinuousEffects; import mage.abilities.effects.ContinuousEffects;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.cards.AdventureCard; import mage.cards.*;
import mage.cards.Card;
import mage.cards.ModalDoubleFacesCard;
import mage.cards.SplitCard;
import mage.constants.Zone; import mage.constants.Zone;
import mage.designations.Designation; import mage.designations.Designation;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
@ -56,7 +53,9 @@ public class GameState implements Serializable, Copyable<GameState> {
private static final Logger logger = Logger.getLogger(GameState.class); private static final Logger logger = Logger.getLogger(GameState.class);
private static final ThreadLocalStringBuilder threadLocalBuilder = new ThreadLocalStringBuilder(1024); private static final ThreadLocalStringBuilder threadLocalBuilder = new ThreadLocalStringBuilder(1024);
public static final String COPIED_FROM_CARD_KEY = "CopiedFromCard"; // save copied cards between game cycles (lki workaround)
// warning, do not use another keys with same starting text cause copy code search and clean all related values
public static final String COPIED_CARD_KEY = "CopiedCard";
private final Players players; private final Players players;
private final PlayerList playerList; private final PlayerList playerList;
@ -845,7 +844,12 @@ public class GameState implements Serializable, Copyable<GameState> {
} }
public void addCard(Card card) { public void addCard(Card card) {
setZone(card.getId(), Zone.OUTSIDE); // all new cards and tokens must enter from outside
addCard(card, Zone.OUTSIDE);
}
private void addCard(Card card, Zone zone) {
setZone(card.getId(), zone);
// add card specific abilities to game // add card specific abilities to game
for (Ability ability : card.getInitAbilities()) { for (Ability ability : card.getInitAbilities()) {
@ -853,28 +857,6 @@ public class GameState implements Serializable, Copyable<GameState> {
} }
} }
public void removeCopiedCard(Card card) {
if (copiedCards.containsKey(card.getId())) {
copiedCards.remove(card.getId());
cardState.remove(card.getId());
zones.remove(card.getId());
zoneChangeCounter.remove(card.getId());
}
// TODO Watchers?
// TODO Abilities?
if (card instanceof SplitCard) {
removeCopiedCard(((SplitCard) card).getLeftHalfCard());
removeCopiedCard(((SplitCard) card).getRightHalfCard());
}
if (card instanceof ModalDoubleFacesCard) {
removeCopiedCard(((ModalDoubleFacesCard) card).getLeftHalfCard());
removeCopiedCard(((ModalDoubleFacesCard) card).getRightHalfCard());
}
if (card instanceof AdventureCard) {
removeCopiedCard(((AdventureCard) card).getSpellCard());
}
}
/** /**
* Used for adding abilities that exist permanent on cards/permanents and * Used for adding abilities that exist permanent on cards/permanents and
* are not only gained for a certain time (e.g. until end of turn). * are not only gained for a certain time (e.g. until end of turn).
@ -1021,6 +1003,27 @@ public class GameState implements Serializable, Copyable<GameState> {
return values.get(valueId); return values.get(valueId);
} }
/**
* Return values list starting with searching key.
* <p>
* Usage example: if you want to find all saved values from related ability/effect
*
* @param startWithValue
* @return
*/
public Map<String, Object> getValues(String startWithValue) {
if (startWithValue == null || startWithValue.isEmpty()) {
throw new IllegalArgumentException("Can't use empty search value");
}
Map<String, Object> res = new HashMap<>();
for (Map.Entry<String, Object> entry : this.values.entrySet()) {
if (entry.getKey().startsWith(startWithValue)) {
res.put(entry.getKey(), entry.getValue());
}
}
return res;
}
/** /**
* Best only use immutable objects, otherwise the states/values of the * Best only use immutable objects, otherwise the states/values of the
* object may be changed by AI simulation or rollbacks, because the Value * object may be changed by AI simulation or rollbacks, because the Value
@ -1035,6 +1038,15 @@ public class GameState implements Serializable, Copyable<GameState> {
values.put(valueId, value); values.put(valueId, value);
} }
/**
* Remove saved value
*
* @param valueId
*/
public void removeValue(String valueId) {
values.remove(valueId);
}
/** /**
* Other abilities are used to implement some special kind of continuous * Other abilities are used to implement some special kind of continuous
* effects that give abilities to non permanents. * effects that give abilities to non permanents.
@ -1251,47 +1263,92 @@ public class GameState implements Serializable, Copyable<GameState> {
return copiedCards.values(); return copiedCards.values();
} }
public Card copyCard(Card cardToCopy, Ability source, Game game) { /**
// main card * Make full copy of the card and all of the card's parts and put to the game.
Card copiedCard = cardToCopy.copy(); *
copiedCard.assignNewId(); * @param mainCardToCopy
copiedCard.setOwnerId(source.getControllerId()); * @param newController
copiedCard.setCopy(true, cardToCopy); * @param game
copiedCards.put(copiedCard.getId(), copiedCard); * @return
addCard(copiedCard); */
public Card copyCard(Card mainCardToCopy, UUID newController, Game game) {
// runtime check
if (!mainCardToCopy.getId().equals(mainCardToCopy.getMainCard().getId())) {
// copyCard allows for main card only, if you catch it then check your targeting code
throw new IllegalArgumentException("Wrong code usage. You can copy only main card.");
}
// other faces // must copy all card's parts
// zcc and zone must be new cause zcc copy logic need card usage info here, but it haven't:
// * reason 1: copied land must be played (+1 zcc), but copied spell must be put on stack and cast (+2 zcc)
// * reason 2: copied card or spell can be used later as blueprint for real copies (see Epic ability)
List<Card> copiedParts = new ArrayList<>();
// main part (prepare must be called after other parts)
Card copiedCard = mainCardToCopy.copy();
copiedParts.add(copiedCard);
// other parts
if (copiedCard instanceof SplitCard) { if (copiedCard instanceof SplitCard) {
// left // left
Card leftCard = ((SplitCard) copiedCard).getLeftHalfCard(); // TODO: must be new ID (bugs with same card copy)? SplitCardHalf leftOriginal = ((SplitCard) copiedCard).getLeftHalfCard();
copiedCards.put(leftCard.getId(), leftCard); SplitCardHalf leftCopied = leftOriginal.copy();
addCard(leftCard); prepareCardForCopy(leftOriginal, leftCopied, newController);
copiedParts.add(leftCopied);
// right // right
Card rightCard = ((SplitCard) copiedCard).getRightHalfCard(); SplitCardHalf rightOriginal = ((SplitCard) copiedCard).getRightHalfCard();
copiedCards.put(rightCard.getId(), rightCard); SplitCardHalf rightCopied = rightOriginal.copy();
addCard(rightCard); prepareCardForCopy(rightOriginal, rightCopied, newController);
copiedParts.add(rightCopied);
// sync parts
((SplitCard) copiedCard).setParts(leftCopied, rightCopied);
} else if (copiedCard instanceof ModalDoubleFacesCard) { } else if (copiedCard instanceof ModalDoubleFacesCard) {
// left // left
Card leftCard = ((ModalDoubleFacesCard) copiedCard).getLeftHalfCard(); // TODO: must be new ID (bugs with same card copy)? ModalDoubleFacesCardHalf leftOriginal = ((ModalDoubleFacesCard) copiedCard).getLeftHalfCard();
copiedCards.put(leftCard.getId(), leftCard); ModalDoubleFacesCardHalf leftCopied = leftOriginal.copy();
addCard(leftCard); prepareCardForCopy(leftOriginal, leftCopied, newController);
copiedParts.add(leftCopied);
// right // right
Card rightCard = ((ModalDoubleFacesCard) copiedCard).getRightHalfCard(); ModalDoubleFacesCardHalf rightOriginal = ((ModalDoubleFacesCard) copiedCard).getRightHalfCard();
copiedCards.put(rightCard.getId(), rightCard); ModalDoubleFacesCardHalf rightCopied = rightOriginal.copy();
addCard(rightCard); prepareCardForCopy(rightOriginal, rightCopied, newController);
copiedParts.add(rightCopied);
// sync parts
((ModalDoubleFacesCard) copiedCard).setParts(leftCopied, rightCopied);
} else if (copiedCard instanceof AdventureCard) { } else if (copiedCard instanceof AdventureCard) {
Card spellCard = ((AdventureCard) copiedCard).getSpellCard(); // right
copiedCards.put(spellCard.getId(), spellCard); AdventureCardSpell rightOriginal = ((AdventureCard) copiedCard).getSpellCard();
addCard(spellCard); AdventureCardSpell rightCopied = rightOriginal.copy();
prepareCardForCopy(rightOriginal, rightCopied, newController);
copiedParts.add(rightCopied);
// sync parts
((AdventureCard) copiedCard).setParts(rightCopied);
} }
// main part prepare (must be called after other parts cause it change ids for all)
prepareCardForCopy(mainCardToCopy, copiedCard, newController);
// add all parts to the game
copiedParts.forEach(card -> {
copiedCards.put(card.getId(), card);
addCard(card);
});
// copied cards removes from game after battlefield/stack leaves, so remember it here as workaround to fix freeze, see https://github.com/magefree/mage/issues/5437 // copied cards removes from game after battlefield/stack leaves, so remember it here as workaround to fix freeze, see https://github.com/magefree/mage/issues/5437
// TODO: remove that workaround after LKI will be rewritten to support cross-steps/turns data transition and support copied cards // TODO: remove that workaround after LKI will be rewritten to support cross-steps/turns data transition and support copied cards
this.setValue(COPIED_FROM_CARD_KEY + copiedCard.getId(), cardToCopy.copy()); copiedParts.forEach(card -> {
this.setValue(COPIED_CARD_KEY + card.getId(), card.copy());
});
return copiedCard; return copiedCard;
} }
private void prepareCardForCopy(Card originalCard, Card copiedCard, UUID newController) {
copiedCard.assignNewId();
copiedCard.setOwnerId(newController);
copiedCard.setCopy(true, originalCard);
}
public int getNextPermanentOrderNumber() { public int getNextPermanentOrderNumber() {
return permanentOrderNumber++; return permanentOrderNumber++;
} }

View file

@ -766,8 +766,31 @@ public class Spell extends StackObjImpl implements Card {
return new Spell(this); return new Spell(this);
} }
public Spell copySpell(UUID newController, Game game) { /**
Spell spellCopy = new Spell(this.card, this.ability.copySpell(), this.controllerId, this.fromZone, game); * Copy current spell on stack, but do not put copy back to stack (you can modify and put it later)
* <p>
* Warning, don't forget to call CopyStackObjectEvent and CopiedStackObjectEvent before and after copy
* CopyStackObjectEvent can change new copies amount, see Twinning Staff
* <p>
* Warning, don't forget to call spell.setZone before push to stack
*
* @param game
* @param newController controller of the copied spell
* @return
*/
public Spell copySpell(Game game, Ability source, UUID newController) {
// copied spells must use copied cards
// spell can be from card's part (mdf/adventure), but you must copy FULL card
Card copiedMainCard = game.copyCard(this.card.getMainCard(), source, newController);
// find copied part
Map<UUID, MageObject> mapOldToNew = CardUtil.getOriginalToCopiedPartsMap(this.card.getMainCard(), copiedMainCard);
if (!mapOldToNew.containsKey(this.card.getId())) {
throw new IllegalStateException("Can't find card id after main card copy: " + copiedMainCard.getName());
}
Card copiedPart = (Card) mapOldToNew.get(this.card.getId());
// copy spell
Spell spellCopy = new Spell(copiedPart, this.ability.copySpell(this.card, copiedPart), this.controllerId, this.fromZone, game);
boolean firstDone = false; boolean firstDone = false;
for (SpellAbility spellAbility : this.getSpellAbilities()) { for (SpellAbility spellAbility : this.getSpellAbilities()) {
if (!firstDone) { if (!firstDone) {
@ -939,6 +962,12 @@ public class Spell extends StackObjImpl implements Card {
this.copyFrom = (copyFrom != null ? copyFrom.copy() : null); this.copyFrom = (copyFrom != null ? copyFrom.copy() : null);
} }
/**
* Game processing a copies as normal cards, so you don't need to check spell's copy for move/exile
* Use this only in exceptional situations or skip unaffected code/choices
*
* @return
*/
@Override @Override
public boolean isCopy() { public boolean isCopy() {
return this.copy; return this.copy;
@ -1006,6 +1035,7 @@ public class Spell extends StackObjImpl implements Card {
@Override @Override
public void setZone(Zone zone, Game game) { public void setZone(Zone zone, Game game) {
card.setZone(zone, game); card.setZone(zone, game);
game.getState().setZone(this.getId(), Zone.STACK);
} }
@Override @Override
@ -1039,8 +1069,8 @@ public class Spell extends StackObjImpl implements Card {
return null; return null;
} }
for (int i = 0; i < gameEvent.getAmount(); i++) { for (int i = 0; i < gameEvent.getAmount(); i++) {
spellCopy = this.copySpell(newControllerId, game); spellCopy = this.copySpell(game, source, newControllerId);
game.getState().setZone(spellCopy.getId(), Zone.STACK); // required for targeting ex: Nivmagus Elemental spellCopy.setZone(Zone.STACK, game); // required for targeting ex: Nivmagus Elemental
game.getStack().push(spellCopy); game.getStack().push(spellCopy);
if (chooseNewTargets) { if (chooseNewTargets) {
spellCopy.chooseNewTargets(game, newControllerId); spellCopy.chooseNewTargets(game, newControllerId);

View file

@ -1228,39 +1228,66 @@ public final class CardUtil {
* @return * @return
*/ */
public static Set<UUID> getObjectParts(MageObject object) { public static Set<UUID> getObjectParts(MageObject object) {
Set<UUID> res = new HashSet<>(); Set<UUID> res = new LinkedHashSet<>(); // set must be ordered
List<MageObject> allParts = getObjectPartsAsObjects(object);
allParts.forEach(part -> {
res.add(part.getId());
});
return res;
}
public static List<MageObject> getObjectPartsAsObjects(MageObject object) {
List<MageObject> res = new ArrayList<>();
if (object == null) { if (object == null) {
return res; return res;
} }
if (object instanceof SplitCard || object instanceof SplitCardHalf) { if (object instanceof SplitCard || object instanceof SplitCardHalf) {
SplitCard mainCard = (SplitCard) ((Card) object).getMainCard(); SplitCard mainCard = (SplitCard) ((Card) object).getMainCard();
res.add(object.getId()); res.add(mainCard);
res.add(mainCard.getId()); res.add(mainCard.getLeftHalfCard());
res.add(mainCard.getLeftHalfCard().getId()); res.add(mainCard.getRightHalfCard());
res.add(mainCard.getRightHalfCard().getId());
} else if (object instanceof ModalDoubleFacesCard || object instanceof ModalDoubleFacesCardHalf) { } else if (object instanceof ModalDoubleFacesCard || object instanceof ModalDoubleFacesCardHalf) {
ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) ((Card) object).getMainCard(); ModalDoubleFacesCard mainCard = (ModalDoubleFacesCard) ((Card) object).getMainCard();
res.add(object.getId()); res.add(mainCard);
res.add(mainCard.getId()); res.add(mainCard.getLeftHalfCard());
res.add(mainCard.getLeftHalfCard().getId()); res.add(mainCard.getRightHalfCard());
res.add(mainCard.getRightHalfCard().getId());
} else if (object instanceof AdventureCard || object instanceof AdventureCardSpell) { } else if (object instanceof AdventureCard || object instanceof AdventureCardSpell) {
AdventureCard mainCard = (AdventureCard) ((Card) object).getMainCard(); AdventureCard mainCard = (AdventureCard) ((Card) object).getMainCard();
res.add(object.getId()); res.add(mainCard);
res.add(mainCard.getId()); res.add(mainCard.getSpellCard());
res.add(mainCard.getSpellCard().getId());
} else if (object instanceof Spell) { } else if (object instanceof Spell) {
// example: activate Lightning Storm's ability from the spell on the stack // example: activate Lightning Storm's ability from the spell on the stack
res.add(object.getId()); res.add(object);
res.addAll(getObjectParts(((Spell) object).getCard())); res.addAll(getObjectPartsAsObjects(((Spell) object).getCard()));
} else if (object instanceof Commander) { } else if (object instanceof Commander) {
// commander can contains double sides // commander can contains double sides
res.add(object.getId()); res.add(object);
res.addAll(getObjectParts(((Commander) object).getSourceObject())); res.addAll(getObjectPartsAsObjects(((Commander) object).getSourceObject()));
} else { } else {
res.add(object.getId()); res.add(object);
} }
return res; return res;
} }
/**
* Find mapping from original to copied card (e.g. map left side with copied left side, etc)
*
* @param originalCard
* @param copiedCard
* @return
*/
public static Map<UUID, MageObject> getOriginalToCopiedPartsMap(Card originalCard, Card copiedCard) {
List<UUID> oldIds = new ArrayList<>(CardUtil.getObjectParts(originalCard));
List<MageObject> newObjects = new ArrayList<>(CardUtil.getObjectPartsAsObjects(copiedCard));
if (oldIds.size() != newObjects.size()) {
throw new IllegalStateException("Found wrong card parts after copy: " + originalCard.getName() + " -> " + copiedCard.getName());
}
Map<UUID, MageObject> mapOldToNew = new HashMap<>();
for (int i = 0; i < oldIds.size(); i++) {
mapOldToNew.put(oldIds.get(i), newObjects.get(i));
}
return mapOldToNew;
}
} }