[TMP] Implement Phyrexian Splicer (part of #5379)

This commit is contained in:
Oleg Agafonov 2024-09-20 15:29:06 +04:00
parent fb63fe0318
commit f55bc2c4fc
5 changed files with 291 additions and 2 deletions

View file

@ -25,12 +25,12 @@ public final class AkromaAngelOfWrath extends CardImpl {
this.power = new MageInt(6);
this.toughness = new MageInt(6);
// Flying, first strike, vigilance, trample, haste, protection from black and from red
this.addAbility(FlyingAbility.getInstance());
this.addAbility(FirstStrikeAbility.getInstance());
this.addAbility(VigilanceAbility.getInstance());
this.addAbility(TrampleAbility.getInstance());
this.addAbility(HasteAbility.getInstance());
// protection from black and from red
this.addAbility(ProtectionAbility.from(ObjectColor.BLACK, ObjectColor.RED));
}

View file

@ -0,0 +1,242 @@
package mage.cards.p;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.effects.common.continuous.LoseAbilityTargetEffect;
import mage.abilities.hint.Hint;
import mage.abilities.keyword.FirstStrikeAbility;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.ShadowAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.choices.Choice;
import mage.choices.ChoiceImpl;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.filter.predicate.other.AnotherTargetPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.stack.StackAbility;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author JayDi85
*/
public final class PhyrexianSplicer extends CardImpl {
static final FilterCreaturePermanent filterLose = new FilterCreaturePermanent("creature with the chosen ability");
private static final FilterCreaturePermanent filterGain = new FilterCreaturePermanent("another target creature");
static {
filterLose.add(Predicates.or(
new AbilityPredicate(FlyingAbility.class),
new AbilityPredicate(FirstStrikeAbility.class),
new AbilityPredicate(TrampleAbility.class),
new AbilityPredicate(ShadowAbility.class)
));
filterLose.add(new AnotherTargetPredicate(1));
filterGain.add(new AnotherTargetPredicate(2));
}
public PhyrexianSplicer(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}");
// {2}, {T}, Choose flying, first strike, trample, or shadow: Until end of turn, target creature with the chosen ability loses it and another target creature gains it.
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new PhyrexianSplicerEffect(), new ManaCostsImpl<>("{2}"));
ability.addCost(new TapSourceCost());
ability.addCost(new PhyrexianSplicerChooseCost());
ability.addTarget(new TargetPermanent(filterLose).withChooseHint("to lose ability").setTargetTag(1));
ability.addTarget(new TargetPermanent(filterGain).withChooseHint("to gain ability").setTargetTag(2));
ability.addHint(PhyrexianSplicerCardHint.instance);
this.addAbility(ability);
}
private PhyrexianSplicer(final PhyrexianSplicer card) {
super(card);
}
@Override
public PhyrexianSplicer copy() {
return new PhyrexianSplicer(this);
}
}
class PhyrexianSplicerEffect extends OneShotEffect {
PhyrexianSplicerEffect() {
super(Outcome.LoseAbility);
this.staticText = "Until end of turn, target creature with the chosen ability loses it and another target creature gains it.";
}
private PhyrexianSplicerEffect(final PhyrexianSplicerEffect effect) {
super(effect);
}
@Override
public PhyrexianSplicerEffect copy() {
return new PhyrexianSplicerEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Ability loseAbility = findChosenAbility(source);
if (loseAbility == null) {
return false;
}
// If the target which is having the ability removed does not have that ability during the resolution of this
// effect, then this effect still grants the chosen ability. The reason is that the second target is still
// legal even if the first one is not.
// (2004-10-04)
Permanent targetLose = game.getPermanent(source.getTargets().get(0).getFirstTarget());
Permanent targetGain = game.getPermanent(source.getTargets().get(1).getFirstTarget());
if (targetGain == null) {
return false;
}
// lose
if (targetLose != null) {
ContinuousEffect effect = new LoseAbilityTargetEffect(loseAbility, Duration.EndOfTurn);
effect.setTargetPointer(new FixedTarget(targetLose, game));
game.addEffect(effect, source);
}
// gain
ContinuousEffect effect = new GainAbilityTargetEffect(loseAbility, Duration.EndOfTurn);
effect.setTargetPointer(new FixedTarget(targetGain, game));
game.addEffect(effect, source);
return true;
}
static Ability findChosenAbility(Ability source) {
return CardUtil
.castStream(source.getCosts().stream(), PhyrexianSplicerChooseCost.class)
.map(PhyrexianSplicerChooseCost::getTargetedAbility)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
}
class PhyrexianSplicerChooseCost extends CostImpl {
private static final Map<String, Ability> allChoices = new LinkedHashMap<>();
static {
allChoices.put("Flying", FlyingAbility.getInstance());
allChoices.put("First Strike", FirstStrikeAbility.getInstance());
allChoices.put("Trample", TrampleAbility.getInstance());
allChoices.put("Shadow", ShadowAbility.getInstance());
}
Ability targetedAbility = null;
public PhyrexianSplicerChooseCost() {
this.text = "Choose flying, first strike, trample, or shadow";
}
private PhyrexianSplicerChooseCost(final PhyrexianSplicerChooseCost cost) {
super(cost);
this.targetedAbility = cost.targetedAbility == null ? null : cost.targetedAbility.copy();
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
this.paid = false;
this.targetedAbility = null;
Permanent losePermanent = game.getPermanent(source.getTargets().get(0).getFirstTarget());
Permanent gainPermanent = game.getPermanent(source.getTargets().get(1).getFirstTarget());
Player controller = game.getPlayer(source.getControllerId());
if (losePermanent == null || gainPermanent == null || controller == null) {
return false;
}
// choose ability to lose
Set<String> choices = allChoices.entrySet().stream()
.filter(entry -> losePermanent.hasAbility(entry.getValue(), game))
.map(Map.Entry::getKey)
.collect(Collectors.toCollection(LinkedHashSet::new));
Ability chosenAbility;
if (choices.size() == 1) {
chosenAbility = allChoices.getOrDefault(choices.stream().findFirst().orElse(null), null);
} else {
Choice choice = new ChoiceImpl(true);
choice.setMessage("Choose ability to remove from " + losePermanent.getLogName() + " to " + gainPermanent.getLogName());
choice.setChoices(choices);
controller.choose(Outcome.LoseAbility, choice, game);
chosenAbility = allChoices.getOrDefault(choice.getChoice(), null);
}
if (chosenAbility == null) {
return false;
}
// all fine
this.targetedAbility = chosenAbility;
paid = true;
// additional logs
game.informPlayers(controller.getLogName() + " chosen ability to lose and gain: "
+ CardUtil.getTextWithFirstCharUpperCase(chosenAbility.getRule()));
return true;
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
return true;
}
@Override
public PhyrexianSplicerChooseCost copy() {
return new PhyrexianSplicerChooseCost(this);
}
Ability getTargetedAbility() {
return this.targetedAbility;
}
}
enum PhyrexianSplicerCardHint implements Hint {
instance;
@Override
public String getText(Game game, Ability ability) {
// works on stack only
if (ability instanceof StackAbility) {
Ability loseAbility = PhyrexianSplicerEffect.findChosenAbility(((StackAbility) ability).getStackAbility());
if (loseAbility != null) {
return String.format("Chosen ability to lose and gain: " + CardUtil.getTextWithFirstCharUpperCase(loseAbility.getRule()));
}
}
return "";
}
@Override
public PhyrexianSplicerCardHint copy() {
return this;
}
}

View file

@ -232,6 +232,7 @@ public final class Tempest extends ExpansionSet {
cards.add(new SetCardInfo("Perish", 147, Rarity.UNCOMMON, mage.cards.p.Perish.class));
cards.add(new SetCardInfo("Phyrexian Grimoire", 301, Rarity.RARE, mage.cards.p.PhyrexianGrimoire.class));
cards.add(new SetCardInfo("Phyrexian Hulk", 302, Rarity.UNCOMMON, mage.cards.p.PhyrexianHulk.class));
cards.add(new SetCardInfo("Phyrexian Splicer", 303, Rarity.UNCOMMON, mage.cards.p.PhyrexianSplicer.class));
cards.add(new SetCardInfo("Pincher Beetles", 244, Rarity.COMMON, mage.cards.p.PincherBeetles.class));
cards.add(new SetCardInfo("Pine Barrens", 321, Rarity.RARE, mage.cards.p.PineBarrens.class));
cards.add(new SetCardInfo("Pit Imp", 148, Rarity.COMMON, mage.cards.p.PitImp.class));

View file

@ -0,0 +1,43 @@
package org.mage.test.cards.single.tmp;
import mage.abilities.keyword.TrampleAbility;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author JayDi85
*/
public class PhyrexianSplicerTest extends CardTestPlayerBase {
@Test
public void test_Normal() {
// {2}, {T}, Choose flying, first strike, trample, or shadow: Until end of turn, target creature with the
// chosen ability loses it and another target creature gains it.
addCard(Zone.BATTLEFIELD, playerA, "Phyrexian Splicer", 1); // {2}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
//
// Flying, first strike, vigilance, trample, haste, protection from black and from red
addCard(Zone.BATTLEFIELD, playerA, "Akroma, Angel of Wrath");
// Shadow
addCard(Zone.BATTLEFIELD, playerA, "Augur il-Vec");
checkAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akroma, Angel of Wrath", TrampleAbility.class, true);
checkAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Augur il-Vec", TrampleAbility.class, false);
// move trample from one to another
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}, Choose");
addTarget(playerA, "Akroma, Angel of Wrath"); // loose
addTarget(playerA, "Augur il-Vec"); // gain
setChoice(playerA, "Trample");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akroma, Angel of Wrath", TrampleAbility.class, false);
checkAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Augur il-Vec", TrampleAbility.class, true);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
}

View file

@ -1125,7 +1125,7 @@ public class TestPlayer implements Player {
}
private Permanent findPermanentWithAssert(PlayerAction action, Game game, Player player, String cardName) {
for (Permanent perm : game.getBattlefield().getAllPermanents()) {
for (Permanent perm : game.getBattlefield().getAllActivePermanents(player.getId())) {
// need by controller
if (!perm.getControllerId().equals(player.getId())) {
continue;
@ -1139,6 +1139,9 @@ public class TestPlayer implements Player {
// all fine
return perm;
}
printStart(game, "Permanents of " + player.getName());
printPermanents(game, game.getBattlefield().getAllActivePermanents(player.getId()), this);
printEnd();
Assert.fail(action.getActionName() + " - can't find permanent to check: " + cardName);
return null;
}