Add finality counters (#11379)

* [LCI] Implement Soulcoil Viper

* add finality counter test

* fix bug, add extra test

* [LCI] Implement Uchbenbak, the Great Mistake
This commit is contained in:
Evan Kranzler 2023-11-01 22:08:57 -04:00 committed by GitHub
parent 6a33c68bb2
commit 595955a3cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 280 additions and 1 deletions

View file

@ -0,0 +1,50 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.ActivateAsSorceryActivatedAbility;
import mage.abilities.costs.common.SacrificeSourceCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldWithCounterTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
import mage.target.common.TargetCardInYourGraveyard;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class SoulcoilViper extends CardImpl {
public SoulcoilViper(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}");
this.subtype.add(SubType.SNAKE);
this.power = new MageInt(2);
this.toughness = new MageInt(3);
// {B}, {T}, Sacrifice Soulcoil Viper: Return target creature card from your graveyard to the battlefield with a finality counter on it. Activate only as a sorcery.
Ability ability = new ActivateAsSorceryActivatedAbility(
new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(CounterType.FINALITY.createInstance()), new ManaCostsImpl<>("{B}")
);
ability.addCost(new TapSourceCost());
ability.addCost(new SacrificeSourceCost());
ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD));
this.addAbility(ability);
}
private SoulcoilViper(final SoulcoilViper card) {
super(card);
}
@Override
public SoulcoilViper copy() {
return new SoulcoilViper(this);
}
}

View file

@ -0,0 +1,52 @@
package mage.cards.u;
import mage.MageInt;
import mage.abilities.condition.common.DescendCondition;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.decorator.ConditionalActivatedAbility;
import mage.abilities.effects.common.ReturnSourceFromGraveyardToBattlefieldWithCounterEffect;
import mage.abilities.keyword.MenaceAbility;
import mage.abilities.keyword.VigilanceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.counters.CounterType;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class UchbenbakTheGreatMistake extends CardImpl {
public UchbenbakTheGreatMistake(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.SKELETON);
this.subtype.add(SubType.HORROR);
this.power = new MageInt(6);
this.toughness = new MageInt(4);
// Vigilance
this.addAbility(VigilanceAbility.getInstance());
// Menace
this.addAbility(new MenaceAbility());
// Descend 8 -- {4}{U}{B}: Return Uchbenbak, the Great Mistake from your graveyard to the battlefield with a finality counter on it. Activate only if there are eight or more permanent cards in your graveyard and only as a sorcery.
this.addAbility(new ConditionalActivatedAbility(
Zone.GRAVEYARD, new ReturnSourceFromGraveyardToBattlefieldWithCounterEffect(CounterType.FINALITY.createInstance(), false),
new ManaCostsImpl<>("{4}{U}{B}"), DescendCondition.EIGHT
).setTiming(TimingRule.SORCERY).setAbilityWord(AbilityWord.DESCEND_8).addHint(DescendCondition.getHint()));
}
private UchbenbakTheGreatMistake(final UchbenbakTheGreatMistake card) {
super(card);
}
@Override
public UchbenbakTheGreatMistake copy() {
return new UchbenbakTheGreatMistake(this);
}
}

View file

