[PIP] and [WHO] card implementations (#12482)

* Last Night Together

* Nanogene Conversion

* Return the Past

* Rose, Cutthroat Raider

* Diamond City

* Fix Apostrophe

* Various fixes

* Replace Diamond City and Celebration Watchers

* LastNightTogether improvements, add hint to ReturnThePast

* Add AttackedThisTurnOpponentsCount hint, ignore new failing Celebration test

* Review fixes, also create ValueConditionHint for value hints with a conditional threshold

* Comments improvements

* Requested changes to make ValueConditionHint extend ConditionHint

* single super call in ValueConditionHint constructor
This commit is contained in:
ssk97 2024-06-25 20:22:45 -07:00 committed by GitHub
parent e367aeae25
commit 2daa2b8820
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 657 additions and 75 deletions

View file

@ -11,7 +11,7 @@ import mage.constants.AbilityWord;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -32,7 +32,7 @@ public final class ArmoryMice extends CardImpl {
new BoostSourceEffect(0, 2, Duration.WhileOnBattlefield),
CelebrationCondition.instance, "{this} gets +0/+2 as long as two or more " +
"nonland permanents entered the battlefield under your control this turn"
)).addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION), new CelebrationWatcher());
)).addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION), new PermanentsEnteredBattlefieldWatcher());
}
private ArmoryMice(final ArmoryMice card) {

View file

@ -14,7 +14,7 @@ import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.counters.CounterType;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -43,7 +43,7 @@ public final class AshPartyCrasher extends CardImpl {
);
ability.setAbilityWord(AbilityWord.CELEBRATION);
ability.addHint(CelebrationCondition.getHint());
this.addAbility(ability, new CelebrationWatcher());
this.addAbility(ability, new PermanentsEnteredBattlefieldWatcher());
}
private AshPartyCrasher(final AshPartyCrasher card) {

View file

@ -15,7 +15,7 @@ import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.TargetController;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -47,7 +47,7 @@ public final class BelligerentOfTheBall extends CardImpl {
ability.addTarget(new TargetControlledCreaturePermanent());
ability.setAbilityWord(AbilityWord.CELEBRATION);
ability.addHint(CelebrationCondition.getHint());
this.addAbility(ability, new CelebrationWatcher());
this.addAbility(ability, new PermanentsEnteredBattlefieldWatcher());
}
private BelligerentOfTheBall(final BelligerentOfTheBall card) {

View file

@ -13,7 +13,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -46,7 +46,7 @@ public final class BespokeBattlegarb extends CardImpl {
ability.addTarget(new TargetControlledCreaturePermanent(0, 1));
ability.setAbilityWord(AbilityWord.CELEBRATION);
ability.addHint(CelebrationCondition.getHint());
this.addAbility(ability, new CelebrationWatcher());
this.addAbility(ability, new PermanentsEnteredBattlefieldWatcher());
// Equip {2}
this.addAbility(new EquipAbility(Outcome.BoostCreature, new GenericManaCost(2)));

View file

@ -0,0 +1,108 @@
package mage.cards.d;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.condition.Condition;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.decorator.ConditionalActivatedAbility;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.effects.common.counter.MoveCountersFromSourceToTargetEffect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueConditionHint;
import mage.abilities.mana.ColorlessManaAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.common.TargetCreaturePermanent;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.List;
import java.util.UUID;
/**
*
* @author notgreat
*/
public final class DiamondCity extends CardImpl {
public DiamondCity(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.LAND}, "");
// Diamond City enters the battlefield with a shield counter on it.
this.addAbility(new EntersBattlefieldAbility(
new AddCountersSourceEffect(CounterType.SHIELD.createInstance(1)),
"with a shield counter on it. <i>(If it would be dealt damage " +
"or destroyed, remove a shield counter from it instead.)</i>"
));
// {T}: Add {C}.
this.addAbility(new ColorlessManaAbility());
// {T}: Move a shield counter from Diamond City onto target creature. Activate only if two or more creatures entered the battlefield under your control this turn.
Ability ability = new ConditionalActivatedAbility(Zone.BATTLEFIELD, new MoveCountersFromSourceToTargetEffect(CounterType.SHIELD),
new TapSourceCost(), DiamondCityCondition.instance);
ability.addTarget(new TargetCreaturePermanent());
ability.addHint(DiamondCityCreaturesThatEnteredThisTurnCount.getHint());
this.addAbility(ability, new PermanentsEnteredBattlefieldWatcher());
}
private DiamondCity(final DiamondCity card) {
super(card);
}
@Override
public DiamondCity copy() {
return new DiamondCity(this);
}
}
enum DiamondCityCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
return DiamondCityCreaturesThatEnteredThisTurnCount.instance.calculate(game, source, null) >= 2;
}
@Override
public String toString() {
return "two or more creatures entered the battlefield under your control this turn";
}
}
enum DiamondCityCreaturesThatEnteredThisTurnCount implements DynamicValue {
instance;
private static final Hint hint = new ValueConditionHint(instance, DiamondCityCondition.instance);
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
PermanentsEnteredBattlefieldWatcher watcher = game.getState().getWatcher(PermanentsEnteredBattlefieldWatcher.class);
if (watcher != null) {
List<Permanent> list = watcher.getThisTurnEnteringPermanents(sourceAbility.getControllerId());
return (int) list.stream().filter(MageObject::isCreature).count();
}
return 0;
}
@Override
public DynamicValue copy() {
return instance;
}
@Override
public String getMessage() {
return "creatures that attacked this turn";
}
public static Hint getHint() {
return hint;
}
}

