Merge pull request 'master' (#22) from External/mage:master into master
All checks were successful
/ example-docker-compose (push) Successful in 28m47s

Reviewed-on: #22
This commit is contained in:
Failure 2025-03-22 16:45:51 -07:00
commit c8496d6bbe
133 changed files with 5368 additions and 224 deletions

View file

@ -36,7 +36,6 @@ public class CastSecondSpellTriggeredAbility extends TriggeredAbilityImpl {
}
/**
*
* @param zone What zone the ability can trigger from (see {@link mage.abilities.Ability#getZone})
* @param effect What effect will happen when this ability triggers (see {@link mage.abilities.Ability#getEffects})
* @param targetController Which player(s) to pay attention to
@ -44,7 +43,7 @@ public class CastSecondSpellTriggeredAbility extends TriggeredAbilityImpl {
* @param setTargetPointer Who to set the target pointer of the effects to. Only accepts NONE, PLAYER (the player who cast the spell), and SPELL (the spell which was cast)
*/
public CastSecondSpellTriggeredAbility(Zone zone, Effect effect, TargetController targetController,
boolean optional, SetTargetPointer setTargetPointer) {
boolean optional, SetTargetPointer setTargetPointer) {
super(zone, effect, optional);
if (targetController == TargetController.YOU) {
this.addHint(hint);
@ -54,7 +53,7 @@ public class CastSecondSpellTriggeredAbility extends TriggeredAbilityImpl {
setTriggerPhrase(generateTriggerPhrase());
}
private CastSecondSpellTriggeredAbility(final CastSecondSpellTriggeredAbility ability) {
protected CastSecondSpellTriggeredAbility(final CastSecondSpellTriggeredAbility ability) {
super(ability);
this.targetController = ability.targetController;
this.setTargetPointer = ability.setTargetPointer;

View file

@ -29,7 +29,7 @@ public class EntersBattlefieldOneOrMoreTriggeredAbility extends TriggeredAbility
setTriggerPhrase(generateTriggerPhrase());
}
private EntersBattlefieldOneOrMoreTriggeredAbility(final EntersBattlefieldOneOrMoreTriggeredAbility ability) {
protected EntersBattlefieldOneOrMoreTriggeredAbility(final EntersBattlefieldOneOrMoreTriggeredAbility ability) {
super(ability);
this.filter = ability.filter;
this.targetController = ability.targetController;

View file

@ -0,0 +1,26 @@
package mage.abilities.common;
import mage.abilities.effects.Effect;
import mage.constants.AbilityWord;
import mage.constants.TargetController;
import mage.constants.Zone;
/**
* @author TheElk801
*/
public class FlurryAbility extends CastSecondSpellTriggeredAbility {
public FlurryAbility(Effect effect) {
super(Zone.BATTLEFIELD, effect, TargetController.YOU, false);
this.setAbilityWord(AbilityWord.FLURRY);
}
private FlurryAbility(final FlurryAbility ability) {
super(ability);
}
@Override
public FlurryAbility copy() {
return new FlurryAbility(this);
}
}

View file

@ -0,0 +1,83 @@
package mage.abilities.common;
import mage.abilities.Ability;
import mage.abilities.costs.common.ExileSourceFromGraveCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.constants.AbilityWord;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.counters.Counter;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.common.TargetCreaturePermanent;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public class RenewAbility extends ActivateAsSorceryActivatedAbility {
public RenewAbility(String manaString, Counter... counters) {
super(Zone.GRAVEYARD, new RenewEffect(counters), new ManaCostsImpl<>(manaString));
this.addCost(new ExileSourceFromGraveCost());
this.addTarget(new TargetCreaturePermanent());
this.setAbilityWord(AbilityWord.RENEW);
}
private RenewAbility(final RenewAbility ability) {
super(ability);
}
@Override
public RenewAbility copy() {
return new RenewAbility(this);
}
}
class RenewEffect extends OneShotEffect {
private final List<Counter> counters = new ArrayList<>();
RenewEffect(Counter... counters) {
super(Outcome.Benefit);
for (Counter counter : counters) {
this.counters.add(counter);
}
staticText = makeText(this.counters);
}
private RenewEffect(final RenewEffect effect) {
super(effect);
for (Counter counter : effect.counters) {
this.counters.add(counter.copy());
}
}
@Override
public RenewEffect copy() {
return new RenewEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (permanent == null) {
return false;
}
for (Counter counter : counters) {
permanent.addCounters(counter, source, game);
}
return true;
}
private static String makeText(List<Counter> counters) {
return "put " + CardUtil.concatWithAnd(
counters.stream().map(Counter::getDescription).collect(Collectors.toList())
) + " on target creature";
}
}

View file

@ -14,27 +14,23 @@ import mage.util.CardUtil;
*
* @author Jmlundeen
*/
public class CardsInExileCondition implements Condition
{
public class CardsInExileCondition implements Condition {
private final ComparisonType type;
private final int count;
private final DynamicValue cardsInExileCount;
public CardsInExileCondition(ComparisonType type, int count)
{
public CardsInExileCondition(ComparisonType type, int count) {
this(type, count, CardsInExileCount.ALL);
}
public CardsInExileCondition(ComparisonType type, int count, DynamicValue cardsInExileCount)
{
public CardsInExileCondition(ComparisonType type, int count, DynamicValue cardsInExileCount) {
this.type = type;
this.count = count;
this.cardsInExileCount = cardsInExileCount;
}
@Override
public boolean apply(Game game, Ability source)
{
public boolean apply(Game game, Ability source) {
int exileCards = cardsInExileCount.calculate(game, source, null);
return ComparisonType.compare(exileCards, type, count);
}

View file

@ -0,0 +1,42 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.costs.common.SacrificeTargetCost;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.util.CardUtil;
import java.util.Collection;
public class SacrificedPermanentCondition implements Condition {
private final FilterPermanent filter;
private boolean useThisWay = false;
public SacrificedPermanentCondition(FilterPermanent filter) {
this.filter = filter;
}
public SacrificedPermanentCondition(FilterPermanent filter, boolean useThisWay) {
this.filter = filter;
this.useThisWay = useThisWay;
}
@Override
public boolean apply(Game game, Ability source) {
return CardUtil.castStream(source.getCosts()
.stream(), SacrificeTargetCost.class)
.map(SacrificeTargetCost::getPermanents)
.flatMap(Collection::stream)
.anyMatch(permanent -> filter.match(permanent, source.getControllerId(), source, game));
}
@Override
public String toString() {
if (useThisWay) {
return "if " + filter.getMessage() + " was sacrificed this way";
}
return "if the sacrificed " + filter.getMessage();
}
}

View file

@ -0,0 +1,109 @@
package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.cards.Card;
import mage.cards.CardsImpl;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.filter.FilterCard;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.TargetPermanent;
import mage.target.common.TargetCardInHand;
import java.util.Optional;
import java.util.UUID;
/**
* @author TheElk801
*/
public class BeholdDragonCost extends CostImpl {
private static final FilterPermanent filterPermanent = new FilterControlledPermanent(SubType.DRAGON);
private static final FilterCard filterCard = new FilterCard("a Dragon card");
static {
filterCard.add(SubType.DRAGON.getPredicate());
}
public BeholdDragonCost() {
super();
this.text = "behold a Dragon";
}
private BeholdDragonCost(final BeholdDragonCost cost) {
super(cost);
}
@Override
public BeholdDragonCost copy() {
return new BeholdDragonCost(this);
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
return Optional
.ofNullable(game.getPlayer(controllerId))
.map(Player::getHand)
.map(cards -> cards.count(filterCard, game) > 0)
.orElse(false)
|| game
.getBattlefield()
.contains(filterPermanent, controllerId, source, game, 1);
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Player player = game.getPlayer(controllerId);
if (player == null) {
paid = false;
return paid;
}
boolean hasPermanent = game
.getBattlefield()
.contains(filterPermanent, controllerId, source, game, 1);
boolean hasHand = player.getHand().count(filterCard, game) > 0;
boolean usePermanent;
if (hasPermanent && hasHand) {
usePermanent = player.chooseUse(
Outcome.Neutral, "Choose a Dragon you control or reveal one from your hand?",
null, "Choose controlled", "Reveal from hand", source, game);
} else if (hasPermanent) {
usePermanent = true;
} else if (hasHand) {
usePermanent = false;
} else {
paid = false;
return paid;
}
if (usePermanent) {
TargetPermanent target = new TargetPermanent(filterPermanent);
target.withNotTarget(true);
player.choose(Outcome.Neutral, target, source, game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent == null) {
paid = false;
return paid;
}
game.informPlayers(player.getLogName() + " chooses to behold " + permanent.getLogName());
paid = true;
return true;
}
TargetCard target = new TargetCardInHand(filterCard);
player.choose(Outcome.Neutral, player.getHand(), target, source, game);
Card card = game.getCard(target.getFirstTarget());
if (card == null) {
paid = false;
return paid;
}
player.revealCards(source, new CardsImpl(card), game);
paid = true;
return paid;
}
}

View file

@ -7,15 +7,34 @@ import mage.constants.TurnPhase;
import mage.game.Game;
import mage.game.turn.TurnMod;
public class AdditionalCombatPhaseEffect extends OneShotEffect {
private final int additionalPhases;
public AdditionalCombatPhaseEffect() {
super(Outcome.Benefit);
this.additionalPhases = 1;
staticText = "after this phase, there is an additional combat phase";
}
public AdditionalCombatPhaseEffect(int additionalPhases) {
super(Outcome.Benefit);
if (additionalPhases < 1) {
throw new IllegalArgumentException("Number of additional phases must be at least 1");
}
if (additionalPhases == 1) {
this.additionalPhases = 1;
staticText = "after this phase, there is an additional combat phase";
} else {
this.additionalPhases = additionalPhases;
staticText = "after this phase, there are " + additionalPhases + " additional combat phases";
}
}
protected AdditionalCombatPhaseEffect(final AdditionalCombatPhaseEffect effect) {
super(effect);
this.additionalPhases = effect.additionalPhases;
}
@Override
@ -25,7 +44,10 @@ public class AdditionalCombatPhaseEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
game.getState().getTurnMods().add(new TurnMod(game.getState().getActivePlayerId()).withExtraPhase(TurnPhase.COMBAT));
for (int i = 0; i < additionalPhases; i++) {
TurnMod combat = new TurnMod(game.getState().getActivePlayerId()).withExtraPhase(TurnPhase.COMBAT);
game.getState().getTurnMods().add(combat);
}
return true;
}
}

View file

@ -121,6 +121,17 @@ public class CreateTokenEffect extends OneShotEffect {
return lastAddedTokenIds;
}
public void sacrificeTokensCreatedAtNextEndStep(Game game, Ability source) {
for (UUID tokenId : this.getLastAddedTokenIds()) {
Permanent tokenPermanent = game.getPermanent(tokenId);
if (tokenPermanent != null) {
SacrificeTargetEffect sacrificeEffect = new SacrificeTargetEffect();
sacrificeEffect.setTargetPointer(new FixedTarget(tokenPermanent, game));
game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(sacrificeEffect), source);
}
}
}
public void exileTokensCreatedAtNextEndStep(Game game, Ability source) {
for (UUID tokenId : this.getLastAddedTokenIds()) {
Permanent tokenPermanent = game.getPermanent(tokenId);

View file

@ -0,0 +1,40 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.SaddleAbility;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author Jmlundeen
*/
public class SaddleTargetMountEffect extends OneShotEffect {
public SaddleTargetMountEffect() {
super(Outcome.Benefit);
staticText = "Target Mount you control becomes saddled until end of turn";
}
public SaddleTargetMountEffect(String rule) {
super(Outcome.Benefit);
staticText = rule;
}
protected SaddleTargetMountEffect(final SaddleTargetMountEffect effect) {
super(effect);
}
@Override
public SaddleTargetMountEffect copy() {
return new SaddleTargetMountEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
return SaddleAbility.applySaddle(game.getPermanent(getTargetPointer().getFirst(game, source)), game);
}
}

View file

@ -36,6 +36,7 @@ public class BecomesCreatureAttachedEffect extends ContinuousEffectImpl {
this.token = token;
this.loseType = loseType;
staticText = text;
this.dependencyTypes.add(DependencyType.BecomeCreature);
}
protected BecomesCreatureAttachedEffect(final BecomesCreatureAttachedEffect effect) {

View file

@ -16,6 +16,7 @@ public class BecomesCreatureIfVehicleEffect extends ContinuousEffectImpl {
public BecomesCreatureIfVehicleEffect() {
super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Benefit);
this.staticText = "As long as enchanted permanent is a Vehicle, it's a creature in addition to its other types";
this.dependencyTypes.add(DependencyType.BecomeCreature);
}
protected BecomesCreatureIfVehicleEffect(final BecomesCreatureIfVehicleEffect effect) {

View file

@ -11,6 +11,7 @@ public class VehiclesBecomeArtifactCreatureEffect extends ContinuousEffectImpl {
public VehiclesBecomeArtifactCreatureEffect(Duration duration) {
super(duration, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.BecomeCreature);
staticText = "Vehicles you control become artifact creatures until end of turn";
this.dependencyTypes.add(DependencyType.BecomeCreature);
}
private VehiclesBecomeArtifactCreatureEffect(final VehiclesBecomeArtifactCreatureEffect effect) {

View file

@ -0,0 +1,57 @@
package mage.abilities.effects.keyword;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.SpiritXXToken;
import mage.players.Player;
import mage.util.CardUtil;
/**
* @author TheElk801
*/
public class EndureSourceEffect extends OneShotEffect {
private final int amount;
public EndureSourceEffect(int amount) {
this(amount, "it");
}
public EndureSourceEffect(int amount, String selfText) {
super(Outcome.Benefit);
staticText = selfText + " endures " + amount;
this.amount = amount;
}
private EndureSourceEffect(final EndureSourceEffect effect) {
super(effect);
this.amount = effect.amount;
}
@Override
public EndureSourceEffect copy() {
return new EndureSourceEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent != null && player.chooseUse(
Outcome.BoostCreature, "Put " + CardUtil.numberToText(amount, "a") + " +1/+1 counter" +
(amount > 1 ? "s" : "") + " on " + permanent.getName() + " or create " +
CardUtil.addArticle("" + amount) + ' ' + amount + '/' + amount + " Spirit token?",
null, "Add counters", "Create token", source, game
)) {
return permanent.addCounters(CounterType.P1P1.createInstance(amount), source, game);
}
return new SpiritXXToken(amount).putOntoBattlefield(1, game, source);
}
}

View file

@ -0,0 +1,65 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.token.RedWarriorToken;
import mage.players.Player;
import mage.util.CardUtil;
/**
* @author balazskristof
*/
public class MobilizeAbility extends AttacksTriggeredAbility {
public MobilizeAbility(int count) {
super(new MobilizeEffect(count), false, "Mobilize " + count + " <i>(Whenever this creature attacks, create "
+ (count == 1 ? "a" : CardUtil.numberToText(count)) + " tapped and attacking 1/1 red Warrior creature "
+ (count == 1 ? "token" : "tokens") + ". Sacrifice " + (count == 1 ? "it" : "them")
+ " at the beginning of the next end step.)");
}
protected MobilizeAbility(final MobilizeAbility ability) {
super(ability);
}
@Override
public MobilizeAbility copy() {
return new MobilizeAbility(this);
}
}
class MobilizeEffect extends OneShotEffect {
private final int count;
MobilizeEffect(int count) {
super(Outcome.Benefit);
this.count = count;
}
private MobilizeEffect(final MobilizeEffect effect) {
super(effect);
this.count = effect.count;
}
@Override
public MobilizeEffect copy() {
return new MobilizeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
CreateTokenEffect effect = new CreateTokenEffect(new RedWarriorToken(), this.count, true, true);
effect.apply(game, source);
effect.sacrificeTokensCreatedAtNextEndStep(game, source);
return true;
}
}

View file

@ -48,6 +48,23 @@ public class SaddleAbility extends SimpleActivatedAbility {
this.value = ability.value;
}
public static boolean applySaddle(Permanent permanent, Game game) {
if (permanent == null) {
return false;
}
SaddleAbility saddleAbility = permanent.getAbilities().stream()
.filter(a -> a instanceof SaddleAbility)
.map(a -> (SaddleAbility) a)
.findFirst()
.orElse(null);
if (saddleAbility != null) {
SaddleEventEffect effect = new SaddleEventEffect();
effect.apply(game, saddleAbility);
return true;
}
return false;
}
@Override
public SaddleAbility copy() {
return new SaddleAbility(this);

View file

@ -49,6 +49,7 @@ public abstract class DeckValidator implements Serializable {
maxCopiesMap.put("Slime Against Humanity", Integer.MAX_VALUE);
maxCopiesMap.put("Templar Knight", Integer.MAX_VALUE);
maxCopiesMap.put("Hare Apparent", Integer.MAX_VALUE);
maxCopiesMap.put("Tempest Hawk", Integer.MAX_VALUE);
maxCopiesMap.put("Once More with Feeling", 1);
maxCopiesMap.put("Seven Dwarves", 7);
maxCopiesMap.put("Nazgul", 9);

View file

@ -31,6 +31,7 @@ public enum AbilityWord {
FATEFUL_HOUR("Fateful hour"),
FATHOMLESS_DESCENT("Fathomless descent"),
FEROCIOUS("Ferocious"),
FLURRY("Flurry"),
FORMIDABLE("Formidable"),
GRANDEUR("Grandeur"),
HATE("Hate"),
@ -51,6 +52,7 @@ public enum AbilityWord {
RADIANCE("Radiance"),
RAID("Raid"),
RALLY("Rally"),
RENEW("Renew"),
REVOLT("Revolt"),
SECRET_COUNCIL("Secret council"),
SPELL_MASTERY("Spell mastery"),

View file

@ -0,0 +1,28 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.abilities.keyword.CrewAbility;
import mage.abilities.keyword.FlyingAbility;
import mage.constants.CardType;
import mage.constants.SubType;
public class NalaarAetherjetToken extends TokenImpl{
public NalaarAetherjetToken(int xValue) {
super("Nalaar Aetherjet", "X/X colorless Vehicle artifact token named Nalaar Aetherjet with flying and crew 2");
cardType.add(CardType.ARTIFACT);
subtype.add(SubType.VEHICLE);
power = new MageInt(xValue);
toughness = new MageInt(xValue);
addAbility(FlyingAbility.getInstance());
addAbility(new CrewAbility(2));
}
private NalaarAetherjetToken(final NalaarAetherjetToken token) {
super(token);
}
public NalaarAetherjetToken copy() {
return new NalaarAetherjetToken(this);
}
}

View file

@ -0,0 +1,29 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.constants.CardType;
import mage.constants.SubType;
/**
* @author balazskristof
*/
public final class RedWarriorToken extends TokenImpl {
public RedWarriorToken() {
super("Warrior Token", "1/1 red Warrior creature token");
cardType.add(CardType.CREATURE);
color.setRed(true);
subtype.add(SubType.WARRIOR);
power = new MageInt(1);
toughness = new MageInt(1);
}
private RedWarriorToken(final RedWarriorToken token) {
super(token);
}
@Override
public RedWarriorToken copy() {
return new RedWarriorToken(this);
}
}

View file

@ -0,0 +1,31 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.constants.CardType;
import mage.constants.SubType;
public class SpiritWarriorToken extends TokenImpl {
public SpiritWarriorToken() {
this(1);
}
public SpiritWarriorToken(int x) {
super("Spirit Warrior Token", "X/X black and green Spirit Warrior creature token, where X is the greatest toughness among creatures you control");
this.cardType.add(CardType.CREATURE);
this.subtype.add(SubType.SPIRIT);
this.subtype.add(SubType.WARRIOR);
this.color.setBlack(true);
this.color.setGreen(true);
this.power = new MageInt(x);
this.toughness = new MageInt(x);
}
private SpiritWarriorToken(final SpiritWarriorToken token) {
super(token);
}
public SpiritWarriorToken copy() {
return new SpiritWarriorToken(this);
}
}

View file

@ -0,0 +1,33 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.constants.CardType;
import mage.constants.SubType;
/**
* @author nantuko
*/
public final class SpiritXXToken extends TokenImpl {
public SpiritXXToken() {
this(0);
}
public SpiritXXToken(int amount) {
super("Spirit Token", amount + '/' + amount + " white Spirit creature token");
cardType.add(CardType.CREATURE);
subtype.add(SubType.SPIRIT);
color.setWhite(true);
power = new MageInt(amount);
toughness = new MageInt(amount);
}
private SpiritXXToken(final SpiritXXToken token) {
super(token);
}
@Override
public SpiritXXToken copy() {
return new SpiritXXToken(this);
}
}