diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 4102df2fefb..3eaa075f4ea 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -1169,7 +1169,7 @@ public class ComputerUtilMana { cost.increaseShard(shardToGrow, manaToAdd); if (!test) { - card.setXManaCostPaid(manaToAdd / cost.getXcounter()); + sa.setXManaCostPaid(manaToAdd / cost.getXcounter()); } } diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 5d67cdbd938..c4e1a378be8 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -157,11 +157,6 @@ public class GameAction { c.removeSVar("EndOfTurnLeavePlay"); } - // Clean up temporary variables such as Sunburst value or announced PayX value - if (!(zoneTo.is(ZoneType.Stack) || zoneTo.is(ZoneType.Battlefield))) { - c.clearTemporaryVars(); - } - if (fromBattlefield && !toBattlefield) { c.getController().setRevolt(true); } @@ -173,7 +168,7 @@ public class GameAction { // if to Battlefield and it is caused by an replacement effect, // try to get previous LKI if able - if (zoneTo.is(ZoneType.Battlefield)) { + if (toBattlefield) { if (cause != null && cause.isReplacementAbility()) { ReplacementEffect re = cause.getReplacementEffect(); if (ReplacementType.Moved.equals(re.getMode())) { @@ -244,6 +239,12 @@ public class GameAction { } } + // Clean up temporary variables such as Sunburst value or announced PayX value + if (!(zoneTo.is(ZoneType.Stack) || zoneTo.is(ZoneType.Battlefield))) { + copied.clearTemporaryVars(); + } + + if (!suppress) { if (zoneFrom == null) { copied.getOwner().addInboundToken(copied); diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index f5b40e4ca3a..1af706a21a6 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -25,6 +25,8 @@ import forge.game.player.Player; import forge.game.player.PlayerCollection; import forge.game.player.PlayerPredicates; import forge.game.spellability.*; +import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.Expressions; import forge.util.TextUtil; @@ -1573,11 +1575,47 @@ public class AbilityUtils { // special logic for xPaid in SpellAbility if (sq[0].contains("xPaid")) { - // ETB effects of cloned cards have xPaid = 0 - if (sa.hasParam("ETB") && sa.getOriginalHost() != null) { + SpellAbility root = sa.getRootAbility(); + + // 107.3i If an object gains an ability, the value of X within that ability is the value defined by that ability, + // or 0 if that ability doesn’t define a value of X. This is an exception to rule 107.3h. This may occur with ability-adding effects, text-changing effects, or copy effects. + if (root.getXManaCostPaid() != null) { + return CardFactoryUtil.doXMath(root.getXManaCostPaid(), expr, c); + } + + // If the chosen creature has X in its mana cost, that X is considered to be 0. + // The value of X in Altered Ego’s last ability will be whatever value was chosen for X while casting Altered Ego. + if (sa.getOriginalHost() != null || !sa.getHostCard().equals(c)) { return 0; } - return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + + if (root.isTrigger()) { + Trigger t = root.getTrigger(); + // 107.3k If an object’s enters-the-battlefield triggered ability or replacement effect refers to X, + // and the spell that became that object as it resolved had a value of X chosen for any of its costs, + // the value of X for that ability is the same as the value of X for that spell, although the value of X for that permanent is 0. + if (TriggerType.ChangesZone.equals(t.getMode()) + && ZoneType.Battlefield.name().equals(t.getParam("Destination"))) { + return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + } else if (TriggerType.SpellCast.equals(t.getMode())) { + // Cast Trigger like Hydroid Krasis + return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + } else if (TriggerType.Cycled.equals(t.getMode())) { + SpellAbility cycleSA = (SpellAbility) sa.getTriggeringObject(AbilityKey.Cause); + if (cycleSA == null) { + return 0; + } + return CardFactoryUtil.doXMath(cycleSA.getXManaCostPaid(), expr, c); + } + } + + if (root.isReplacementAbility()) { + if (sa.hasParam("ETB")) { + return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c); + } + } + + return 0; } // Count$Kicked.. diff --git a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java index d90b9c90396..81f48f70209 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -112,9 +112,8 @@ public abstract class SpellAbilityEffect { sb.append(" "); sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace(svar,"=",String.valueOf(amount)))); } else{ - if (sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null && - sa.getPayCosts().getCostMana().getAmountOfX() > 0) { - int amount = sa.getHostCard().getXManaCostPaid(); + if (sa.costHasManaX()) { + int amount = sa.getXManaCostPaid() == null ? 0 : sa.getXManaCostPaid(); sb.append(" "); sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace("X","=",String.valueOf(amount)))); } diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 008b30f72c5..b16350a164b 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -222,7 +222,6 @@ public class Card extends GameEntity implements Comparable { private int turnInZone; - private int xManaCostPaid = 0; private Map xManaCostPaidByColor; private int sunburstValue = 0; @@ -1089,10 +1088,11 @@ public class Card extends GameEntity implements Comparable { } public final int getXManaCostPaid() { - return xManaCostPaid; - } - public final void setXManaCostPaid(final int n) { - xManaCostPaid = n; + if (getCastSA() != null) { + Integer paid = getCastSA().getXManaCostPaid(); + return paid == null ? 0 : paid; + } + return 0; } public final Map getXManaCostPaidByColor() { @@ -6430,7 +6430,6 @@ public class Card extends GameEntity implements Comparable { removeSVar("PayX"); // Temporary AI X announcement variable removeSVar("IsCastFromPlayEffect"); // Temporary SVar indicating that the spell is cast indirectly via AF Play setSunburstValue(0); // Sunburst - setXManaCostPaid(0); setXManaCostPaidByColor(null); setKickerMagnitude(0); setPseudoMultiKickerMagnitude(0); diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index 5fa4868727e..163df5aefb8 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -185,18 +185,12 @@ public class CardFactory { c.setCopiedSpell(true); if (bCopyDetails) { - c.setXManaCostPaid(original.getXManaCostPaid()); c.setXManaCostPaidByColor(original.getXManaCostPaidByColor()); c.setKickerMagnitude(original.getKickerMagnitude()); // Rule 706.10 : Madness is copied if (original.isInZone(ZoneType.Stack)) { c.setMadness(original.isMadness()); - - final SpellAbilityStackInstance si = controller.getGame().getStack().getInstanceFromSpellAbility(sa); - if (si != null) { - c.setXManaCostPaid(si.getXManaPaid()); - } } for (OptionalCost cost : original.getOptionalCostsPaid()) { @@ -875,6 +869,13 @@ public class CardFactory { } } + for (final Trigger trigger : state.getTriggers()) { + final SpellAbility newSa = trigger.getOverridingAbility(); + if (newSa != null && newSa.getOriginalHost() == null) { + newSa.setOriginalHost(in); + } + } + if (sa.hasParam("GainTextOf") && originalState != null) { state.setSetCode(originalState.getSetCode()); state.setRarity(originalState.getRarity()); diff --git a/forge-game/src/main/java/forge/game/cost/CostPartWithList.java b/forge-game/src/main/java/forge/game/cost/CostPartWithList.java index b934d3d0be7..a8dabcbc86f 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPartWithList.java +++ b/forge-game/src/main/java/forge/game/cost/CostPartWithList.java @@ -122,8 +122,10 @@ public abstract class CostPartWithList extends CostPart { // always returns true, made this to inline with return public boolean executePayment(SpellAbility ability, CardCollectionView targetCards) { - if (canPayListAtOnce()) { // This is used by reveal. Without it when opponent would reveal hand, you'll get N message boxes. - lkiList.addAll(targetCards); + if (canPayListAtOnce()) { // This is used by reveal. Without it when opponent would reveal hand, you'll get N message boxes. + for (Card c: targetCards) { + lkiList.add(CardUtil.getLKICopy(c)); + } cardList.addAll(doListPayment(ability, targetCards)); handleChangeZoneTrigger(ability); return true; diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 2bfe8b388a4..9680f81d482 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -130,6 +130,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private final List payingMana = Lists.newArrayList(); private final List paidAbilities = Lists.newArrayList(); + private Integer xManaCostPaid = null; private HashMap paidLists = Maps.newHashMap(); @@ -447,6 +448,23 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit payCosts = abCost; } + public boolean costHasX() { + if (getPayCosts() == null) { + return false; + } + return getPayCosts().hasXInAnyCostPart(); + } + + public boolean costHasManaX() { + if (getPayCosts() == null) { + return false; + } + if (getPayCosts().hasNoManaCost()) { + return false; + } + return getPayCosts().getCostMana().getAmountOfX() > 0; + } + public SpellAbilityRestriction getRestrictions() { return restrictions; } @@ -1963,4 +1981,11 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void setAlternativeCost(AlternativeCost ac) { altCost = ac; } + + public Integer getXManaCostPaid() { + return xManaCostPaid; + } + public void setXManaCostPaid(final Integer n) { + xManaCostPaid = n; + } } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java index 15dd3573998..6a0b8b90de9 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java @@ -83,7 +83,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { // private ArrayList payingMana = new ArrayList(); // private ArrayList paidAbilities = new // ArrayList(); - private int xManaPaid = 0; + private Integer xManaPaid = null; // Other Paid things private final HashMap paidHash; @@ -116,7 +116,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { splicedCards = sa.getSplicedCards(); // TODO getXManaCostPaid should be on the SA, not the Card - xManaPaid = sa.getHostCard().getXManaCostPaid(); + xManaPaid = sa.getXManaCostPaid(); // Triggering info triggeringObjects = sa.getTriggeringObjects(); @@ -200,7 +200,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { // Set Cost specific things here ability.setPaidHash(paidHash); ability.setSplicedCards(splicedCards); - ability.getHostCard().setXManaCostPaid(xManaPaid); + ability.setXManaCostPaid(xManaPaid); // Triggered ability.setTriggeringObjects(triggeringObjects); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java b/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java index 3fc236f94bc..88311eadac2 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java @@ -53,7 +53,7 @@ public class TriggerCycled extends Trigger { /** {@inheritDoc} */ @Override public final void setTriggeringObjects(final SpellAbility sa, Map runParams) { - sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card); + sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card, AbilityKey.Cause); } @Override diff --git a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java index 1a3a9e84fe2..15f8869ec02 100644 --- a/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java +++ b/forge-game/src/main/java/forge/game/trigger/WrappedAbility.java @@ -556,4 +556,11 @@ public class WrappedAbility extends Ability { public void setAlternativeCost(AlternativeCost ac) { sa.setAlternativeCost(ac); } + + public Integer getXManaCostPaid() { + return sa.getXManaCostPaid(); + } + public void setXManaCostPaid(final Integer n) { + sa.setXManaCostPaid(n); + } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index ee8571e5eb5..f3e90a992f8 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -314,9 +314,9 @@ public class MagicStack /* extends MyObservable */ implements Iterable cycleParams = AbilityKey.mapFromCard(sp.getHostCard()); + cycleParams.put(AbilityKey.Cause, sp); + game.getTriggerHandler().runTrigger(TriggerType.Cycled, cycleParams, false); } if (sp.hasParam("Crew")) { diff --git a/forge-gui/res/cardsfolder/upcoming/shark_typhoon.txt b/forge-gui/res/cardsfolder/upcoming/shark_typhoon.txt new file mode 100644 index 00000000000..ba6e2a6a6c4 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/shark_typhoon.txt @@ -0,0 +1,11 @@ +Name:Shark Typhoon +ManaCost:5 U +Types:Enchantment +T:Mode$ SpellCast | ValidCard$ Card.nonCreature | ValidActivatingPlayer$ You | Execute$ TrigToken | TriggerZones$ Battlefield | TriggerDescription$ Whenever you cast a noncreature spell, create an X/X blue Shark creature token with flying, where X is that spell's converted mana cost. +SVar:TrigToken:DB$ Token | TokenAmount$ 1 | TokenScript$ u_x_x_shark_flying | TokenOwner$ You | LegacyImage$ u x x shark flying iko | TokenPower$ Y | TokenToughness$ Y | References$ Y | TokenAmount$ 1 +SVar:Y:TriggeredCard$CardManaCost +K:Cycling:X 1 U +T:Mode$ Cycled | ValidCard$ Card.Self | Execute$ TrigToken2 | TriggerZones$ Hand | TriggerDescription$ When you cycle CARDNAME, create an X/X blue Shark creature token with flying. +SVar:TrigToken2:DB$ Token | TokenAmount$ 1 | TokenScript$ u_x_x_shark_flying | TokenOwner$ You | LegacyImage$ u x x shark flying iko | TokenPower$ X | TokenToughness$ X | References$ X | TokenAmount$ 1 +SVar:X:Count$xPaid +Oracle:Whenever you cast a noncreature spell, create an X/X blue Shark creature token with flying, where X is that spell's converted mana cost.\nCycling {X}{1}{U} ({X}{1}{U}, Discard this card: Draw a card.)\nWhen you cycle Shark Typhoon, create an X/X blue Shark creature token with flying. diff --git a/forge-gui/res/tokenscripts/u_x_x_shark_flying.txt b/forge-gui/res/tokenscripts/u_x_x_shark_flying.txt new file mode 100644 index 00000000000..8da980b6426 --- /dev/null +++ b/forge-gui/res/tokenscripts/u_x_x_shark_flying.txt @@ -0,0 +1,7 @@ +Name:Shark +ManaCost:no cost +Types:Creature Shark +Colors:blue +PT:*/* +K:Flying +Oracle:Flying diff --git a/forge-gui/src/main/java/forge/player/HumanPlay.java b/forge-gui/src/main/java/forge/player/HumanPlay.java index a8cf3e0ac8d..b65361d9835 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlay.java +++ b/forge-gui/src/main/java/forge/player/HumanPlay.java @@ -748,10 +748,10 @@ public class HumanPlay { if (mc.getAmountOfX() > 0 && !"Count$xPaid".equals(xInCard)) { // announce X will overwrite whatever was in card script int xPaid = AbilityUtils.calculateAmount(source, "X", ability); toPay.setXManaCostPaid(xPaid, ability.getParam("XColor")); - source.setXManaCostPaid(xPaid); + ability.setXManaCostPaid(xPaid); } - else if (source.getXManaCostPaid() > 0) { //ensure pre-announced X value retained - toPay.setXManaCostPaid(source.getXManaCostPaid(), ability.getParam("XColor")); + else if (ability.getXManaCostPaid() != null) { //ensure pre-announced X value retained + toPay.setXManaCostPaid(ability.getXManaCostPaid(), ability.getParam("XColor")); } int timesMultikicked = source.getKickerMagnitude(); diff --git a/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java b/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java index 36f124105a3..5a863164800 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java +++ b/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java @@ -314,10 +314,10 @@ public class HumanPlaySpellAbility { if (value == null) { return false; } - card.setXManaCostPaid(value); + ability.setXManaCostPaid(value); } } else if (manaCost.getMana().isZero() && ability.isSpell()) { - card.setXManaCostPaid(0); + ability.setXManaCostPaid(0); } } return true;