Merge branch 'phasing' into 'master'

Phasing Fixes

Closes #531

See merge request core-developers/forge!4808
This commit is contained in:
Michael Kamensky
2021-06-05 14:27:11 +00:00
12 changed files with 31 additions and 20 deletions

View File

@@ -676,7 +676,6 @@ public class AiAttackController {
* @return a {@link forge.game.combat.Combat} object. * @return a {@link forge.game.combat.Combat} object.
*/ */
public final void declareAttackers(final Combat combat) { public final void declareAttackers(final Combat combat) {
if (this.attackers.isEmpty()) { if (this.attackers.isEmpty()) {
return; return;
} }

View File

@@ -6,7 +6,6 @@ package forge.card;
*/ */
public class CardAiHints { public class CardAiHints {
private final boolean isRemovedFromAIDecks; private final boolean isRemovedFromAIDecks;
private final boolean isRemovedFromRandomDecks; private final boolean isRemovedFromRandomDecks;
private final boolean isRemovedFromNonCommanderDecks; private final boolean isRemovedFromNonCommanderDecks;
@@ -15,7 +14,6 @@ public class CardAiHints {
private final DeckHints deckNeeds; private final DeckHints deckNeeds;
private final DeckHints deckHas; private final DeckHints deckHas;
public CardAiHints(boolean remAi, boolean remRandom, boolean remUnlessCommander, DeckHints dh, DeckHints dn, DeckHints has) { public CardAiHints(boolean remAi, boolean remRandom, boolean remUnlessCommander, DeckHints dh, DeckHints dn, DeckHints has) {
isRemovedFromAIDecks = remAi; isRemovedFromAIDecks = remAi;
isRemovedFromRandomDecks = remRandom; isRemovedFromRandomDecks = remRandom;
@@ -90,5 +88,4 @@ public class CardAiHints {
} }
} }
} }

View File

@@ -756,7 +756,7 @@ public class Game {
// Rule 800.4 Losing a Multiplayer game // Rule 800.4 Losing a Multiplayer game
CardCollectionView cards = this.getCardsInGame(); CardCollectionView cards = this.getCardsInGame();
boolean planarControllerLost = false; boolean planarControllerLost = false;
boolean isMultiplayer = this.getPlayers().size() > 2; boolean isMultiplayer = getPlayers().size() > 2;
// 702.142f & 707.9 // 702.142f & 707.9
// If a player leaves the game, all face-down cards that player owns must be revealed to all players. // If a player leaves the game, all face-down cards that player owns must be revealed to all players.
@@ -793,16 +793,16 @@ public class Game {
} }
getAction().ceaseToExist(c, false); getAction().ceaseToExist(c, false);
// CR 603.2f owner of trigger source lost game // CR 603.2f owner of trigger source lost game
triggerHandler.clearDelayedTrigger(c); getTriggerHandler().clearDelayedTrigger(c);
} else { } else {
// return stolen permanents // return stolen permanents
if (c.getController().equals(p) && c.isInZone(ZoneType.Battlefield)) { if ((c.getController().equals(p) || c.getZone().getPlayer().equals(p)) && c.isInZone(ZoneType.Battlefield)) {
c.removeTempController(p); c.removeTempController(p);
getAction().controllerChangeZoneCorrection(c); getAction().controllerChangeZoneCorrection(c);
} }
c.removeTempController(p); c.removeTempController(p);
if (c.getController().equals(p)) { if (c.getController().equals(p)) {
this.getAction().exile(c, null); getAction().exile(c, null);
} }
} }
} else { } else {
@@ -830,7 +830,7 @@ public class Game {
} }
// Remove leftover items from // Remove leftover items from
this.getStack().removeInstancesControlledBy(p); getStack().removeInstancesControlledBy(p);
getTriggerHandler().onPlayerLost(p); getTriggerHandler().onPlayerLost(p);

View File

