From 9ab63b73ca7eee4f92a3c5b5ae86a4375d29cb15 Mon Sep 17 00:00:00 2001 From: Sean Walsh <40175938+stwalsh4118@users.noreply.github.com> Date: Mon, 20 Feb 2023 20:01:05 -0600 Subject: [PATCH] [ONE] Implement Kaito, Dancing Shadow (#10009) --- .../src/mage/cards/k/KaitoDancingShadow.java | 296 ++++++++++++++++++ Mage.Sets/src/mage/cards/o/OathOfTeferi.java | 2 +- .../mage/cards/u/UrzaAssemblesTheTitans.java | 2 +- .../src/mage/cards/u/UrzaPlaneswalker.java | 8 +- .../src/mage/sets/PhyrexiaAllWillBeOne.java | 1 + .../java/mage/game/permanent/Permanent.java | 2 + .../mage/game/permanent/PermanentImpl.java | 7 + .../mage/game/permanent/token/DroneToken.java | 40 +++ 8 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/k/KaitoDancingShadow.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/DroneToken.java diff --git a/Mage.Sets/src/mage/cards/k/KaitoDancingShadow.java b/Mage.Sets/src/mage/cards/k/KaitoDancingShadow.java new file mode 100644 index 0000000000..f78f50f78f --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KaitoDancingShadow.java @@ -0,0 +1,296 @@ +package mage.cards.k; + +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.Filter; +import mage.filter.StaticFilters; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.combat.CantAttackTargetEffect; +import mage.abilities.effects.common.combat.CantBlockTargetEffect; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPlayerBatchEvent; +import mage.game.events.DamagedPlayerEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.DroneToken; +import mage.players.Player; +import mage.target.TargetObject; +import mage.target.TargetPermanent; +import mage.watchers.Watcher; + +import java.util.*; + +/** + * + * @author @stwalsh4118 + */ +public final class KaitoDancingShadow extends CardImpl { + + public KaitoDancingShadow(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{U}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.KAITO); + this.setStartingLoyalty(3); + + // Whenever one or more creatures you control deal combat damage to a player, you may return one of them to its owner's hand. If you do, you may activate loyalty abilities of Kaito twice this turn rather than only once. + Ability ability = new KaitoDancingShadowTriggeredAbility(); + this.addAbility(ability); + + // +1: Up to one target creature can't attack or block until your next turn. + Ability KaitoCantAttackOrBlockAbility = new LoyaltyAbility(new CantAttackTargetEffect(Duration.UntilYourNextTurn).setText("Up to one target creature can't attack "), 1); + KaitoCantAttackOrBlockAbility.addEffect(new CantBlockTargetEffect(Duration.UntilYourNextTurn).setText("or block until your next turn")); + KaitoCantAttackOrBlockAbility.addTarget(new TargetPermanent(0, 1, StaticFilters.FILTER_PERMANENT_CREATURE)); + this.addAbility(KaitoCantAttackOrBlockAbility); + + // 0: Draw a card. + this.addAbility(new LoyaltyAbility(new DrawCardSourceControllerEffect(1), 0)); + + // -2: Create a 2/2 colorless Drone artifact creature token with deathtouch and "When this creature leaves the battlefield, each opponent loses 2 life and you gain 2 life." + this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new DroneToken()), -2)); + } + + private KaitoDancingShadow(final KaitoDancingShadow card) { + super(card); + } + + @Override + public KaitoDancingShadow copy() { + return new KaitoDancingShadow(this); + } +} + +class KaitoDancingShadowTriggeredAbility extends TriggeredAbilityImpl { + + KaitoDancingShadowTriggeredAbility() { + super(Zone.BATTLEFIELD, new KaitoDancingShadowEffect()); + this.setTriggerPhrase("Whenever one or more creatures you control deal combat damage to a player, "); + this.addWatcher(new KaitoDancingShadowWatcher()); + + } + + private KaitoDancingShadowTriggeredAbility(final KaitoDancingShadowTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PLAYER_BATCH; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + DamagedPlayerBatchEvent dEvent = (DamagedPlayerBatchEvent) event; + for (DamagedEvent damagedEvent : dEvent.getEvents()) { + if (!damagedEvent.isCombatDamage()) { + continue; + } + Permanent permanent = game.getPermanent(damagedEvent.getSourceId()); + if (permanent != null && permanent.isControlledBy(getControllerId())) { + return true; + } + } + return false; + } + + @Override + public KaitoDancingShadowTriggeredAbility copy() { + return new KaitoDancingShadowTriggeredAbility(this); + } +} + +class KaitoDancingShadowEffect extends OneShotEffect { + + KaitoDancingShadowEffect() { + super(Outcome.Benefit); + this.setText("you may return one of them to its owner's hand. If you do, you may activate loyalty abilities of Kaito twice this turn rather than only once"); + } + + private KaitoDancingShadowEffect(final KaitoDancingShadowEffect effect) { + super(effect); + } + + @Override + public KaitoDancingShadowEffect copy() { + return new KaitoDancingShadowEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + KaitoDancingShadowWatcher watcher = game.getState().getWatcher(KaitoDancingShadowWatcher.class); + if (watcher == null) { + return false; + } + TargetCreatureThatDealtCombatDamage target = new TargetCreatureThatDealtCombatDamage(0, 1, watcher.getPermanents()); + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + if (controller.chooseUse(outcome, "Return a creature card that dealt damage to hand?", source, game) && target.chooseTarget(Outcome.ReturnToHand, source.getControllerId(), source, game)) { + Card card = game.getCard(target.getFirstTarget()); + if (card != null) { + controller.moveCards(card, Zone.HAND, source, game); + + ContinuousEffectImpl effect = new KaitoDancingShadowIncreaseLoyaltyUseEffect(); + game.addEffect(effect, source); + } + } + return true; + } +} + +class KaitoDancingShadowWatcher extends Watcher { + + private final List permanents = new ArrayList<>(); + + KaitoDancingShadowWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.COMBAT_DAMAGE_STEP_POST) { + permanents.clear(); + return; + } + if (event.getType() != GameEvent.EventType.DAMAGED_PLAYER + || !((DamagedPlayerEvent) event).isCombatDamage()) { + return; + } + Permanent creature = game.getPermanent(event.getSourceId()); + if (creature == null) { + return; + } + permanents.add(creature); + } + + public List getPermanents() { + return permanents; + } +} + +class TargetCreatureThatDealtCombatDamage extends TargetObject { + + protected List permanents; + private Permanent firstTarget = null; + + public TargetCreatureThatDealtCombatDamage() { + super(); + } + + public TargetCreatureThatDealtCombatDamage(final TargetCreatureThatDealtCombatDamage target) { + super(target); + this.firstTarget = target.firstTarget; + } + + + public TargetCreatureThatDealtCombatDamage(int minNumTargets, int maxNumTargets, List permanents) { + super(minNumTargets, maxNumTargets, Zone.BATTLEFIELD, true); + this.permanents = permanents; + } + + + @Override + public boolean canTarget(UUID id, Game game) { + Card card = game.getCard(id); + if (card != null && game.getState().getZone(card.getId()) == Zone.BATTLEFIELD) { + for (Permanent permanent : permanents) { + if (permanent.getId().equals(id)) { + return true; + } + } + } + return false; + } + + @Override + public boolean chooseTarget(Outcome outcome, UUID playerId, Ability source, Game game) { + firstTarget = game.getPermanent(source.getFirstTarget()); + return super.chooseTarget(Outcome.Benefit, playerId, source, game); + } + + + @Override + public TargetCreatureThatDealtCombatDamage copy() { + return new TargetCreatureThatDealtCombatDamage(this); + } + + @Override + public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { + return permanents.size() > 0; + } + + @Override + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + Set possibleTargets = new HashSet<>(); + for (Permanent permanent : permanents) { + if (permanent != null && permanent.isControlledBy(sourceControllerId)) { + possibleTargets.add(permanent.getId()); + } + } + return possibleTargets; + } + + @Override + public Filter getFilter() { + return null; + } + + @Override + public boolean canChoose(UUID sourceControllerId, Game game) { + // TODO Auto-generated method stub + return false; + } + + @Override + public Set possibleTargets(UUID sourceControllerId, Game game) { + // TODO Auto-generated method stub + return null; + } +} + +class KaitoDancingShadowIncreaseLoyaltyUseEffect extends ContinuousEffectImpl { + + public KaitoDancingShadowIncreaseLoyaltyUseEffect() { + super(Duration.EndOfTurn, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit); + } + + public KaitoDancingShadowIncreaseLoyaltyUseEffect(final KaitoDancingShadowIncreaseLoyaltyUseEffect effect) { + super(effect); + } + + @Override + public KaitoDancingShadowIncreaseLoyaltyUseEffect copy() { + return new KaitoDancingShadowIncreaseLoyaltyUseEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (Permanent permanent : game.getBattlefield().getActivePermanents( + StaticFilters.FILTER_CONTROLLED_PERMANENT_PLANESWALKER, + source.getControllerId(), source, game + )) { + permanent.setLoyaltyActivationsAvailable(2); + } + + return true; + } + + @Override + public boolean hasLayer(Layer layer) { + return layer == Layer.RulesEffects; + } +} + diff --git a/Mage.Sets/src/mage/cards/o/OathOfTeferi.java b/Mage.Sets/src/mage/cards/o/OathOfTeferi.java index 6c090f76dc..b9261a4952 100644 --- a/Mage.Sets/src/mage/cards/o/OathOfTeferi.java +++ b/Mage.Sets/src/mage/cards/o/OathOfTeferi.java @@ -118,7 +118,7 @@ class OathOfTeferiLoyaltyEffect extends ContinuousEffectImpl { StaticFilters.FILTER_CONTROLLED_PERMANENT_PLANESWALKER, source.getControllerId(), source, game )) { - permanent.incrementLoyaltyActivationsAvailable(2); + permanent.setLoyaltyActivationsAvailable(2); } return true; } diff --git a/Mage.Sets/src/mage/cards/u/UrzaAssemblesTheTitans.java b/Mage.Sets/src/mage/cards/u/UrzaAssemblesTheTitans.java index ed937ffaeb..c7e6abd117 100644 --- a/Mage.Sets/src/mage/cards/u/UrzaAssemblesTheTitans.java +++ b/Mage.Sets/src/mage/cards/u/UrzaAssemblesTheTitans.java @@ -115,7 +115,7 @@ class UrzaAssemblesTheTitansLoyaltyEffect extends ContinuousEffectImpl { StaticFilters.FILTER_CONTROLLED_PERMANENT_PLANESWALKER, source.getControllerId(), source, game )) { - permanent.incrementLoyaltyActivationsAvailable(2); + permanent.setLoyaltyActivationsAvailable(2); } return true; } diff --git a/Mage.Sets/src/mage/cards/u/UrzaPlaneswalker.java b/Mage.Sets/src/mage/cards/u/UrzaPlaneswalker.java index ba920958f8..5ab6b94726 100644 --- a/Mage.Sets/src/mage/cards/u/UrzaPlaneswalker.java +++ b/Mage.Sets/src/mage/cards/u/UrzaPlaneswalker.java @@ -111,8 +111,12 @@ class UrzaPlaneswalkerEffect extends ContinuousEffectImpl { @Override public boolean apply(Game game, Ability source) { - Optional.ofNullable(source.getSourcePermanentIfItStillExists(game)) - .ifPresent(Permanent::incrementLoyaltyActivationsAvailable); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null) { + return false; + } + + permanent.setLoyaltyActivationsAvailable(2); return true; } } diff --git a/Mage.Sets/src/mage/sets/PhyrexiaAllWillBeOne.java b/Mage.Sets/src/mage/sets/PhyrexiaAllWillBeOne.java index 7438cf29df..33445d0e79 100644 --- a/Mage.Sets/src/mage/sets/PhyrexiaAllWillBeOne.java +++ b/Mage.Sets/src/mage/sets/PhyrexiaAllWillBeOne.java @@ -121,6 +121,7 @@ public final class PhyrexiaAllWillBeOne extends ExpansionSet { cards.add(new SetCardInfo("Jace, the Perfected Mind", 57, Rarity.MYTHIC, mage.cards.j.JaceThePerfectedMind.class)); cards.add(new SetCardInfo("Jawbone Duelist", 18, Rarity.UNCOMMON, mage.cards.j.JawboneDuelist.class)); cards.add(new SetCardInfo("Jor Kadeen, First Goldwarden", 203, Rarity.RARE, mage.cards.j.JorKadeenFirstGoldwarden.class)); + cards.add(new SetCardInfo("Kaito, Dancing Shadow", 204, Rarity.RARE, mage.cards.k.KaitoDancingShadow.class)); cards.add(new SetCardInfo("Karumonix, the Rat King", 98, Rarity.RARE, mage.cards.k.KarumonixTheRatKing.class)); cards.add(new SetCardInfo("Kaya, Intangible Slayer", 205, Rarity.RARE, mage.cards.k.KayaIntangibleSlayer.class)); cards.add(new SetCardInfo("Kemba, Kha Enduring", 19, Rarity.RARE, mage.cards.k.KembaKhaEnduring.class)); diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 29e67ea0e8..9976deca3f 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -216,6 +216,8 @@ public interface Permanent extends Card, Controllable { void incrementLoyaltyActivationsAvailable(int max); + void setLoyaltyActivationsAvailable(int loyaltyActivationsAvailable); + void addLoyaltyUsed(); boolean canLoyaltyBeUsed(Game game); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 4bc81c5389..8cdc856243 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -480,6 +480,13 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { } } + @Override + public void setLoyaltyActivationsAvailable(int setActivations) { + if(this.loyaltyActivationsAvailable < setActivations) { + this.loyaltyActivationsAvailable = setActivations; + } + } + @Override public void addLoyaltyUsed() { this.timesLoyaltyUsed++; diff --git a/Mage/src/main/java/mage/game/permanent/token/DroneToken.java b/Mage/src/main/java/mage/game/permanent/token/DroneToken.java new file mode 100644 index 0000000000..1c7f834c6e --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/DroneToken.java @@ -0,0 +1,40 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.LeavesBattlefieldTriggeredAbility; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.constants.CardType; +import mage.constants.SubType; + +/** + * @author @stwalsh4118 + */ +public class DroneToken extends TokenImpl { + + public DroneToken() { + super("Drone Token", "2/2 colorless Drone artifact creature token with deathtouch and \"When this creature leaves the battlefield, each opponent loses 2 life and you gain 2 life.\""); + cardType.add(CardType.CREATURE); + cardType.add(CardType.ARTIFACT); + subtype.add(SubType.DRONE); + power = new MageInt(2); + toughness = new MageInt(2); + addAbility(DeathtouchAbility.getInstance()); + + Ability ability = new LeavesBattlefieldTriggeredAbility(new LoseLifeOpponentsEffect(2), false).setTriggerPhrase("When this creature leaves the battlefield, "); + ability.addEffect(new GainLifeEffect(2).concatBy("and")); + addAbility(ability); + + } + + private DroneToken(final DroneToken token) { + super(token); + } + + @Override + public DroneToken copy() { + return new DroneToken(this); + } +}