Merge branch 'master' into refactor/multiple-names

This commit is contained in:
theelk801 2024-09-30 12:26:42 -04:00
commit f4c55dfc45
67 changed files with 1228 additions and 169 deletions

View file

@ -1,6 +1,7 @@
package mage.abilities.common;
import mage.abilities.SpellAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.cards.Card;
@ -14,14 +15,21 @@ import mage.util.CardUtil;
*/
public class PayMoreToCastAsThoughtItHadFlashAbility extends SpellAbility {
private final ManaCosts costsToAdd;
private final Cost costsToAdd;
public PayMoreToCastAsThoughtItHadFlashAbility(Card card, ManaCosts<ManaCost> costsToAdd) {
super(card.getSpellAbility().getManaCosts().copy(), card.getName() + " as though it had flash", Zone.HAND, SpellAbilityType.BASE_ALTERNATE);
public PayMoreToCastAsThoughtItHadFlashAbility(Card card, Cost costsToAdd) {
super(card.getSpellAbility().getManaCosts().copy(), card.getName(), Zone.HAND, SpellAbilityType.BASE_ALTERNATE);
this.costsToAdd = costsToAdd;
this.timing = TimingRule.INSTANT;
this.setRuleAtTheTop(true);
CardUtil.increaseCost(this, costsToAdd);
if(costsToAdd instanceof ManaCosts<?>) {
ManaCosts<ManaCost> manaCosts = (ManaCosts<ManaCost>) costsToAdd;
CardUtil.increaseCost(this, manaCosts);
}
else {
this.addCost(costsToAdd);
}
}
protected PayMoreToCastAsThoughtItHadFlashAbility(final PayMoreToCastAsThoughtItHadFlashAbility ability) {
@ -38,10 +46,13 @@ public class PayMoreToCastAsThoughtItHadFlashAbility extends SpellAbility {
public String getRule(boolean all) {
return getRule();
}
@Override
public String getRule() {
return "You may cast {this} as though it had flash if you pay " + costsToAdd.getText() + " more to cast it. <i>(You may cast it any time you could cast an instant.)</i>";
if (costsToAdd instanceof ManaCosts) {
return "You may cast {this} as though it had flash if you pay " + costsToAdd.getText() + " more to cast it. <i>(You may cast it any time you could cast an instant.)</i>";
} else {
return "You may cast {this} as though it had flash by " + costsToAdd.getText() + " in addition to paying its other costs.";
}
}
}
}

View file

@ -60,7 +60,8 @@ public class SpellCastAllTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Spell spell = game.getStack().getSpell(event.getTargetId());
if (!filter.match(spell, getControllerId(), this, game)) {
if (!filter.match(spell, getControllerId(), this, game)
|| !game.getState().getPlayersInRange(getControllerId(), game, false).contains(event.getPlayerId())) {
return false;
}
getEffects().setValue("spellCast", spell);

View file

@ -74,6 +74,8 @@ public interface ContinuousEffect extends Effect {
boolean isYourNextEndStep(Game game);
boolean isTheNextEndStep(Game game);
boolean isYourNextUpkeepStep(Game game);
@Override

View file

@ -56,9 +56,11 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
// until your next turn or until end of your next turn
private UUID startingControllerId; // player to check for turn duration (can't different with real controller ability)
private UUID activePlayerId; // Player whose turn the effect started on
private boolean startingTurnWasActive; // effect started during related players turn and related players turn was already active
private int effectStartingOnTurn = 0; // turn the effect started
private int effectStartingEndStep = 0;
private int effectControllerStartingEndStep = 0;
private int effectActivePlayerStartingEndStep = 0;
private int nextTurnNumber = Integer.MAX_VALUE; // effect is waiting for a step during your next turn, we store it if found.
// set to the turn number on your next turn.
private int effectStartingStepNum = 0; // Some continuous are waiting for the next step of a kind.
@ -93,7 +95,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
this.startingControllerId = effect.startingControllerId;
this.startingTurnWasActive = effect.startingTurnWasActive;
this.effectStartingOnTurn = effect.effectStartingOnTurn;
this.effectStartingEndStep = effect.effectStartingEndStep;
this.effectControllerStartingEndStep = effect.effectControllerStartingEndStep;
this.dependencyTypes = effect.dependencyTypes;
this.dependendToTypes = effect.dependendToTypes;
this.characterDefining = effect.characterDefining;
@ -251,10 +253,12 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
@Override
public void setStartingControllerAndTurnNum(Game game, UUID startingController, UUID activePlayerId) {
this.startingControllerId = startingController;
this.activePlayerId = activePlayerId;
this.startingTurnWasActive = activePlayerId != null
&& activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too
this.effectStartingOnTurn = game.getTurnNum();
this.effectStartingEndStep = EndStepCountWatcher.getCount(startingController, game);
this.effectControllerStartingEndStep = EndStepCountWatcher.getCount(startingController, game);
this.effectActivePlayerStartingEndStep = EndStepCountWatcher.getCount(activePlayerId, game);
this.effectStartingStepNum = game.getState().getStepNum();
}
@ -266,7 +270,12 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
@Override
public boolean isYourNextEndStep(Game game) {
return EndStepCountWatcher.getCount(startingControllerId, game) > effectStartingEndStep;
return EndStepCountWatcher.getCount(startingControllerId, game) > effectControllerStartingEndStep;
}
@Override
public boolean isTheNextEndStep(Game game) {
return EndStepCountWatcher.getCount(activePlayerId, game) > effectActivePlayerStartingEndStep;
}
public boolean isEndCombatOfYourNextTurn(Game game) {
@ -298,6 +307,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
case UntilYourNextTurn:
case UntilEndOfYourNextTurn:
case UntilYourNextEndStep:
case UntilTheNextEndStep:
case UntilEndCombatOfYourNextTurn:
case UntilYourNextUpkeepStep:
break;
@ -342,6 +352,10 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
return this.isYourNextEndStep(game);
}
break;
case UntilTheNextEndStep:
if (player != null && player.isInGame()) {
return this.isTheNextEndStep(game);
}
case UntilEndCombatOfYourNextTurn:
if (player != null && player.isInGame()) {
return this.isEndCombatOfYourNextTurn(game);

View file

@ -156,6 +156,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
case UntilEndOfYourNextTurn:
case UntilEndCombatOfYourNextTurn:
case UntilYourNextEndStep:
case UntilTheNextEndStep:
case UntilYourNextUpkeepStep:
// until your turn effects continue until real turn reached, their used it's own inactive method
// 514.2 Second, the following actions happen simultaneously: all damage marked on permanents

View file

@ -18,21 +18,28 @@ public class IfAbilityHasResolvedXTimesEffect extends OneShotEffect {
private final int resolutionNumber;
private final Effects effects;
private final boolean orMore;
public IfAbilityHasResolvedXTimesEffect(int resolutionNumber, Effect effect) {
this(effect.getOutcome(), resolutionNumber, effect);
}
public IfAbilityHasResolvedXTimesEffect(Outcome outcome, int resolutionNumber, Effect... effects) {
this(outcome, resolutionNumber, false, effects);
}
public IfAbilityHasResolvedXTimesEffect(Outcome outcome, int resolutionNumber, boolean orMore, Effect... effects) {
super(outcome);
this.resolutionNumber = resolutionNumber;
this.effects = new Effects(effects);
this.orMore = orMore;
}
private IfAbilityHasResolvedXTimesEffect(final IfAbilityHasResolvedXTimesEffect effect) {
super(effect);
this.resolutionNumber = effect.resolutionNumber;
this.effects = effect.effects.copy();
this.orMore = effect.orMore;
}
@Override
@ -42,7 +49,8 @@ public class IfAbilityHasResolvedXTimesEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
if (AbilityResolvedWatcher.getResolutionCount(game, source) != resolutionNumber) {
int resolutionCount = AbilityResolvedWatcher.getResolutionCount(game, source);
if (resolutionCount < resolutionNumber || (!orMore && resolutionCount > resolutionNumber)) {
return false;
}
boolean result = false;
@ -62,6 +70,9 @@ public class IfAbilityHasResolvedXTimesEffect extends OneShotEffect {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
if (orMore) {
return "otherwise, " + effects.getText(mode);
}
return "if this is the " + CardUtil.numberToOrdinalText(resolutionNumber) +
" time this ability has resolved this turn, " + effects.getText(mode);
}

View file

@ -112,7 +112,8 @@ public class EmergeAbility extends SpellAbility {
Permanent creature = game.getPermanent(target.getFirstTarget());
if (creature != null) {
CardUtil.reduceCost(this, creature.getManaValue());
if (super.activate(game, allowedIdentifiers, false)) {
boolean reducedToZero = this.getManaCostsToPay().isEmpty();
if (super.activate(game, allowedIdentifiers, reducedToZero)) {
MageObjectReference mor = new MageObjectReference(creature, game);
if (creature.sacrifice(this, game)) {
this.setCostsTag(EMERGE_ACTIVATION_CREATURE_REFERENCE, mor); //Can access with LKI afterwards

View file

@ -20,6 +20,7 @@ public class CardNameUtil {
.replace("û", "u")
.replace("í", "i")
.replace("ï", "i")
.replace("î", "i")
.replace("â", "a")
.replace("á", "a")
.replace("à", "a")

View file

@ -33,6 +33,7 @@ public enum TokenRepository {
public static final String XMAGE_IMAGE_NAME_NIGHT = "Night";
public static final String XMAGE_IMAGE_NAME_THE_MONARCH = "The Monarch";
public static final String XMAGE_IMAGE_NAME_RADIATION = "Radiation";
public static final String XMAGE_IMAGE_NAME_THE_RING = "The Ring";
public static final String XMAGE_IMAGE_NAME_HELPER_EMBLEM = "Helper Emblem";
private static final Logger logger = Logger.getLogger(TokenRepository.class);
@ -306,6 +307,9 @@ public enum TokenRepository {
// Radiation (for trigger)
res.add(createXmageToken(XMAGE_IMAGE_NAME_RADIATION, 1, "https://api.scryfall.com/cards/tpip/22/en?format=image"));
// The Ring
res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_RING, 1, "https://api.scryfall.com/cards/tltr/H13/en?format=image"));
// Helper emblem (for global card hints)
// use backface for it
res.add(createXmageToken(XMAGE_IMAGE_NAME_HELPER_EMBLEM, 1, "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"));

View file

@ -13,6 +13,7 @@ public enum Duration {
EndOfTurn("until end of turn", true, true),
UntilYourNextTurn("until your next turn", true, true),
UntilYourNextEndStep("until your next end step", true, true),
UntilTheNextEndStep("until your next end step", true, true),
UntilEndCombatOfYourNextTurn("until end of combat on your next turn", true, true),
UntilYourNextUpkeepStep("until your next upkeep", true, true),
UntilEndOfYourNextTurn("until the end of your next turn", true, true),

View file

@ -583,11 +583,10 @@ public abstract class GameImpl implements Game {
if (emblem != null) {
return emblem;
}
TheRingEmblem newEmblem = new TheRingEmblem(playerId);
// TODO: add image info
state.addCommandObject(newEmblem);
return newEmblem;
}
@ -1966,16 +1965,13 @@ public abstract class GameImpl implements Game {
@Override
public void addEmblem(Emblem emblem, MageObject sourceObject, UUID toPlayerId) {
Emblem newEmblem = emblem.copy();
newEmblem.setSourceObject(sourceObject);
newEmblem.setSourceObjectAndInitImage(sourceObject);
newEmblem.setControllerId(toPlayerId);
newEmblem.assignNewId();
newEmblem.getAbilities().newId();
for (Ability ability : newEmblem.getAbilities()) {
ability.setSourceId(newEmblem.getId());
}
// image info setup in setSourceObject
state.addCommandObject(newEmblem);
}
@ -1997,17 +1993,15 @@ public abstract class GameImpl implements Game {
}
}
Plane newPlane = plane.copy();
newPlane.setSourceObject();
newPlane.setSourceObjectAndInitImage();
newPlane.setControllerId(toPlayerId);
newPlane.assignNewId();
newPlane.getAbilities().newId();
for (Ability ability : newPlane.getAbilities()) {
ability.setSourceId(newPlane.getId());
}
// image info setup in setSourceObject
state.addCommandObject(newPlane);
informPlayers("You have planeswalked to " + newPlane.getLogName());
// Fire off the planeswalked event
@ -2028,7 +2022,6 @@ public abstract class GameImpl implements Game {
@Override
public Dungeon addDungeon(Dungeon dungeon, UUID playerId) {
dungeon.setControllerId(playerId);
dungeon.setSourceObject();
state.addCommandObject(dungeon);
return dungeon;
}

View file

@ -1239,6 +1239,11 @@ public class GameState implements Serializable, Copyable<GameState> {
this.isPlaneChase = isPlaneChase;
}
/**
* Add object to command zone.
* <p>
* Warning, all object data must be initialized before adding, including image info
*/
public void addCommandObject(CommandObject commandObject) {
getCommand().add(commandObject);
setZone(commandObject.getId(), Zone.COMMAND);

View file

@ -154,24 +154,35 @@ public class Dungeon extends CommandObjectImpl {
}
public static Dungeon createDungeon(String name, boolean isNameMustExists) {
Dungeon res;
switch (name) {
case "Tomb of Annihilation":
return new TombOfAnnihilationDungeon();
res = new TombOfAnnihilationDungeon();
break;
case "Lost Mine of Phandelver":
return new LostMineOfPhandelverDungeon();
res = new LostMineOfPhandelverDungeon();
break;
case "Dungeon of the Mad Mage":
return new DungeonOfTheMadMageDungeon();
res = new DungeonOfTheMadMageDungeon();
break;
default:
if (isNameMustExists) {
throw new UnsupportedOperationException("A dungeon should have been chosen");
} else {
return null;
res = null;
}
}
// dungeon don't have source, so image data can be initialized immediately
if (res != null) {
res.setSourceObjectAndInitImage();
}
return res;
}
public void setSourceObject() {
// choose set code due source
public void setSourceObjectAndInitImage() {
// image
TokenInfo foundInfo = TokenRepository.instance.findPreferredTokenInfoForClass(this.getClass().getName(), null);
if (foundInfo != null) {
this.setExpansionSetCode(foundInfo.getSetCode());

View file

@ -58,7 +58,7 @@ public abstract class Emblem extends CommandObjectImpl {
return frameStyle;
}
public void setSourceObject(MageObject sourceObject) {
public void setSourceObjectAndInitImage(MageObject sourceObject) {
this.sourceObject = sourceObject;
// choose set code due source

View file

@ -66,7 +66,7 @@ public abstract class Plane extends CommandObjectImpl {
return frameStyle;
}
public void setSourceObject() {
public void setSourceObjectAndInitImage() {
this.sourceObject = null;
// choose set code due source

View file

@ -94,7 +94,7 @@ public final class EmblemOfCard extends Emblem {
}
@Override
public void setSourceObject(MageObject sourceObject) {
public void setSourceObjectAndInitImage(MageObject sourceObject) {
this.sourceObject = sourceObject;
// super method would try and fail to find the emblem image here
// (not sure why that would be setSoureObject's job; we get our image during construction)

View file

@ -45,7 +45,7 @@ public class RadiationEmblem extends Emblem {
this.setImageFileName(""); // use default
this.setImageNumber(foundInfo.getImageNumber());
} else {
// how-to fix: add emblem to the tokens-database TokenRepository->loadXmageTokens
// how-to fix: add image to the tokens-database TokenRepository->loadXmageTokens
throw new IllegalArgumentException("Wrong code usage: can't find xmage token info for: " + TokenRepository.XMAGE_IMAGE_NAME_RADIATION);
}
}

View file

@ -12,6 +12,8 @@ import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect;
import mage.abilities.effects.common.DrawDiscardControllerEffect;
import mage.abilities.effects.common.LoseLifeOpponentsEffect;
import mage.abilities.effects.common.SacrificeTargetEffect;
import mage.cards.repository.TokenInfo;
import mage.cards.repository.TokenRepository;
import mage.constants.*;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
@ -41,6 +43,19 @@ public final class TheRingEmblem extends Emblem {
public TheRingEmblem(UUID controllerId) {
super("The Ring");
this.setControllerId(controllerId);
// ring don't have source, so image can be initialized immediately
TokenInfo foundInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_THE_RING, null);
if (foundInfo != null) {
this.setExpansionSetCode(foundInfo.getSetCode());
this.setUsesVariousArt(false);
this.setCardNumber("");
this.setImageFileName(""); // use default
this.setImageNumber(foundInfo.getImageNumber());
} else {
// how-to fix: add image to the tokens-database TokenRepository->loadXmageTokens
throw new IllegalArgumentException("Wrong code usage: can't find xmage token info for: " + TokenRepository.XMAGE_IMAGE_NAME_THE_RING);
}
}
private TheRingEmblem(final TheRingEmblem card) {

View file

@ -28,7 +28,7 @@ public class XmageHelperEmblem extends Emblem {
this.setImageFileName(""); // use default
this.setImageNumber(foundInfo.getImageNumber());
} else {
// how-to fix: add emblem to the tokens-database TokenRepository->loadXmageTokens
// how-to fix: add image to the tokens-database TokenRepository->loadXmageTokens
throw new IllegalArgumentException("Wrong code usage: can't find xmage token info for: " + TokenRepository.XMAGE_IMAGE_NAME_HELPER_EMBLEM);
}
}

View file

@ -81,7 +81,9 @@ public class SpellStack extends ArrayDeque<StackObject> {
counteredObjectName = "Ability (" + stackObject.getStackAbility().getRule(targetSourceName) + ") of " + targetSourceName;
}
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.COUNTER, objectId, source, stackObject.getControllerId()))) {
if (!(stackObject instanceof Spell)) { // spells are removed from stack by the card movement
// spells are removed from stack by the card movement
if (!(stackObject instanceof Spell)
|| stackObject.isCopy()) { // !ensure that copies of stackobjects have their history recorded ie: Swan Song
this.remove(stackObject, game);
game.rememberLKI(Zone.STACK, stackObject);
}

View file

@ -1035,6 +1035,7 @@ public final class CardUtil {
|| text.startsWith("any ")
|| text.startsWith("{this} ")
|| text.startsWith("your ")
|| text.startsWith("their ")
|| text.startsWith("one ")) {
return text;
}