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:
ssk97 2023-10-09 18:06:19 -07:00 committed by GitHub
parent ac20483b73
commit 5e095afdb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 946 additions and 17 deletions

View file

@ -442,6 +442,9 @@ public abstract class AbilityImpl implements Ability {
// mandatory additional costs the spell has, such as that of Tormenting Voice. (2018-12-07)
canUseAdditionalCost = true;
break;
case PROTOTYPE:
// Notably, casting a spell as a prototype does not count as paying an alternative cost.
// https://magic.wizards.com/en/news/feature/comprehensive-rules-changes
case NORMAL:
canUseAlternativeCost = true;
canUseAdditionalCost = true;

View file

@ -322,7 +322,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
}
if (spellCharacteristics != null) {
if (getSpellAbilityCastMode() != SpellAbilityCastMode.NORMAL) {
spellCharacteristics = getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(spellCharacteristics, game);
spellCharacteristics = getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(spellCharacteristics, this);
}
}
return spellCharacteristics;

View file

@ -146,6 +146,7 @@ public class CopyEffect extends ContinuousEffectImpl {
//permanent.setSecondCardFace(targetPermanent.getSecondCardFace());
permanent.setFlipCard(targetPermanent.isFlipCard());
permanent.setFlipCardName(targetPermanent.getFlipCardName());
permanent.setPrototyped(targetPermanent.isPrototyped());
}
CardUtil.copySetAndCardNumber(permanent, copyFromObject);

View file

@ -1,21 +1,48 @@
package mage.abilities.keyword;
import mage.MageObject;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.cards.Card;
import mage.constants.*;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
* @author TheElk801, Susucr, notgreat
*/
public class PrototypeAbility extends SpellAbility {
private final int power;
private final int toughness;
private final String manaString;
private final String rule;
public PrototypeAbility(Card card, String manaString, int power, int toughness) {
super(new ManaCostsImpl<>(manaString), card.getName());
// TODO: implement this
this.setSpellAbilityCastMode(SpellAbilityCastMode.PROTOTYPE);
this.setTiming(TimingRule.SORCERY);
this.addSubAbility(new SimpleStaticAbility(
Zone.BATTLEFIELD, new PrototypeEffect(power, toughness, manaString)
).setRuleVisible(false));
this.rule = "Prototype " + manaString + " &mdash; " + power + "/" + toughness +
" <i>(You may cast this spell with different mana cost, color, and size. It keeps its abilities and types.)</i>";
setRuleAtTheTop(true);
this.power = power;
this.toughness = toughness;
this.manaString = manaString;
}
private PrototypeAbility(final PrototypeAbility ability) {
super(ability);
this.rule = ability.rule;
this.power = ability.power;
this.toughness = ability.toughness;
this.manaString = ability.manaString;
}
@Override
@ -25,6 +52,68 @@ public class PrototypeAbility extends SpellAbility {
@Override
public String getRule() {
return "Prototype";
return rule;
}
//based on TransformAbility
public Card prototypeCardSpell(Card original) {
Card newCard = original.copy();
newCard.setManaCost(new ManaCostsImpl<>(manaString));
newCard.getPower().setModifiedBaseValue(power);
newCard.getToughness().setModifiedBaseValue(toughness);
newCard.getColor().setColor(new ObjectColor(manaString));
return newCard;
}
public void prototypePermanent(MageObject targetObject, Game game) {
if (targetObject instanceof Permanent) {
((Permanent)targetObject).setPrototyped(true);
}
targetObject.getColor(game).setColor(new ObjectColor(manaString));
targetObject.setManaCost(new ManaCostsImpl<>(manaString));
targetObject.getPower().setModifiedBaseValue(power);
targetObject.getToughness().setModifiedBaseValue(toughness);
}
}
class PrototypeEffect extends ContinuousEffectImpl {
private final int power;
private final int toughness;
private final String manaString;
private final ObjectColor color;
PrototypeEffect(int power, int toughness, String manaString) {
super(Duration.EndOfGame, Layer.CopyEffects_1, SubLayer.CopyEffects_1a, Outcome.Benefit);
this.power = power;
this.toughness = toughness;
this.manaString = manaString;
this.color = new ObjectColor(manaString);
}
private PrototypeEffect(final PrototypeEffect effect) {
super(effect);
this.power = effect.power;
this.toughness = effect.toughness;
this.manaString = effect.manaString;
this.color = effect.color;
}
@Override
public PrototypeEffect copy() {
return new PrototypeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent == null || !permanent.isPrototyped()) {
return false;
}
permanent.setManaCost(new ManaCostsImpl<>(manaString));
permanent.getColor(game).setColor(color);
permanent.getPower().setModifiedBaseValue(power);
permanent.getToughness().setModifiedBaseValue(toughness);
return true;
}
}