@@ -21,6 +21,7 @@ import forge.game.GameObject;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardUtil;
import forge.game.card.CardZoneTable; import forge.game.card.CardZoneTable;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
@@ -296,7 +297,7 @@ public abstract class SpellAbilityEffect {
} }
delTrig.append("| TriggerDescription$ ").append(desc); delTrig.append("| TriggerDescription$ ").append(desc);
final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), sa.getHostCard(), intrinsic); final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), CardUtil.getLKICopy(sa.getHostCard()), intrinsic);
for (final Card c : crds) { for (final Card c : crds) {
trig.addRemembered(c); trig.addRemembered(c);

View File

@@ -502,7 +502,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
// gameCard is LKI in that case, the card is not in game anymore // gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change // or the timestamp did change
// this should check Self too // this should check Self too
if (gameCard == null || !tgtC.equalsWithTimestamp(gameCard)) { if (gameCard == null || !tgtC.equalsWithTimestamp(gameCard) || gameCard.isPhasedOut()) {
continue; continue;
} }
if (sa.usesTargeting() && !gameCard.canBeTargetedBy(sa)) { if (sa.usesTargeting() && !gameCard.canBeTargetedBy(sa)) {

View File

@@ -72,7 +72,7 @@ public class PhasesEffect extends SpellAbilityEffect {
for (final Card tgtC : tgtCards) { for (final Card tgtC : tgtCards) {
if (!tgtC.isPhasedOut()) { if (!tgtC.isPhasedOut()) {
tgtC.phase(false); tgtC.phase(false);
if ( tgtC.isPhasedOut()) { if (tgtC.isPhasedOut()) {
tgtC.setWontPhaseInNormal(wontPhaseInNormal); tgtC.setWontPhaseInNormal(wontPhaseInNormal);
} }
} }

View File

@@ -4609,8 +4609,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
setDirectlyPhasedOut(direct); setDirectlyPhasedOut(direct);
} }
if (hasCardAttachments()) { // CR 702.25g
for (final Card eq : getAttachedCards()) { if (!getAllAttachedCards().isEmpty()) {
for (final Card eq : getAllAttachedCards()) {
if (eq.isPhasedOut() == phasingIn) { if (eq.isPhasedOut() == phasingIn) {
eq.phase(fromUntapStep, false); eq.phase(fromUntapStep, false);
} }
@@ -4627,7 +4628,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
private boolean switchPhaseState(final boolean fromUntapStep) { private boolean switchPhaseState(final boolean fromUntapStep) {
if (phasedOut && hasKeyword("CARDNAME can't phase in.")) { if (phasedOut && hasKeyword("CARDNAME can't phase in.")) {
return false; return false;
} }
@@ -4646,6 +4646,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// If this is currently PhasedIn, it's about to phase out. // If this is currently PhasedIn, it's about to phase out.
// Run trigger before it does because triggers don't work with phased out objects // Run trigger before it does because triggers don't work with phased out objects
getGame().getTriggerHandler().runTrigger(TriggerType.PhaseOut, runParams, false); getGame().getTriggerHandler().runTrigger(TriggerType.PhaseOut, runParams, false);
// when it doesn't exist the game will no longer see it as tapped
runUntapCommands();
// TODO need to run UntilHostLeavesPlay commands but only when worded "for as long as"
} }
setPhasedOut(!phasedOut); setPhasedOut(!phasedOut);
@@ -4659,6 +4662,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
getGame().getTriggerHandler().registerActiveTrigger(this, false); getGame().getTriggerHandler().registerActiveTrigger(this, false);
getGame().getTriggerHandler().runTrigger(TriggerType.PhaseIn, runParams, false); getGame().getTriggerHandler().runTrigger(TriggerType.PhaseIn, runParams, false);
} }
game.updateLastStateForCard(this);
return true; return true;
} }
@@ -5769,11 +5774,11 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
public final boolean canBeDestroyed() { public final boolean canBeDestroyed() {
return isInPlay() && (!hasKeyword(Keyword.INDESTRUCTIBLE) || (isCreature() && getNetToughness() <= 0)); return isInPlay() && !isPhasedOut() && (!hasKeyword(Keyword.INDESTRUCTIBLE) || (isCreature() && getNetToughness() <= 0));
} }
public final boolean canBeSacrificed() { public final boolean canBeSacrificed() {
return isInPlay() && !this.isPhasedOut() && !hasKeyword("CARDNAME can't be sacrificed."); return isInPlay() && !isPhasedOut() && !hasKeyword("CARDNAME can't be sacrificed.");
} }
@Override @Override

View File

@@ -47,6 +47,11 @@ public class CardProperty {
final Card lki = game.getChangeZoneLKIInfo(card); final Card lki = game.getChangeZoneLKIInfo(card);
final Player controller = lki.getController(); final Player controller = lki.getController();
// CR 702.25b if card is phased out it will not count unless specifically asked for
if (card.isPhasedOut() && !property.contains("phasedOut")) {
return false;
}
// by name can also have color names, so needs to happen before colors. // by name can also have color names, so needs to happen before colors.
if (property.startsWith("named")) { if (property.startsWith("named")) {
String name = TextUtil.fastReplace(property.substring(5), ";", ","); // for some legendary cards String name = TextUtil.fastReplace(property.substring(5), ";", ","); // for some legendary cards

View File

@@ -253,6 +253,8 @@ public final class CardUtil {
newCopy.setCounters(Maps.newHashMap(in.getCounters())); newCopy.setCounters(Maps.newHashMap(in.getCounters()));
newCopy.setColor(in.determineColor().getColor()); newCopy.setColor(in.determineColor().getColor());
newCopy.setPhasedOut(in.isPhasedOut());
newCopy.setReceivedDamageFromThisTurn(in.getReceivedDamageFromThisTurn()); newCopy.setReceivedDamageFromThisTurn(in.getReceivedDamageFromThisTurn());
newCopy.setDamageHistory(in.getDamageHistory()); newCopy.setDamageHistory(in.getDamageHistory());
for (Card c : in.getBlockedThisTurn()) { for (Card c : in.getBlockedThisTurn()) {

View File

@@ -475,7 +475,7 @@ public class PhaseHandler implements java.io.Serializable {
boolean manaBurns = game.getRules().hasManaBurn() || boolean manaBurns = game.getRules().hasManaBurn() ||
(game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.manaBurn)); (game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.manaBurn));
if (manaBurns) { if (manaBurns) {
p.loseLife(burn,true); p.loseLife(burn, true);
} }
} }
@@ -485,7 +485,7 @@ public class PhaseHandler implements java.io.Serializable {
break; break;
case UPKEEP: case UPKEEP:
for (Card c : game.getCardsIn(ZoneType.Battlefield)) { for (Card c : game.getCardsIncludePhasingIn(ZoneType.Battlefield)) {
c.getDamageHistory().setNotAttackedSinceLastUpkeepOf(playerTurn); c.getDamageHistory().setNotAttackedSinceLastUpkeepOf(playerTurn);
c.getDamageHistory().setNotBlockedSinceLastUpkeepOf(playerTurn); c.getDamageHistory().setNotBlockedSinceLastUpkeepOf(playerTurn);
c.getDamageHistory().setNotBeenBlockedSinceLastUpkeepOf(playerTurn); c.getDamageHistory().setNotBeenBlockedSinceLastUpkeepOf(playerTurn);

View File

@@ -71,6 +71,8 @@ public class Untap extends Phase {
final Player turn = game.getPhaseHandler().getPlayerTurn(); final Player turn = game.getPhaseHandler().getPlayerTurn();
Untap.doPhasing(turn); Untap.doPhasing(turn);
game.getAction().checkStaticAbilities();
doUntap(); doUntap();
} }

View File

@@ -8,5 +8,5 @@ SVar:WinGame:DB$ WinsGame | Defined$ You | ConditionCheckSVar$ TalkedEnough | Co
SVar:TalkedEnough:Count$CardCounters.FILIBUSTER SVar:TalkedEnough:Count$CardCounters.FILIBUSTER
T:Mode$ DamageDone | ValidTarget$ You | TriggerZones$ Battlefield | Execute$ RemoveCounter | TriggerDescription$ Whenever a source deals damage to you, remove a filibuster counter from CARDNAME. T:Mode$ DamageDone | ValidTarget$ You | TriggerZones$ Battlefield | Execute$ RemoveCounter | TriggerDescription$ Whenever a source deals damage to you, remove a filibuster counter from CARDNAME.
SVar:RemoveCounter:DB$ RemoveCounter | Defined$ Self | CounterType$ FILIBUSTER | CounterNum$ 1 SVar:RemoveCounter:DB$ RemoveCounter | Defined$ Self | CounterType$ FILIBUSTER | CounterNum$ 1
SVar:Picture:http://www.wizards.com/global/images/magic/general/azors_elocutors.jpg SVar:AIEvaluationModifier:30
Oracle:At the beginning of your upkeep, put a filibuster counter on Azor's Elocutors. Then if Azor's Elocutors has five or more filibuster counters on it, you win the game.\nWhenever a source deals damage to you, remove a filibuster counter from Azor's Elocutors. Oracle:At the beginning of your upkeep, put a filibuster counter on Azor's Elocutors. Then if Azor's Elocutors has five or more filibuster counters on it, you win the game.\nWhenever a source deals damage to you, remove a filibuster counter from Azor's Elocutors.