Merge branch 'suspendHaste' into 'master'

Suspend: reworked suspend

See merge request core-developers/forge!573
This commit is contained in:
Michael Kamensky
2018-05-22 05:39:40 +00:00
11 changed files with 35 additions and 104 deletions

View File

@@ -44,7 +44,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView {
/** The temporarily suppressed. */ /** The temporarily suppressed. */
protected boolean temporarilySuppressed = false; protected boolean temporarilySuppressed = false;
private Map<String, String> sVars = Maps.newHashMap(); protected Map<String, String> sVars = Maps.newHashMap();
/** Keys of descriptive (text) parameters. */ /** Keys of descriptive (text) parameters. */
private static final ImmutableList<String> descriptiveKeys = ImmutableList.<String>builder() private static final ImmutableList<String> descriptiveKeys = ImmutableList.<String>builder()

View File

@@ -105,10 +105,6 @@ public class GameAction {
boolean fromBattlefield = zoneFrom != null && zoneFrom.is(ZoneType.Battlefield); boolean fromBattlefield = zoneFrom != null && zoneFrom.is(ZoneType.Battlefield);
boolean toHand = zoneTo.is(ZoneType.Hand); boolean toHand = zoneTo.is(ZoneType.Hand);
// TODO: part of a workaround for suspend-cast creaturs bounced to hand
boolean zoneChangedEarly = false;
Zone originalZone = c.getZone();
//Rule 110.5g: A token that has left the battlefield can't move to another zone //Rule 110.5g: A token that has left the battlefield can't move to another zone
if (c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Command)) { if (c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Command)) {
return c; return c;
@@ -158,13 +154,6 @@ public class GameAction {
c.setState(CardStateName.Original, true); c.setState(CardStateName.Original, true);
} }
if (fromBattlefield && toHand && c.wasSuspendCast()) {
// TODO: This has to be set early for suspend-cast creatures bounced to hand, otherwise they
// end up in a state when they are considered on the battlefield. There should be a better solution.
c.setZone(zoneTo);
zoneChangedEarly = true;
}
// Clean up the temporary Dash SVar when the Dashed card leaves the battlefield // Clean up the temporary Dash SVar when the Dashed card leaves the battlefield
if (fromBattlefield && c.getSVar("EndOfTurnLeavePlay").equals("Dash")) { if (fromBattlefield && c.getSVar("EndOfTurnLeavePlay").equals("Dash")) {
c.removeSVar("EndOfTurnLeavePlay"); c.removeSVar("EndOfTurnLeavePlay");
@@ -295,10 +284,6 @@ public class GameAction {
ReplacementResult repres = game.getReplacementHandler().run(repParams); ReplacementResult repres = game.getReplacementHandler().run(repParams);
if (repres != ReplacementResult.NotReplaced) { if (repres != ReplacementResult.NotReplaced) {
if (zoneChangedEarly) {
c.setZone(originalZone); // TODO: part of a workaround for bounced suspend-cast cards
}
// reset failed manifested Cards back to original // reset failed manifested Cards back to original
if (c.isManifested()) { if (c.isManifested()) {
c.turnFaceUp(false, false); c.turnFaceUp(false, false);
@@ -315,10 +300,6 @@ public class GameAction {
copied.getOwner().removeInboundToken(copied); copied.getOwner().removeInboundToken(copied);
if (c.wasSuspendCast()) {
copied = GameAction.addSuspendTriggers(copied);
}
if (suppress) { if (suppress) {
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone); game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
} }
@@ -396,7 +377,7 @@ public class GameAction {
} }
game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, true); game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, true);
if (zoneFrom != null && zoneFrom.is(ZoneType.Battlefield)) { if (zoneFrom != null && zoneFrom.is(ZoneType.Battlefield) && !zoneFrom.getPlayer().equals(zoneTo.getPlayer())) {
final Map<String, Object> runParams2 = Maps.newHashMap(); final Map<String, Object> runParams2 = Maps.newHashMap();
runParams2.put("Card", lastKnownInfo); runParams2.put("Card", lastKnownInfo);
runParams2.put("OriginalController", zoneFrom.getPlayer()); runParams2.put("OriginalController", zoneFrom.getPlayer());
@@ -436,7 +417,6 @@ public class GameAction {
if (fromBattlefield) { if (fromBattlefield) {
if (!c.isToken()) { if (!c.isToken()) {
copied.setSuspendCast(false);
copied.setState(CardStateName.Original, true); copied.setState(CardStateName.Original, true);
} }
// Soulbond unpairing // Soulbond unpairing
@@ -1508,44 +1488,6 @@ public class GameAction {
return sacrificed != null; return sacrificed != null;
} }
private static Card addSuspendTriggers(final Card c) {
if (c.getSVar("HasteFromSuspend").equals("True")) {
return c;
}
c.setSVar("HasteFromSuspend", "True");
final GameCommand intoPlay = new GameCommand() {
private static final long serialVersionUID = -4514610171270596654L;
@Override
public void run() {
if (c.isInPlay() && c.isCreature()) {
c.addExtrinsicKeyword("Haste");
c.updateStateForView();
}
} // execute()
};
c.addComesIntoPlayCommand(intoPlay);
final GameCommand loseControl = new GameCommand() {
private static final long serialVersionUID = -4514610171270596654L;
@Override
public void run() {
if (c.getSVar("HasteFromSuspend").equals("True")) {
c.setSVar("HasteFromSuspend", "False");
c.removeExtrinsicKeyword("Haste");
c.updateStateForView();
}
} // execute()
};
c.addChangeControllerCommand(loseControl);
c.addLeavesPlayCommand(loseControl);
return c;
}
/** /**
* @return the sacrificed Card in its new location, or {@code null} if the * @return the sacrificed Card in its new location, or {@code null} if the
* sacrifice wasn't successful. * sacrifice wasn't successful.

View File

@@ -185,10 +185,6 @@ public class PlayEffect extends SpellAbilityEffect {
} }
} }
if(sa.hasParam("SuspendCast")) {
tgtCard.setSuspendCast(true);
}
// lands will be played // lands will be played
if (tgtCard.isLand()) { if (tgtCard.isLand()) {
if (controller.playLand(tgtCard, true)) { if (controller.playLand(tgtCard, true)) {

View File

@@ -60,9 +60,6 @@ public class PumpAllEffect extends SpellAbilityEffect {
for (String kw : hiddenkws) { for (String kw : hiddenkws) {
tgtC.addHiddenExtrinsicKeyword(kw); tgtC.addHiddenExtrinsicKeyword(kw);
} }
if (suspend && !tgtC.hasSuspend()) {
tgtC.setSuspend(true);
}
if (sa.hasParam("RememberAllPumped")) { if (sa.hasParam("RememberAllPumped")) {
sa.getHostCard().addRemembered(tgtC); sa.getHostCard().addRemembered(tgtC);

View File

@@ -30,9 +30,11 @@ public class PumpEffect extends SpellAbilityEffect {
private static void applyPump(final SpellAbility sa, final Card applyTo, private static void applyPump(final SpellAbility sa, final Card applyTo,
final int a, final int d, final List<String> keywords, final int a, final int d, final List<String> keywords,
final long timestamp) { final long timestamp) {
final Card host = sa.getHostCard();
//if host is not on the battlefield don't apply //if host is not on the battlefield don't apply
// Suspend should does Affect the Stack
if (sa.hasParam("UntilLoseControlOfHost") if (sa.hasParam("UntilLoseControlOfHost")
&& !sa.getHostCard().isInPlay()) { && !(host.isInPlay() || host.isInZone(ZoneType.Stack))) {
return; return;
} }
final Game game = sa.getActivatingPlayer().getGame(); final Game game = sa.getActivatingPlayer().getGame();
@@ -45,9 +47,6 @@ public class PumpEffect extends SpellAbilityEffect {
redrawPT |= kw.contains("CARDNAME's power and toughness are switched"); redrawPT |= kw.contains("CARDNAME's power and toughness are switched");
} else { } else {
kws.add(kw); kws.add(kw);
if (kw.equals("Suspend") && !applyTo.hasSuspend()) {
applyTo.setSuspend(true);
}
} }
} }

View File

@@ -161,8 +161,6 @@ public class Card extends GameEntity implements Comparable<Card> {
private long bestowTimestamp = -1; private long bestowTimestamp = -1;
private long transformedTimestamp = 0; private long transformedTimestamp = 0;
private boolean suspendCast = false;
private boolean suspend = false;
private boolean tributed = false; private boolean tributed = false;
private boolean embalmed = false; private boolean embalmed = false;
private boolean eternalized = false; private boolean eternalized = false;
@@ -3754,17 +3752,8 @@ public class Card extends GameEntity implements Comparable<Card> {
} }
public final boolean hasSuspend() { public final boolean hasSuspend() {
return suspend; return hasKeyword(Keyword.SUSPEND) && getLastKnownZone().is(ZoneType.Exile)
} && getCounters(CounterType.TIME) >= 1;
public final void setSuspend(final boolean b) {
suspend = b;
}
public final boolean wasSuspendCast() {
return suspendCast;
}
public final void setSuspendCast(final boolean b) {
suspendCast = b;
} }
public final boolean isPhasedOut() { public final boolean isPhasedOut() {

View File

@@ -2950,10 +2950,27 @@ public class CardFactoryUtil {
playTrig.append(" | TriggerDescription$ When the last time counter is removed from this card, if it's exiled, play it without paying its mana cost if able. "); playTrig.append(" | TriggerDescription$ When the last time counter is removed from this card, if it's exiled, play it without paying its mana cost if able. ");
playTrig.append("If you can't, it remains exiled. If you cast a creature spell this way, it gains haste until you lose control of the spell or the permanent it becomes."); playTrig.append("If you can't, it remains exiled. If you cast a creature spell this way, it gains haste until you lose control of the spell or the permanent it becomes.");
final String abPlay = "DB$ Play | Defined$ Self | WithoutManaCost$ True | SuspendCast$ True"; String abPlay = "DB$ Play | Defined$ Self | WithoutManaCost$ True | SuspendCast$ True";
if (card.isPermanent()) {
abPlay += "| RememberPlayed$ True";
}
final SpellAbility saPlay = AbilityFactory.getAbility(abPlay, card);
if (card.isPermanent()) {
final String abPump = "DB$ Pump | Defined$ Remembered | KW$ Haste | PumpZone$ Stack "
+ "| ConditionDefined$ Remembered | ConditionPresent$ Creature | UntilLoseControlOfHost$ True";
final AbilitySub saPump = (AbilitySub)AbilityFactory.getAbility(abPump, card);
String dbClean = "DB$ Cleanup | ClearRemembered$ True";
final AbilitySub saCleanup = (AbilitySub) AbilityFactory.getAbility(dbClean, card);
saPump.setSubAbility(saCleanup);
saPlay.setSubAbility(saPump);
}
final Trigger parsedPlayTrigger = TriggerHandler.parseTrigger(playTrig.toString(), card, intrinsic); final Trigger parsedPlayTrigger = TriggerHandler.parseTrigger(playTrig.toString(), card, intrinsic);
parsedPlayTrigger.setOverridingAbility(AbilityFactory.getAbility(abPlay, card)); parsedPlayTrigger.setOverridingAbility(saPlay);
inst.addTrigger(parsedUpkeepTrig); inst.addTrigger(parsedUpkeepTrig);
inst.addTrigger(parsedPlayTrigger); inst.addTrigger(parsedPlayTrigger);
@@ -4026,8 +4043,6 @@ public class CardFactoryUtil {
inst.addSpellAbility(newSA); inst.addSpellAbility(newSA);
} else if (keyword.startsWith("Suspend") && !keyword.equals("Suspend")) { } else if (keyword.startsWith("Suspend") && !keyword.equals("Suspend")) {
// really needed?
card.setSuspend(true);
// only add it if suspend has counter and cost // only add it if suspend has counter and cost
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
@@ -4051,8 +4066,6 @@ public class CardFactoryUtil {
public void resolve() { public void resolve() {
final Game game = card.getGame(); final Game game = card.getGame();
final Card c = game.getAction().exile(this.getHostCard(), this); final Card c = game.getAction().exile(this.getHostCard(), this);
// better check?
c.setSuspend(true);
int counters = AbilityUtils.calculateAmount(c, k[1], this); int counters = AbilityUtils.calculateAmount(c, k[1], this);
c.addCounter(CounterType.TIME, counters, c, true); c.addCounter(CounterType.TIME, counters, c, true);

View File

@@ -1357,8 +1357,7 @@ public class CardProperty {
return false; return false;
} }
} else if (property.startsWith("suspended")) { } else if (property.startsWith("suspended")) {
if (!card.hasSuspend() || !game.isCardExiled(card) if (!card.hasSuspend()) {
|| !(card.getCounters(CounterType.TIME) >= 1)) {
return false; return false;
} }
} else if (property.startsWith("delved")) { } else if (property.startsWith("delved")) {

View File

@@ -138,7 +138,7 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
} }
if (!(isInstant || activator.canCastSorcery() || flash || getRestrictions().isInstantSpeed() if (!(isInstant || activator.canCastSorcery() || flash || getRestrictions().isInstantSpeed()
|| this.hasSVar("IsCastFromPlayEffect") || hasSVar("IsCastFromPlayEffect")
|| (card.isFaceDown() && !card.getLastKnownZone().is(ZoneType.Battlefield) && card.getState(CardStateName.Original).getType().isInstant()))) { || (card.isFaceDown() && !card.getLastKnownZone().is(ZoneType.Battlefield) && card.getState(CardStateName.Original).getType().isInstant()))) {
return false; return false;
} }
@@ -148,7 +148,11 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
} }
// for uncastables like lotus bloom, check if manaCost is blank (except for morph spells) // for uncastables like lotus bloom, check if manaCost is blank (except for morph spells)
if (!isCastFaceDown() && isBasicSpell() && card.getState(card.isFaceDown() ? CardStateName.Original : card.getCurrentStateName()).getManaCost().isNoCost()) { // but ignore if it comes from PlayEffect
if (!isCastFaceDown()
&& !hasSVar("IsCastFromPlayEffect")
&& isBasicSpell()
&& card.getState(card.isFaceDown() ? CardStateName.Original : card.getCurrentStateName()).getManaCost().isNoCost()) {
return false; return false;
} }

View File

@@ -854,6 +854,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
// clear maps for copy, the values will be added later // clear maps for copy, the values will be added later
clone.additionalAbilities = Maps.newHashMap(); clone.additionalAbilities = Maps.newHashMap();
clone.additionalAbilityLists = Maps.newHashMap(); clone.additionalAbilityLists = Maps.newHashMap();
clone.sVars = Maps.newHashMap();
// run special copy Ability to make a deep copy // run special copy Ability to make a deep copy
CardFactory.copySpellAbility(this, clone, host, activ, lki); CardFactory.copySpellAbility(this, clone, host, activ, lki);
} catch (final CloneNotSupportedException e) { } catch (final CloneNotSupportedException e) {

View File

@@ -89,16 +89,7 @@ public class HumanPlay {
// extra play check // extra play check
if (sa.isSpell() && !sa.canPlay()) { if (sa.isSpell() && !sa.canPlay()) {
// Exceptional cases where canPlay should not run return false;
boolean exemptFromCheck = false;
if (source.hasSuspend() && p.getGame().isCardExiled(source) && source.getCounters(CounterType.TIME) == 0) {
// A card is about to ETB from Suspend
exemptFromCheck = true;
}
if (!exemptFromCheck) {
return false;
}
} }
if (flippedToCast && !castFaceDown) { if (flippedToCast && !castFaceDown) {