Merge remote-tracking branch 'Xmage/master' into fork/Jmlundeen/guidelight-matrix-kolodin

# Conflicts:
#	Mage.Sets/src/mage/sets/Aetherdrift.java
This commit is contained in:
jmlundeen 2025-03-16 15:10:50 -05:00
commit 83ba735149
92 changed files with 4006 additions and 479 deletions

View file

@ -76,7 +76,8 @@ public enum MageIdentifier {
TheRuinousPowersAlternateCast,
FiresOfMountDoomAlternateCast,
PrimalPrayersAlternateCast,
QuilledGreatwurmAlternateCast;
QuilledGreatwurmAlternateCast,
WickerfolkIndomitableAlternateCast;
/**
* Additional text if there is need to differentiate two very similar effects

View file

@ -0,0 +1,65 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.CardsInExileCount;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.constants.ComparisonType;
import mage.game.Game;
import mage.util.CardUtil;
/**
* Cards in exile condition
*
* @author Jmlundeen
*/
public class CardsInExileCondition implements Condition
{
private final ComparisonType type;
private final int count;
private final DynamicValue cardsInExileCount;
public CardsInExileCondition(ComparisonType type, int count)
{
this(type, count, CardsInExileCount.ALL);
}
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)
{
int exileCards = cardsInExileCount.calculate(game, source, null);
return ComparisonType.compare(exileCards, type, count);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("there are ");
String countString = CardUtil.numberToText(count);
switch (type) {
case MORE_THAN:
sb.append("more than ").append(countString).append(" ");
break;
case FEWER_THAN:
sb.append("fewer than ").append(countString).append(" ");
break;
case OR_LESS:
sb.append(countString).append(" or less ");
break;
case OR_GREATER:
sb.append(countString).append(" or more ");
break;
default:
throw new IllegalArgumentException("comparison rules for " + type + " missing");
}
sb.append("cards in exile");
return sb.toString();
}
}

View file

@ -0,0 +1,81 @@
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.cards.Card;
import mage.game.Game;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Stream;
/**
* @author Jmlundeen
*/
public enum CardsInExileCount implements DynamicValue {
YOU("you"),
ALL("all players"),
OPPONENTS("your opponents'");
private final String message;
private final ValueHint hint;
CardsInExileCount(String message) {
this.message = "The number of cards owned by " + message + " in exile";
this.hint = new ValueHint(this.message, this);
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return getExileCards(game, sourceAbility)
.mapToInt(x -> 1)
.sum();
}
@Override
public CardsInExileCount copy() {
return this;
}
@Override
public String toString() {
return "X";
}
@Override
public String getMessage() {
return message;
}
public Hint getHint() {
return hint;
}
public Stream<Card> getExileCards(Game game, Ability ability) {
Collection<UUID> playerIds;
switch (this) {
case YOU:
playerIds = Collections.singletonList(ability.getControllerId());
break;
case OPPONENTS:
playerIds = game.getOpponents(ability.getControllerId());
break;
case ALL:
playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game);
break;
default:
throw new IllegalArgumentException("Wrong code usage: miss implementation for " + this);
}
return playerIds.stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
.map(player -> game.getExile().getAllCards(game, player.getId()))
.flatMap(Collection::stream)
.filter(Objects::nonNull);
}
}

View file

@ -0,0 +1,33 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.watchers.common.CreaturesDiedWatcher;
public enum CreaturesYouControlDiedCount implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return game.getState()
.getWatcher(CreaturesDiedWatcher.class)
.getAmountOfCreaturesDiedThisTurnByController(sourceAbility.getControllerId());
}
@Override
public CreaturesYouControlDiedCount copy() {
return this;
}
@Override
public String getMessage() {
return "creature that died under your control this turn";
}
@Override
public String toString() {
return "1";
}
}

View file