View file

@ -12,7 +12,7 @@ import mage.cards.CardSetInfo;
import mage.constants.AbilityWord;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -37,7 +37,7 @@ public final class GallantPieWielder extends CardImpl {
new GainAbilitySourceEffect(DoubleStrikeAbility.getInstance()),
CelebrationCondition.instance, "{this} has double strike as long as two or " +
"more nonland permanents entered the battlefield under your control this turn"
)).addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION), new CelebrationWatcher());
)).addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION), new PermanentsEnteredBattlefieldWatcher());
}
private GallantPieWielder(final GallantPieWielder card) {

View file

@ -16,7 +16,7 @@ import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.common.FilterCreaturePermanent;
import mage.game.permanent.token.custom.CreatureToken;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -65,7 +65,7 @@ public final class GoddricCloakedReveler extends CardImpl {
));
ability.setAbilityWord(AbilityWord.CELEBRATION);
ability.addHint(CelebrationCondition.getHint());
this.addAbility(ability, new CelebrationWatcher());
this.addAbility(ability, new PermanentsEnteredBattlefieldWatcher());
}
private GoddricCloakedReveler(final GoddricCloakedReveler card) {

View file

@ -14,7 +14,7 @@ import mage.constants.AbilityWord;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -42,7 +42,7 @@ public final class GrandBallGuest extends CardImpl {
+ "permanents entered the battlefield under your control this turn"
));
ability.addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION);
this.addAbility(ability, new CelebrationWatcher());
this.addAbility(ability, new PermanentsEnteredBattlefieldWatcher());
}
private GrandBallGuest(final GrandBallGuest card) {

View file

@ -11,7 +11,7 @@ import mage.constants.AbilityWord;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.TargetController;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -35,7 +35,7 @@ public final class LadyOfLaughter extends CardImpl {
this.addAbility(new BeginningOfEndStepTriggeredAbility(
new DrawCardSourceControllerEffect(1), TargetController.YOU,
CelebrationCondition.instance, false
).addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION), new CelebrationWatcher());
).addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION), new PermanentsEnteredBattlefieldWatcher());
}
private LadyOfLaughter(final LadyOfLaughter card) {

View file

@ -0,0 +1,137 @@
package mage.cards.l;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.UntapTargetEffect;
import mage.abilities.effects.common.combat.CantAttackAllEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.IndestructibleAbility;
import mage.abilities.keyword.VigilanceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.TurnPhase;
import mage.counters.CounterType;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.permanent.PermanentReferenceInCollectionPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.turn.TurnMod;
import mage.target.common.TargetCreaturePermanent;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author notgreat
*/
public final class LastNightTogether extends CardImpl {
public LastNightTogether(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{R}{G}");
// Choose two target creatures. Untap them. Put two +1/+1 counters on each of them. They gain vigilance, indestructible, and haste until end of turn. After this main phase, there is an additional combat phase. Only the chosen creatures can attack during that combat phase.
this.getSpellAbility().addEffect(new UntapTargetEffect().setText("Choose two target creatures. Untap them"));
this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance(2)).setText("Put two +1/+1 counters on each of them"));
this.getSpellAbility().addEffect(new GainAbilityTargetEffect(VigilanceAbility.getInstance()).setText("They gain vigilance"));
this.getSpellAbility().addEffect(new GainAbilityTargetEffect(IndestructibleAbility.getInstance()).setText("indestructible").concatBy(","));
this.getSpellAbility().addEffect(new GainAbilityTargetEffect(HasteAbility.getInstance()).setText("haste until end of turn").concatBy(", and"));
this.getSpellAbility().addEffect(new LastNightTogetherEffect());
this.getSpellAbility().addTarget(new TargetCreaturePermanent(2));
}
private LastNightTogether(final LastNightTogether card) {
super(card);
}
@Override
public LastNightTogether copy() {
return new LastNightTogether(this);
}
}
//Based on AddCombatAndMainPhaseEffect
class LastNightTogetherEffect extends OneShotEffect {
LastNightTogetherEffect() {
super(Outcome.Benefit);
staticText = "After this main phase, there is an additional combat phase. Only the chosen creatures can attack during that combat phase";
}
private LastNightTogetherEffect(final LastNightTogetherEffect effect) {
super(effect);
}
@Override
public LastNightTogetherEffect copy() {
return new LastNightTogetherEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
// 15.07.2006 If it's somehow not a main phase when Fury of the Horde resolves, all it does is untap all creatures that attacked that turn. No new phases are created.
// Same ruling applies here
if (game.getTurnPhaseType() == TurnPhase.PRECOMBAT_MAIN || game.getTurnPhaseType() == TurnPhase.POSTCOMBAT_MAIN) {
// At the start of that combat, add a restriction effect preventing other creatures from attacking.
TurnMod combat = new TurnMod(game.getState().getActivePlayerId()).withExtraPhase(TurnPhase.COMBAT);
game.getState().getTurnMods().add(combat);
List<UUID> targets = source.getTargets().get(0).getTargets();
LastNightTogetherDelayedCantAttackAbility delayedTriggeredAbility = new LastNightTogetherDelayedCantAttackAbility(targets, game, combat.getId());
game.addDelayedTriggeredAbility(delayedTriggeredAbility, source);
return true;
}
return true;
}
}
class LastNightTogetherDelayedCantAttackAbility extends DelayedTriggeredAbility {
private final UUID connectedTurnMod;
LastNightTogetherDelayedCantAttackAbility(List<UUID> targets, Game game, UUID connectedTurnMod) {
super(null, Duration.EndOfTurn);
FilterCreaturePermanent filterRestriction = new FilterCreaturePermanent();
Set<MageObjectReference> targetRefs = targets.stream().map(x -> new MageObjectReference(x, game)).collect(Collectors.toSet());
filterRestriction.add(Predicates.not(new PermanentReferenceInCollectionPredicate(targetRefs)));
this.addEffect(new CantAttackAllEffect(Duration.EndOfCombat, filterRestriction));
this.usesStack = false; // don't show this to the user
this.connectedTurnMod = connectedTurnMod;
}
LastNightTogetherDelayedCantAttackAbility(LastNightTogetherDelayedCantAttackAbility ability) {
super(ability);
this.connectedTurnMod = ability.connectedTurnMod;
}
@Override
public LastNightTogetherDelayedCantAttackAbility copy() {
return new LastNightTogetherDelayedCantAttackAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.PHASE_CHANGED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return (event.getType() == GameEvent.EventType.PHASE_CHANGED && this.connectedTurnMod.equals(event.getSourceId()));
}
@Override
public String getRule() {
return "Only the chosen creatures can attack during that combat phase";
}
}

