diff --git a/Mage.Sets/src/mage/cards/a/AcrobaticCheerleader.java b/Mage.Sets/src/mage/cards/a/AcrobaticCheerleader.java new file mode 100644 index 00000000000..11b89648186 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AcrobaticCheerleader.java @@ -0,0 +1,43 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.TriggeredAbility; +import mage.abilities.abilityword.SurvivalAbility; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; + +import java.util.UUID; + +/** + * @author markort147 + */ +public final class AcrobaticCheerleader extends CardImpl { + + public AcrobaticCheerleader(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SURVIVOR); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Survival -- At the beginning of your second main phase, if Acrobatic Cheerleader is tapped, put a flying counter on it. This ability triggers only once. + TriggeredAbility ability = new SurvivalAbility(new AddCountersSourceEffect(CounterType.FLYING.createInstance())) + .setTriggersLimitEachGame(1) + .withRuleTextReplacement(true); + this.addAbility(ability); + } + + private AcrobaticCheerleader(final AcrobaticCheerleader card) { + super(card); + } + + @Override + public AcrobaticCheerleader copy() { + return new AcrobaticCheerleader(this); + } +} diff --git a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java index 069107adced..4ddb9287005 100644 --- a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java +++ b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorror.java @@ -23,6 +23,7 @@ public final class DuskmournHouseOfHorror extends ExpansionSet { cards.add(new SetCardInfo("Abandoned Campground", 255, Rarity.COMMON, mage.cards.a.AbandonedCampground.class)); cards.add(new SetCardInfo("Abhorrent Oculus", 42, Rarity.MYTHIC, mage.cards.a.AbhorrentOculus.class)); + cards.add(new SetCardInfo("Acrobatic Cheerleader", 1, Rarity.COMMON, mage.cards.a.AcrobaticCheerleader.class)); cards.add(new SetCardInfo("Altanak, the Thrice-Called", 166, Rarity.UNCOMMON, mage.cards.a.AltanakTheThriceCalled.class)); cards.add(new SetCardInfo("Anthropede", 167, Rarity.COMMON, mage.cards.a.Anthropede.class)); cards.add(new SetCardInfo("Appendage Amalgam", 83, Rarity.COMMON, mage.cards.a.AppendageAmalgam.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/rules/TriggerAbilityOnlyLimitedTimesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/rules/TriggerAbilityOnlyLimitedTimesTest.java new file mode 100644 index 00000000000..6a382c3830c --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/rules/TriggerAbilityOnlyLimitedTimesTest.java @@ -0,0 +1,108 @@ + + +package org.mage.test.cards.rules; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author markort147 + */ + +public class TriggerAbilityOnlyLimitedTimesTest extends CardTestPlayerBase { + + /** + * Enduring Innocence {1}{W}{W} + * Enchantment Creature - Sheep Glimmer + * 2/1 + * Lifelink + * Whenever one or more other creatures you control with power 2 or less enter, draw a card. This ability triggers only once each turn. + * When Enduring Innocence dies, if it was a creature, return it to the battlefield under its owner's control. It's an enchantment. (It's not a creature.) + */ + @Test + public void testTriggerOnceEachTurn() { + addCard(Zone.HAND, playerA, "Llanowar Elves", 2); + addCard(Zone.BATTLEFIELD, playerA, "Enduring Innocence", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Llanowar Elves", true); // Draw a card + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Llanowar Elves", true); // Do not draw a card + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertHandCount(playerA, 1); + } + + /** + * Momentary Blink {1}{W} + * Instant + * Exile target creature you control, then return it to the battlefield under its owner's control. + * Flashback (You may cast this card from your graveyard for its flashback cost. Then exile it.) + */ + @Test + public void testTriggerTwiceSameTurnIfBlinked() { + + addCard(Zone.HAND, playerA, "Llanowar Elves", 2); + addCard(Zone.HAND, playerA, "Momentary Blink"); + addCard(Zone.BATTLEFIELD, playerA, "Enduring Innocence", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Llanowar Elves", true); // Draw a card + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Momentary Blink", "Enduring Innocence", true); // Blink + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Llanowar Elves", true); // Draw a card again + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertHandCount(playerA, 2); + assertPermanentCount(playerA, "Enduring Innocence", 1); + } + + /** + * Acrobatic Cheerleader {1}{W} + * Creature - Human Survivor + * 2/2 + * Survival — At the beginning of your second main phase, if Acrobatic Cheerleader is tapped, put a flying counter on it. This ability triggers only once. + */ + @Test + public void testTriggerOnceEachGame() { + addCard(Zone.BATTLEFIELD, playerA, "Acrobatic Cheerleader", 1); + + attack(3, playerA, "Acrobatic Cheerleader", playerB); // Put a flying counter + attack(5, playerA, "Acrobatic Cheerleader", playerB); // Do not put another flying counter + + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertCounterCount(playerA, "Acrobatic Cheerleader", CounterType.FLYING, 1); + } + + /** + * Momentary Blink {1}{W} + * Instant + * Exile target creature you control, then return it to the battlefield under its owner's control. + * Flashback (You may cast this card from your graveyard for its flashback cost. Then exile it.) + */ + @Test + public void testTriggerTwiceSameGameIfBlinked() { + addCard(Zone.BATTLEFIELD, playerA, "Acrobatic Cheerleader", 1); + addCard(Zone.HAND, playerA, "Momentary Blink"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + attack(3, playerA, "Acrobatic Cheerleader", playerB); // Put a flying counter + castSpell(3, PhaseStep.END_TURN, playerA, "Momentary Blink", "Acrobatic Cheerleader"); // Blinks and loses the counter + attack(5, playerA, "Acrobatic Cheerleader", playerB); // Put a flying counter + + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertCounterCount(playerA, "Acrobatic Cheerleader", CounterType.FLYING, 1); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilities.java b/Mage/src/main/java/mage/abilities/TriggeredAbilities.java index 3670dc461f6..3d9a78b5acb 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbilities.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbilities.java @@ -244,7 +244,8 @@ public class TriggeredAbilities extends LinkedHashMap NumberOfTriggersEvent numberOfTriggersEvent = new NumberOfTriggersEvent(ability, event); // event == null - state based triggers like StateTriggeredAbility, must be ignored for number event if (event == null || !game.replaceEvent(numberOfTriggersEvent, ability)) { - int numTriggers = Integer.min(ability.getRemainingTriggersLimitEachTurn(game), numberOfTriggersEvent.getAmount()); + int limit = Integer.min(ability.getRemainingTriggersLimitEachGame(game), ability.getRemainingTriggersLimitEachTurn(game)); + int numTriggers = Integer.min(limit, numberOfTriggersEvent.getAmount()); for (int i = 0; i < numTriggers; i++) { if (this.enableIntegrityLogs) { logger.info("trigger will be USED: " + ability); diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbility.java b/Mage/src/main/java/mage/abilities/TriggeredAbility.java index f47a10f76c6..f948f9ffe4b 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbility.java @@ -40,6 +40,11 @@ public interface TriggeredAbility extends Ability { */ TriggeredAbility setTriggersLimitEachTurn(int limit); + /** + * limit the number of triggers each game + */ + TriggeredAbility setTriggersLimitEachGame(int limit); + /** * Get the number of times the trigger may trigger this turn. * e.g. 0, 1 or 2 for a trigger that is limited to trigger twice each turn. @@ -47,6 +52,13 @@ public interface TriggeredAbility extends Ability { */ int getRemainingTriggersLimitEachTurn(Game game); + /** + * Get the number of times the trigger may trigger this game. + * e.g. 0, 1 or 2 for a trigger that is limited to trigger twice each game. + * Integer.MAX_VALUE when no limit. + */ + int getRemainingTriggersLimitEachGame(Game game); + TriggeredAbility setDoOnlyOnceEachTurn(boolean doOnlyOnce); /** diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java index 212300758ed..cb87938acdc 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java @@ -10,9 +10,7 @@ import mage.constants.Zone; import mage.game.Game; import mage.game.events.BatchEvent; import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeBatchEvent; import mage.game.events.ZoneChangeEvent; -import mage.game.permanent.Permanent; import mage.game.permanent.PermanentToken; import mage.players.Player; import mage.util.CardUtil; @@ -31,6 +29,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge private Condition interveningIfCondition; private boolean leavesTheBattlefieldTrigger; private int triggerLimitEachTurn = Integer.MAX_VALUE; // for "triggers only once|twice each turn" + private int triggerLimitEachGame = Integer.MAX_VALUE; // for "triggers only once|twice" private boolean doOnlyOnceEachTurn = false; private boolean replaceRuleText = false; // if true, replace "{this}" with "it" in effect text private GameEvent triggerEvent = null; @@ -60,6 +59,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge this.interveningIfCondition = ability.interveningIfCondition; this.leavesTheBattlefieldTrigger = ability.leavesTheBattlefieldTrigger; this.triggerLimitEachTurn = ability.triggerLimitEachTurn; + this.triggerLimitEachGame = ability.triggerLimitEachGame; this.doOnlyOnceEachTurn = ability.doOnlyOnceEachTurn; this.replaceRuleText = ability.replaceRuleText; this.triggerEvent = ability.triggerEvent; @@ -70,7 +70,8 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge public void trigger(Game game, UUID controllerId, GameEvent triggeringEvent) { //20091005 - 603.4 if (checkInterveningIfClause(game)) { - setLastTrigger(game); + updateTurnCount(game); + updateGameCount(game); game.addTriggeredAbility(this, triggeringEvent); } } @@ -89,7 +90,14 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge ); } - private void setLastTrigger(Game game) { + // Used for triggers with a per-game limit. + private String getKeyGameTriggeredCount(Game game) { + return CardUtil.getCardZoneString( + "gameTriggeredCount|" + getOriginalId(), getSourceId(), game + ); + } + + private void updateTurnCount(Game game) { if (triggerLimitEachTurn == Integer.MAX_VALUE) { return; } @@ -108,6 +116,16 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge } } + private void updateGameCount(Game game) { + if (triggerLimitEachGame == Integer.MAX_VALUE) { + return; + } + String keyGameTriggeredCount = getKeyGameTriggeredCount(game); + int lastCount = Optional.ofNullable((Integer) game.getState().getValue(keyGameTriggeredCount)).orElse(0); + // Incrementing the count. + game.getState().setValue(keyGameTriggeredCount, lastCount + 1); + } + @Override public TriggeredAbilityImpl setTriggerPhrase(String triggerPhrase) { this.triggerPhrase = triggerPhrase; @@ -126,7 +144,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge @Override public boolean checkTriggeredLimit(Game game) { - return getRemainingTriggersLimitEachTurn(game) > 0; + return getRemainingTriggersLimitEachGame(game) > 0 && getRemainingTriggersLimitEachTurn(game) > 0; } @Override @@ -146,6 +164,12 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge return this; } + @Override + public TriggeredAbility setTriggersLimitEachGame(int limit) { + this.triggerLimitEachGame = limit; + return this; + } + @Override public int getRemainingTriggersLimitEachTurn(Game game) { if (triggerLimitEachTurn == Integer.MAX_VALUE) { @@ -165,6 +189,16 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge } } + @Override + public int getRemainingTriggersLimitEachGame(Game game) { + if (triggerLimitEachGame == Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + String keyGameTriggeredCount = getKeyGameTriggeredCount(game); + int count = Optional.ofNullable((Integer) game.getState().getValue(keyGameTriggeredCount)).orElse(0); + return Math.max(0, triggerLimitEachGame - count); + } + @Override public TriggeredAbility setDoOnlyOnceEachTurn(boolean doOnlyOnce) { this.doOnlyOnceEachTurn = doOnlyOnce; @@ -300,6 +334,21 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge } sb.append(" each turn."); } + if (triggerLimitEachGame != Integer.MAX_VALUE) { + sb.append(" This ability triggers only "); + switch (triggerLimitEachGame) { + case 1: + sb.append("once."); + break; + case 2: + // No card with that behavior yet, so feel free to change the text once one exist + sb.append("twice."); + break; + default: + // No card with that behavior yet, so feel free to change the text once one exist + sb.append(CardUtil.numberToText(triggerLimitEachGame)).append(" times."); + } + } if (doOnlyOnceEachTurn) { sb.append(" Do this only once each turn."); }