@ -98,16 +98,12 @@ public class DoIfCostPaid extends OneShotEffect {
if (player == null || mageObject == null) {
return false;
}
// nothing to pay (do not support mana cost - it's true all the time)
if (!this.cost.canPay(source, source, player.getId(), game)) {
return false;
}
String message = CardUtil.replaceSourceName(makeChooseText(game, source), mageObject.getName());
Outcome payOutcome = executingEffects.getOutcome(source, this.outcome);
// nothing to pay (do not support mana cost - it's true all the time)
boolean canPay = cost.canPay(source, source, player.getId(), game);
boolean didPay = false;
if (!optional || player.chooseUse(payOutcome, message, source, game)) {
if (canPay && (!optional || player.chooseUse(payOutcome, message, source, game))) {
cost.clearPaid();
int bookmark = game.bookmarkState();
if (cost.pay(source, game, source, player.getId(), false)) {

View file

@ -10,6 +10,8 @@ import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author LevelX2
*/
@ -38,12 +40,13 @@ public class MillCardsTargetEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(getTargetPointer().getFirst(game, source));
if (player != null) {
player.millCards(numberCards.calculate(game, source, this), source, game);
return true;
for (UUID playerId : getTargetPointer().getTargets(game, source)) {
Player player = game.getPlayer(playerId);
if (player != null) {
player.millCards(numberCards.calculate(game, source, this), source, game);
}
}
return false;
return true;
}
@Override

View file

@ -1,48 +0,0 @@
package mage.abilities.effects.common;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.watchers.common.AttackedThisTurnWatcher;
import java.util.Set;
/**
* @author LevelX2
*/
public class UntapAllThatAttackedEffect extends OneShotEffect {
public UntapAllThatAttackedEffect() {
super(Outcome.Benefit);
staticText = "Untap all creatures that attacked this turn";
}
protected UntapAllThatAttackedEffect(final UntapAllThatAttackedEffect effect) {
super(effect);
}
@Override
public UntapAllThatAttackedEffect copy() {
return new UntapAllThatAttackedEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
AttackedThisTurnWatcher watcher = game.getState().getWatcher(AttackedThisTurnWatcher.class);
if (watcher != null) {
Set<MageObjectReference> attackedThisTurn = watcher.getAttackedThisTurnCreatures();
for (MageObjectReference mor : attackedThisTurn) {
Permanent permanent = mor.getPermanent(game);
if (permanent != null && permanent.isCreature(game)) {
permanent.untap(game);
}
}
return true;
}
return false;
}
}

View file

@ -0,0 +1,42 @@
package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.constants.*;
import mage.game.Game;
import mage.game.permanent.Permanent;
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";
}
private VehiclesBecomeArtifactCreatureEffect(final VehiclesBecomeArtifactCreatureEffect effect) {
super(effect);
}
@Override
public VehiclesBecomeArtifactCreatureEffect copy() {
return new VehiclesBecomeArtifactCreatureEffect(this);
}
@Override
public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) {
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(source.getControllerId())) {
if (permanent != null && permanent.hasSubtype(SubType.VEHICLE, game)) {
if (sublayer == SubLayer.NA) {
permanent.addCardType(game, CardType.ARTIFACT);
permanent.addCardType(game, CardType.CREATURE);// TODO: Check if giving CREATURE Type is correct
}
}
}
return true;
}
@Override
public boolean apply(Game game, Ability source) {
return false;
}
}

View file

@ -3,20 +3,36 @@ package mage.abilities.keyword;
import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.costs.Cost;
import mage.abilities.effects.Effect;
import mage.constants.AsThoughEffectType;
import mage.constants.Zone;
import mage.game.Game;
/**
* @author TheElk801
*/
public class ExhaustAbility extends ActivatedAbilityImpl {
private boolean withReminderText = true;
public ExhaustAbility(Effect effect, Cost cost) {
super(Zone.BATTLEFIELD, effect, cost);
}
public ExhaustAbility(Effect effect, Cost cost, boolean withReminderText) {
super(Zone.BATTLEFIELD, effect, cost);
this.setRuleVisible(false);
this.withReminderText = withReminderText;
}
private ExhaustAbility(final ExhaustAbility ability) {
super(ability);
this.maxActivationsPerGame = 1;
this.withReminderText = ability.withReminderText;
}
public ExhaustAbility withReminderText(boolean withReminderText) {
this.withReminderText = withReminderText;
return this;
}
@Override
@ -24,8 +40,23 @@ public class ExhaustAbility extends ActivatedAbilityImpl {
return new ExhaustAbility(this);
}
@Override
public boolean hasMoreActivationsThisTurn(Game game) {
ActivationInfo info = getActivationInfo(game);
if (info != null && info.totalActivations >= maxActivationsPerGame) {
boolean canActivate = !game.getContinuousEffects()
.asThough(sourceId, AsThoughEffectType.ALLOW_EXHAUST_PER_TURN, this, controllerId, game)
.isEmpty();
if (canActivate) {
return true;
}
}
return super.hasMoreActivationsThisTurn(game);
}
@Override
public String getRule() {
return "Exhaust &mdash; " + super.getRule() + " <i>(Activate each exhaust ability only once.)</i>";
return "Exhaust &mdash; " + super.getRule() +
(withReminderText ? " <i>(Activate each exhaust ability only once.)</i>" : "");
}
}

