diff --git a/Mage.Sets/src/mage/cards/q/QuilledCharger.java b/Mage.Sets/src/mage/cards/q/QuilledCharger.java new file mode 100644 index 00000000000..1d41b2ae941 --- /dev/null +++ b/Mage.Sets/src/mage/cards/q/QuilledCharger.java @@ -0,0 +1,51 @@ +package mage.cards.q; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksWhileSaddledTriggeredAbility; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.MenaceAbility; +import mage.abilities.keyword.SaddleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class QuilledCharger extends CardImpl { + + public QuilledCharger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); + + this.subtype.add(SubType.PORCUPINE); + this.subtype.add(SubType.MOUNT); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Whenever Quilled Charger attacks while saddled, it gets +1/+2 and gains menace until end of turn. + Ability ability = new AttacksWhileSaddledTriggeredAbility( + new BoostSourceEffect(1, 2, Duration.EndOfTurn).setText("it gets +1/+2") + ); + ability.addEffect(new GainAbilitySourceEffect(new MenaceAbility(false)) + .setText("and gains menace until end of turn")); + this.addAbility(ability); + + // Saddle 2 + this.addAbility(new SaddleAbility(2)); + } + + private QuilledCharger(final QuilledCharger card) { + super(card); + } + + @Override + public QuilledCharger copy() { + return new QuilledCharger(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TrainedArynx.java b/Mage.Sets/src/mage/cards/t/TrainedArynx.java new file mode 100644 index 00000000000..8b7882ca5cc --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TrainedArynx.java @@ -0,0 +1,51 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksWhileSaddledTriggeredAbility; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.effects.keyword.ScryEffect; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.SaddleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TrainedArynx extends CardImpl { + + public TrainedArynx(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.CAT); + this.subtype.add(SubType.BEAST); + this.subtype.add(SubType.MOUNT); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // Whenever Trained Arynx attacks while saddled, it gains first strike until end of turn. Scry 1. + Ability ability = new AttacksWhileSaddledTriggeredAbility(new GainAbilitySourceEffect( + FirstStrikeAbility.getInstance(), Duration.EndOfTurn + ).setText("it gains first strike until end of turn")); + ability.addEffect(new ScryEffect(1)); + this.addAbility(ability); + + // Saddle 2 + this.addAbility(new SaddleAbility(2)); + } + + private TrainedArynx(final TrainedArynx card) { + super(card); + } + + @Override + public TrainedArynx copy() { + return new TrainedArynx(this); + } +} diff --git a/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java b/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java index c88997310a2..663a6a4701a 100644 --- a/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java +++ b/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java @@ -108,6 +108,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Plains", 272, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Plan the Heist", 62, Rarity.UNCOMMON, mage.cards.p.PlanTheHeist.class)); cards.add(new SetCardInfo("Prosperity Tycoon", 25, Rarity.UNCOMMON, mage.cards.p.ProsperityTycoon.class)); + cards.add(new SetCardInfo("Quilled Charger", 139, Rarity.COMMON, mage.cards.q.QuilledCharger.class)); cards.add(new SetCardInfo("Railway Brawler", 175, Rarity.MYTHIC, mage.cards.r.RailwayBrawler.class)); cards.add(new SetCardInfo("Rakish Crew", 99, Rarity.UNCOMMON, mage.cards.r.RakishCrew.class)); cards.add(new SetCardInfo("Rattleback Apothecary", 100, Rarity.UNCOMMON, mage.cards.r.RattlebackApothecary.class)); @@ -135,6 +136,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Swamp", 274, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Terror of the Peaks", 149, Rarity.MYTHIC, mage.cards.t.TerrorOfThePeaks.class)); cards.add(new SetCardInfo("Tomb Trawler", 250, Rarity.UNCOMMON, mage.cards.t.TombTrawler.class)); + cards.add(new SetCardInfo("Trained Arynx", 36, Rarity.COMMON, mage.cards.t.TrainedArynx.class)); cards.add(new SetCardInfo("Treasure Dredger", 110, Rarity.UNCOMMON, mage.cards.t.TreasureDredger.class)); cards.add(new SetCardInfo("Tumbleweed Rising", 187, Rarity.COMMON, mage.cards.t.TumbleweedRising.class)); cards.add(new SetCardInfo("Unscrupulous Contractor", 112, Rarity.UNCOMMON, mage.cards.u.UnscrupulousContractor.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SaddleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SaddleTest.java new file mode 100644 index 00000000000..e251ed7ca6e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SaddleTest.java @@ -0,0 +1,67 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.abilities.keyword.MenaceAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class SaddleTest extends CardTestPlayerBase { + + private static final String charger = "Quilled Charger"; + private static final String bear = "Grizzly Bears"; + + private void assertSaddled(String name, boolean saddled) { + Permanent permanent = getPermanent(name); + Assert.assertEquals( + name + " should " + (saddled ? "" : "not ") + "be saddled", + saddled, permanent.isSaddled() + ); + } + + @Test + public void testNoSaddle() { + addCard(Zone.BATTLEFIELD, playerA, charger); + + attack(1, playerA, charger, playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTapped(charger, true); + assertSaddled(charger, false); + assertAbility(playerA, charger, new MenaceAbility(false), false); + assertLife(playerB, 20 - 4); + } + + @Test + public void testSaddle() { + addCard(Zone.BATTLEFIELD, playerA, charger); + addCard(Zone.BATTLEFIELD, playerA, bear); + + setChoice(playerA, bear); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Saddle"); + attack(1, playerA, charger, playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTapped(bear, true); + assertTapped(charger, true); + assertSaddled(charger, true); + assertAbility(playerA, charger, new MenaceAbility(false), true); + assertLife(playerB, 20 - 4 - 1); + + setStopAt(2, PhaseStep.UPKEEP); + execute(); + + assertSaddled(charger, false); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/AttacksWhileSaddledTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/AttacksWhileSaddledTriggeredAbility.java new file mode 100644 index 00000000000..a1e422f800b --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/AttacksWhileSaddledTriggeredAbility.java @@ -0,0 +1,37 @@ +package mage.abilities.common; + +import mage.abilities.effects.Effect; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; + +import java.util.Optional; + +/** + * @author TheElk801 + */ +public class AttacksWhileSaddledTriggeredAbility extends AttacksTriggeredAbility { + + public AttacksWhileSaddledTriggeredAbility(Effect effect) { + super(effect); + this.setTriggerPhrase("Whenever {this} attacks while saddled, "); + } + + private AttacksWhileSaddledTriggeredAbility(final AttacksWhileSaddledTriggeredAbility ability) { + super(ability); + } + + @Override + public AttacksWhileSaddledTriggeredAbility copy() { + return new AttacksWhileSaddledTriggeredAbility(this); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return super.checkTrigger(event, game) + && Optional + .ofNullable(getSourcePermanentIfItStillExists(game)) + .map(Permanent::isSaddled) + .orElse(false); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/SaddledCondition.java b/Mage/src/main/java/mage/abilities/condition/common/SaddledCondition.java new file mode 100644 index 00000000000..a2c1d4770e6 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/SaddledCondition.java @@ -0,0 +1,23 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.Optional; + +/** + * @author TheElk801 + */ +public enum SaddledCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return Optional + .ofNullable(source.getSourcePermanentIfItStillExists(game)) + .map(Permanent::isSaddled) + .orElse(false); + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/SaddleAbility.java b/Mage/src/main/java/mage/abilities/keyword/SaddleAbility.java new file mode 100644 index 00000000000..09432f063ca --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/SaddleAbility.java @@ -0,0 +1,177 @@ +package mage.abilities.keyword; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.condition.common.SaddledCondition; +import mage.abilities.costs.Cost; +import mage.abilities.costs.CostImpl; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.abilities.hint.HintUtils; +import mage.constants.*; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.filter.predicate.permanent.TappedPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.Target; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.awt.*; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class SaddleAbility extends SimpleActivatedAbility { + + private final int value; + private static final Hint hint = new ConditionHint(SaddledCondition.instance, "This permanent is saddled"); + + public SaddleAbility(int value) { + super(new SaddleEffect(), new SaddleCost(value)); + this.value = value; + this.addHint(hint); + this.setTiming(TimingRule.SORCERY); + } + + private SaddleAbility(final SaddleAbility ability) { + super(ability); + this.value = ability.value; + } + + @Override + public SaddleAbility copy() { + return new SaddleAbility(this); + } + + @Override + public String getRule() { + return "Saddle " + value + " (Tap any number of other creatures you control with total power " + + value + " or more: This Mount becomes saddled until end of turn. Saddle only as a sorcery.)"; + } +} + +class SaddleEffect extends ContinuousEffectImpl { + + SaddleEffect() { + super(Duration.EndOfTurn, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit); + } + + private SaddleEffect(final SaddleEffect effect) { + super(effect); + } + + @Override + public SaddleEffect copy() { + return new SaddleEffect(this); + } + + @Override + public void init(Ability source, Game game) { + super.init(source, game); + game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.MOUNT_SADDLED, + source.getSourceId(), + source, source.getControllerId() + )); + } + + @Override + public boolean apply(Game game, Ability source) { + Optional.ofNullable(source.getSourcePermanentIfItStillExists(game)) + .ifPresent(permanent -> permanent.setSaddled(true)); + return true; + } +} + +class SaddleCost extends CostImpl { + + + private static final FilterControlledCreaturePermanent filter + = new FilterControlledCreaturePermanent("another untapped creature you control"); + + static { + filter.add(TappedPredicate.UNTAPPED); + filter.add(AnotherPredicate.instance); + } + + private final int value; + + SaddleCost(int value) { + this.value = value; + } + + private SaddleCost(final SaddleCost cost) { + super(cost); + this.value = cost.value; + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + Target target = new TargetControlledCreaturePermanent(0, Integer.MAX_VALUE, filter, true) { + @Override + public String getMessage() { + // shows selected power + int selectedPower = this.targets.keySet().stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .sum(); + String extraInfo = "(selected power " + selectedPower + " of " + value + ")"; + if (selectedPower >= value) { + extraInfo = HintUtils.prepareText(extraInfo, Color.GREEN); + } + return super.getMessage() + " " + extraInfo; + } + }; + + // can cancel + if (target.choose(Outcome.Tap, controllerId, source.getSourceId(), source, game)) { + int sumPower = 0; + for (UUID targetId : target.getTargets()) { + GameEvent event = new GameEvent(GameEvent.EventType.SADDLE_MOUNT, targetId, source, controllerId); + if (!game.replaceEvent(event)) { + Permanent permanent = game.getPermanent(targetId); + if (permanent != null && permanent.tap(source, game)) { + sumPower += permanent.getPower().getValue(); + } + } + } + paid = sumPower >= value; + if (paid) { + for (UUID targetId : target.getTargets()) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.SADDLED_MOUNT, targetId, source, controllerId)); + } + } + } else { + return false; + } + + return paid; + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + int sumPower = 0; + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, controllerId, game)) { + sumPower += Math.max(permanent.getPower().getValue(), 0); + if (sumPower >= value) { + return true; + } + } + return false; + } + + @Override + public SaddleCost copy() { + return new SaddleCost(this); + } +} diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java index 2b44ce9bcfc..d611e0ba973 100644 --- a/Mage/src/main/java/mage/constants/SubType.java +++ b/Mage/src/main/java/mage/constants/SubType.java @@ -315,6 +315,7 @@ public enum SubType { PINCHER("Pincher", SubTypeSet.CreatureType), PIRATE("Pirate", SubTypeSet.CreatureType), PLANT("Plant", SubTypeSet.CreatureType), + PORCUPINE("Porcupine", SubTypeSet.CreatureType), PRAETOR("Praetor", SubTypeSet.CreatureType), PRIMARCH("Primarch", SubTypeSet.CreatureType), PRISM("Prism", SubTypeSet.CreatureType), diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index a7e6e65efa3..e23f19c9aeb 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -160,7 +160,7 @@ public class GameEvent implements Serializable { */ CREW_VEHICLE, /* CREW_VEHICLE - targetId the id of the creature that crewed a vehicle + targetId the id of the creature that will crew a vehicle sourceId sourceId of the vehicle playerId the id of the controlling player */ @@ -176,6 +176,24 @@ public class GameEvent implements Serializable { sourceId sourceId of the vehicle playerId the id of the controlling player */ + SADDLE_MOUNT, + /* SADDLE_MOUNT + targetId the id of the creature that will saddle a mount + sourceId sourceId of the mount + playerId the id of the controlling player + */ + SADDLED_MOUNT, + /* SADDLED_MOUNT + targetId the id of the creature that saddled a mount + sourceId sourceId of the mount + playerId the id of the controlling player + */ + MOUNT_SADDLED, + /* MOUNT_SADDLED + targetId the id of the mount + sourceId sourceId of the mount + playerId the id of the controlling player + */ X_MANA_ANNOUNCE, /* X_MANA_ANNOUNCE mana x-costs announced by players (X value can be changed by replace events like Unbound Flourishing) diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 6ae7a036b65..ce3323d0635 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -78,6 +78,10 @@ public interface Permanent extends Card, Controllable { void setSuspected(boolean value, Game game, Ability source); + boolean isSaddled(); + + void setSaddled(boolean value); + boolean isPrototyped(); void setPrototyped(boolean value); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 4c880619af0..59b53c6a5d7 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -72,6 +72,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { protected boolean monstrous; protected boolean renowned; protected boolean suspected; + protected boolean saddled; protected boolean manifested = false; protected boolean morphed = false; protected boolean disguised = false; @@ -175,6 +176,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.monstrous = permanent.monstrous; this.renowned = permanent.renowned; this.suspected = permanent.suspected; + this.saddled = permanent.saddled; this.ringBearerFlag = permanent.ringBearerFlag; this.classLevel = permanent.classLevel; this.goadingPlayers.addAll(permanent.goadingPlayers); @@ -203,7 +205,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { + ":" + getCardNumber() + ":" + getImageFileName() + ":" + getImageNumber(); - return name + return name + ", " + (getBasicMageObject() instanceof Token ? "T" : "C") + ", " + getBasicMageObject().getClass().getSimpleName() + ", " + imageInfo @@ -237,6 +239,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.maxBlockedBy = 0; this.copy = false; this.goadingPlayers.clear(); + this.saddled = false; this.loyaltyActivationsAvailable = 1; this.legendRuleApplies = true; this.canBeSacrificed = true; @@ -1722,6 +1725,16 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { } } + @Override + public boolean isSaddled() { + return saddled; + } + + @Override + public void setSaddled(boolean saddled) { + this.saddled = saddled; + } + // Used as key for the ring bearer info. private static final String ringbearerInfoKey = "IS_RINGBEARER"; diff --git a/Utils/keywords.txt b/Utils/keywords.txt index 5b6f4bdd061..e29a81de8e4 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -107,6 +107,7 @@ Reconfigure|manaString| Renown|number| Replicate|manaString| Riot|new| +Saddle|number| Scavenge|cost| Shadow|instance| Shroud|instance|