mirror of
https://github.com/correl/mage.git
synced 2024-11-25 11:09:53 +00:00
* 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:
parent
8704b9cb9b
commit
b36f915d74
22 changed files with 537 additions and 179 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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()));
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -88,7 +82,7 @@ class GuileReplacementEffect extends ReplacementEffectImpl {
|
||||||
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)) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<>();
|
||||||
|
|
|
@ -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++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue