From cbb3012b056808fd8a1b94786d6acabad80204c6 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Fri, 21 Feb 2025 21:19:34 -0600 Subject: [PATCH 1/3] [DFT] Implement Ketramose, the New Dawn and implement exile condition and dynamic value --- .../src/mage/cards/k/KetramoseTheNewDawn.java | 113 ++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 4 + .../common/CardsInExileCondition.java | 34 ++++++ .../common/CardsInExileCount.java | 110 +++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java create mode 100644 Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java diff --git a/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java b/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java new file mode 100644 index 00000000000..f87abbbf1db --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java @@ -0,0 +1,113 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.CardsInExileCondition; +import mage.abilities.dynamicvalue.common.CardsInExileCount; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.effects.common.combat.CantAttackBlockUnlessConditionSourceEffect; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeBatchEvent; +import mage.game.events.ZoneChangeEvent; + +import java.util.UUID; + +/** + * @author Jmlundeen + */ +public final class KetramoseTheNewDawn extends CardImpl { + + public KetramoseTheNewDawn(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}{B}"); + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.GOD); + + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Menace + this.addAbility(new MenaceAbility()); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // Indestructible + this.addAbility(IndestructibleAbility.getInstance()); + + // Ketramose can't attack or block unless there are seven or more cards in exile. + this.addAbility(new SimpleStaticAbility( + new CantAttackBlockUnlessConditionSourceEffect(new CardsInExileCondition(ComparisonType.MORE_THAN, 6, CardsInExileCount.ALL)) + .setText("{this} can't attack or block unless there are seven or more cards in exile") + ).addHint(CardsInExileCount.ALL.getHint())); + + // Whenever one or more cards are put into exile from graveyards and/or the battlefield during your turn, you draw a card and lose 1 life. + this.addAbility(new KetramoseTriggeredAbility()); + + } + + private KetramoseTheNewDawn(final KetramoseTheNewDawn card) { + super(card); + } + + @Override + public KetramoseTheNewDawn copy() { + return new KetramoseTheNewDawn(this); + } + +} +class KetramoseTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { + KetramoseTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false); + this.addEffect(new LoseLifeSourceControllerEffect(1)); + } + + private KetramoseTriggeredAbility(final KetramoseTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; + } + + @Override + public boolean checkEvent(ZoneChangeEvent event, Game game) { + if (event.getToZone() != Zone.EXILED) { + return false; + } + return event.getFromZone() == Zone.GRAVEYARD || event.getFromZone() == Zone.BATTLEFIELD; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return game.getActivePlayerId().equals(getControllerId()) + && !getFilteredEvents((ZoneChangeBatchEvent) event, game).isEmpty(); + } + + @Override + public TriggeredAbility copy() + { + return new KetramoseTriggeredAbility(this); + } + + @Override + public String getRule() { + return "Whenever one or more cards are put into exile from graveyards" + + " and/or the battlefield during your turn, you draw a card and lose 1 life."; + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index dfcef6ecbec..5b3bee14ac7 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -131,6 +131,10 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Jungle Hollow", 256, Rarity.COMMON, mage.cards.j.JungleHollow.class)); cards.add(new SetCardInfo("Kalakscion, Hunger Tyrant", 93, Rarity.UNCOMMON, mage.cards.k.KalakscionHungerTyrant.class)); cards.add(new SetCardInfo("Keen Buccaneer", 48, Rarity.COMMON, mage.cards.k.KeenBuccaneer.class)); + cards.add(new SetCardInfo("Ketramose, the New Dawn", 209, Rarity.MYTHIC, mage.cards.k.KetramoseTheNewDawn.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ketramose, the New Dawn", 350, Rarity.MYTHIC, mage.cards.k.KetramoseTheNewDawn.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ketramose, the New Dawn", 482, Rarity.MYTHIC, mage.cards.k.KetramoseTheNewDawn.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ketramose, the New Dawn", 549, Rarity.MYTHIC, mage.cards.k.KetramoseTheNewDawn.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Kickoff Celebrations", 135, Rarity.COMMON, mage.cards.k.KickoffCelebrations.class)); cards.add(new SetCardInfo("Lagorin, Soul of Alacria", 211, Rarity.UNCOMMON, mage.cards.l.LagorinSoulOfAlacria.class)); cards.add(new SetCardInfo("Leonin Surveyor", 18, Rarity.COMMON, mage.cards.l.LeoninSurveyor.class)); diff --git a/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java b/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java new file mode 100644 index 00000000000..6fa1ba75005 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java @@ -0,0 +1,34 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.dynamicvalue.common.CardsInExileCount; +import mage.constants.ComparisonType; +import mage.game.Game; + + +public class CardsInExileCondition implements Condition +{ + private final ComparisonType type; + private final int count; + private CardsInExileCount cardsInExileCount; + + public CardsInExileCondition(ComparisonType type, int count) + { + this(type, count, CardsInExileCount.YOU); + } + + public CardsInExileCondition(ComparisonType type, int count, CardsInExileCount cardsInExileCount) + { + this.type = type; + this.count = count; + this.cardsInExileCount = cardsInExileCount; + } + + @Override + public boolean apply(Game game, Ability source) + { + int exileCards = cardsInExileCount.calculate(game, source, null); + return ComparisonType.compare(exileCards, type, count); + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java new file mode 100644 index 00000000000..6db633c0c2f --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java @@ -0,0 +1,110 @@ +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.hint.Hint; +import mage.cards.Card; +import mage.constants.CardType; +import mage.game.Game; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author JayDi85 + */ +public enum CardsInExileCount implements DynamicValue { + YOU("you"), + ALL("all players"), + OPPONENTS("your opponents'"); + + private final String message; + private final CardsInExileHint hint; + + CardsInExileCount(String message) { + this.message = "The number of cards owned by " + message + " in exile"; + this.hint = new CardsInExileHint(this); + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return getExileCards(game, sourceAbility) + .mapToInt(x -> 1) + .sum(); + } + + @Override + public CardsInExileCount copy() { + return this; + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return message; + } + + public Hint getHint() { + return hint; + } + + public Stream getExileCards(Game game, Ability ability) { + Collection playerIds; + switch (this) { + case YOU: + playerIds = Collections.singletonList(ability.getControllerId()); + break; + case OPPONENTS: + playerIds = game.getOpponents(ability.getControllerId()); + break; + case ALL: + playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game); + break; + default: + throw new IllegalArgumentException("Wrong code usage: miss implementation for " + this); + } + return playerIds.stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .map(player -> game.getExile().getAllCards(game, player.getId())) + .flatMap(Collection::stream) + .filter(Objects::nonNull); + } +} + +class CardsInExileHint implements Hint { + + CardsInExileCount value; + + CardsInExileHint(CardsInExileCount value) { + this.value = value; + } + + private CardsInExileHint(final CardsInExileHint hint) { + this.value = hint.value; + } + + @Override + public String getText(Game game, Ability ability) { + int count = value.getExileCards(game, ability) + .mapToInt(x -> 1) + .sum(); + + return this.value.getMessage() + ": " + count; + } + + @Override + public CardsInExileHint copy() { + return new CardsInExileHint(this); + } +} \ No newline at end of file From b9fa82f602ef138e16d5779cabf0da27496d1d14 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sat, 22 Feb 2025 14:13:08 -0600 Subject: [PATCH 2/3] Replace unnecessary custom hint class with ValueHint --- .../common/CardsInExileCount.java | 35 ++----------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java index 6db633c0c2f..851747f81a8 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java @@ -4,16 +4,14 @@ import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; import mage.cards.Card; -import mage.constants.CardType; import mage.game.Game; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Objects; import java.util.UUID; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -25,11 +23,11 @@ public enum CardsInExileCount implements DynamicValue { OPPONENTS("your opponents'"); private final String message; - private final CardsInExileHint hint; + private final ValueHint hint; CardsInExileCount(String message) { this.message = "The number of cards owned by " + message + " in exile"; - this.hint = new CardsInExileHint(this); + this.hint = new ValueHint(this.message, this); } @Override @@ -80,31 +78,4 @@ public enum CardsInExileCount implements DynamicValue { .flatMap(Collection::stream) .filter(Objects::nonNull); } -} - -class CardsInExileHint implements Hint { - - CardsInExileCount value; - - CardsInExileHint(CardsInExileCount value) { - this.value = value; - } - - private CardsInExileHint(final CardsInExileHint hint) { - this.value = hint.value; - } - - @Override - public String getText(Game game, Ability ability) { - int count = value.getExileCards(game, ability) - .mapToInt(x -> 1) - .sum(); - - return this.value.getMessage() + ": " + count; - } - - @Override - public CardsInExileHint copy() { - return new CardsInExileHint(this); - } } \ No newline at end of file From 121ab378e719ed2c045afe99f9f0370278da516e Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sat, 8 Mar 2025 12:52:39 -0600 Subject: [PATCH 3/3] Change CardsInExileCondition constructor and toString change CardsInExileCondition to accept generic DynamicValue create a toString override to generate rule text --- .../src/mage/cards/k/KetramoseTheNewDawn.java | 3 +- .../common/CardsInExileCondition.java | 39 +++++++++++++++++-- .../common/CardsInExileCount.java | 2 +- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java b/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java index f87abbbf1db..a319d936d67 100644 --- a/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java +++ b/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java @@ -51,8 +51,7 @@ public final class KetramoseTheNewDawn extends CardImpl { // Ketramose can't attack or block unless there are seven or more cards in exile. this.addAbility(new SimpleStaticAbility( - new CantAttackBlockUnlessConditionSourceEffect(new CardsInExileCondition(ComparisonType.MORE_THAN, 6, CardsInExileCount.ALL)) - .setText("{this} can't attack or block unless there are seven or more cards in exile") + new CantAttackBlockUnlessConditionSourceEffect(new CardsInExileCondition(ComparisonType.OR_GREATER, 7)) ).addHint(CardsInExileCount.ALL.getHint())); // Whenever one or more cards are put into exile from graveyards and/or the battlefield during your turn, you draw a card and lose 1 life. diff --git a/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java b/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java index 6fa1ba75005..1f8f4a3b784 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java @@ -2,23 +2,30 @@ package mage.abilities.condition.common; import mage.abilities.Ability; import mage.abilities.condition.Condition; +import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.CardsInExileCount; +import mage.abilities.dynamicvalue.common.StaticValue; import mage.constants.ComparisonType; import mage.game.Game; +import mage.util.CardUtil; - +/** + * Cards in exile condition + * + * @author Jmlundeen + */ public class CardsInExileCondition implements Condition { private final ComparisonType type; private final int count; - private CardsInExileCount cardsInExileCount; + private final DynamicValue cardsInExileCount; public CardsInExileCondition(ComparisonType type, int count) { - this(type, count, CardsInExileCount.YOU); + this(type, count, CardsInExileCount.ALL); } - public CardsInExileCondition(ComparisonType type, int count, CardsInExileCount cardsInExileCount) + public CardsInExileCondition(ComparisonType type, int count, DynamicValue cardsInExileCount) { this.type = type; this.count = count; @@ -31,4 +38,28 @@ public class CardsInExileCondition implements Condition int exileCards = cardsInExileCount.calculate(game, source, null); return ComparisonType.compare(exileCards, type, count); } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("there are "); + String countString = CardUtil.numberToText(count); + switch (type) { + case MORE_THAN: + sb.append("more than ").append(countString).append(" "); + break; + case FEWER_THAN: + sb.append("fewer than ").append(countString).append(" "); + break; + case OR_LESS: + sb.append(countString).append(" or less "); + break; + case OR_GREATER: + sb.append(countString).append(" or more "); + break; + default: + throw new IllegalArgumentException("comparison rules for " + type + " missing"); + } + sb.append("cards in exile"); + return sb.toString(); + } } diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java index 851747f81a8..2a6b6a1bfd7 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java @@ -15,7 +15,7 @@ import java.util.UUID; import java.util.stream.Stream; /** - * @author JayDi85 + * @author Jmlundeen */ public enum CardsInExileCount implements DynamicValue { YOU("you"),