View file

@ -36,7 +36,8 @@ public final class MilitantAngel extends CardImpl {
// When Militant Angel enters the battlefield, create a number of 2/2 white Knight creature tokens with vigilance equal to the number of opponents you attacked this turn.
Effect effect = new CreateTokenEffect(new KnightToken(), AttackedThisTurnOpponentsCount.instance);
effect.setText("create a number of 2/2 white Knight creature tokens with vigilance equal to the number of opponents you attacked this turn");
this.addAbility(new EntersBattlefieldTriggeredAbility(effect), new PlayersAttackedThisTurnWatcher());
this.addAbility(new EntersBattlefieldTriggeredAbility(effect).addHint(AttackedThisTurnOpponentsCount.getHint()),
new PlayersAttackedThisTurnWatcher());
}
private MilitantAngel(final MilitantAngel card) {

View file

@ -0,0 +1,76 @@
package mage.cards.n;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.SuperType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.util.functions.CopyApplier;
import java.util.UUID;
/**
*
* @author notgreat
*/
public final class NanogeneConversion extends CardImpl {
public NanogeneConversion(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{U}");
// Choose target creature you control. Each other creature becomes a copy of that creature until end of turn, except it isn't legendary.
this.getSpellAbility().addEffect(new NanogeneConversionEffect());
this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent());
}
private NanogeneConversion(final NanogeneConversion card) {
super(card);
}
@Override
public NanogeneConversion copy() {
return new NanogeneConversion(this);
}
}
//Based on Augmenter Pugilist's EchoingEquationEffect
class NanogeneConversionEffect extends OneShotEffect {
NanogeneConversionEffect() {
super(Outcome.Benefit);
staticText = "choose target creature you control. Each other creature you control becomes a copy of it until end of turn, except those creatures aren't legendary";
}
private NanogeneConversionEffect(final NanogeneConversionEffect effect) {
super(effect);
}
@Override
public NanogeneConversionEffect copy() {
return new NanogeneConversionEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent copyFrom = game.getPermanent(source.getFirstTarget());
if (copyFrom != null) {
game.getBattlefield().getActivePermanents(source.getControllerId(), game).stream()
.filter(permanent -> permanent.isCreature(game) && !permanent.getId().equals(copyFrom.getId()))
.forEach(copyTo -> game.copyPermanent(Duration.EndOfTurn, copyFrom, copyTo.getId(), source, new CopyApplier() {
@Override
public boolean apply(Game game, MageObject blueprint, Ability source, UUID targetObjectId) {
blueprint.removeSuperType(SuperType.LEGENDARY);
return true;
}
}));
return true;
}
return false;
}
}