@ -150,6 +150,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet {
cards.add(new SetCardInfo("Skullcap Snail", 119, Rarity.COMMON, mage.cards.s.SkullcapSnail.class)); cards.add(new SetCardInfo("Skullcap Snail", 119, Rarity.COMMON, mage.cards.s.SkullcapSnail.class));
cards.add(new SetCardInfo("Song of Stupefaction", 77, Rarity.COMMON, mage.cards.s.SongOfStupefaction.class)); cards.add(new SetCardInfo("Song of Stupefaction", 77, Rarity.COMMON, mage.cards.s.SongOfStupefaction.class));
cards.add(new SetCardInfo("Sorcerous Spyglass", 261, Rarity.UNCOMMON, mage.cards.s.SorcerousSpyglass.class)); cards.add(new SetCardInfo("Sorcerous Spyglass", 261, Rarity.UNCOMMON, mage.cards.s.SorcerousSpyglass.class));
cards.add(new SetCardInfo("Soulcoil Viper", 120, Rarity.UNCOMMON, mage.cards.s.SoulcoilViper.class));
cards.add(new SetCardInfo("Souls of the Lost", 121, Rarity.RARE, mage.cards.s.SoulsOfTheLost.class)); cards.add(new SetCardInfo("Souls of the Lost", 121, Rarity.RARE, mage.cards.s.SoulsOfTheLost.class));
cards.add(new SetCardInfo("Sovereign Okinec Ahau", 240, Rarity.MYTHIC, mage.cards.s.SovereignOkinecAhau.class)); cards.add(new SetCardInfo("Sovereign Okinec Ahau", 240, Rarity.MYTHIC, mage.cards.s.SovereignOkinecAhau.class));
cards.add(new SetCardInfo("Sovereign's Macuahuitl", 155, Rarity.COMMON, mage.cards.s.SovereignsMacuahuitl.class)); cards.add(new SetCardInfo("Sovereign's Macuahuitl", 155, Rarity.COMMON, mage.cards.s.SovereignsMacuahuitl.class));
@ -184,6 +185,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet {
cards.add(new SetCardInfo("Treasure Cove", 267, Rarity.RARE, mage.cards.t.TreasureCove.class)); cards.add(new SetCardInfo("Treasure Cove", 267, Rarity.RARE, mage.cards.t.TreasureCove.class));
cards.add(new SetCardInfo("Treasure Map", 267, Rarity.RARE, mage.cards.t.TreasureMap.class)); cards.add(new SetCardInfo("Treasure Map", 267, Rarity.RARE, mage.cards.t.TreasureMap.class));
cards.add(new SetCardInfo("Trumpeting Carnosaur", 324, Rarity.RARE, mage.cards.t.TrumpetingCarnosaur.class)); cards.add(new SetCardInfo("Trumpeting Carnosaur", 324, Rarity.RARE, mage.cards.t.TrumpetingCarnosaur.class));
cards.add(new SetCardInfo("Uchbenbak, the Great Mistake", 242, Rarity.UNCOMMON, mage.cards.u.UchbenbakTheGreatMistake.class));
cards.add(new SetCardInfo("Vanguard of the Rose", 42, Rarity.UNCOMMON, mage.cards.v.VanguardOfTheRose.class)); cards.add(new SetCardInfo("Vanguard of the Rose", 42, Rarity.UNCOMMON, mage.cards.v.VanguardOfTheRose.class));
cards.add(new SetCardInfo("Vito, Fanatic of Aclazotz", 243, Rarity.MYTHIC, mage.cards.v.VitoFanaticOfAclazotz.class)); cards.add(new SetCardInfo("Vito, Fanatic of Aclazotz", 243, Rarity.MYTHIC, mage.cards.v.VitoFanaticOfAclazotz.class));
cards.add(new SetCardInfo("Waterlogged Hulk", 83, Rarity.UNCOMMON, mage.cards.w.WaterloggedHulk.class)); cards.add(new SetCardInfo("Waterlogged Hulk", 83, Rarity.UNCOMMON, mage.cards.w.WaterloggedHulk.class));

View file

@ -0,0 +1,113 @@
package org.mage.test.cards.replacement;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author TheElk801
*/
public class FinalityCounterTest extends CardTestPlayerBase {
private static final String viper = "Soulcoil Viper";
private static final String corpse = "Walking Corpse";
@Test
public void testCounterAdded() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp");
addCard(Zone.BATTLEFIELD, playerA, viper);
addCard(Zone.GRAVEYARD, playerA, corpse);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{B},", corpse);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, viper, 0);
assertGraveyardCount(playerA, viper, 1);
assertGraveyardCount(playerA, corpse, 0);
assertCounterCount(playerA, corpse, CounterType.FINALITY, 1);
}
private static final String murder = "Murder";
@Test
public void testCounterApplies() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1 + 3);
addCard(Zone.BATTLEFIELD, playerA, viper);
addCard(Zone.HAND, playerA, murder);
addCard(Zone.GRAVEYARD, playerA, corpse);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{B},", corpse);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, corpse);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, viper, 0);
assertGraveyardCount(playerA, viper, 1);
assertPermanentCount(playerA, corpse, 0);
assertGraveyardCount(playerA, corpse, 0);
assertExileCount(playerA, corpse, 1);
}
private static final String hexmage = "Vampire Hexmage";
@Test
public void testCounterRemoved() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1 + 3);
addCard(Zone.BATTLEFIELD, playerA, viper);
addCard(Zone.BATTLEFIELD, playerA, hexmage);
addCard(Zone.HAND, playerA, murder);
addCard(Zone.GRAVEYARD, playerA, corpse);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{B},", corpse);
activateAbility(1, PhaseStep.BEGIN_COMBAT, playerA, "Sacrifice", corpse);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, murder, corpse);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, viper, 0);
assertGraveyardCount(playerA, viper, 1);
assertPermanentCount(playerA, corpse, 0);
assertGraveyardCount(playerA, corpse, 1);
assertExileCount(playerA, corpse, 0);
}
private static final String unsummon = "Unsummon";
@Test
public void testCounterDontApply() {
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 1 + 1);
addCard(Zone.BATTLEFIELD, playerA, viper);
addCard(Zone.HAND, playerA, unsummon);
addCard(Zone.GRAVEYARD, playerA, corpse);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{B},", corpse);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, unsummon, corpse);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertPermanentCount(playerA, viper, 0);
assertGraveyardCount(playerA, viper, 1);
assertPermanentCount(playerA, corpse, 0);
assertHandCount(playerA, corpse, 1);
assertExileCount(playerA, corpse, 0);
}
}

