Support for copying permanent spells (WIP, do not merge) (#7084)

* added initial support for permanent tokens

* [ZNR] Implemented Lithoform Engine

* [ZNR] Implemented Verazol, the Split Current

* permanent spell tokens no longer count as created

* small change to token generation

* added test, currently incomplete

* found a potential solution for kicker issue, possibly too much of a hack

* fixed a test failure

* reversed hack changes

* skipped failing tests

* added more tests
This commit is contained in:
Evan Kranzler 2020-09-27 10:54:44 -04:00 committed by GitHub
parent f32597b164
commit 7647a3d8f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 531 additions and 29 deletions

View file

@ -19,6 +19,7 @@ public class CopyTargetSpellEffect extends OneShotEffect {
private final boolean useController;
private final boolean useLKI;
private String copyThatSpellName = "that spell";
private final boolean chooseTargets;
public CopyTargetSpellEffect() {
this(false);
@ -29,9 +30,14 @@ public class CopyTargetSpellEffect extends OneShotEffect {
}
public CopyTargetSpellEffect(boolean useController, boolean useLKI) {
this(useController, useLKI, true);
}
public CopyTargetSpellEffect(boolean useController, boolean useLKI, boolean chooseTargets) {
super(Outcome.Copy);
this.useController = useController;
this.useLKI = useLKI;
this.chooseTargets = chooseTargets;
}
public CopyTargetSpellEffect(final CopyTargetSpellEffect effect) {
@ -39,6 +45,7 @@ public class CopyTargetSpellEffect extends OneShotEffect {
this.useLKI = effect.useLKI;
this.useController = effect.useController;
this.copyThatSpellName = effect.copyThatSpellName;
this.chooseTargets = effect.chooseTargets;
}
public Effect withSpellName(String copyThatSpellName) {
@ -58,7 +65,7 @@ public class CopyTargetSpellEffect extends OneShotEffect {
spell = (Spell) game.getLastKnownInformation(targetPointer.getFirst(game, source), Zone.STACK);
}
if (spell != null) {
StackObject newStackObject = spell.createCopyOnStack(game, source, useController ? spell.getControllerId() : source.getControllerId(), true);
StackObject newStackObject = spell.createCopyOnStack(game, source, useController ? spell.getControllerId() : source.getControllerId(), chooseTargets);
Player player = game.getPlayer(source.getControllerId());
if (player != null && newStackObject instanceof Spell) {
String activateMessage = ((Spell) newStackObject).getActivatedMessage(game);
@ -91,8 +98,9 @@ public class CopyTargetSpellEffect extends OneShotEffect {
} else {
sb.append(copyThatSpellName);
}
sb.append(". You may choose new targets for the copy");
if (chooseTargets) {
sb.append(". You may choose new targets for the copy");
}
return sb.toString();
}
}

View file

@ -247,7 +247,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
sb.append(' ').append(remarkText);
}
return sb.toString().replace(" .",".");
return sb.toString().replace(" .", ".");
}
@Override

View file

@ -532,7 +532,6 @@ public abstract class GameImpl implements Game, Serializable {
if (card == null) {
card = (Card) state.getValue(GameState.COPIED_FROM_CARD_KEY + cardId.toString());
}
return card;
}

View file

@ -5,11 +5,11 @@ import mage.MageObject;
import mage.abilities.Ability;
import mage.cards.Card;
import mage.game.Game;
import java.util.List;
import java.util.UUID;
/**
*
* @author ArcadeMode
*/
public interface Token extends MageObject {
@ -33,6 +33,8 @@ public interface Token extends MageObject {
boolean putOntoBattlefield(int amount, Game game, UUID sourceId, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer);
boolean putOntoBattlefield(int amount, Game game, UUID sourceId, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, boolean created);
void setPower(int power);
void setToughness(int toughness);

View file

@ -1,9 +1,5 @@
package mage.game.permanent.token;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import mage.MageObject;
import mage.MageObjectImpl;
import mage.abilities.Ability;
@ -18,6 +14,11 @@ import mage.game.permanent.PermanentToken;
import mage.players.Player;
import mage.util.RandomUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
public abstract class TokenImpl extends MageObjectImpl implements Token {
protected String description;
@ -158,6 +159,11 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
@Override
public boolean putOntoBattlefield(int amount, Game game, UUID sourceId, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer) {
return putOntoBattlefield(amount, game, sourceId, controllerId, tapped, attacking, attackedPlayer, true);
}
@Override
public boolean putOntoBattlefield(int amount, Game game, UUID sourceId, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, boolean created) {
Player controller = game.getPlayer(controllerId);
if (controller == null) {
return false;
@ -168,14 +174,14 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
lastAddedTokenIds.clear();
CreateTokenEvent event = new CreateTokenEvent(sourceId, controllerId, amount, this);
if (!game.replaceEvent(event)) {
putOntoBattlefieldHelper(event, game, tapped, attacking, attackedPlayer);
if (!created || !game.replaceEvent(event)) {
putOntoBattlefieldHelper(event, game, tapped, attacking, attackedPlayer, created);
return true;
}
return false;
}
private static void putOntoBattlefieldHelper(CreateTokenEvent event, Game game, boolean tapped, boolean attacking, UUID attackedPlayer) {
private static void putOntoBattlefieldHelper(CreateTokenEvent event, Game game, boolean tapped, boolean attacking, UUID attackedPlayer, boolean created) {
Player controller = game.getPlayer(event.getPlayerId());
Token token = event.getToken();
int amount = event.getAmount();
@ -212,14 +218,16 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
((TokenImpl) token).lastAddedTokenId = permanent.getId();
}
game.addSimultaneousEvent(new ZoneChangeEvent(permanent, permanent.getControllerId(), Zone.OUTSIDE, Zone.BATTLEFIELD));
if (permanent instanceof PermanentToken) {
if (permanent instanceof PermanentToken && created) {
game.addSimultaneousEvent(new CreatedTokenEvent(event.getSourceId(), (PermanentToken) permanent));
}
if (attacking && game.getCombat() != null && game.getActivePlayerId().equals(permanent.getControllerId())) {
game.getCombat().addAttackingCreature(permanent.getId(), game, attackedPlayer);
}
if (!game.isSimulation()) {
if (created) {
game.informPlayers(controller.getLogName() + " creates a " + permanent.getLogName() + " token");
} else {
game.informPlayers(permanent.getLogName() + " enters the battlefield as a token under " + controller.getLogName() + "'s control'");
}
}

View file

@ -9,10 +9,10 @@ import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.SpellAbility;
import mage.abilities.costs.AlternativeSourceCosts;
import mage.abilities.costs.Cost;
import mage.abilities.costs.mana.ActivationManaAbilityStep;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.keyword.BestowAbility;
import mage.abilities.keyword.MorphAbility;
import mage.abilities.text.TextPart;
import mage.cards.*;
@ -27,7 +27,9 @@ import mage.game.events.GameEvent.EventType;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.token.EmptyToken;
import mage.players.Player;
import mage.util.CardUtil;
import mage.util.GameLog;
import mage.util.SubTypeList;
@ -248,11 +250,25 @@ public class Spell extends StackObjImpl implements Card {
card.getSubtype(game).add(SubType.AURA);
}
}
if (controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null)) {
UUID permId = null;
boolean flag = false;
if (!isCopy()) {
permId = card.getId();
flag = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
} else {
EmptyToken token = new EmptyToken();
CardUtil.copyTo(token).from(card);
// The token that a resolving copy of a spell becomes isnt said to have been created. (2020-09-25)
if (token.putOntoBattlefield(1, game, ability.getSourceId(), getControllerId(), false, false, null, false)) {
permId = token.getLastAddedToken();
flag = true;
}
}
if (flag) {
if (bestow) {
// card will be copied during putOntoBattlefield, so the card of CardPermanent has to be changed
// TODO: Find a better way to prevent bestow creatures from being effected by creature affecting abilities
Permanent permanent = game.getPermanent(card.getId());
Permanent permanent = game.getPermanent(permId);
if (permanent instanceof PermanentCard) {
permanent.setSpellAbility(ability); // otherwise spell ability without bestow will be set
if (!card.getCardType().contains(CardType.CREATURE)) {
@ -261,6 +277,20 @@ public class Spell extends StackObjImpl implements Card {
card.getSubtype(game).remove(SubType.AURA);
}
}
if (isCopy()) {
Permanent token = game.getPermanent(permId);
if (token == null) {
return false;
}
for (Ability ability2 : token.getAbilities()) {
if (!bestow || ability2 instanceof BestowAbility) {
ability2.getTargets().get(0).add(ability.getFirstTarget(), game);
ability2.getEffects().get(0).apply(game, ability2);
return ability2.resolve(game);
}
}
return false;
}
return ability.resolve(game);
}
if (bestow) {
@ -287,24 +317,27 @@ public class Spell extends StackObjImpl implements Card {
counter(null, game);
return false;
}
} else if (isCopy()) {
EmptyToken token = new EmptyToken();
CardUtil.copyTo(token).from(card);
// The token that a resolving copy of a spell becomes isnt said to have been created. (2020-09-25)
token.putOntoBattlefield(1, game, ability.getSourceId(), getControllerId(), false, false, null, false);
return true;
} else {
return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
}
}
private boolean hasTargets(SpellAbility spellAbility, Game game) {
if (spellAbility.getModes().getSelectedModes().size() > 1) {
for (UUID modeId : spellAbility.getModes().getSelectedModes()) {
Mode mode = spellAbility.getModes().get(modeId);
if (!mode.getTargets().isEmpty()) {
return true;
}
}
return false;
} else {
if (spellAbility.getModes().getSelectedModes().size() < 2) {
return !spellAbility.getTargets().isEmpty();
}
for (UUID modeId : spellAbility.getModes().getSelectedModes()) {
if (!spellAbility.getModes().get(modeId).getTargets().isEmpty()) {
return true;
}
}
return false;
}
/**