From 97b09a6eaed8248f5f6ceb7facec9e04ab065180 Mon Sep 17 00:00:00 2001 From: padfoothelix Date: Fri, 16 May 2025 22:18:28 +0200 Subject: [PATCH] Implement River Song's Diary --- .../src/mage/cards/r/RiverSongsDiary.java | 195 ++++++++++++++++++ Mage.Sets/src/mage/sets/DoctorWho.java | 8 +- 2 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/r/RiverSongsDiary.java diff --git a/Mage.Sets/src/mage/cards/r/RiverSongsDiary.java b/Mage.Sets/src/mage/cards/r/RiverSongsDiary.java new file mode 100644 index 00000000000..917600c5aa5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RiverSongsDiary.java @@ -0,0 +1,195 @@ +package mage.cards.r; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardImpl; +import mage.cards.CardsImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.ExileZone; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author padfoothelix + */ +public final class RiverSongsDiary extends CardImpl { + + public RiverSongsDiary(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); + + + // Imprint -- Whenever a player casts an instant or sorcery spell from their hand, exile it instead of putting it into a graveyard as it resolves. + this.addAbility(new RiverSongsDiaryImprintAbility().setAbilityWord(AbilityWord.IMPRINT)); + + // At the beginning of your upkeep, if there are four or more cards exiled with River Song's Diary, choose one of them at random. You may cast it without paying its mana cost. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new BeginningOfUpkeepTriggeredAbility( + new RiverSongsDiaryCastEffect() + ), RiverSongsDiaryCondition.instance, + "At the beginning of your upkeep, if there are four or more cards exiled with " + + " {this}, choose one of them at random. You may cast it without paying its mana cost." + )); + + } + + private RiverSongsDiary(final RiverSongsDiary card) { + super(card); + } + + @Override + public RiverSongsDiary copy() { + return new RiverSongsDiary(this); + } +} + +enum RiverSongsDiaryCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + ExileZone exileZone = game.getExile().getExileZone(CardUtil.getExileZoneId( + game, source.getSourceId(), game.getState().getZoneChangeCounter(source.getSourceId()) + )); + return exileZone != null && exileZone.size() > 3; + } +} + +class RiverSongsDiaryImprintAbility extends TriggeredAbilityImpl { + + public RiverSongsDiaryImprintAbility() { + super(Zone.BATTLEFIELD, null, false); + setTriggerPhrase("Whenever a player casts an instant or sorcery spell from their hand" + + ", exile it instead of putting it into a graveyard as it resolves."); + } + + private RiverSongsDiaryImprintAbility(final RiverSongsDiaryImprintAbility ability) { + super(ability); + } + + @Override + public RiverSongsDiaryImprintAbility copy() { + return new RiverSongsDiaryImprintAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SPELL_CAST; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (event.getZone() != Zone.HAND) { + return false; + } + Spell spell = game.getStack().getSpell(event.getTargetId()); + if (spell == null || !spell.isInstantOrSorcery(game)) { + return false; + } + this.getEffects().clear(); + this.addEffect(new RiverSongsDiaryExileEffect(spell, game)); + return true; + } +} + +class RiverSongsDiaryExileEffect extends ReplacementEffectImpl { + + private final MageObjectReference mor; + + RiverSongsDiaryExileEffect(Spell spell, Game game) { + super(Duration.OneUse, Outcome.Benefit); + this.mor = new MageObjectReference(spell.getCard(), game); + } + + private RiverSongsDiaryExileEffect(final RiverSongsDiaryExileEffect effect) { + super(effect); + this.mor = effect.mor; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player player = game.getPlayer(source.getControllerId()); + Spell sourceSpell = game.getStack().getSpell(event.getTargetId()); + if (player == null) { + return false; + } + player.moveCardsToExile( + sourceSpell, source, game, false, + CardUtil.getExileZoneId(game, source), + CardUtil.getSourceName(game, source) + ); + return true; + } + + @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) { + ZoneChangeEvent zEvent = ((ZoneChangeEvent) event); + if (zEvent.getFromZone() != Zone.STACK + || zEvent.getToZone() != Zone.GRAVEYARD + || event.getSourceId() == null + || !event.getSourceId().equals(event.getTargetId()) + || !mor.equals(new MageObjectReference(event.getTargetId(), game))) { + return false; + } + return true; + } + + @Override + public RiverSongsDiaryExileEffect copy() { + return new RiverSongsDiaryExileEffect(this); + } +} + +class RiverSongsDiaryCastEffect extends OneShotEffect { + + RiverSongsDiaryCastEffect() { + super(Outcome.Benefit); + } + + private RiverSongsDiaryCastEffect(final RiverSongsDiaryCastEffect effect) { + super(effect); + } + + @Override + public RiverSongsDiaryCastEffect copy() { + return new RiverSongsDiaryCastEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + ExileZone exileZone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source)); + if (exileZone == null || exileZone.isEmpty()) { + return false; + } + Cards cards = new CardsImpl(exileZone); + if (player == null || cards.isEmpty()) { + return false; + } + Card randomCard = cards.getRandom(game); + CardUtil.castSpellWithAttributesForFree(player, source, game, randomCard); + return true; + } +} + diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index e90aca9d94d..79586c65b5c 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -652,10 +652,10 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("River Song", 436, Rarity.RARE, mage.cards.r.RiverSong.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("River Song", 547, Rarity.RARE, mage.cards.r.RiverSong.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("River Song", 757, Rarity.RARE, mage.cards.r.RiverSong.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("River Song's Diary", 1051, Rarity.RARE, mage.cards.r.RiverSongsDiary.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("River Song's Diary", 182, Rarity.RARE, mage.cards.r.RiverSongsDiary.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("River Song's Diary", 460, Rarity.RARE, mage.cards.r.RiverSongsDiary.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("River Song's Diary", 787, Rarity.RARE, mage.cards.r.RiverSongsDiary.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("River Song's Diary", 1051, Rarity.RARE, mage.cards.r.RiverSongsDiary.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("River Song's Diary", 182, Rarity.RARE, mage.cards.r.RiverSongsDiary.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("River Song's Diary", 460, Rarity.RARE, mage.cards.r.RiverSongsDiary.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("River Song's Diary", 787, Rarity.RARE, mage.cards.r.RiverSongsDiary.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("RMS Titanic", 389, Rarity.RARE, mage.cards.r.RMSTitanic.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("RMS Titanic", 698, Rarity.RARE, mage.cards.r.RMSTitanic.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("RMS Titanic", 93, Rarity.RARE, mage.cards.r.RMSTitanic.class, NON_FULL_USE_VARIOUS));