mirror of
https://github.com/magefree/mage.git
synced 2026-01-26 21:29:17 -08:00
Fixed different ZCC in split card's parts (flashback fix, see 95075cf33e);
Improve moveToZone code and fixed some cards with wrong commands queue (e.g. directly removes card from zone and then calls moveToZone again);
This commit is contained in:
parent
f010454cb2
commit
e95ae2675b
21 changed files with 116 additions and 36 deletions
|
|
@ -172,7 +172,6 @@ class ChandraAblazeEffect5 extends OneShotEffect {
|
|||
Card card = game.getCard(target.getFirstTarget());
|
||||
if (card != null) {
|
||||
player.cast(card.getSpellAbility(), game, true, new MageObjectReference(source.getSourceObject(game), game));
|
||||
player.getGraveyard().remove(card);
|
||||
cards.remove(card);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ class DarkRevenantEffect extends OneShotEffect {
|
|||
if (card != null && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
|
||||
Player owner = game.getPlayer(card.getOwnerId());
|
||||
if(owner != null) {
|
||||
owner.getGraveyard().remove(card);
|
||||
return card.moveToZone(Zone.LIBRARY, source.getSourceId(), game, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ class DwellOnThePastEffect extends OneShotEffect {
|
|||
Card card = game.getCard(targetId);
|
||||
if (card != null) {
|
||||
if (player.getGraveyard().contains(card.getId())) {
|
||||
player.getGraveyard().remove(card);
|
||||
card.moveToZone(Zone.LIBRARY, source.getSourceId(), game, true);
|
||||
shuffle = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ class GaeasBlessingEffect extends OneShotEffect {
|
|||
Card card = game.getCard(targetId);
|
||||
if (card != null) {
|
||||
if (player.getGraveyard().contains(card.getId())) {
|
||||
player.getGraveyard().remove(card);
|
||||
card.moveToZone(Zone.LIBRARY, source.getSourceId(), game, true);
|
||||
shuffle = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ class GraveyardShovelEffect extends OneShotEffect {
|
|||
if (targetPlayer.chooseTarget(Outcome.Exile, target, source, game)) {
|
||||
Card card = game.getCard(target.getFirstTarget());
|
||||
if (card != null) {
|
||||
targetPlayer.getGraveyard().remove(card);
|
||||
card.moveToExile(null, "", source.getSourceId(), game);
|
||||
if (card.isCreature()) {
|
||||
controller.gainLife(2, game, source);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ class KrosanReclamationEffect extends OneShotEffect {
|
|||
Card card = game.getCard(targetId);
|
||||
if (card != null) {
|
||||
if (player.getGraveyard().contains(card.getId())) {
|
||||
player.getGraveyard().remove(card);
|
||||
card.moveToZone(Zone.LIBRARY, source.getSourceId(), game, true);
|
||||
shuffle = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ class MemorysJourneyEffect extends OneShotEffect {
|
|||
Card card = game.getCard(targetId);
|
||||
if (card != null) {
|
||||
if (player.getGraveyard().contains(card.getId())) {
|
||||
player.getGraveyard().remove(card);
|
||||
card.moveToZone(Zone.LIBRARY, source.getSourceId(), game, true);
|
||||
shuffle = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ class SereneRemembranceEffect extends OneShotEffect {
|
|||
for (Player player : game.getPlayers().values()) {
|
||||
if (player.getGraveyard().contains(card.getId())) {
|
||||
graveyardPlayer = player;
|
||||
player.getGraveyard().remove(card);
|
||||
result |= card.moveToZone(Zone.LIBRARY, source.getSourceId(), game, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class SpellweaverVoluteEffect extends OneShotEffect {
|
|||
&& controller.chooseUse(Outcome.Copy, "Create a copy of " + enchantedCard.getName() + '?', source, game)) {
|
||||
Card copiedCard = game.copyCard(enchantedCard, source, source.getControllerId());
|
||||
if (copiedCard != null) {
|
||||
ownerEnchanted.getGraveyard().add(copiedCard);
|
||||
controller.getGraveyard().add(copiedCard);
|
||||
game.getState().setZone(copiedCard.getId(), Zone.GRAVEYARD);
|
||||
if (controller.chooseUse(Outcome.PlayForFree, "Cast the copied card without paying mana cost?", source, game)) {
|
||||
if (copiedCard.getSpellAbility() != null) {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ class StreamOfConsciousnessEffect extends OneShotEffect {
|
|||
Card card = game.getCard(targetId);
|
||||
if (card != null) {
|
||||
if (player.getGraveyard().contains(card.getId())) {
|
||||
player.getGraveyard().remove(card);
|
||||
card.moveToZone(Zone.LIBRARY, source.getSourceId(), game, true);
|
||||
shuffle = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ class UndyingBeastEffect extends OneShotEffect {
|
|||
if (card != null && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
|
||||
Player owner = game.getPlayer(card.getOwnerId());
|
||||
if(owner != null) {
|
||||
owner.getGraveyard().remove(card);
|
||||
return card.moveToZone(Zone.LIBRARY, source.getSourceId(), game, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,43 @@ public class CastSplitCardsWithFlashbackTest extends CardTestPlayerBase {
|
|||
assertExileCount(playerA, "Wear // Tear", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Flashback_SplitLeft_ZCCChanged() {
|
||||
// {1}{U}
|
||||
// When Snapcaster Mage enters the battlefield, target instant or sorcery card in your graveyard gains flashback until end of turn.
|
||||
addCard(Zone.HAND, playerA, "Snapcaster Mage", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
|
||||
//
|
||||
// Wear {1}{R} Destroy target artifact.
|
||||
// Tear {W} Destroy target enchantment.
|
||||
addCard(Zone.HAND, playerA, "Wear // Tear", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2 + 2); // for first Wear cast
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Bident of Thassa", 1); // Legendary Enchantment Artifact
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Bow of Nylea", 1); // Legendary Enchantment Artifact
|
||||
|
||||
// play card as normal to change ZCC for split cards (simulate GUI session - ZCC's was bugged and card's parts was able to have different ZCC)
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wear", "Bow of Nylea");
|
||||
|
||||
// add flashback
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snapcaster Mage");
|
||||
addTarget(playerA, "Wear // Tear");
|
||||
|
||||
// cast as flashback
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flashback {1}{R}", "Bident of Thassa");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
assertAllCommandsUsed();
|
||||
|
||||
assertGraveyardCount(playerB, "Bident of Thassa", 1);
|
||||
assertGraveyardCount(playerB, "Bow of Nylea", 1);
|
||||
assertExileCount(playerA, "Wear // Tear", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Flashback_SplitRight() {
|
||||
// {1}{U}
|
||||
|
|
|
|||
|
|
@ -1601,13 +1601,12 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param turnNum
|
||||
* @param step
|
||||
* @param player
|
||||
* @param ability
|
||||
* @param targetName use NO_TARGET if there is no target to set
|
||||
* @param spellOnStack
|
||||
* @param targetName use NO_TARGET if there is no target to set
|
||||
* @param spellOnStack
|
||||
*/
|
||||
public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, String targetName, String spellOnStack) {
|
||||
// TODO: it's uses computerPlayer to execute, only ability target will work, but choices and targets commands aren't
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ public class PutOnLibrarySourceEffect extends OneShotEffect {
|
|||
} else if (sourceObject instanceof Card && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD) {
|
||||
for (Player player : game.getPlayers().values()) {
|
||||
if (player.getGraveyard().contains(sourceObject.getId())) {
|
||||
player.getGraveyard().remove(((Card) sourceObject));
|
||||
((Card) sourceObject).moveToZone(Zone.LIBRARY, source.getSourceId(), game, onTop);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package mage.cards;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import mage.abilities.Abilities;
|
||||
import mage.abilities.AbilitiesImpl;
|
||||
import mage.abilities.Ability;
|
||||
|
|
@ -9,6 +7,10 @@ import mage.abilities.SpellAbility;
|
|||
import mage.constants.CardType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ZoneChangeEvent;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
|
|
@ -64,6 +66,22 @@ public abstract class AdventureCard extends CardImpl {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeFromZone(Game game, Zone fromZone, UUID sourceId) {
|
||||
// zone contains only one main card
|
||||
return super.removeFromZone(game, fromZone, sourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
|
||||
if (isCopy()) { // same as meld cards
|
||||
super.updateZoneChangeCounter(game, event);
|
||||
return;
|
||||
}
|
||||
super.updateZoneChangeCounter(game, event);
|
||||
getSpellCard().updateZoneChangeCounter(game, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
|
||||
switch (ability.getSpellAbilityType()) {
|
||||
|
|
|
|||
|
|
@ -523,7 +523,6 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
case EXILED:
|
||||
if (game.getExile().getCard(getId(), game) != null) {
|
||||
removed = game.getExile().removeCard(this, game);
|
||||
|
||||
}
|
||||
break;
|
||||
case STACK:
|
||||
|
|
@ -533,15 +532,18 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
} else {
|
||||
stackObject = game.getStack().getSpell(this.getId(), false);
|
||||
}
|
||||
|
||||
if (stackObject == null && (this instanceof SplitCard)) { // handle if half of Split cast is on the stack
|
||||
stackObject = game.getStack().getSpell(((SplitCard) this).getLeftHalfCard().getId(), false);
|
||||
if (stackObject == null) {
|
||||
stackObject = game.getStack().getSpell(((SplitCard) this).getRightHalfCard().getId(), false);
|
||||
}
|
||||
}
|
||||
|
||||
if (stackObject == null && (this instanceof AdventureCard)) {
|
||||
stackObject = game.getStack().getSpell(((AdventureCard) this).getSpellCard().getId(), false);
|
||||
}
|
||||
|
||||
if (stackObject == null) {
|
||||
stackObject = game.getStack().getSpell(getId(), false);
|
||||
}
|
||||
|
|
@ -589,6 +591,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
}
|
||||
} else {
|
||||
logger.warn("Couldn't find card in fromZone, card=" + getIdName() + ", fromZone=" + fromZone);
|
||||
// possible reason: you to remove card from wrong zone or card already removed,
|
||||
// e.g. you added copy card to wrong graveyard (see owner) or removed card from graveyard before moveToZone call
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public interface Cards extends Set<UUID>, Serializable {
|
|||
|
||||
Card get(UUID cardId, Game game);
|
||||
|
||||
void remove(Card card);
|
||||
boolean remove(Card card);
|
||||
|
||||
void setOwner(UUID ownerId, Game game);
|
||||
|
||||
|
|
|
|||
|
|
@ -64,11 +64,11 @@ public class CardsImpl extends LinkedHashSet<UUID> implements Cards, Serializabl
|
|||
}
|
||||
|
||||
@Override
|
||||
public void remove(Card card) {
|
||||
public boolean remove(Card card) {
|
||||
if (card == null) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this.remove(card.getId());
|
||||
return this.remove(card.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
package mage.cards;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
|
|
@ -13,7 +12,6 @@ import java.util.List;
|
|||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author emerald000
|
||||
*/
|
||||
public abstract class MeldCard extends CardImpl {
|
||||
|
|
@ -142,6 +140,24 @@ public abstract class MeldCard extends CardImpl {
|
|||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToZone(Zone toZone, UUID sourceId, Game game, boolean flag, List<UUID> appliedEffects) {
|
||||
// TODO: missing override method for meld cards? See removeFromZone, updateZoneChangeCounter, etc
|
||||
return super.moveToZone(toZone, sourceId, game, flag, appliedEffects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZone(Zone zone, Game game) {
|
||||
// TODO: missing override method for meld cards? See removeFromZone, updateZoneChangeCounter, etc
|
||||
super.setZone(zone, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToExile(UUID exileId, String name, UUID sourceId, Game game, List<UUID> appliedEffects) {
|
||||
// TODO: missing override method for meld cards? See removeFromZone, updateZoneChangeCounter, etc
|
||||
return super.moveToExile(exileId, name, sourceId, game, appliedEffects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeFromZone(Game game, Zone fromZone, UUID sourceId) {
|
||||
if (isCopy()) {
|
||||
|
|
@ -170,7 +186,7 @@ public abstract class MeldCard extends CardImpl {
|
|||
super.updateZoneChangeCounter(game, event);
|
||||
return;
|
||||
}
|
||||
game.getState().updateZoneChangeCounter(objectId);
|
||||
super.updateZoneChangeCounter(game, event);
|
||||
if (topLastZoneChangeCounter == topHalfCard.getZoneChangeCounter(game)
|
||||
&& halves.contains(topHalfCard.getId())) {
|
||||
topHalfCard.updateZoneChangeCounter(game, event);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import mage.constants.CardType;
|
|||
import mage.constants.SpellAbilityType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ZoneChangeEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
|
@ -74,6 +75,13 @@ public abstract class SplitCard extends CardImpl {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZone(Zone zone, Game game) {
|
||||
super.setZone(zone, game);
|
||||
game.setZone(getLeftHalfCard().getId(), zone);
|
||||
game.setZone(getRightHalfCard().getId(), zone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToExile(UUID exileId, String name, UUID sourceId, Game game, List<UUID> appliedEffects) {
|
||||
if (super.moveToExile(exileId, name, sourceId, game, appliedEffects)) {
|
||||
|
|
@ -85,6 +93,23 @@ public abstract class SplitCard extends CardImpl {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeFromZone(Game game, Zone fromZone, UUID sourceId) {
|
||||
// zone contains only one main card
|
||||
return super.removeFromZone(game, fromZone, sourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
|
||||
if (isCopy()) { // same as meld cards
|
||||
super.updateZoneChangeCounter(game, event);
|
||||
return;
|
||||
}
|
||||
super.updateZoneChangeCounter(game, event);
|
||||
getLeftHalfCard().updateZoneChangeCounter(game, event);
|
||||
getRightHalfCard().updateZoneChangeCounter(game, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
|
||||
switch (ability.getSpellAbilityType()) {
|
||||
|
|
@ -99,13 +124,6 @@ public abstract class SplitCard extends CardImpl {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZone(Zone zone, Game game) {
|
||||
super.setZone(zone, game);
|
||||
game.setZone(getLeftHalfCard().getId(), zone);
|
||||
game.setZone(getRightHalfCard().getId(), zone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Abilities<Ability> getAbilities() {
|
||||
Abilities<Ability> allAbilites = new AbilitiesImpl<>();
|
||||
|
|
@ -168,7 +186,5 @@ public abstract class SplitCard extends CardImpl {
|
|||
leftHalfCard.setOwnerId(ownerId);
|
||||
rightHalfCard.getAbilities().setControllerId(ownerId);
|
||||
rightHalfCard.setOwnerId(ownerId);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -691,6 +691,8 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
return false;
|
||||
}
|
||||
library.remove(card.getId(), game);
|
||||
// must return true all the time (some cards can be removed directly from library, see getLibrary().removeFromTop)
|
||||
// TODO: replace removeFromTop logic to normal with moveToZone
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -919,8 +921,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
|
||||
@Override
|
||||
public boolean removeFromGraveyard(Card card, Game game) {
|
||||
this.graveyard.remove(card);
|
||||
return true;
|
||||
return this.graveyard.remove(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue