From 42ed14df525f18578906f85d52c417381b80dd8b Mon Sep 17 00:00:00 2001 From: Oleg Agafonov <jaydi85@gmail.com> Date: Thu, 23 May 2019 12:40:45 +0400 Subject: [PATCH] * Commander: added support of lands as commander (#5795); --- Mage.Sets/src/mage/cards/m/Musician.java | 66 ++--------------- Mage.Sets/src/mage/cards/m/MythUnbound.java | 31 ++++---- Mage.Sets/src/mage/cards/o/OpalPalace.java | 28 ++++---- .../cards/continuous/CommandersCastTest.java | 63 ++++++++++++++++ .../test/commander/duel/OpalPalaceTest.java | 11 ++- .../common/CastCommanderAbility.java | 28 ++------ .../common/PlayLandAsCommanderAbility.java | 28 ++++++++ .../costs/common/CommanderAdditionalCost.java | 34 +++++++++ .../common/DynamicValueGenericManaCost.java | 46 ++++++++++++ .../common/CommanderPlaysCount.java | 52 ++++++++++++++ .../cost/CommanderCostModification.java | 54 -------------- .../keyword/CommanderStormAbility.java | 8 +-- .../java/mage/game/GameCommanderImpl.java | 11 +-- .../java/mage/game/GameTinyLeadersImpl.java | 9 +-- .../java/mage/game/command/Commander.java | 24 +++++-- .../main/java/mage/players/PlayerImpl.java | 71 +++++++++++++++++-- .../watchers/common/CommanderInfoWatcher.java | 16 +++-- .../common/CommanderPlaysCountWatcher.java | 62 ++++++++++++++++ 18 files changed, 441 insertions(+), 201 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/common/PlayLandAsCommanderAbility.java create mode 100644 Mage/src/main/java/mage/abilities/costs/common/CommanderAdditionalCost.java create mode 100644 Mage/src/main/java/mage/abilities/costs/common/DynamicValueGenericManaCost.java create mode 100644 Mage/src/main/java/mage/abilities/dynamicvalue/common/CommanderPlaysCount.java delete mode 100644 Mage/src/main/java/mage/abilities/effects/common/cost/CommanderCostModification.java create mode 100644 Mage/src/main/java/mage/watchers/common/CommanderPlaysCountWatcher.java diff --git a/Mage.Sets/src/mage/cards/m/Musician.java b/Mage.Sets/src/mage/cards/m/Musician.java index 4b99c7b004..886e9c8897 100644 --- a/Mage.Sets/src/mage/cards/m/Musician.java +++ b/Mage.Sets/src/mage/cards/m/Musician.java @@ -1,17 +1,12 @@ package mage.cards.m; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.Cost; -import mage.abilities.costs.CostImpl; +import mage.abilities.costs.common.DynamicValueGenericManaCost; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; -import mage.constants.SubType; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.CountersSourceCount; import mage.abilities.effects.Effect; import mage.abilities.effects.common.DestroySourceEffect; @@ -21,17 +16,13 @@ import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.abilities.keyword.CumulativeUpkeepAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.TargetController; -import mage.constants.Zone; +import mage.constants.*; import mage.counters.CounterType; -import mage.game.Game; -import mage.players.Player; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * * @author jeffwadsworth */ public final class Musician extends CardImpl { @@ -51,8 +42,8 @@ public final class Musician extends CardImpl { Effect effect = new DoUnlessControllerPaysEffect( new DestroySourceEffect(), new DynamicValueGenericManaCost( - new CountersSourceCount( - CounterType.MUSIC))); + new CountersSourceCount(CounterType.MUSIC), + "{1} for each music counter on {this}")); effect.setText("destroy this creature unless you pay {1} for each music counter on it"); Ability ability = new BeginningOfUpkeepTriggeredAbility( Zone.BATTLEFIELD, @@ -87,48 +78,3 @@ public final class Musician extends CardImpl { } } -class DynamicValueGenericManaCost extends CostImpl { - - DynamicValue amount; - - public DynamicValueGenericManaCost(DynamicValue amount) { - this.amount = amount; - setText(); - } - - public DynamicValueGenericManaCost(DynamicValueGenericManaCost cost) { - super(cost); - this.amount = cost.amount; - } - - @Override - public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { - Player controller = game.getPlayer(controllerId); - if (controller == null) { - return false; - } - int convertedCost = amount.calculate(game, ability, null); - Cost cost = new GenericManaCost(convertedCost); - return cost.canPay(ability, sourceId, controllerId, game); - } - - @Override - public boolean pay(Ability ability, Game game, UUID sourceId, UUID controllerId, boolean noMana, Cost costToPay) { - Player controller = game.getPlayer(controllerId); - int convertedCost = amount.calculate(game, ability, null); - Cost cost = new GenericManaCost(convertedCost); - if (controller != null) { - paid = cost.pay(ability, game, sourceId, controllerId, noMana); - } - return paid; - } - - @Override - public DynamicValueGenericManaCost copy() { - return new DynamicValueGenericManaCost(this); - } - - private void setText() { - text = ("{1} for each music counter on {this}"); - } -} diff --git a/Mage.Sets/src/mage/cards/m/MythUnbound.java b/Mage.Sets/src/mage/cards/m/MythUnbound.java index 86e13abb32..6e23fdbbc8 100644 --- a/Mage.Sets/src/mage/cards/m/MythUnbound.java +++ b/Mage.Sets/src/mage/cards/m/MythUnbound.java @@ -1,7 +1,7 @@ package mage.cards.m; -import java.util.UUID; import mage.abilities.Ability; +import mage.abilities.PlayLandAbility; import mage.abilities.SpellAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.ZoneChangeAllTriggeredAbility; @@ -9,22 +9,18 @@ import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.CostModificationType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.TargetController; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.predicate.other.OwnerPredicate; import mage.filter.predicate.permanent.CommanderPredicate; import mage.game.Game; -import mage.game.stack.Spell; import mage.players.Player; import mage.util.CardUtil; +import mage.watchers.common.CommanderPlaysCountWatcher; + +import java.util.UUID; /** - * * @author TheElk801 */ public final class MythUnbound extends CardImpl { @@ -50,7 +46,7 @@ public final class MythUnbound extends CardImpl { Zone.BATTLEFIELD, Zone.ALL, Zone.COMMAND, new DrawCardSourceControllerEffect(1), filter, "Whenever your commander is put into " - + "the command zone from anywhere, ", false + + "the command zone from anywhere, ", false )); } @@ -80,9 +76,10 @@ class MythUnboundCostReductionEffect extends CostModificationEffectImpl { public boolean apply(Game game, Ability source, Ability abilityToModify) { Ability spellAbility = abilityToModify; if (spellAbility != null) { - Integer amount = (Integer) game.getState().getValue(abilityToModify.getSourceId() + "_castCount"); - if (amount != null && amount > 0) { - CardUtil.reduceCost(spellAbility, amount); + CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); + int castCount = watcher.getPlaysCount(abilityToModify.getSourceId()); + if (castCount > 0) { + CardUtil.reduceCost(spellAbility, castCount); return true; } } @@ -95,12 +92,10 @@ class MythUnboundCostReductionEffect extends CostModificationEffectImpl { if (player == null) { return false; } - if (abilityToModify instanceof SpellAbility) { + + if (abilityToModify instanceof SpellAbility || abilityToModify instanceof PlayLandAbility) { if (abilityToModify.isControlledBy(source.getControllerId())) { - Spell spell = (Spell) game.getStack().getStackObject(abilityToModify.getId()); - if (spell != null) { - return player.getCommandersIds().contains(spell.getSourceId()); - } + return player.getCommandersIds().contains(abilityToModify.getSourceId()); } } return false; diff --git a/Mage.Sets/src/mage/cards/o/OpalPalace.java b/Mage.Sets/src/mage/cards/o/OpalPalace.java index e010390c4f..92fa165d6a 100644 --- a/Mage.Sets/src/mage/cards/o/OpalPalace.java +++ b/Mage.Sets/src/mage/cards/o/OpalPalace.java @@ -1,9 +1,5 @@ - package mage.cards.o; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.TapSourceCost; @@ -14,11 +10,7 @@ import mage.abilities.mana.CommanderColorIdentityManaAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.WatcherScope; -import mage.constants.Zone; +import mage.constants.*; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.EntersTheBattlefieldEvent; @@ -28,15 +20,19 @@ import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.players.Player; import mage.watchers.Watcher; +import mage.watchers.common.CommanderPlaysCountWatcher; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; /** - * * @author LevelX2 */ public final class OpalPalace extends CardImpl { public OpalPalace(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.LAND},""); + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // {T}: Add {C}. this.addAbility(new ColorlessManaAbility()); @@ -77,7 +73,7 @@ class OpalPalaceWatcher extends Watcher { this.originalId = watcher.originalId; } - public boolean manaUsedToCastCommander(UUID id){ + public boolean manaUsedToCastCommander(UUID id) { return commanderId.contains(id); } @@ -135,16 +131,16 @@ class OpalPalaceEntersBattlefieldEffect extends ReplacementEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { OpalPalaceWatcher watcher = game.getState().getWatcher(OpalPalaceWatcher.class, source.getSourceId()); - return watcher != null - && watcher.manaUsedToCastCommander(event.getTargetId()); + return watcher != null && watcher.manaUsedToCastCommander(event.getTargetId()); } @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { Permanent permanent = ((EntersTheBattlefieldEvent) event).getTarget(); if (permanent != null) { - Integer castCount = (Integer) game.getState().getValue(permanent.getId() + "_castCount"); - if (castCount != null && castCount > 0) { + CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); + int castCount = watcher.getPlaysCount(permanent.getId()); + if (castCount > 0) { permanent.addCounters(CounterType.P1P1.createInstance(castCount), source, game); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersCastTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersCastTest.java index 02d57293aa..fa09404e36 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersCastTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersCastTest.java @@ -27,6 +27,7 @@ public class CommandersCastTest extends CardTestCommander4Players { assertCommandZoneCount(playerA, "Balduvian Bears", 0); assertPermanentCount(playerA, "Balduvian Bears", 1); + assertTappedCount("Forest", true, 2); } @Test @@ -62,5 +63,67 @@ public class CommandersCastTest extends CardTestCommander4Players { assertCommandZoneCount(playerA, "Balduvian Bears", 0); assertPermanentCount(playerA, "Balduvian Bears", 1); assertGraveyardCount(playerB, "Lightning Bolt", 1); + assertTappedCount("Forest", true, 2 + 4); + } + + @Test + public void test_PlayAsLandOneTime() { + addCard(Zone.COMMAND, playerA, "Academy Ruins", 1); + + showAvaileableAbilities("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Academy Ruins"); + //castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Academy Ruins"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertCommandZoneCount(playerA, "Academy Ruins", 0); + assertPermanentCount(playerA, "Academy Ruins", 1); + } + + @Test + public void test_PlayAsLandTwoTimes() { + // Player order: A -> D -> C -> B + addCard(Zone.COMMAND, playerA, "Academy Ruins", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); // 0 + 2 + // + addCard(Zone.HAND, playerA, "Pillage", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + // cast 1 + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Academy Ruins"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after play 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Academy Ruins", 1); + + // destroy commander land + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pillage", "Academy Ruins"); + setChoice(playerA, "Yes"); // put to command zone again + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after destroy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Academy Ruins", 0); + + // remove unnecessary mana, only 2 forest need (workaround to remove random mana payments) + activateManaAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + activateManaAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + activateManaAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + + // cast 2 + playLand(5, PhaseStep.POSTCOMBAT_MAIN, playerA, "Academy Ruins"); + waitStackResolved(5, PhaseStep.POSTCOMBAT_MAIN); + checkPermanentCount("after cast 2", 5, PhaseStep.POSTCOMBAT_MAIN, playerA, "Academy Ruins", 1); + + showBattlefield("end battlefield", 5, PhaseStep.END_TURN, playerA); + + setStopAt(5, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertCommandZoneCount(playerA, "Academy Ruins", 0); + assertPermanentCount(playerA, "Academy Ruins", 1); + assertGraveyardCount(playerA, "Pillage", 1); + assertTappedCount("Forest", true, 2); + assertTappedCount("Mountain", true, 3); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/commander/duel/OpalPalaceTest.java b/Mage.Tests/src/test/java/org/mage/test/commander/duel/OpalPalaceTest.java index 1c4bd3ce18..8061ad716d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/commander/duel/OpalPalaceTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/commander/duel/OpalPalaceTest.java @@ -1,4 +1,3 @@ - package org.mage.test.commander.duel; import mage.constants.PhaseStep; @@ -8,7 +7,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestCommanderDuelBase; /** - * * @author LevelX2 */ public class OpalPalaceTest extends CardTestCommanderDuelBase { @@ -29,10 +27,19 @@ public class OpalPalaceTest extends CardTestCommanderDuelBase { // equal to the number of times it's been cast from the command zone this game. addCard(Zone.BATTLEFIELD, playerA, "Opal Palace", 1); + showHand("hand", 1, PhaseStep.PRECOMBAT_MAIN, playerA); + showCommand("command", 1, PhaseStep.PRECOMBAT_MAIN, playerA); + showAvaileableAbilities("abi", 1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}"); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}, {T}"); + setChoice(playerA, "Opal Palace"); // activate mana replace effect first (+3 counters) castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ob Nixilis of the Black Oath"); // {3}{B}{B} + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertLife(playerA, 40); assertLife(playerB, 40); diff --git a/Mage/src/main/java/mage/abilities/common/CastCommanderAbility.java b/Mage/src/main/java/mage/abilities/common/CastCommanderAbility.java index c31137dfe8..40aafbd62b 100644 --- a/Mage/src/main/java/mage/abilities/common/CastCommanderAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CastCommanderAbility.java @@ -1,15 +1,13 @@ package mage.abilities.common; import mage.abilities.SpellAbility; -import mage.abilities.costs.CostsImpl; +import mage.abilities.costs.common.CommanderAdditionalCost; import mage.cards.Card; import mage.constants.SpellAbilityType; -import mage.constants.TimingRule; import mage.constants.Zone; -import mage.game.Game; /** - * @author Plopman + * @author Plopman, JayDi85 */ public class CastCommanderAbility extends SpellAbility { @@ -20,9 +18,11 @@ public class CastCommanderAbility extends SpellAbility { this.getEffects().addAll(card.getSpellAbility().getEffects().copy()); this.getTargets().addAll(card.getSpellAbility().getTargets().copy()); this.timing = card.getSpellAbility().getTiming(); + + // extra cost + this.addCost(new CommanderAdditionalCost()); } else { - this.costs = new CostsImpl<>(); - this.timing = TimingRule.SORCERY; + throw new IllegalStateException("Cast commander ability must be used with spell ability only: " + card.getName()); } this.usesStack = true; this.controllerId = card.getOwnerId(); @@ -33,22 +33,6 @@ public class CastCommanderAbility extends SpellAbility { super(ability); } - @Override - public boolean activate(Game game, boolean noMana) { - if (super.activate(game, noMana)) { - // save amount of times commander was cast - Integer castCount = (Integer) game.getState().getValue(sourceId + "_castCount"); - if (castCount == null) { - castCount = 1; - } else { - castCount++; - } - game.getState().setValue(sourceId + "_castCount", castCount); - return true; - } - return false; - } - @Override public CastCommanderAbility copy() { return new CastCommanderAbility(this); diff --git a/Mage/src/main/java/mage/abilities/common/PlayLandAsCommanderAbility.java b/Mage/src/main/java/mage/abilities/common/PlayLandAsCommanderAbility.java new file mode 100644 index 0000000000..a3ca4505c3 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/PlayLandAsCommanderAbility.java @@ -0,0 +1,28 @@ +package mage.abilities.common; + +import mage.abilities.PlayLandAbility; +import mage.abilities.costs.common.CommanderAdditionalCost; +import mage.constants.Zone; + +/** + * @author JayDi85 + */ +public class PlayLandAsCommanderAbility extends PlayLandAbility { + + public PlayLandAsCommanderAbility(PlayLandAbility originalAbility) { + super(originalAbility); + zone = Zone.COMMAND; + + // extra cost + this.addCost(new CommanderAdditionalCost()); + } + + private PlayLandAsCommanderAbility(PlayLandAsCommanderAbility ability) { + super(ability); + } + + @Override + public PlayLandAsCommanderAbility copy() { + return new PlayLandAsCommanderAbility(this); + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/common/CommanderAdditionalCost.java b/Mage/src/main/java/mage/abilities/costs/common/CommanderAdditionalCost.java new file mode 100644 index 0000000000..08a4160e48 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/common/CommanderAdditionalCost.java @@ -0,0 +1,34 @@ +package mage.abilities.costs.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.CommanderPlaysCount; +import mage.game.Game; + +/** + * @author JayDi85 + */ +public class CommanderAdditionalCost extends DynamicValueGenericManaCost { + + /* + 903.8. A player may cast a commander they own from the command zone. A commander cast from the + command zone costs an additional {2} for each previous time the player casting it has cast it from + the command zone that game. This additional cost is informally known as the “commander tax.” + */ + + public CommanderAdditionalCost() { + super(new CommanderPlaysCount(2), "{2} for each previous time the player casting it has cast it from the command zone"); + } + + public CommanderAdditionalCost(final CommanderAdditionalCost cost) { + super(cost); + } + + @Override + public CommanderAdditionalCost copy() { + return new CommanderAdditionalCost(this); + } + + public boolean isEmptyPay(Ability ability, Game game) { + return amount.calculate(game, ability, null) == 0; + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/costs/common/DynamicValueGenericManaCost.java b/Mage/src/main/java/mage/abilities/costs/common/DynamicValueGenericManaCost.java new file mode 100644 index 0000000000..8f97a60517 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/common/DynamicValueGenericManaCost.java @@ -0,0 +1,46 @@ +package mage.abilities.costs.common; + +import mage.abilities.Ability; +import mage.abilities.costs.Cost; +import mage.abilities.costs.CostImpl; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.game.Game; + +import java.util.UUID; + +public class DynamicValueGenericManaCost extends CostImpl { + + DynamicValue amount; + + public DynamicValueGenericManaCost(DynamicValue amount, String text) { + this.amount = amount; + setText(text); + } + + public DynamicValueGenericManaCost(DynamicValueGenericManaCost cost) { + super(cost); + this.amount = cost.amount; + } + + @Override + public boolean canPay(Ability ability, UUID sourceId, UUID controllerId, Game game) { + int convertedCost = amount.calculate(game, ability, null); + Cost cost = new GenericManaCost(convertedCost); + return cost.canPay(ability, sourceId, controllerId, game); + } + + @Override + public boolean pay(Ability ability, Game game, UUID sourceId, UUID controllerId, boolean noMana, Cost costToPay) { + int convertedCost = amount.calculate(game, ability, null); + Cost cost = new GenericManaCost(convertedCost); + paid = cost.pay(ability, game, sourceId, controllerId, noMana); + return paid; + } + + @Override + public DynamicValueGenericManaCost copy() { + return new DynamicValueGenericManaCost(this); + } + +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CommanderPlaysCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CommanderPlaysCount.java new file mode 100644 index 0000000000..65051b3f2b --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CommanderPlaysCount.java @@ -0,0 +1,52 @@ +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.game.Game; +import mage.watchers.common.CommanderPlaysCountWatcher; + +/** + * @author JayDi85 + */ +public class CommanderPlaysCount implements DynamicValue { + + private Integer multiplier; + + public CommanderPlaysCount() { + this(1); + } + + public CommanderPlaysCount(Integer multiplier) { + this.multiplier = multiplier; + } + + public CommanderPlaysCount(final CommanderPlaysCount dynamicValue) { + this.multiplier = dynamicValue.multiplier; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); + int value = 0; + if (watcher != null) { + value = watcher.getPlaysCount(sourceAbility.getSourceId()); + } + return value * multiplier; + } + + @Override + public CommanderPlaysCount copy() { + return new CommanderPlaysCount(this); + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return ""; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/CommanderCostModification.java b/Mage/src/main/java/mage/abilities/effects/common/cost/CommanderCostModification.java deleted file mode 100644 index cd9b3cfbbf..0000000000 --- a/Mage/src/main/java/mage/abilities/effects/common/cost/CommanderCostModification.java +++ /dev/null @@ -1,54 +0,0 @@ - -package mage.abilities.effects.common.cost; - -import java.util.UUID; -import mage.abilities.Ability; -import mage.abilities.common.CastCommanderAbility; -import mage.abilities.costs.mana.GenericManaCost; -import mage.constants.CostModificationType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.game.Game; - -/** - * - * @author Plopman - */ -//20130711 -/*903.10. A player may cast a commander he or she owns from the command zone. - * Doing so costs that player an additional {2} for each previous time he or she cast that commander from the command zone that game. - * */ -public class CommanderCostModification extends CostModificationEffectImpl { - - private final UUID commanderId; - - public CommanderCostModification(UUID commanderId) { - super(Duration.Custom, Outcome.Neutral, CostModificationType.INCREASE_COST); - this.commanderId = commanderId; - } - - public CommanderCostModification(final CommanderCostModification effect) { - super(effect); - this.commanderId = effect.commanderId; - } - - @Override - public boolean apply(Game game, Ability source, Ability abilityToModify) { - Integer castCount = (Integer) game.getState().getValue(commanderId + "_castCount"); - if (castCount > 0) { - abilityToModify.getManaCostsToPay().add(new GenericManaCost(2 * castCount)); - } - return true; - - } - - @Override - public boolean applies(Ability abilityToModify, Ability source, Game game) { - return abilityToModify instanceof CastCommanderAbility && abilityToModify.getSourceId().equals(commanderId); - } - - @Override - public CommanderCostModification copy() { - return new CommanderCostModification(this); - } -} diff --git a/Mage/src/main/java/mage/abilities/keyword/CommanderStormAbility.java b/Mage/src/main/java/mage/abilities/keyword/CommanderStormAbility.java index a40d3de2f5..fb0d3beb18 100644 --- a/Mage/src/main/java/mage/abilities/keyword/CommanderStormAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/CommanderStormAbility.java @@ -13,9 +13,9 @@ import mage.game.events.GameEvent.EventType; import mage.game.stack.Spell; import mage.game.stack.StackObject; import mage.players.Player; +import mage.watchers.common.CommanderPlaysCountWatcher; /** - * * @author Plopman */ public class CommanderStormAbility extends TriggeredAbilityImpl { @@ -83,9 +83,9 @@ class CommanderStormEffect extends OneShotEffect { if (player == null) { return false; } - stormCount = player.getCommandersIds().stream().map( - (commanderId) -> (Integer) game.getState().getValue(commanderId + "_castCount") - ).map((castCount) -> castCount).reduce(stormCount, Integer::sum); + stormCount = player.getCommandersIds().stream() + .map((commanderId) -> game.getState().getWatcher(CommanderPlaysCountWatcher.class).getPlaysCount(commanderId)) + .reduce(stormCount, Integer::sum); if (stormCount == 0) { return true; } diff --git a/Mage/src/main/java/mage/game/GameCommanderImpl.java b/Mage/src/main/java/mage/game/GameCommanderImpl.java index 47a6ee79a4..715295c645 100644 --- a/Mage/src/main/java/mage/game/GameCommanderImpl.java +++ b/Mage/src/main/java/mage/game/GameCommanderImpl.java @@ -4,7 +4,6 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.continuous.CommanderReplacementEffect; -import mage.abilities.effects.common.cost.CommanderCostModification; import mage.cards.Card; import mage.constants.MultiplayerAttackOption; import mage.constants.PhaseStep; @@ -14,6 +13,7 @@ import mage.game.mulligan.Mulligan; import mage.game.turn.TurnMod; import mage.players.Player; import mage.watchers.common.CommanderInfoWatcher; +import mage.watchers.common.CommanderPlaysCountWatcher; import java.util.Map; import java.util.UUID; @@ -40,7 +40,11 @@ public abstract class GameCommanderImpl extends GameImpl { @Override protected void init(UUID choosingPlayerId) { - //Move commander to command zone + + // plays watcher + state.addWatcher(new CommanderPlaysCountWatcher()); + + // move commanders to command zone for (UUID playerId : state.getPlayerList(startingPlayerId)) { Player player = getPlayer(playerId); if (player != null) { @@ -62,6 +66,7 @@ public abstract class GameCommanderImpl extends GameImpl { } } } + super.init(choosingPlayerId); if (startingPlayerSkipsDraw) { state.getTurnMods().add(new TurnMod(startingPlayerId, PhaseStep.DRAW)); @@ -73,8 +78,6 @@ public abstract class GameCommanderImpl extends GameImpl { commander.moveToZone(Zone.COMMAND, null, this, true); commander.getAbilities().setControllerId(player.getId()); ability.addEffect(new CommanderReplacementEffect(commander.getId(), alsoHand, alsoLibrary)); - ability.addEffect(new CommanderCostModification(commander.getId())); - getState().setValue(commander.getId() + "_castCount", 0); CommanderInfoWatcher watcher = new CommanderInfoWatcher(commander.getId(), checkCommanderDamage); getState().addWatcher(watcher); watcher.addCardInfoToCommander(this); diff --git a/Mage/src/main/java/mage/game/GameTinyLeadersImpl.java b/Mage/src/main/java/mage/game/GameTinyLeadersImpl.java index 48e1efa89f..f43c71e92b 100644 --- a/Mage/src/main/java/mage/game/GameTinyLeadersImpl.java +++ b/Mage/src/main/java/mage/game/GameTinyLeadersImpl.java @@ -5,7 +5,6 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.continuous.CommanderReplacementEffect; -import mage.abilities.effects.common.cost.CommanderCostModification; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -16,6 +15,7 @@ import mage.game.mulligan.Mulligan; import mage.game.turn.TurnMod; import mage.players.Player; import mage.watchers.common.CommanderInfoWatcher; +import mage.watchers.common.CommanderPlaysCountWatcher; import java.util.HashSet; import java.util.Set; @@ -42,7 +42,10 @@ public abstract class GameTinyLeadersImpl extends GameImpl { @Override protected void init(UUID choosingPlayerId) { - //Move tiny leader to command zone + // plays watcher + state.addWatcher(new CommanderPlaysCountWatcher()); + + // move tiny leader to command zone for (UUID playerId : state.getPlayerList(startingPlayerId)) { Player player = getPlayer(playerId); if (player != null) { @@ -55,10 +58,8 @@ public abstract class GameTinyLeadersImpl extends GameImpl { commander.moveToZone(Zone.COMMAND, null, this, true); Ability ability = new SimpleStaticAbility(Zone.COMMAND, new InfoEffect("Commander effects")); ability.addEffect(new CommanderReplacementEffect(commander.getId(), alsoHand, alsoLibrary)); - ability.addEffect(new CommanderCostModification(commander.getId())); // Commander rule #4 was removed Jan. 18, 2016 // ability.addEffect(new CommanderManaReplacementEffect(player.getId(), CardUtil.getColorIdentity(commander))); - getState().setValue(commander.getId() + "_castCount", 0); CommanderInfoWatcher watcher = new CommanderInfoWatcher(commander.getId(), false); getState().addWatcher(watcher); watcher.addCardInfoToCommander(this); diff --git a/Mage/src/main/java/mage/game/command/Commander.java b/Mage/src/main/java/mage/game/command/Commander.java index 7163f8683e..ecf856a973 100644 --- a/Mage/src/main/java/mage/game/command/Commander.java +++ b/Mage/src/main/java/mage/game/command/Commander.java @@ -3,11 +3,9 @@ package mage.game.command; import mage.MageInt; import mage.MageObject; import mage.ObjectColor; -import mage.abilities.Abilities; -import mage.abilities.AbilitiesImpl; -import mage.abilities.Ability; -import mage.abilities.SpellAbility; +import mage.abilities.*; import mage.abilities.common.CastCommanderAbility; +import mage.abilities.common.PlayLandAsCommanderAbility; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.text.TextPart; @@ -34,9 +32,23 @@ public class Commander implements CommandObject { public Commander(Card card) { this.sourceObject = card; - abilities.add(new CastCommanderAbility(card)); + + // replace spell ability by commander cast spell (to cast from command zone) + if (card.getSpellAbility() != null) { + abilities.add(new CastCommanderAbility(card)); + } + + // replace play land with commander play land (to play from command zone) for (Ability ability : card.getAbilities()) { - if (!(ability instanceof SpellAbility)) { + if (ability instanceof PlayLandAbility) { + Ability newAbility = new PlayLandAsCommanderAbility((PlayLandAbility) ability); + abilities.add(newAbility); + } + } + + // other abilities + for (Ability ability : card.getAbilities()) { + if (!(ability instanceof SpellAbility) && !(ability instanceof PlayLandAbility)) { Ability newAbility = ability.copy(); abilities.add(newAbility); } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index dce0116701..a131973ae7 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -8,9 +8,11 @@ import mage.Mana; import mage.abilities.*; import mage.abilities.ActivatedAbility.ActivationStatus; import mage.abilities.common.PassAbility; +import mage.abilities.common.PlayLandAsCommanderAbility; import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; import mage.abilities.costs.*; +import mage.abilities.costs.common.CommanderAdditionalCost; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; @@ -1145,10 +1147,13 @@ public abstract class PlayerImpl implements Player, Serializable { } } + // warning, if you change code here then fix it in activateAbility too (play commander as land) + //20091005 - 305.1 if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject()))) { // int bookmark = game.bookmarkState(); game.fireEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject())); + if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) { landsPlayed++; game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject())); @@ -1252,10 +1257,60 @@ public abstract class PlayerImpl implements Player, Serializable { pass(game); return true; } - if (ability instanceof PlayLandAbility) { - Card card = game.getCard(ability.getSourceId()); + + Card card = game.getCard(ability.getSourceId()); + if (ability instanceof PlayLandAsCommanderAbility) { + // LAND as commander: + // * first time - play without cost as land + // * second+ times -- cast as spell with cost and stack + + // code from playLand + + ActivationStatus activationStatus = ability.canActivate(this.playerId, game); + if (!activationStatus.canActivate() || !this.canPlayLand()) { + return false; + } + if (card == null) { + return false; + } + + // workaround to find out empty pay in commander land + boolean isEmptyPay = true; + Costs<Cost> costs = ability.getCosts().copy(); + for (Cost cost : costs) { + if (!(cost instanceof CommanderAdditionalCost) || !((CommanderAdditionalCost) cost).isEmptyPay(ability, game)) { + isEmptyPay = false; + } + } + + if (isEmptyPay) { + // play as land + result = playLand(card, game, false); + } else { + // cast as spell with cost, but with all land's restrictions and events like Damping Engine + // look at code in playLand + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject()))) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject())); + + SpellAbility spellAbility = new SpellAbility(null, card.getName(), game.getState().getZone(card.getId())); + spellAbility.addCost(costs); + spellAbility.setControllerId(this.getId()); + spellAbility.setSourceId(card.getId()); + result = cast(spellAbility, game, false, activationStatus.getPermittingObject()); + + if (result) { + landsPlayed++; + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED, card.getId(), card.getId(), playerId, activationStatus.getPermittingObject())); + } + } else { + result = false; + } + } + } else if (ability instanceof PlayLandAbility) { + // LAND as normal card: without cost and stack result = playLand(card, game, false); } else { + // ABILITY ActivationStatus activationStatus = ability.canActivate(this.playerId, game); if (!activationStatus.canActivate()) { return false; @@ -3030,6 +3085,7 @@ public abstract class PlayerImpl implements Player, Serializable { for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) { availableMana.addMana(conditionalMana); } + if (hidden) { for (Card card : hand.getUniqueCards(game)) { for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) @@ -3058,6 +3114,7 @@ public abstract class PlayerImpl implements Player, Serializable { } } } + for (Card card : graveyard.getUniqueCards(game)) { // Handle split cards in graveyard to support Aftermath if (card instanceof SplitCard) { @@ -3074,6 +3131,7 @@ public abstract class PlayerImpl implements Player, Serializable { getOtherUseableActivatedAbilities(card, Zone.GRAVEYARD, game, useable); playable.addAll(useable.values()); } + for (ExileZone exile : game.getExile().getExileZones()) { for (Card card : exile.getCards(game)) { if (null != game.getContinuousEffects().asThough(card.getId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, null, this.getId(), game)) { @@ -3091,7 +3149,8 @@ public abstract class PlayerImpl implements Player, Serializable { } } } - // Check to play revealed cards + + // check to play revealed cards for (Cards cards : game.getState().getRevealed().values()) { for (Card card : cards.getCards(game)) { if (null != game.getContinuousEffects().asThough(card.getId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, null, this.getId(), game)) { @@ -3103,6 +3162,7 @@ public abstract class PlayerImpl implements Player, Serializable { } } } + // check if it's possible to play the top card of a library for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { Player player = game.getPlayer(playerInRangeId); @@ -3119,6 +3179,7 @@ public abstract class PlayerImpl implements Player, Serializable { } } } + // eliminate duplicate activated abilities Map<String, Ability> playableActivated = new HashMap<>(); for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) { @@ -3127,6 +3188,7 @@ public abstract class PlayerImpl implements Player, Serializable { playableActivated.putIfAbsent(ability.toString(), ability); } } + // activated abilities from stack objects for (StackObject stackObject : game.getState().getStack()) { for (ActivatedAbility ability : stackObject.getAbilities().getActivatedAbilities(Zone.STACK)) { @@ -3136,15 +3198,16 @@ public abstract class PlayerImpl implements Player, Serializable { } } + // activated abilities from objects in the command zone (emblems or commanders) for (CommandObject commandObject : game.getState().getCommand()) { for (ActivatedAbility ability : commandObject.getAbilities().getActivatedAbilities(Zone.COMMAND)) { if (ability.isControlledBy(getId()) && canPlay(ability, availableMana, game.getObject(ability.getSourceId()), game)) { playableActivated.put(ability.toString(), ability); } - } } + playable.addAll(playableActivated.values()); } diff --git a/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java b/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java index 99e7996ac0..d72635b400 100644 --- a/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/CommanderInfoWatcher.java @@ -1,9 +1,5 @@ - package mage.watchers.common; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; import mage.MageObject; import mage.cards.Card; import mage.constants.WatcherScope; @@ -15,6 +11,10 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.watchers.Watcher; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + /* 20130711 *903.14a A player that's been dealt 21 or more combat damage by the same commander * over the course of the game loses the game. (This is a state-based action. See rule 704.) @@ -79,11 +79,13 @@ public class CommanderInfoWatcher extends Watcher { if (object != null) { StringBuilder sb = new StringBuilder(); sb.append("<b>Commander</b>"); - Integer castCount = (Integer) game.getState().getValue(sourceId + "_castCount"); - if (castCount != null) { - sb.append(' ').append(castCount).append(castCount == 1 ? " time" : " times").append(" casted from the command zone."); + CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); + int playsCount = watcher.getPlaysCount(sourceId); + if (playsCount > 0) { + sb.append(' ').append(playsCount).append(playsCount == 1 ? " time" : " times").append(" played from the command zone."); } this.addInfo(object, "Commander", sb.toString(), game); + if (checkCommanderDamage) { for (Map.Entry<UUID, Integer> entry : damageToPlayer.entrySet()) { Player damagedPlayer = game.getPlayer(entry.getKey()); diff --git a/Mage/src/main/java/mage/watchers/common/CommanderPlaysCountWatcher.java b/Mage/src/main/java/mage/watchers/common/CommanderPlaysCountWatcher.java new file mode 100644 index 0000000000..789cb2d2c8 --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/CommanderPlaysCountWatcher.java @@ -0,0 +1,62 @@ +package mage.watchers.common; + +import mage.constants.WatcherScope; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.GameEvent.EventType; +import mage.players.Player; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Calcs commanders play count (spell or land) + * + * @author JayDi85 + */ +public class CommanderPlaysCountWatcher extends Watcher { + + private final Map<UUID, Integer> playsCount = new HashMap<>(); + + public CommanderPlaysCountWatcher() { + super(WatcherScope.GAME); + } + + public CommanderPlaysCountWatcher(final CommanderPlaysCountWatcher watcher) { + super(watcher); + this.playsCount.putAll(watcher.playsCount); + } + + @Override + public CommanderPlaysCountWatcher copy() { + return new CommanderPlaysCountWatcher(this); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != EventType.LAND_PLAYED && event.getType() != EventType.SPELL_CAST) { + return; + } + + UUID possibleCommanderId = event.getSourceId(); + boolean isCommanderObject = false; + for (Player player : game.getPlayers().values()) { + if (player.getCommandersIds().contains(possibleCommanderId)) { + isCommanderObject = true; + break; + } + } + + if (isCommanderObject) { + int count = playsCount.getOrDefault(possibleCommanderId, 0); + count++; + playsCount.put(possibleCommanderId, count); + } + } + + public int getPlaysCount(UUID commanderId) { + return this.playsCount.getOrDefault(commanderId, 0); + } +}