diff --git a/Mage.Sets/src/mage/cards/d/DemonicCovenant.java b/Mage.Sets/src/mage/cards/d/DemonicCovenant.java new file mode 100644 index 00000000000..02be019387c --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DemonicCovenant.java @@ -0,0 +1,129 @@ +package mage.cards.d; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Controllable; +import mage.game.Game; +import mage.game.events.DefenderAttackedEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.DemonToken; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.Arrays; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DemonicCovenant extends CardImpl { + + public DemonicCovenant(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.KINDRED, CardType.ENCHANTMENT}, "{4}{B}{B}"); + + this.subtype.add(SubType.DEMON); + + // Whenever one or more Demons you control attack a player, you draw a card and lose 1 life. + this.addAbility(new DemonicCovenantTriggeredAbility()); + + // At the beginning of your end step, create a 5/5 black Demon creature token with flying, then mill two cards. If two cards that share all their card types were milled this way, sacrifice Demonic Covenant. + Ability ability = new BeginningOfEndStepTriggeredAbility(new CreateTokenEffect(new DemonToken())); + ability.addEffect(new DemonicCovenantEffect()); + this.addAbility(ability); + } + + private DemonicCovenant(final DemonicCovenant card) { + super(card); + } + + @Override + public DemonicCovenant copy() { + return new DemonicCovenant(this); + } +} + +class DemonicCovenantTriggeredAbility extends TriggeredAbilityImpl { + + DemonicCovenantTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1, true)); + this.addEffect(new LoseLifeSourceControllerEffect(1).setText("and lose 1 life")); + this.setTriggerPhrase("Whenever one or more Demons you control attack a player, "); + } + + private DemonicCovenantTriggeredAbility(final DemonicCovenantTriggeredAbility ability) { + super(ability); + } + + @Override + public DemonicCovenantTriggeredAbility copy() { + return new DemonicCovenantTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return game.getPlayer(event.getTargetId()) != null + && ((DefenderAttackedEvent) event) + .getAttackers(game) + .stream() + .filter(permanent -> permanent.hasSubtype(SubType.DEMON, game)) + .map(Controllable::getControllerId) + .anyMatch(this::isControlledBy); + } +} + +class DemonicCovenantEffect extends OneShotEffect { + + DemonicCovenantEffect() { + super(Outcome.Benefit); + staticText = ", then mill two cards. If two cards that share " + + "all their card types were milled this way, sacrifice {this}"; + } + + private DemonicCovenantEffect(final DemonicCovenantEffect effect) { + super(effect); + } + + @Override + public DemonicCovenantEffect copy() { + return new DemonicCovenantEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Cards cards = player.millCards(2, source, game); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + return cards.size() >= 2 + && permanent != null + && CardUtil + .checkAnyPairs( + cards.getCards(game), + (c1, c2) -> Arrays + .stream(CardType.values()) + .allMatch(cardType -> c1.getCardType(game).contains(cardType) + == c2.getCardType(game).contains(cardType)) + ) + && permanent.sacrifice(source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java index 1cc4ae264cc..fd1d6c4671f 100644 --- a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java +++ b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java @@ -89,6 +89,7 @@ public final class DuskmournHouseOfHorrorCommander extends ExpansionSet { cards.add(new SetCardInfo("Deluge of Doom", 18, Rarity.RARE, mage.cards.d.DelugeOfDoom.class)); cards.add(new SetCardInfo("Demolisher Spawn", 31, Rarity.RARE, mage.cards.d.DemolisherSpawn.class)); cards.add(new SetCardInfo("Demon of Fate's Design", 137, Rarity.RARE, mage.cards.d.DemonOfFatesDesign.class)); + cards.add(new SetCardInfo("Demonic Covenant", 19, Rarity.RARE, mage.cards.d.DemonicCovenant.class)); cards.add(new SetCardInfo("Diabolic Vision", 87, Rarity.UNCOMMON, mage.cards.d.DiabolicVision.class)); cards.add(new SetCardInfo("Dig Through Time", 115, Rarity.RARE, mage.cards.d.DigThroughTime.class)); cards.add(new SetCardInfo("Dimir Aqueduct", 270, Rarity.UNCOMMON, mage.cards.d.DimirAqueduct.class)); diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 5bc55367adc..5c32c6b6d3a 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -58,6 +58,9 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.*; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; import java.util.function.ToIntFunction; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -2230,6 +2233,71 @@ public final class CardUtil { return stream.filter(clazz::isInstance).map(clazz::cast).filter(Objects::nonNull); } + public static boolean checkAnyPairs(Collection collection, BiPredicate predicate) { + return streamPairsWithMap(collection, (t1, t2) -> predicate.test(t1, t2)).anyMatch(x -> x); + } + + public static Stream streamAllPairwiseMatches(Collection collection, BiPredicate predicate) { + return streamPairsWithMap( + collection, + (t1, t2) -> predicate.test(t1, t2) + ? Stream.of(t1, t2) + : Stream.empty() + ).flatMap(Function.identity()).distinct(); + } + + private static class IntPairIterator implements Iterator> { + private final int amount; + private int firstCounter = 0; + private int secondCounter = 1; + + IntPairIterator(int amount) { + this.amount = amount; + } + + @Override + public boolean hasNext() { + return firstCounter + 1 < amount; + } + + @Override + public AbstractMap.SimpleImmutableEntry next() { + AbstractMap.SimpleImmutableEntry value + = new AbstractMap.SimpleImmutableEntry(firstCounter, secondCounter); + secondCounter++; + if (secondCounter == amount) { + firstCounter++; + secondCounter = firstCounter + 1; + } + return value; + } + + public int getMax() { + // amount choose 2 + return (amount * amount - amount) / 2; + } + } + + public static Stream streamPairsWithMap(Collection collection, BiFunction function) { + if (collection.size() < 2) { + return Stream.empty(); + } + List list; + if (collection instanceof List) { + list = (List) collection; + } else { + list = new ArrayList<>(collection); + } + IntPairIterator it = new IntPairIterator(list.size()); + return Stream + .generate(it::next) + .limit(it.getMax()) + .map(pair -> function.apply( + list.get(pair.getKey()), + list.get(pair.getValue()) + )); + } + public static void AssertNoControllerOwnerPredicates(Target target) { List list = new ArrayList<>(); Predicates.collectAllComponents(target.getFilter().getPredicates(), target.getFilter().getExtraPredicates(), list);