Merge branch 'master' into youreInCommand

This commit is contained in:
Jetz
2024-08-23 19:06:02 -04:00
254 changed files with 1740 additions and 537 deletions

View File

@@ -170,7 +170,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
*
* @return a boolean.
*/
public final boolean isSecondary() {
public boolean isSecondary() {
return getParamOrDefault("Secondary", "False").equals("True");
}

View File

@@ -875,17 +875,23 @@ public class Game {
// unattach all "Enchant Player"
c.removeAttachedTo(p);
if (c.getOwner().equals(p)) {
for (Card cc : cards) {
cc.removeImprintedCard(c);
cc.removeEncodedCard(c);
cc.removeRemembered(c);
cc.removeAttachedTo(c);
cc.removeAttachedCard(c);
if (c.getEffectSource() != null && !c.isEmblem()) {
// move effect to another player so they continue to work
c.getZone().remove(c);
getNextPlayerAfter(p).getZone(ZoneType.Command).add(c);
} else {
for (Card cc : cards) {
cc.removeImprintedCard(c);
cc.removeEncodedCard(c);
cc.removeRemembered(c);
cc.removeAttachedTo(c);
cc.removeAttachedCard(c);
}
triggerList.put(c.getZone().getZoneType(), null, c);
getAction().ceaseToExist(c, false);
// CR 603.2f owner of trigger source lost game
getTriggerHandler().clearDelayedTrigger(c);
}
triggerList.put(c.getZone().getZoneType(), null, c);
getAction().ceaseToExist(c, false);
// CR 603.2f owner of trigger source lost game
getTriggerHandler().clearDelayedTrigger(c);
} else {
// return stolen permanents
if (c.isInPlay() && (c.getController().equals(p) || c.getZone().getPlayer().equals(p))) {

View File

@@ -2307,7 +2307,6 @@ public class GameAction {
p.createMonarchEffect(set);
game.setMonarch(p);
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p);
game.getTriggerHandler().runTrigger(TriggerType.BecomeMonarch, runParams, false);
}
@@ -2332,7 +2331,6 @@ public class GameAction {
}
// You can take the initiative even if you already have it
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p);
game.getTriggerHandler().runTrigger(TriggerType.TakesInitiative, runParams, false);
}

View File

@@ -91,10 +91,10 @@ public final class GameActionUtil {
return alternatives;
}
if (sa.isSpell()) {
if (sa.isSpell() || sa.isLandAbility()) {
boolean lkicheck = false;
Card newHost = ((Spell)sa).getAlternateHost(source);
Card newHost = sa.getAlternateHost(source);
if (newHost != null) {
source = newHost;
lkicheck = true;
@@ -605,6 +605,22 @@ public final class GameActionUtil {
result.setOptionalKeywordAmount(ki, 1);
reset = true;
}
} else if (o.startsWith("Multikicker")) {
String costStr = o.split(":")[1];
final Cost cost = new Cost(costStr, false);
String str = "Choose Amount for Multikicker: " + cost.toSimpleString();
int v = pc.chooseNumberForKeywordCost(sa, cost, ki, str, Integer.MAX_VALUE);
for (int i = 0; i < v; i++) {
if (result == null) {
result = sa.copy();
}
result.getPayCosts().add(cost);
reset = true;
}
result.setOptionalKeywordAmount(ki, v);
} else if (o.startsWith("Offspring")) {
String[] k = o.split(":");
final Cost cost = new Cost(k[1], false);

View File

@@ -2958,7 +2958,8 @@ public class AbilityUtils {
}
for (SpellAbility s : list) {
if (s instanceof LandAbility) {
if (s.isLandAbility()) {
s.setActivatingPlayer(controller);
// CR 305.3
if (controller.getGame().getPhaseHandler().isPlayerTurn(controller) && controller.canPlayLand(tgtCard, true, s)) {
sas.add(s);
@@ -2986,9 +2987,7 @@ public class AbilityUtils {
private static void collectSpellsForPlayEffect(final List<SpellAbility> result, final CardState state, final Player controller, final boolean withAltCost) {
if (state.getType().isLand()) {
LandAbility la = new LandAbility(state.getCard(), controller, null);
la.setCardState(state);
result.add(la);
result.add(state.getFirstSpellAbility());
}
final Iterable<SpellAbility> spells = state.getSpellAbilities();
for (SpellAbility sa : spells) {

View File

@@ -496,8 +496,7 @@ public abstract class SpellAbilityEffect {
String effect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true);
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
final Trigger addedTrigger = card.addTrigger(parsedTrigger);
addedTrigger.setIntrinsic(true);
card.addTrigger(parsedTrigger);
}
protected static void addExileOnCounteredTrigger(final Card card) {
@@ -505,8 +504,7 @@ public abstract class SpellAbilityEffect {
String effect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true);
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
final Trigger addedTrigger = card.addTrigger(parsedTrigger);
addedTrigger.setIntrinsic(true);
card.addTrigger(parsedTrigger);
}
protected static void addForgetOnPhasedInTrigger(final Card card) {
@@ -531,6 +529,14 @@ public abstract class SpellAbilityEffect {
card.addTrigger(parsedTrigger2);
}
protected static void addExileOnLostTrigger(final Card card) {
String trig = "Mode$ LosesGame | ValidPlayer$ You | TriggerController$ Player | TriggerZones$ Command | Static$ True";
String effect = "DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true);
parsedTrigger.setOverridingAbility(AbilityFactory.getAbility(effect, card));
card.addTrigger(parsedTrigger);
}
protected static void addLeaveBattlefieldReplacement(final Card card, final SpellAbility sa, final String zone) {
final Card host = sa.getHostCard();
final Game game = card.getGame();

View File

@@ -20,7 +20,7 @@ public class AlterAttributeEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) {
boolean activate = Boolean.parseBoolean(sa.getParamOrDefault("Activate", "true"));
String[] attributes = sa.getParam("Attributes").split(",");
CardCollection defined = getDefinedCardsOrTargeted(sa, "Defined");
CardCollection defined = getDefinedCardsOrTargeted(sa);
if (sa.hasParam("Optional")) {
final String targets = Lang.joinHomogenous(defined);

View File

@@ -1,7 +1,5 @@
package forge.game.ability.effects;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
@@ -15,7 +13,7 @@ import forge.game.cost.CostPart;
import forge.game.cost.CostReveal;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
@@ -94,7 +92,7 @@ public class DiscoverEffect extends SpellAbilityEffect {
List<SpellAbility> sas = AbilityUtils.getBasicSpellsFromPlayEffect(found, p);
// filter out land abilities due to MDFC or similar
Iterables.removeIf(sas, Predicates.instanceOf(LandAbility.class));
sas.removeIf(sp -> sp.isLandAbility());
// the spell must also have a mana value equal to or less than the discover number
sas.removeIf(sp -> sp.getPayCosts().getTotalMana().getCMC() > num);

View File

@@ -247,6 +247,10 @@ public class EffectEffect extends SpellAbilityEffect {
addForgetOnCastTrigger(eff, sa.getParam("ForgetOnCast"));
}
if (sa.hasParam("ExileOnLost")) {
addExileOnLostTrigger(eff);
}
// Set Imprinted
if (effectImprinted != null) {
eff.addImprintedCards(AbilityUtils.getDefinedCards(hostCard, effectImprinted, sa));

View File

@@ -35,7 +35,7 @@ public class InternalRadiationEffect extends SpellAbilityEffect {
final CardCollectionView milled = game.getAction().mill(new PlayerCollection(p), numRad, ZoneType.Graveyard, sa, moveParams);
table.triggerChangesZoneAll(game, sa);
int n = CardLists.count(milled, Predicates.not(CardPredicates.Presets.LANDS));
if (StaticAbilityGainLifeRadiation.gainLifeRadiation(p)) {
p.gainLife(n, sa.getHostCard(), sa);
} else {
@@ -49,7 +49,7 @@ public class InternalRadiationEffect extends SpellAbilityEffect {
game.getTriggerHandler().runTrigger(TriggerType.LifeLostAll, runParams, false);
}
}
// and remove n rad counter
p.removeRadCounters(n);
}

View File

@@ -39,7 +39,7 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.AlternativeCost;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.zone.Zone;
@@ -343,7 +343,7 @@ public class PlayEffect extends SpellAbilityEffect {
final Zone originZone = tgtCard.getZone();
// lands will be played
if (tgtSA instanceof LandAbility) {
if (tgtSA.isLandAbility()) {
tgtSA.resolve();
amount--;
if (remember) {

View File

@@ -57,7 +57,7 @@ public class PlayLandVariantEffect extends SpellAbilityEffect {
PaperCard ran = Aggregates.random(cards);
random = CardFactory.getCard(ran, activator, game);
cards.remove(ran);
} while (!activator.canPlayLand(random, false));
} while (!activator.canPlayLand(random, false, random.getFirstSpellAbility()));
source.addCloneState(CardFactory.getCloneStates(random, source, sa), game.getNextTimestamp());
source.updateStateForView();

View File

@@ -2499,6 +2499,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
sb.append("exile it haunting target creature.");
}
sb.append(")");
} else if (keyword.startsWith("Bands with other")) {
final String[] k = keyword.split(":");
String desc = k.length > 2 ? k[2] : CardType.getPluralType(k[1]);
sbLong.append(k[0]).append(" ").append(desc).append(" (").append(inst.getReminderText()).append(")");
} else if (keyword.equals("Convoke") || keyword.equals("Dethrone")|| keyword.equals("Fear")
|| keyword.equals("Melee") || keyword.equals("Improvise")|| keyword.equals("Shroud")
|| keyword.equals("Banding") || keyword.equals("Intimidate")|| keyword.equals("Evolve")
@@ -4312,7 +4316,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
currentState.setAttractionLights(attractionLights);
}
public final int getBasePower() {
return currentState.getBasePower();
}
@@ -4704,11 +4707,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
}
private int multiKickerMagnitude = 0;
public final void setKickerMagnitude(final int n) { multiKickerMagnitude = n; }
public final int getKickerMagnitude() {
if (multiKickerMagnitude > 0) {
return multiKickerMagnitude;
if (this.getCastSA() != null && getCastSA().hasOptionalKeywordAmount(Keyword.MULTIKICKER)) {
return getCastSA().getOptionalKeywordAmount(Keyword.MULTIKICKER);
}
boolean hasK1 = isOptionalCostPaid(OptionalCost.Kicker1);
return hasK1 == isOptionalCostPaid(OptionalCost.Kicker2) ? (hasK1 ? 2 : 0) : 1;
@@ -7459,7 +7460,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
for (SpellAbility sa : getState(CardStateName.Modal).getSpellAbilities()) {
//add alternative costs as additional spell abilities
// only add Spells there
if (sa.isSpell()) {
if (sa.isSpell() || sa.isLandAbility()) {
abilities.add(sa);
abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player, false));
}
@@ -7495,106 +7496,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
abilities.removeAll(toRemove);
// Land Abilities below, move them to CardFactory after MayPlayRefactor
if (getLastKnownZone().is(ZoneType.Battlefield)) {
return abilities;
}
if (getState(CardStateName.Original).getType().isLand()) {
LandAbility la = new LandAbility(this, player, null);
la.setCardState(oState);
if (la.canPlay()) {
abilities.add(la);
}
Card source = this;
boolean lkicheck = false;
// if Card is Facedown, need to check if MayPlay still applies
if (isFaceDown()) {
lkicheck = true;
source = CardCopyService.getLKICopy(source);
source.forceTurnFaceUp();
}
if (lkicheck) {
// double freeze tracker, so it doesn't update view
game.getTracker().freeze();
CardCollection preList = new CardCollection(source);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(source), preList);
}
// extra for MayPlay
for (CardPlayOption o : source.mayPlay(player)) {
la = new LandAbility(this, player, o);
la.setCardState(oState);
if (la.canPlay()) {
abilities.add(la);
}
}
// reset static abilities
if (lkicheck) {
game.getAction().checkStaticAbilities(false);
// clear delayed changes, this check should not have updated the view
game.getTracker().clearDelayed();
// need to unfreeze tracker
game.getTracker().unfreeze();
}
}
if (isModal() && hasState(CardStateName.Modal)) {
CardState modal = getState(CardStateName.Modal);
if (modal.getType().isLand()) {
LandAbility la = new LandAbility(this, player, null);
la.setCardState(modal);
Card source = CardCopyService.getLKICopy(this);
boolean lkicheck = true;
// if Card is Facedown, need to check if MayPlay still applies
if (isFaceDown()) {
source.forceTurnFaceUp();
}
// the modal state is not copied with lki, need to copy it extra
if (!source.hasState(CardStateName.Modal)) {
source.addAlternateState(CardStateName.Modal, false);
source.getState(CardStateName.Modal).copyFrom(this.getState(CardStateName.Modal), true);
}
source.setSplitStateToPlayAbility(la);
if (la.canPlay(source)) {
abilities.add(la);
}
if (lkicheck) {
// double freeze tracker, so it doesn't update view
game.getTracker().freeze();
CardCollection preList = new CardCollection(source);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(source), preList);
}
// extra for MayPlay
for (CardPlayOption o : source.mayPlay(player)) {
la = new LandAbility(this, player, o);
la.setCardState(modal);
if (la.canPlay(source)) {
abilities.add(la);
}
}
// reset static abilities
if (lkicheck) {
game.getAction().checkStaticAbilities(false);
// clear delayed changes, this check should not have updated the view
game.getTracker().clearDelayed();
// need to unfreeze tracker
game.getTracker().unfreeze();
}
}
}
return abilities;
}

View File

@@ -379,8 +379,6 @@ public class CardCopyService {
newCopy.updateKeywordsCache(newCopy.getState(s));
}
newCopy.setKickerMagnitude(copyFrom.getKickerMagnitude());
if (copyFrom.getCastSA() != null) {
SpellAbility castSA = copyFrom.getCastSA().copy(newCopy, true);
castSA.setLastStateBattlefield(CardCollection.EMPTY);

View File

@@ -107,7 +107,6 @@ public class CardFactory {
copy.setCopiedPermanent(original);
copy.setXManaCostPaidByColor(original.getXManaCostPaidByColor());
copy.setKickerMagnitude(original.getKickerMagnitude());
copy.setPromisedGift(original.getPromisedGift());
if (targetSA.isBestow()) {
@@ -414,17 +413,16 @@ public class CardFactory {
// SpellPermanent only for Original State
if (c.getCurrentStateName() == CardStateName.Original || c.getCurrentStateName() == CardStateName.Modal || c.getCurrentStateName().toString().startsWith("Specialize")) {
// this is the "default" spell for permanents like creatures and artifacts
if (c.isPermanent() && !c.isAura() && !c.isLand()) {
if (c.isLand()) {
SpellAbility sa = new LandAbility(c);
sa.setCardState(c.getCurrentState());
c.addSpellAbility(sa);
} else if (c.isPermanent() && !c.isAura()) {
// this is the "default" spell for permanents like creatures and artifacts
SpellAbility sa = new SpellPermanent(c);
// Currently only for Modal, might react different when state is always set
//if (c.getCurrentStateName() == CardStateName.Modal) {
sa.setCardState(c.getCurrentState());
//}
sa.setCardState(c.getCurrentState());
c.addSpellAbility(sa);
}
// TODO add LandAbility there when refactor MayPlay
}
CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities());

View File

@@ -3268,11 +3268,6 @@ public class CardFactoryUtil {
sa.putParam("AfterDescription", "(Converted)");
sa.setIntrinsic(intrinsic);
inst.addSpellAbility(sa);
} else if (keyword.startsWith("Multikicker")) {
final String[] n = keyword.split(":");
final SpellAbility sa = card.getFirstSpellAbility();
sa.setMultiKickerManaCost(new ManaCost(new ManaCostParser(n[1])));
sa.addAnnounceVar("Multikicker");
} else if (keyword.startsWith("Mutate")) {
final String[] params = keyword.split(":");
final String cost = params[1];

View File

@@ -60,6 +60,7 @@ public final class CardUtil {
"Fortify", "Transfigure", "Champion", "Evoke", "Prowl", "Freerunning",
"Reinforce", "Unearth", "Level up", "Miracle", "Overload", "Cleave",
"Scavenge", "Encore", "Bestow", "Outlast", "Dash", "Surge", "Emerge", "Hexproof:",
"Bands with other",
"etbCounter", "Reflect", "Ward").build();
/** List of keyword endings of keywords that could be modified by text changes. */
public static final ImmutableList<String> modifiableKeywordEndings = ImmutableList.<String>builder().add(

View File

@@ -5,6 +5,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import java.util.List;
@@ -39,28 +40,20 @@ public class AttackingBand {
return true;
}
// Legends lands, Master of the Hunt, Old Fogey (just in case)
// Since Bands With Other is a dead keyword, no major reason to make this more generic
// But if someone is super motivated, feel free to do it. Just make sure you update Tolaria and Shelkie Brownie
String[] bandsWithString = { "Bands with Other Legendary Creatures", "Bands with Other Creatures named Wolves of the Hunt",
"Bands with Other Dinosaurs" };
String[] validString = { "Legendary.Creature", "Creature.namedWolves of the Hunt", "Dinosaur" };
for (Card c : CardLists.getKeyword(band, Keyword.BANDSWITH)) {
for (KeywordInterface kw : c.getKeywords(Keyword.BANDSWITH)) {
String o = kw.getOriginal();
String m[] = o.split(":");
Card source = band.get(0);
for (int i = 0; i < bandsWithString.length; i++) {
String keyword = bandsWithString[i];
String valid = validString[i];
// Check if a bands with other keyword exists in band, and each creature in the band fits the valid quality
if (!CardLists.getKeyword(band, keyword).isEmpty() &&
CardLists.getValidCards(band, valid, source.getController(), source, null).size() == band.size()) {
return true;
if (CardLists.getValidCards(band, m[1], c.getController(), c, null).size() == band.size()) {
return true;
}
}
}
return false;
}
public boolean canJoinBand(Card card) {
// Trying to join an existing band, attackers should be non-empty and card should exist
CardCollection newBand = new CardCollection(attackers);

View File

@@ -22,6 +22,7 @@ public enum Keyword {
AWAKEN("Awaken", KeywordWithCostAndAmount.class, false, "If you cast this spell for %s, also put {%d:+1/+1 counter} on target land you control and it becomes a 0/0 Elemental creature with haste. It's still a land."),
BACKUP("Backup", KeywordWithAmount.class, false, "When this creature enters, put {%1$d:+1/+1 counter} on target creature. If that's another creature, it gains the following ability until end of turn."),
BANDING("Banding", SimpleKeyword.class, true, "Any creatures with banding, and up to one without, can attack in a band. Bands are blocked as a group. If any creatures with banding you control are blocking or being blocked by a creature, you divide that creature's combat damage, not its controller, among any of the creatures it's being blocked by or is blocking."),
BANDSWITH("Bands with other", KeywordWithType.class, false, "can attack in a band with another %s"),
BARGAIN("Bargain", SimpleKeyword.class, false, "You may sacrifice an artifact, enchantment, or token as you cast this spell."),
BATTLE_CRY("Battle cry", SimpleKeyword.class, false, "Whenever this creature attacks, each other attacking creature gets +1/+0 until end of turn."),
BESTOW("Bestow", KeywordWithCost.class, false, "If you cast this card for its bestow cost, it's an Aura spell with enchant creature. It becomes a creature again if it's not attached to a creature."),

View File

@@ -20,6 +20,7 @@ public class KeywordWithType extends KeywordInstance<KeywordWithType> {
type = "artifact, legendary, and/or Saga permanent";
}
break;
case BANDSWITH:
case HEXPROOF:
case LANDWALK:
type = details.split(":")[1];

View File

@@ -32,7 +32,7 @@ import forge.game.event.*;
import forge.game.player.Player;
import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -509,6 +509,8 @@ public class PhaseHandler implements java.io.Serializable {
// done this after check state effects, so it only has effect next check
game.getCleanup().executeUntil(playerTurn);
handleMultiplayerEffects();
// "Trigger" for begin turn to get around a phase skipping
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(playerTurn);
game.getTriggerHandler().runTrigger(TriggerType.TurnBegin, runParams, false);
@@ -1062,7 +1064,7 @@ public class PhaseHandler implements java.io.Serializable {
final Zone currentZone = saHost.getZone();
// Need to check if Zone did change
if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa instanceof LandAbility)) {
if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa.isLandAbility())) {
// currently there can be only one Spell put on the Stack at once, or Land Abilities be played
final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost);
@@ -1267,4 +1269,33 @@ public class PhaseHandler implements java.io.Serializable {
}
return count;
}
private void handleMultiplayerEffects() {
// CR 800.4m When a player leaves the game, any continuous effects with durations that last until that
// players next turn or until a specific point in that turn will last until that turn would have begun
int oldPlayerIdx = game.getRegisteredPlayers().indexOf(playerPreviousTurn);
final int playerIdx = game.getRegisteredPlayers().indexOf(playerTurn);
final int direction = game.getTurnOrder().getShift();
while (oldPlayerIdx != playerIdx) {
oldPlayerIdx += direction;
if (oldPlayerIdx < 0) {
oldPlayerIdx = game.getRegisteredPlayers().size() - 1;
} else if (oldPlayerIdx > game.getRegisteredPlayers().size() - 1) {
oldPlayerIdx = 0;
}
Player p = game.getRegisteredPlayers().get(oldPlayerIdx);
if (p.hasLost()) {
// CR 702.26n
Untap.doPhasing(p);
game.getUntap().executeUntil(p);
game.getUpkeep().executeUntil(p);
game.getUpkeep().executeUntilEndOfPhase(p);
game.getEndOfCombat().executeUntilEndOfPhase(p);
game.getEndOfTurn().executeUntil(p);
game.getEndOfTurn().executeUntilEndOfPhase(p);
game.getCleanup().executeUntil(p);
}
}
}
}

View File

@@ -258,7 +258,7 @@ public class Untap extends Phase {
return untap;
}
private static void doPhasing(final Player turn) {
public static void doPhasing(final Player turn) {
// Needs to include phased out cards
final List<Card> list = CardLists.filter(turn.getGame().getCardsIncludePhasingIn(ZoneType.Battlefield),
c -> (c.isPhasedOut(turn) && c.isDirectlyPhasedOut())

View File

@@ -43,7 +43,7 @@ import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.*;
import forge.game.trigger.Trigger;
@@ -1692,9 +1692,9 @@ public class Player extends GameEntity implements Comparable<Player> {
game.fireEvent(new GameEventShuffle(this));
}
public final boolean playLand(final Card land, final boolean ignoreZoneAndTiming) {
public final boolean playLand(final Card land, final boolean ignoreZoneAndTiming, SpellAbility cause) {
// Dakkon Blackblade Avatar will use a similar effect
if (canPlayLand(land, ignoreZoneAndTiming)) {
if (canPlayLand(land, ignoreZoneAndTiming, cause)) {
playLandNoCheck(land, null);
return true;
}
@@ -1707,7 +1707,7 @@ public class Player extends GameEntity implements Comparable<Player> {
land.setController(this, 0);
if (land.isFaceDown()) {
land.turnFaceUp(null);
if (cause instanceof LandAbility) {
if (cause.isLandAbility()) {
land.changeToState(cause.getCardStateName());
}
}
@@ -1731,12 +1731,6 @@ public class Player extends GameEntity implements Comparable<Player> {
return c;
}
public final boolean canPlayLand(final Card land) {
return canPlayLand(land, false);
}
public final boolean canPlayLand(final Card land, final boolean ignoreZoneAndTiming) {
return canPlayLand(land, ignoreZoneAndTiming, null);
}
public final boolean canPlayLand(final Card land, final boolean ignoreZoneAndTiming, SpellAbility landSa) {
if (!ignoreZoneAndTiming) {
// CR 305.3

View File

@@ -21,24 +21,20 @@ import forge.card.CardStateName;
import forge.card.mana.ManaCost;
import forge.game.card.Card;
import forge.game.card.CardCopyService;
import forge.game.card.CardPlayOption;
import forge.game.cost.Cost;
import forge.game.player.Player;
import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
import forge.util.CardTranslation;
import forge.util.Localizer;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
public class LandAbility extends Ability {
public class LandAbility extends AbilityStatic {
public LandAbility(Card sourceCard, Player p, CardPlayOption mayPlay) {
super(sourceCard, new Cost(ManaCost.NO_COST, false));
setActivatingPlayer(p);
setMayPlay(mayPlay);
}
public LandAbility(Card sourceCard) {
this(sourceCard, sourceCard.getController(), null);
super(sourceCard, ManaCost.NO_COST);
getRestrictions().setZone(ZoneType.Hand);
}
public boolean canPlay(Card newHost) {
@@ -46,11 +42,21 @@ public class LandAbility extends Ability {
return p.canPlayLand(newHost, false, this);
}
@Override
public boolean isLandAbility() { return true; }
@Override
public boolean isSecondary() {
return true;
}
@Override
public boolean canPlay() {
Card land = this.getHostCard();
final Player p = this.getActivatingPlayer();
if (p == null) {
return false;
}
if (this.getCardState() != null && land.getCurrentStateName() != this.getCardStateName()) {
if (!land.isLKI()) {
land = CardCopyService.getLKICopy(land);
@@ -113,4 +119,41 @@ public class LandAbility extends Ability {
return sb.toString();
}
@Override
public Card getAlternateHost(Card source) {
boolean lkicheck = false;
// need to be done before so it works with Vivien and Zoetic Cavern
if (source.isFaceDown() && source.isInZone(ZoneType.Exile)) {
if (!source.isLKI()) {
source = CardCopyService.getLKICopy(source);
}
source.forceTurnFaceUp();
lkicheck = true;
}
if (getCardState() != null && source.getCurrentStateName() != getCardStateName()) {
if (!source.isLKI()) {
source = CardCopyService.getLKICopy(source);
}
CardStateName stateName = getCardState().getStateName();
if (!source.hasState(stateName)) {
source.addAlternateState(stateName, false);
source.getState(stateName).copyFrom(getHostCard().getState(stateName), true);
}
source.setState(stateName, false);
if (getHostCard().isDoubleFaced()) {
source.setBackSide(getHostCard().getRules().getSplitType().getChangedStateName().equals(stateName));
}
// need to reset CMC
source.setLKICMC(-1);
source.setLKICMC(source.getCMC());
lkicheck = true;
}
return lkicheck ? source : null;
}
}

View File

@@ -153,6 +153,7 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
this.castFaceDown = faceDown;
}
@Override
public Card getAlternateHost(Card source) {
boolean lkicheck = false;

View File

@@ -31,7 +31,6 @@ import forge.card.CardStateName;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.card.mana.ManaCost;
import forge.game.CardTraitBase;
import forge.game.ForgeScript;
import forge.game.Game;
@@ -110,7 +109,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private Pair<Long, Player> controlledByPlayer;
private ManaCostBeingPaid manaCostBeingPaid;
private ManaCost multiKickerManaCost;
private int spentPhyrexian = 0;
private int paidLifeAmount = 0;
@@ -456,13 +454,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
// all Spell's and Abilities must override this method
public abstract void resolve();
public ManaCost getMultiKickerManaCost() {
return multiKickerManaCost;
}
public void setMultiKickerManaCost(final ManaCost cost) {
multiKickerManaCost = cost;
}
public Player getActivatingPlayer() {
return activatingPlayer;
}
@@ -536,6 +527,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public boolean isSpell() { return false; }
public boolean isAbility() { return true; }
public boolean isActivatedAbility() { return false; }
public boolean isLandAbility() { return false; }
public boolean isTurnFaceUp() {
return isMorphUp() || isDisguiseUp() || isManifestUp() || isCloakUp();
@@ -797,7 +789,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public boolean isKicked() {
return isOptionalCostPaid(OptionalCost.Kicker1) || isOptionalCostPaid(OptionalCost.Kicker2) ||
getHostCard().getKickerMagnitude() > 0;
getRootAbility().getOptionalKeywordAmount(Keyword.MULTIKICKER) > 0;
}
public boolean isEntwine() {
@@ -2194,7 +2186,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
}
else if (incR[0].contains("LandAbility")) {
if (!(root instanceof LandAbility)) {
if (!(root.isLandAbility())) {
return testFailed;
}
}
@@ -2553,7 +2545,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
if (getRestrictions().isInstantSpeed()) {
return true;
}
if ((isSpell() || this instanceof LandAbility) && (isCastFromPlayEffect() || host.isInstant() || host.hasKeyword(Keyword.FLASH))) {
if ((isSpell() || this.isLandAbility()) && (isCastFromPlayEffect() || host.isInstant() || host.hasKeyword(Keyword.FLASH))) {
return true;
}
@@ -2598,6 +2590,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return true;
}
public Card getAlternateHost(Card source) {
return null;
}
public boolean hasOptionalKeywordAmount(KeywordInterface kw) {
return this.optionalKeywordAmount.contains(kw.getKeyword(), Pair.of(kw.getIdx(), kw.getStaticId()));
}
@@ -2611,7 +2607,13 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public int getOptionalKeywordAmount(KeywordInterface kw) {
return ObjectUtils.firstNonNull(this.optionalKeywordAmount.get(kw.getKeyword(), Pair.of(kw.getIdx(), kw.getStaticId())), 0);
}
public int getOptionalKeywordAmount(Keyword kw) {
return this.optionalKeywordAmount.row(kw).values().stream().mapToInt(i->i).sum();
}
public void setOptionalKeywordAmount(KeywordInterface kw, int amount) {
this.optionalKeywordAmount.put(kw.getKeyword(), Pair.of(kw.getIdx(), kw.getStaticId()), amount);
}
public void clearOptionalKeywordAmount() {
optionalKeywordAmount.clear();
}
}

View File

@@ -17,7 +17,6 @@ public class TriggerPhaseOutAll extends Trigger {
@Override
public boolean performTest(Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidCards", runParams.get(AbilityKey.Cards))) {
return false;
}

View File

@@ -15,7 +15,6 @@ public class TriggerTakesInitiative extends Trigger {
@Override
public boolean performTest(Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) {
return false;
}

View File

@@ -361,15 +361,6 @@ public class WrappedAbility extends Ability {
sa.setDescription(s);
}
@Override
public ManaCost getMultiKickerManaCost() {
return sa.getMultiKickerManaCost();
}
@Override
public void setMultiKickerManaCost(final ManaCost cost) {
sa.setMultiKickerManaCost(cost);
}
@Override
public void setPayCosts(final Cost abCost) {
sa.setPayCosts(abCost);