View file

@ -12,7 +12,7 @@ import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.TargetController;
import mage.counters.CounterType;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -35,7 +35,7 @@ public final class PestsOfHonor extends CardImpl {
TargetController.YOU, false
), CelebrationCondition.instance, "At the beginning of combat on your turn, if two or more " +
"nonland permanents entered the battlefield under your control this turn, put a +1/+1 counter on {this}."
).addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION), new CelebrationWatcher());
).addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION), new PermanentsEnteredBattlefieldWatcher());
}
private PestsOfHonor(final PestsOfHonor card) {

View file

@ -16,7 +16,7 @@ import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import mage.watchers.common.SpellsCastWatcher;
import java.util.UUID;
@ -51,7 +51,7 @@ public final class RagingBattleMouse extends CardImpl {
ability.addTarget(new TargetControlledCreaturePermanent());
ability.setAbilityWord(AbilityWord.CELEBRATION);
ability.addHint(CelebrationCondition.getHint());
this.addAbility(ability, new CelebrationWatcher());
this.addAbility(ability, new PermanentsEnteredBattlefieldWatcher());
}
private RagingBattleMouse(final RagingBattleMouse card) {

View file

@ -0,0 +1,74 @@
package mage.cards.r;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.hint.common.MyTurnHint;
import mage.abilities.keyword.FlashbackAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.players.Player;
import java.util.UUID;
/**
*
* @author notgreat
*/
public final class ReturnThePast extends CardImpl {
public ReturnThePast(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{R}{R}");
// As long as it's your turn, each instant and sorcery card in your graveyard has flashback. Its flashback cost is equal to its mana cost.
this.addAbility(new SimpleStaticAbility(new ReturnThePastEffect()).addHint(MyTurnHint.instance));
}
private ReturnThePast(final ReturnThePast card) {
super(card);
}
@Override
public ReturnThePast copy() {
return new ReturnThePast(this);
}
}
//Based on LierDiscipleOfTheDrownedFlashbackEffect
class ReturnThePastEffect extends ContinuousEffectImpl {
ReturnThePastEffect() {
super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.staticText = "As long as it's your turn, each instant and sorcery card in your graveyard has flashback. " +
"Its flashback cost is equal to that card's mana cost";
}
private ReturnThePastEffect(final ReturnThePastEffect effect) {
super(effect);
}
@Override
public ReturnThePastEffect copy() {
return new ReturnThePastEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null || game.getActivePlayerId() != source.getControllerId()) {
return false;
}
for (Card card : player.getGraveyard().getCards(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY, game)) {
Ability ability = new FlashbackAbility(card, card.getManaCost());
ability.setSourceId(card.getId());
ability.setControllerId(card.getOwnerId());
game.getState().addOtherAbility(card, ability);
}
return true;
}
}

View file

@ -0,0 +1,71 @@
package mage.cards.r;
import mage.MageInt;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.common.EndOfCombatTriggeredAbility;
import mage.abilities.common.SacrificePermanentTriggeredAbility;
import mage.abilities.condition.common.RaidCondition;
import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility;
import mage.abilities.dynamicvalue.common.AttackedThisTurnOpponentsCount;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.mana.AddManaToManaPoolSourceControllerEffect;
import mage.abilities.keyword.FirstStrikeAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.AbilityWord;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.FilterPermanent;
import mage.game.permanent.token.JunkToken;
import mage.watchers.common.PlayerAttackedWatcher;
import mage.watchers.common.PlayersAttackedThisTurnWatcher;
import java.util.UUID;
/**
* @author notgreat
*/
public final class RoseCutthroatRaider extends CardImpl {
private static final FilterPermanent filter = new FilterPermanent("a Junk");
static {
filter.add(SubType.JUNK.getPredicate());
}
public RoseCutthroatRaider(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{2}{R}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.ROBOT);
this.power = new MageInt(3);
this.toughness = new MageInt(2);
// First strike
this.addAbility(FirstStrikeAbility.getInstance());
// Raid -- At end of combat on your turn, if you attacked this turn, create a Junk token for each opponent you attacked.
Ability ability = new ConditionalInterveningIfTriggeredAbility(new EndOfCombatTriggeredAbility(
new CreateTokenEffect(new JunkToken(), AttackedThisTurnOpponentsCount.instance), false), RaidCondition.instance,
"At end of combat on your turn, if you attacked this turn, create a Junk token for each opponent you attacked.");
ability.setAbilityWord(AbilityWord.RAID);
ability.addHint(AttackedThisTurnOpponentsCount.getHint());
ability.addWatcher(new PlayerAttackedWatcher());
ability.addWatcher(new PlayersAttackedThisTurnWatcher());
this.addAbility(ability);
// Whenever you sacrifice a Junk, add {R}.
this.addAbility(new SacrificePermanentTriggeredAbility(new AddManaToManaPoolSourceControllerEffect(Mana.RedMana(1)), filter));
}
private RoseCutthroatRaider(final RoseCutthroatRaider card) {
super(card);
}
@Override
public RoseCutthroatRaider copy() {
return new RoseCutthroatRaider(this);
}
}