View file

@ -54,7 +54,7 @@ public class NinjutsuAbility extends ActivatedAbilityImpl {
}
public NinjutsuAbility(Cost cost, boolean commander) {
super(commander ? Zone.ALL : Zone.HAND, new NinjutsuEffect(), cost);
super(commander ? Zone.ALL : Zone.HAND, new NinjutsuEffect(commander), cost);
this.addCost(new RevealNinjutsuCardCost(commander));
this.addCost(new ReturnAttackerToHandTargetCost());
this.commander = commander;
@ -84,14 +84,18 @@ public class NinjutsuAbility extends ActivatedAbilityImpl {
class NinjutsuEffect extends OneShotEffect {
public NinjutsuEffect() {
private final boolean commander;
NinjutsuEffect(boolean commander) {
super(Outcome.PutCreatureInPlay);
this.commander = commander;
this.staticText = "Put this card onto the battlefield "
+ "from your hand tapped and attacking";
}
protected NinjutsuEffect(final NinjutsuEffect effect) {
private NinjutsuEffect(final NinjutsuEffect effect) {
super(effect);
this.commander = effect.commander;
}
@Override
@ -102,11 +106,12 @@ class NinjutsuEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
Card card = game.getCard(source.getSourceId());
if (controller == null || card == null) {
return false;
}
Card card = game.getCard(source.getSourceId());
if (card != null) {
Zone cardZone = game.getState().getZone(card.getId());
if (cardZone == Zone.HAND || (commander && cardZone == Zone.COMMAND)) {
controller.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null);
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) {
@ -144,6 +149,7 @@ class ReturnAttackerToHandTargetCost extends CostImpl {
public ReturnAttackerToHandTargetCost(ReturnAttackerToHandTargetCost cost) {
super(cost);
this.defendingPlayerId = cost.defendingPlayerId;
}
@Override

View file

@ -59,7 +59,10 @@ public enum AsThoughEffectType {
//
// ALLOW_FORETELL_ANYTIME:
// For Cosmos Charger effect
ALLOW_FORETELL_ANYTIME;
ALLOW_FORETELL_ANYTIME,
// ALLOW_EXHAUST_ACTIVE_ABILITY:
// Elvish Refueler effect allows Exhaust on your turn as though it hasn't been activated
ALLOW_EXHAUST_PER_TURN(true, false);
private final boolean needAffectedAbility; // mark what AsThough check must be called for specific ability, not full object (example: spell check)
private final boolean needPlayCardAbility; // mark what AsThough check must be called for play/cast abilities

View file

@ -0,0 +1,25 @@
package mage.filter.predicate.permanent;
import mage.filter.predicate.Predicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.watchers.common.AttackedThisTurnWatcher;
/**
*
* @author balazskristof
*/
public enum AttackedThisTurnPredicate implements Predicate<Permanent> {
instance;
@Override
public boolean apply(Permanent input, Game game) {
AttackedThisTurnWatcher watcher = game.getState().getWatcher(AttackedThisTurnWatcher.class);
return watcher != null && watcher.checkIfAttacked(input, game);
}
@Override
public String toString() {
return "attacked this turn";
}
}

View file

@ -10,7 +10,7 @@ import mage.constants.SubType;
public final class CarnivoreToken extends TokenImpl {
public CarnivoreToken() {
super("Carnivore Token", "3/1 red Beast creature token");
super("Carnivore", "3/1 red Beast creature token");
cardType.add(CardType.CREATURE);
color.setRed(true);
subtype.add(SubType.BEAST);

View file

@ -1346,6 +1346,7 @@ public abstract class PlayerImpl implements Player, Serializable {
castEvent.setZone(fromZone);
game.fireEvent(castEvent);
if (spell.activate(game, allowedIdentifiers, noMana)) {
game.processAction();
GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST,
ability.getId(), ability, playerId, approvingObject);
castedEvent.setZone(fromZone);