forked from External/mage
Implement Prototype ability (#11249)
Prototype is a SpellAbilityType, for which alternate costs are permitted. It has a continuous effect that applies on the battlefield as well as a method to modify the spell on the stack. Permanents have an isPrototyped flag that copy effects can check explicitly (same brittle method as transformed permanents use; reworking copy effects to streamline them is a separate scope). Many test cases have been added to confirm functionality (thanks to Zerris for additional test suggestions). --------- Co-authored-by: Susucre <34709007+Susucre@users.noreply.github.com> Co-authored-by: Evan Kranzler <theelk801@gmail.com> Co-authored-by: xenohedron <xenohedron@users.noreply.github.com>
This commit is contained in:
parent
ac20483b73
commit
5e095afdb0
23 changed files with 946 additions and 17 deletions
|
|
@ -1978,6 +1978,14 @@ public abstract class GameImpl implements Game {
|
|||
if (copyFromPermanent.isTransformed()) {
|
||||
TransformAbility.transformPermanent(newBluePrint, newBluePrint.getSecondCardFace(), this, source);
|
||||
}
|
||||
if (copyFromPermanent.isPrototyped()) {
|
||||
Abilities<Ability> abilities = copyFromPermanent.getAbilities();
|
||||
for (Ability ability : abilities){
|
||||
if (ability instanceof PrototypeAbility) {
|
||||
((PrototypeAbility) ability).prototypePermanent(newBluePrint, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (applier != null) {
|
||||
applier.apply(this, newBluePrint, source, copyToPermanentId);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package mage.game;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.keyword.TransformAbility;
|
||||
import mage.cards.*;
|
||||
import mage.constants.Outcome;
|
||||
|
|
@ -361,8 +362,16 @@ public final class ZonesHandler {
|
|||
// put onto battlefield with possible counters
|
||||
game.getPermanentsEntering().put(permanent.getId(), permanent);
|
||||
card.checkForCountersToAdd(permanent, source, game);
|
||||
|
||||
permanent.setTapped(info instanceof ZoneChangeInfo.Battlefield
|
||||
&& ((ZoneChangeInfo.Battlefield) info).tapped);
|
||||
|
||||
if (Zone.STACK == event.getFromZone()) {
|
||||
Spell spell = game.getStack().getSpell(event.getTargetId());
|
||||
if (spell != null) {
|
||||
permanent.setPrototyped(spell.isPrototyped());
|
||||
}
|
||||
}
|
||||
|
||||
permanent.setFaceDown(info.faceDown, game);
|
||||
if (info.faceDown) {
|
||||
|
|
|
|||
|
|
@ -238,6 +238,11 @@ public class Commander extends CommandObjectImpl {
|
|||
return sourceObject.getManaCost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setManaCost(ManaCosts<ManaCost> costs) {
|
||||
throw new UnsupportedOperationException("Unsupported operation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getManaValue() {
|
||||
return sourceObject.getManaValue();
|
||||
|
|
|
|||
|
|
@ -255,6 +255,11 @@ public class Dungeon extends CommandObjectImpl {
|
|||
return emptyCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setManaCost(ManaCosts<ManaCost> costs) {
|
||||
throw new UnsupportedOperationException("Unsupported operation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getManaValue() {
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -170,6 +170,11 @@ public abstract class Emblem extends CommandObjectImpl {
|
|||
return emptyCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setManaCost(ManaCosts<ManaCost> costs) {
|
||||
throw new UnsupportedOperationException("Unsupported operation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getManaValue() {
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -195,6 +195,11 @@ public abstract class Plane extends CommandObjectImpl {
|
|||
return emptyCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setManaCost(ManaCosts<ManaCost> costs) {
|
||||
throw new UnsupportedOperationException("Unsupported operation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getManaValue() {
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ public interface Permanent extends Card, Controllable {
|
|||
|
||||
void setRenowned(boolean value);
|
||||
|
||||
boolean isPrototyped();
|
||||
|
||||
void setPrototyped(boolean value);
|
||||
|
||||
int getClassLevel();
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
protected Map<String, String> info = new LinkedHashMap<>(); // additional info for permanent's rules
|
||||
protected int createOrder;
|
||||
protected boolean legendRuleApplies = true;
|
||||
protected boolean prototyped;
|
||||
|
||||
private static final List<UUID> emptyList = Collections.unmodifiableList(new ArrayList<>());
|
||||
|
||||
|
|
@ -179,6 +180,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
this.morphed = permanent.morphed;
|
||||
this.manifested = permanent.manifested;
|
||||
this.createOrder = permanent.createOrder;
|
||||
this.prototyped = permanent.prototyped;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1616,6 +1618,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
return this.monstrous;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPrototyped() {
|
||||
return this.prototyped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMonstrous(boolean value) {
|
||||
this.monstrous = value;
|
||||
|
|
@ -1829,6 +1836,10 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
this.secondSideCard = card;
|
||||
}
|
||||
|
||||
public void setPrototyped(boolean prototyped) {
|
||||
this.prototyped = prototyped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRingBearer() {
|
||||
return ringBearerFlag;
|
||||
|
|
|
|||
|
|
@ -331,6 +331,11 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
|
|||
allAddedTokens.add((PermanentToken) permanent);
|
||||
}
|
||||
|
||||
// prototyped spell tokens make prototyped permanent tokens on resolution.
|
||||
if (source instanceof SpellAbility && ((SpellAbility) source).getSpellAbilityCastMode() == SpellAbilityCastMode.PROTOTYPE) {
|
||||
permanent.setPrototyped(true);
|
||||
}
|
||||
|
||||
// if token was created (not a spell copy) handle auras coming into the battlefield
|
||||
// that must determine what to enchant
|
||||
// see #9583 for the root cause issue of why this convoluted searching is necessary
|
||||
|
|
@ -345,6 +350,7 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
|
|||
if (!(ability instanceof SpellAbility)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auraOutcome = ability.getEffects().getOutcome(ability);
|
||||
for (Effect effect : ability.getEffects()) {
|
||||
if (!(effect instanceof AttachEffect)) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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.keyword.PrototypeAbility;
|
||||
import mage.abilities.keyword.TransformAbility;
|
||||
import mage.cards.*;
|
||||
import mage.constants.*;
|
||||
|
|
@ -46,6 +47,7 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
private final List<SpellAbility> spellAbilities = new ArrayList<>();
|
||||
|
||||
private final Card card;
|
||||
private ManaCosts<ManaCost> manaCost;
|
||||
private final ObjectColor color;
|
||||
private final ObjectColor frameColor;
|
||||
private final FrameStyle frameStyle;
|
||||
|
|
@ -62,6 +64,7 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
private boolean resolving = false;
|
||||
private UUID commandedByPlayerId = null; // controller of the spell resolve, example: Word of Command
|
||||
private String commandedByInfo; // info about spell commanded, e.g. source
|
||||
private boolean prototyped;
|
||||
private int startingLoyalty;
|
||||
private int startingDefense;
|
||||
|
||||
|
|
@ -79,8 +82,13 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
// simulate another side as new card (another code part in continues effect from disturb ability)
|
||||
affectedCard = TransformAbility.transformCardSpellStatic(card, card.getSecondCardFace(), game);
|
||||
}
|
||||
if (ability instanceof PrototypeAbility){
|
||||
affectedCard = ((PrototypeAbility)ability).prototypeCardSpell(card);
|
||||
this.prototyped = true;
|
||||
}
|
||||
|
||||
this.card = affectedCard;
|
||||
this.manaCost = this.card.getManaCost().copy();
|
||||
this.color = affectedCard.getColor(null).copy();
|
||||
this.frameColor = affectedCard.getFrameColor(null).copy();
|
||||
this.frameStyle = affectedCard.getFrameStyle();
|
||||
|
|
@ -109,6 +117,7 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
} else {
|
||||
spellAbilities.add(ability);
|
||||
}
|
||||
|
||||
this.controllerId = controllerId;
|
||||
this.fromZone = fromZone;
|
||||
this.countered = false;
|
||||
|
|
@ -128,6 +137,7 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
this.card = spell.card.copy();
|
||||
|
||||
this.fromZone = spell.fromZone;
|
||||
this.manaCost = spell.getManaCost().copy();
|
||||
this.color = spell.color.copy();
|
||||
this.frameColor = spell.color.copy();
|
||||
this.frameStyle = spell.frameStyle;
|
||||
|
|
@ -143,6 +153,7 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
|
||||
this.currentActivatingManaAbilitiesStep = spell.currentActivatingManaAbilitiesStep;
|
||||
this.targetChanged = spell.targetChanged;
|
||||
this.prototyped = spell.prototyped;
|
||||
this.startingLoyalty = spell.startingLoyalty;
|
||||
this.startingDefense = spell.startingDefense;
|
||||
}
|
||||
|
|
@ -632,9 +643,12 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
|
||||
@Override
|
||||
public ManaCosts<ManaCost> getManaCost() {
|
||||
return card.getManaCost();
|
||||
return this.manaCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setManaCost(ManaCosts<ManaCost> costs) { this.manaCost = costs.copy(); }
|
||||
|
||||
/**
|
||||
* 202.3b When calculating the converted mana cost of an object with an {X}
|
||||
* in its mana cost, X is treated as 0 while the object is not on the stack,
|
||||
|
|
@ -652,7 +666,7 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
for (SpellAbility spellAbility : spellAbilities) {
|
||||
cmc += spellAbility.getConvertedXManaCost(getCard());
|
||||
}
|
||||
cmc += getCard().getManaCost().manaValue();
|
||||
cmc += this.manaCost.manaValue();
|
||||
return cmc;
|
||||
}
|
||||
|
||||
|
|
@ -789,6 +803,10 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
return false;
|
||||
}
|
||||
|
||||
public boolean isPrototyped() {
|
||||
return prototyped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spell copy() {
|
||||
return new Spell(this);
|
||||
|
|
|
|||
|
|
@ -246,6 +246,11 @@ public class StackAbility extends StackObjectImpl implements Ability {
|
|||
return emptyCost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setManaCost(ManaCosts<ManaCost> costs) {
|
||||
throw new UnsupportedOperationException("Unsupported operation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getManaCostSymbols() {
|
||||
return super.getManaCostSymbols();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue