mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
[DSK] Implement Acrobatic Cheerleader and per-game trigger limits (#13232)
This commit is contained in:
parent
3d147552d1
commit
b58fbbdd84
6 changed files with 220 additions and 6 deletions
43
Mage.Sets/src/mage/cards/a/AcrobaticCheerleader.java
Normal file
43
Mage.Sets/src/mage/cards/a/AcrobaticCheerleader.java
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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("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("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("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("Anthropede", 167, Rarity.COMMON, mage.cards.a.Anthropede.class));
|
||||||
cards.add(new SetCardInfo("Appendage Amalgam", 83, Rarity.COMMON, mage.cards.a.AppendageAmalgam.class));
|
cards.add(new SetCardInfo("Appendage Amalgam", 83, Rarity.COMMON, mage.cards.a.AppendageAmalgam.class));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -244,7 +244,8 @@ public class TriggeredAbilities extends LinkedHashMap<String, TriggeredAbility>
|
||||||
NumberOfTriggersEvent numberOfTriggersEvent = new NumberOfTriggersEvent(ability, event);
|
NumberOfTriggersEvent numberOfTriggersEvent = new NumberOfTriggersEvent(ability, event);
|
||||||
// event == null - state based triggers like StateTriggeredAbility, must be ignored for number event
|
// event == null - state based triggers like StateTriggeredAbility, must be ignored for number event
|
||||||
if (event == null || !game.replaceEvent(numberOfTriggersEvent, ability)) {
|
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++) {
|
for (int i = 0; i < numTriggers; i++) {
|
||||||
if (this.enableIntegrityLogs) {
|
if (this.enableIntegrityLogs) {
|
||||||
logger.info("trigger will be USED: " + ability);
|
logger.info("trigger will be USED: " + ability);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ public interface TriggeredAbility extends Ability {
|
||||||
*/
|
*/
|
||||||
TriggeredAbility setTriggersLimitEachTurn(int limit);
|
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.
|
* 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.
|
* 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);
|
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);
|
TriggeredAbility setDoOnlyOnceEachTurn(boolean doOnlyOnce);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@ import mage.constants.Zone;
|
||||||
import mage.game.Game;
|
import mage.game.Game;
|
||||||
import mage.game.events.BatchEvent;
|
import mage.game.events.BatchEvent;
|
||||||
import mage.game.events.GameEvent;
|
import mage.game.events.GameEvent;
|
||||||
import mage.game.events.ZoneChangeBatchEvent;
|
|
||||||
import mage.game.events.ZoneChangeEvent;
|
import mage.game.events.ZoneChangeEvent;
|
||||||
import mage.game.permanent.Permanent;
|
|
||||||
import mage.game.permanent.PermanentToken;
|
import mage.game.permanent.PermanentToken;
|
||||||
import mage.players.Player;
|
import mage.players.Player;
|
||||||
import mage.util.CardUtil;
|
import mage.util.CardUtil;
|
||||||
|
|
@ -31,6 +29,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
|
||||||
private Condition interveningIfCondition;
|
private Condition interveningIfCondition;
|
||||||
private boolean leavesTheBattlefieldTrigger;
|
private boolean leavesTheBattlefieldTrigger;
|
||||||
private int triggerLimitEachTurn = Integer.MAX_VALUE; // for "triggers only once|twice each turn"
|
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 doOnlyOnceEachTurn = false;
|
||||||
private boolean replaceRuleText = false; // if true, replace "{this}" with "it" in effect text
|
private boolean replaceRuleText = false; // if true, replace "{this}" with "it" in effect text
|
||||||
private GameEvent triggerEvent = null;
|
private GameEvent triggerEvent = null;
|
||||||
|
|
@ -60,6 +59,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
|
||||||
this.interveningIfCondition = ability.interveningIfCondition;
|
this.interveningIfCondition = ability.interveningIfCondition;
|
||||||
this.leavesTheBattlefieldTrigger = ability.leavesTheBattlefieldTrigger;
|
this.leavesTheBattlefieldTrigger = ability.leavesTheBattlefieldTrigger;
|
||||||
this.triggerLimitEachTurn = ability.triggerLimitEachTurn;
|
this.triggerLimitEachTurn = ability.triggerLimitEachTurn;
|
||||||
|
this.triggerLimitEachGame = ability.triggerLimitEachGame;
|
||||||
this.doOnlyOnceEachTurn = ability.doOnlyOnceEachTurn;
|
this.doOnlyOnceEachTurn = ability.doOnlyOnceEachTurn;
|
||||||
this.replaceRuleText = ability.replaceRuleText;
|
this.replaceRuleText = ability.replaceRuleText;
|
||||||
this.triggerEvent = ability.triggerEvent;
|
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) {
|
public void trigger(Game game, UUID controllerId, GameEvent triggeringEvent) {
|
||||||
//20091005 - 603.4
|
//20091005 - 603.4
|
||||||
if (checkInterveningIfClause(game)) {
|
if (checkInterveningIfClause(game)) {
|
||||||
setLastTrigger(game);
|
updateTurnCount(game);
|
||||||
|
updateGameCount(game);
|
||||||
game.addTriggeredAbility(this, triggeringEvent);
|
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) {
|
if (triggerLimitEachTurn == Integer.MAX_VALUE) {
|
||||||
return;
|
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
|
@Override
|
||||||
public TriggeredAbilityImpl setTriggerPhrase(String triggerPhrase) {
|
public TriggeredAbilityImpl setTriggerPhrase(String triggerPhrase) {
|
||||||
this.triggerPhrase = triggerPhrase;
|
this.triggerPhrase = triggerPhrase;
|
||||||
|
|
@ -126,7 +144,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean checkTriggeredLimit(Game game) {
|
public boolean checkTriggeredLimit(Game game) {
|
||||||
return getRemainingTriggersLimitEachTurn(game) > 0;
|
return getRemainingTriggersLimitEachGame(game) > 0 && getRemainingTriggersLimitEachTurn(game) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -146,6 +164,12 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TriggeredAbility setTriggersLimitEachGame(int limit) {
|
||||||
|
this.triggerLimitEachGame = limit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRemainingTriggersLimitEachTurn(Game game) {
|
public int getRemainingTriggersLimitEachTurn(Game game) {
|
||||||
if (triggerLimitEachTurn == Integer.MAX_VALUE) {
|
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
|
@Override
|
||||||
public TriggeredAbility setDoOnlyOnceEachTurn(boolean doOnlyOnce) {
|
public TriggeredAbility setDoOnlyOnceEachTurn(boolean doOnlyOnce) {
|
||||||
this.doOnlyOnceEachTurn = doOnlyOnce;
|
this.doOnlyOnceEachTurn = doOnlyOnce;
|
||||||
|
|
@ -300,6 +334,21 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
|
||||||
}
|
}
|
||||||
sb.append(" each turn.");
|
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) {
|
if (doOnlyOnceEachTurn) {
|
||||||
sb.append(" Do this only once each turn.");
|
sb.append(" Do this only once each turn.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue