mirror of
https://github.com/magefree/mage.git
synced 2025-12-22 11:32:00 -08:00
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:
parent
6a33c68bb2
commit
595955a3cc
7 changed files with 280 additions and 1 deletions
50
Mage.Sets/src/mage/cards/s/SoulcoilViper.java
Normal file
50
Mage.Sets/src/mage/cards/s/SoulcoilViper.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Mage.Sets/src/mage/cards/u/UchbenbakTheGreatMistake.java
Normal file
52
Mage.Sets/src/mage/cards/u/UchbenbakTheGreatMistake.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue