From d745141b7bf09fbfe5f3cd92ece615af80e7d988 Mon Sep 17 00:00:00 2001 From: Daniel Bomar Date: Mon, 18 Apr 2022 21:04:51 -0500 Subject: [PATCH] (WIP) [SNC] Implemented Ob Nixilis, the Adversary (#8838) * [SNC] Implemented Ob Nixilis, the Adversary * Allow starting loyalty to be changed on the stack Co-authored-by: Evan Kranzler --- .../mage/cards/o/ObNixilisTheAdversary.java | 225 ++++++++++++++++++ .../src/mage/sets/StreetsOfNewCapenna.java | 1 + Mage/src/main/java/mage/game/stack/Spell.java | 6 +- .../util/functions/CopyTokenFunction.java | 2 + 4 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/o/ObNixilisTheAdversary.java diff --git a/Mage.Sets/src/mage/cards/o/ObNixilisTheAdversary.java b/Mage.Sets/src/mage/cards/o/ObNixilisTheAdversary.java new file mode 100644 index 0000000000..967d15906f --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/ObNixilisTheAdversary.java @@ -0,0 +1,225 @@ +package mage.cards.o; + +import java.util.List; +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.StaticAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DrawCardTargetEffect; +import mage.abilities.effects.common.InfoEffect; +import mage.abilities.effects.common.LoseLifeTargetEffect; +import mage.cards.Card; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.StaticFilters; +import mage.filter.predicate.mageobject.MageObjectReferencePredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.DevilToken; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; +import mage.players.Player; +import mage.target.TargetPlayer; +import mage.target.common.TargetControlledPermanent; +import mage.util.functions.StackObjectCopyApplier; + +/** + * + * @author weirddan455 + */ +public final class ObNixilisTheAdversary extends CardImpl { + + public ObNixilisTheAdversary(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{1}{B}{R}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.NIXILIS); + this.setStartingLoyalty(3); + + // Casualty X. The copy isn't legendary and has starting loyalty X. + this.addAbility(new ObNixilisTheAdversaryCasualtyAbility(this)); + + // +1: Each opponent loses 2 life unless they discard a card. If you control a Demon or Devil, you gain 2 life. + this.addAbility(new LoyaltyAbility(new ObNixilisTheAdversaryDiscardEffect(), 1)); + + // −2: Create a 1/1 red Devil creature token with "When this creature dies, it deals 1 damage to any target." + this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new DevilToken()), -2)); + + // −7: Target player draws seven cards and loses 7 life. + Ability ability = new LoyaltyAbility(new DrawCardTargetEffect(7), -7); + ability.addEffect(new LoseLifeTargetEffect(7).setText("and loses 7 life")); + ability.addTarget(new TargetPlayer()); + this.addAbility(ability); + } + + private ObNixilisTheAdversary(final ObNixilisTheAdversary card) { + super(card); + } + + @Override + public ObNixilisTheAdversary copy() { + return new ObNixilisTheAdversary(this); + } +} + +class ObNixilisTheAdversaryCasualtyAbility extends StaticAbility { + + public ObNixilisTheAdversaryCasualtyAbility(Card card) { + super(Zone.ALL, new InfoEffect( + "Casualty X. (As you cast this spell, " + + "you may sacrifice a creature with power X. " + + "When you do, copy this spell. The copy becomes a token.)" + )); + card.getSpellAbility().addCost(new ObNixilisTheAdversaryCost()); + this.setRuleAtTheTop(true); + } + + private ObNixilisTheAdversaryCasualtyAbility(final ObNixilisTheAdversaryCasualtyAbility ability) { + super(ability); + } + + @Override + public ObNixilisTheAdversaryCasualtyAbility copy() { + return new ObNixilisTheAdversaryCasualtyAbility(this); + } +} + +class ObNixilisTheAdversaryCost extends SacrificeTargetCost { + + public ObNixilisTheAdversaryCost() { + super(new TargetControlledPermanent(0, 1, StaticFilters.FILTER_CONTROLLED_CREATURE_SHORT_TEXT, true)); + this.text = ""; + } + + private ObNixilisTheAdversaryCost(final ObNixilisTheAdversaryCost cost) { + super(cost); + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + if (!super.pay(ability, game, source, controllerId, noMana, costToPay)) { + return false; + } + List sacrificedPermanents = getPermanents(); + if (!sacrificedPermanents.isEmpty()) { + StackObjectCopyApplier applier = new ObNixilisTheAdversaryApplier(sacrificedPermanents.get(0).getPower().getValue()); + game.fireReflexiveTriggeredAbility(new ReflexiveTriggeredAbility( + new ObNixilisTheAdversaryCopyEffect(applier), false, "when you do, copy this spell" + ), source); + } + return true; + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + return true; + } + + @Override + public ObNixilisTheAdversaryCost copy() { + return new ObNixilisTheAdversaryCost(this); + } +} + +class ObNixilisTheAdversaryCopyEffect extends OneShotEffect { + + private final StackObjectCopyApplier applier; + + public ObNixilisTheAdversaryCopyEffect(StackObjectCopyApplier applier) { + super(Outcome.Copy); + this.applier = applier; + this.staticText = "copy {this}"; + } + + private ObNixilisTheAdversaryCopyEffect(final ObNixilisTheAdversaryCopyEffect effect) { + super(effect); + this.applier = effect.applier; + } + + @Override + public ObNixilisTheAdversaryCopyEffect copy() { + return new ObNixilisTheAdversaryCopyEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Spell spell = game.getSpellOrLKIStack(source.getSourceId()); + if (spell == null) { + return false; + } + spell.createCopyOnStack(game, source, source.getControllerId(), true, 1, applier); + return true; + } +} + +class ObNixilisTheAdversaryApplier implements StackObjectCopyApplier { + + private final int loyalty; + + public ObNixilisTheAdversaryApplier(int loyalty) { + this.loyalty = loyalty; + } + + @Override + public void modifySpell(StackObject stackObject, Game game) { + stackObject.getSuperType().remove(SuperType.LEGENDARY); + stackObject.setStartingLoyalty(loyalty); + } + + @Override + public MageObjectReferencePredicate getNextNewTargetType(int copyNumber) { + return null; + } +} + +class ObNixilisTheAdversaryDiscardEffect extends OneShotEffect { + + public ObNixilisTheAdversaryDiscardEffect() { + super(Outcome.LoseLife); + this.staticText = "Each opponent loses 2 life unless they discard a card. If you control a Demon or Devil, you gain 2 life"; + } + + private ObNixilisTheAdversaryDiscardEffect(final ObNixilisTheAdversaryDiscardEffect effect) { + super(effect); + } + + @Override + public ObNixilisTheAdversaryDiscardEffect copy() { + return new ObNixilisTheAdversaryDiscardEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + UUID controllerId = source.getControllerId(); + Player controller = game.getPlayer(controllerId); + if (controller == null) { + return false; + } + for (UUID opponentId : game.getOpponents(controllerId)) { + Player opponent = game.getPlayer(opponentId); + if (opponent == null) { + continue; + } + if (opponent.getHand().isEmpty() || !opponent.chooseUse( + Outcome.Discard, "Discard a card or lose 2 life?", + null, "Discard Card", "Lose 2 Life", source, game) + || opponent.discard(1, false, false, source, game).isEmpty()) { + opponent.loseLife(2, game, source, false); + } + } + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(controllerId)) { + if (permanent.hasSubtype(SubType.DEMON, game) || permanent.hasSubtype(SubType.DEVIL, game)) { + controller.gainLife(2, game, source); + break; + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/StreetsOfNewCapenna.java b/Mage.Sets/src/mage/sets/StreetsOfNewCapenna.java index 5fb3ea856d..81f7b81325 100644 --- a/Mage.Sets/src/mage/sets/StreetsOfNewCapenna.java +++ b/Mage.Sets/src/mage/sets/StreetsOfNewCapenna.java @@ -164,6 +164,7 @@ public final class StreetsOfNewCapenna extends ExpansionSet { cards.add(new SetCardInfo("Murder", 88, Rarity.COMMON, mage.cards.m.Murder.class)); cards.add(new SetCardInfo("Night Clubber", 89, Rarity.UNCOMMON, mage.cards.n.NightClubber.class)); cards.add(new SetCardInfo("Nimble Larcenist", 205, Rarity.UNCOMMON, mage.cards.n.NimbleLarcenist.class)); + cards.add(new SetCardInfo("Ob Nixilis, the Adversary", 206, Rarity.MYTHIC, mage.cards.o.ObNixilisTheAdversary.class)); cards.add(new SetCardInfo("Obscura Ascendancy", 207, Rarity.RARE, mage.cards.o.ObscuraAscendancy.class)); cards.add(new SetCardInfo("Obscura Charm", 208, Rarity.UNCOMMON, mage.cards.o.ObscuraCharm.class)); cards.add(new SetCardInfo("Obscura Initiate", 50, Rarity.COMMON, mage.cards.o.ObscuraInitiate.class)); diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 931bba0ba3..56c90ba6bd 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -62,6 +62,7 @@ public class Spell extends StackObjectImpl implements Card { private boolean countered; private boolean resolving = false; private UUID commandedBy = null; // for Word of Command + private int startingLoyalty; private ActivationManaAbilityStep currentActivatingManaAbilitiesStep = ActivationManaAbilityStep.BEFORE; @@ -78,6 +79,7 @@ public class Spell extends StackObjectImpl implements Card { this.color = affectedCard.getColor(null).copy(); this.frameColor = affectedCard.getFrameColor(null).copy(); this.frameStyle = affectedCard.getFrameStyle(); + this.startingLoyalty = affectedCard.getStartingLoyalty(); this.id = ability.getId(); this.zoneChangeCounter = affectedCard.getZoneChangeCounter(game); // sync card's ZCC with spell (copy spell settings) this.ability = ability; @@ -131,6 +133,7 @@ public class Spell extends StackObjectImpl implements Card { this.currentActivatingManaAbilitiesStep = spell.currentActivatingManaAbilitiesStep; this.targetChanged = spell.targetChanged; + this.startingLoyalty = spell.startingLoyalty; } public boolean activate(Game game, boolean noMana) { @@ -654,11 +657,12 @@ public class Spell extends StackObjectImpl implements Card { @Override public int getStartingLoyalty() { - return card.getStartingLoyalty(); + return this.startingLoyalty; } @Override public void setStartingLoyalty(int startingLoyalty) { + this.startingLoyalty = startingLoyalty; } @Override diff --git a/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java b/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java index 48e4c673f6..2ac2cd6521 100644 --- a/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java +++ b/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java @@ -109,6 +109,8 @@ public class CopyTokenFunction implements Function { if (spell != null) { // copied spell puts to battlefield as token, so that token's ZCC must be synced with spell instead card (card can be moved before resolve) target.setZoneChangeCounter(spell.getZoneChangeCounter(game), game); + // Copy starting loyalty from spell (Ob Nixilis, the Adversary) + target.setStartingLoyalty(spell.getStartingLoyalty()); } else { target.setZoneChangeCounter(source.getZoneChangeCounter(game), game); }