View file

@ -15,7 +15,7 @@ import mage.constants.AbilityWord;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.watchers.common.CelebrationWatcher;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.UUID;
@ -46,7 +46,7 @@ public final class TuinvaleGuide extends CardImpl {
+ "permanents entered the battlefield under your control this turn"
));
ability.addHint(CelebrationCondition.getHint()).setAbilityWord(AbilityWord.CELEBRATION);
this.addAbility(ability, new CelebrationWatcher());
this.addAbility(ability, new PermanentsEnteredBattlefieldWatcher());
}
private TuinvaleGuide(final TuinvaleGuide card) {

View file

@ -137,6 +137,7 @@ public final class DoctorWho extends ExpansionSet {
cards.add(new SetCardInfo("Karvanista, Loyal Lupari", 106, Rarity.RARE, mage.cards.k.KarvanistaLoyalLupari.class));
cards.add(new SetCardInfo("Kate Stewart", 139, Rarity.RARE, mage.cards.k.KateStewart.class));
cards.add(new SetCardInfo("Laser Screwdriver", 178, Rarity.UNCOMMON, mage.cards.l.LaserScrewdriver.class));
cards.add(new SetCardInfo("Last Night Together", 140, Rarity.RARE, mage.cards.l.LastNightTogether.class));
cards.add(new SetCardInfo("Lavaclaw Reaches", 289, Rarity.RARE, mage.cards.l.LavaclawReaches.class));
cards.add(new SetCardInfo("Leela, Sevateem Warrior", 107, Rarity.RARE, mage.cards.l.LeelaSevateemWarrior.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Leela, Sevateem Warrior", 398, Rarity.RARE, mage.cards.l.LeelaSevateemWarrior.class, NON_FULL_USE_VARIOUS));
@ -152,6 +153,7 @@ public final class DoctorWho extends ExpansionSet {
cards.add(new SetCardInfo("Mountain", 202, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Myriad Landscape", 290, Rarity.UNCOMMON, mage.cards.m.MyriadLandscape.class));
cards.add(new SetCardInfo("Mystic Monastery", 291, Rarity.UNCOMMON, mage.cards.m.MysticMonastery.class));
cards.add(new SetCardInfo("Nanogene Conversion", 49, Rarity.RARE, mage.cards.n.NanogeneConversion.class));
cards.add(new SetCardInfo("Ominous Cemetery", 189, Rarity.UNCOMMON, mage.cards.o.OminousCemetery.class));
cards.add(new SetCardInfo("Out of Time", 209, Rarity.RARE, mage.cards.o.OutOfTime.class));
cards.add(new SetCardInfo("Overgrown Farmland", 292, Rarity.RARE, mage.cards.o.OvergrownFarmland.class));
@ -169,6 +171,7 @@ public final class DoctorWho extends ExpansionSet {
cards.add(new SetCardInfo("Regenerations Restored", 151, Rarity.RARE, mage.cards.r.RegenerationsRestored.class));
cards.add(new SetCardInfo("Reliquary Tower", 296, Rarity.UNCOMMON, mage.cards.r.ReliquaryTower.class));
cards.add(new SetCardInfo("Renegade Silent", 53, Rarity.UNCOMMON, mage.cards.r.RenegadeSilent.class));
cards.add(new SetCardInfo("Return the Past", 92, Rarity.RARE, mage.cards.r.ReturnThePast.class));
cards.add(new SetCardInfo("Return to Dust", 211, Rarity.UNCOMMON, mage.cards.r.ReturnToDust.class));
cards.add(new SetCardInfo("Reverse the Polarity", 54, Rarity.RARE, mage.cards.r.ReverseThePolarity.class));
cards.add(new SetCardInfo("River of Tears", 297, Rarity.RARE, mage.cards.r.RiverOfTears.class));

View file

@ -103,6 +103,7 @@ public final class Fallout extends ExpansionSet {
cards.add(new SetCardInfo("Darkwater Catacombs", 260, Rarity.RARE, mage.cards.d.DarkwaterCatacombs.class));
cards.add(new SetCardInfo("Deadly Dispute", 184, Rarity.COMMON, mage.cards.d.DeadlyDispute.class));
cards.add(new SetCardInfo("Desolate Mire", 146, Rarity.RARE, mage.cards.d.DesolateMire.class));
cards.add(new SetCardInfo("Diamond City", 147, Rarity.RARE, mage.cards.d.DiamondCity.class));
cards.add(new SetCardInfo("Dispatch", 159, Rarity.UNCOMMON, mage.cards.d.Dispatch.class));
cards.add(new SetCardInfo("Dogmeat, Ever Loyal", 2, Rarity.MYTHIC, mage.cards.d.DogmeatEverLoyal.class));
cards.add(new SetCardInfo("Dr. Madison Li", 3, Rarity.MYTHIC, mage.cards.d.DrMadisonLi.class));
@ -282,6 +283,7 @@ public final class Fallout extends ExpansionSet {
cards.add(new SetCardInfo("Robobrain War Mind", 38, Rarity.UNCOMMON, mage.cards.r.RobobrainWarMind.class));
cards.add(new SetCardInfo("Rogue's Passage", 283, Rarity.UNCOMMON, mage.cards.r.RoguesPassage.class));
cards.add(new SetCardInfo("Rootbound Crag", 284, Rarity.RARE, mage.cards.r.RootboundCrag.class));
cards.add(new SetCardInfo("Rose, Cutthroat Raider", 66, Rarity.RARE, mage.cards.r.RoseCutthroatRaider.class));
cards.add(new SetCardInfo("Ruinous Ultimatum", 220, Rarity.RARE, mage.cards.r.RuinousUltimatum.class));
cards.add(new SetCardInfo("Rustvale Bridge", 285, Rarity.COMMON, mage.cards.r.RustvaleBridge.class));
cards.add(new SetCardInfo("Ruthless Radrat", 48, Rarity.UNCOMMON, mage.cards.r.RuthlessRadrat.class));

View file

@ -0,0 +1,82 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author notgreat
*/
public class CelebrationTest extends CardTestPlayerBase {
@Test
public void testBasic1() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
addCard(Zone.HAND, playerA, "Armory Mice");
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Armory Mice");
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPowerToughness(playerA, "Armory Mice", 3, 1);
assertGraveyardCount(playerA, 0);
}
@Test
public void testBasic2() {
addCard(Zone.HAND, playerA, "Armory Mice");
addCard(Zone.HAND, playerA, "Black Lotus");
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Black Lotus", true);
setChoice(playerA, "White");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Armory Mice");
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPowerToughness(playerA, "Armory Mice", 3, 3);
assertGraveyardCount(playerA, 1);
}
@Test
public void testContinuousModifier1() {
addCard(Zone.BATTLEFIELD, playerA, "Ashaya, Soul of the Wild");
addCard(Zone.HAND, playerA, "Armory Mice");
addCard(Zone.HAND, playerA, "Black Lotus");
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Black Lotus", true);
setChoice(playerA, "White");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Armory Mice");
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPowerToughness(playerA, "Armory Mice", 3, 1);
assertGraveyardCount(playerA, 1);
}
@Test
@Ignore //Currently failing due to PermanentsEnteredBattlefieldWatcher not storing permanents' current state
public void testContinuousModifier2() {
addCard(Zone.BATTLEFIELD, playerA, "Ashaya, Soul of the Wild");
addCard(Zone.HAND, playerA, "Armory Mice");
addCard(Zone.HAND, playerA, "Swords to Plowshares");
addCard(Zone.HAND, playerA, "Black Lotus");
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Black Lotus", true);
setChoice(playerA, "White");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Armory Mice", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swords to Plowshares", "Ashaya, Soul of the Wild", true);
//Even though the Ashaya is now gone, the Armory Mice entered as a land
//Thus only the only nonland permanent that ETB this turn is Black Lotus, and Celebration should not be on
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPowerToughness(playerA, "Armory Mice", 3, 1);
assertGraveyardCount(playerA, 2);
}
}

View file

@ -3,22 +3,26 @@ package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.hint.ConditionHint;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueConditionHint;
import mage.game.Game;
import mage.watchers.common.CelebrationWatcher;
import mage.game.permanent.Permanent;
import mage.watchers.common.PermanentsEnteredBattlefieldWatcher;
import java.util.List;
/**
* @author Susucr
*/
public enum CelebrationCondition implements Condition {
instance;
private static final Hint hint = new ConditionHint(instance, "You had two or more nonland permanents enter this turn");
private static final Hint hint = new ValueConditionHint(CelebrationNonlandsThatEnteredThisTurnCount.instance, instance);
@Override
public boolean apply(Game game, Ability source) {
CelebrationWatcher watcher = game.getState().getWatcher(CelebrationWatcher.class);
return watcher != null && watcher.celebrationActive(source.getControllerId());
return CelebrationNonlandsThatEnteredThisTurnCount.instance.calculate(game, source, null) >= 2;
}
@Override
@ -30,3 +34,29 @@ public enum CelebrationCondition implements Condition {
return hint;
}
}
enum CelebrationNonlandsThatEnteredThisTurnCount implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
PermanentsEnteredBattlefieldWatcher watcher = game.getState().getWatcher(PermanentsEnteredBattlefieldWatcher.class);
if (watcher != null) {
List<Permanent> list = watcher.getThisTurnEnteringPermanents(sourceAbility.getControllerId());
return (int) list.stream().filter(x -> !x.isLand(game)).count();
}
return 0;
}
@Override
public DynamicValue copy() {
return instance;
}
@Override
public String getMessage() {
return "nonland permanents that entered under your control this turn";
}
}

View file

@ -4,6 +4,8 @@ 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.abilities.hint.ValueHint;
import mage.game.Game;
import mage.watchers.common.PlayersAttackedThisTurnWatcher;
@ -14,6 +16,7 @@ import mage.watchers.common.PlayersAttackedThisTurnWatcher;
*/
public enum AttackedThisTurnOpponentsCount implements DynamicValue {
instance;
private static final Hint hint = new ValueHint("the number of opponents you attacked this turn", instance);
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
@ -38,4 +41,8 @@ public enum AttackedThisTurnOpponentsCount implements DynamicValue {
public String getMessage() {
return "the number of opponents you attacked this turn";
}
public static Hint getHint() {
return hint;
}
}

View file

@ -0,0 +1,38 @@
package mage.abilities.hint;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.game.Game;
/**
* @author notgreat
*/
public class ValueConditionHint extends ConditionHint {
private final DynamicValue value;
public ValueConditionHint(DynamicValue value, Condition condition) {
this(condition.toString(), value, condition);
}
public ValueConditionHint(String name, DynamicValue value, Condition condition) {
super(condition, name);
this.value = value;
}
private ValueConditionHint(final ValueConditionHint hint) {
super(hint);
this.value = hint.value.copy();
}
@Override
public String getText(Game game, Ability ability) {
return super.getText(game, ability) + " (current: " + value.calculate(game, ability, null) + ")";
}
@Override
public ValueConditionHint copy() {
return new ValueConditionHint(this);
}
}

View file

@ -1,47 +0,0 @@
package mage.watchers.common;
import mage.constants.WatcherScope;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.util.CardUtil;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author Susucr
*/
public class CelebrationWatcher extends Watcher {
// playerId -> number of nonland permanents entered the battlefield this turn under that player's control.
private final Map<UUID, Integer> celebrationCounts = new HashMap<>();
public CelebrationWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() != GameEvent.EventType.ENTERS_THE_BATTLEFIELD) {
return;
}
Permanent permanent = game.getPermanent(event.getTargetId());
if (permanent != null && !permanent.isLand(game)) {
celebrationCounts.compute(permanent.getControllerId(), CardUtil::setOrIncrementValue);
}
}
public boolean celebrationActive(UUID playerId) {
return celebrationCounts.getOrDefault(playerId, 0) >= 2;
}
@Override
public void reset() {
super.reset();
celebrationCounts.clear();
}
}