View file

@ -0,0 +1,57 @@
package mage.abilities.effects.keyword;
import mage.abilities.Ability;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
*/
public class FinalityCounterEffect extends ReplacementEffectImpl {
public FinalityCounterEffect() {
super(Duration.Custom, Outcome.Tap);
this.staticText = "If a creature with a finality counter on it would die, exile it instead.";
}
private FinalityCounterEffect(final FinalityCounterEffect effect) {
super(effect);
}
@Override
public FinalityCounterEffect copy() {
return new FinalityCounterEffect(this);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
((ZoneChangeEvent) event).setToZone(Zone.EXILED);
return false;
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
if (!((ZoneChangeEvent) event).isDiesEvent()) {
return false;
}
Permanent permanent = game.getPermanent(event.getTargetId());
return permanent != null && permanent.getCounters(game).getCount(CounterType.FINALITY) > 0;
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
}

View file

@ -73,6 +73,7 @@ public enum CounterType {
FEATHER("feather"), FEATHER("feather"),
FETCH("fetch"), FETCH("fetch"),
FILIBUSTER("filibuster"), FILIBUSTER("filibuster"),
FINALITY("finality"),
FIRST_STRIKE("first strike"), FIRST_STRIKE("first strike"),
FLAME("flame"), FLAME("flame"),
FLOOD("flood"), FLOOD("flood"),

View file

@ -14,6 +14,7 @@ import mage.abilities.effects.Effect;
import mage.abilities.effects.PreventionEffectData; import mage.abilities.effects.PreventionEffectData;
import mage.abilities.effects.common.CopyEffect; import mage.abilities.effects.common.CopyEffect;
import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.keyword.FinalityCounterEffect;
import mage.abilities.effects.keyword.ShieldCounterEffect; import mage.abilities.effects.keyword.ShieldCounterEffect;
import mage.abilities.effects.keyword.StunCounterEffect; import mage.abilities.effects.keyword.StunCounterEffect;
import mage.abilities.keyword.*; import mage.abilities.keyword.*;
@ -1182,6 +1183,9 @@ public abstract class GameImpl implements Game {
// Apply stun counter mechanic // Apply stun counter mechanic
state.addAbility(new SimpleStaticAbility(Zone.ALL, new StunCounterEffect()), null); state.addAbility(new SimpleStaticAbility(Zone.ALL, new StunCounterEffect()), null);
// Apply finality counter mechanic
state.addAbility(new SimpleStaticAbility(Zone.ALL, new FinalityCounterEffect()), null);
// Handle companions // Handle companions
Map<Player, Card> playerCompanionMap = new HashMap<>(); Map<Player, Card> playerCompanionMap = new HashMap<>();
for (Player player : state.getPlayers().values()) { for (Player player : state.getPlayers().values()) {
@ -1993,7 +1997,7 @@ public abstract class GameImpl implements Game {
} }
if (copyFromPermanent.isPrototyped()) { if (copyFromPermanent.isPrototyped()) {
Abilities<Ability> abilities = copyFromPermanent.getAbilities(); Abilities<Ability> abilities = copyFromPermanent.getAbilities();
for (Ability ability : abilities){ for (Ability ability : abilities) {
if (ability instanceof PrototypeAbility) { if (ability instanceof PrototypeAbility) {
((PrototypeAbility) ability).prototypePermanent(newBluePrint, this); ((PrototypeAbility) ability).prototypePermanent(newBluePrint, this);
} }