diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index e9a293566d9..54a6f78f78d 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -365,7 +365,9 @@ public class ComputerPlayer extends PlayerImpl implements Player { Card card = pickTarget(cards, outcome, target, null, game); if (card != null && alreadyTargetted != null && !alreadyTargetted.contains(card.getId())) { target.add(card.getId(), game); - return true; + if (target.isChosen()) { + return true; + } } } return false; @@ -757,17 +759,20 @@ public class ComputerPlayer extends PlayerImpl implements Player { protected Card pickTarget(List cards, Outcome outcome, Target target, Ability source, Game game) { Card card; while (!cards.isEmpty()) { + if (outcome.isGood()) { card = pickBestCard(cards, null, target, source, game); } else { card = pickWorstCard(cards, null, target, source, game); } - if (source != null) { - if (target.canTarget(getId(), card.getId(), source, game)) { + if (!target.getTargets().contains(card.getId())) { + if (source != null) { + if (target.canTarget(getId(), card.getId(), source, game)) { + return card; + } + } else { return card; } - } else { - return card; } cards.remove(card); } diff --git a/Mage.Sets/src/mage/cards/f/FallOfTheThran.java b/Mage.Sets/src/mage/cards/f/FallOfTheThran.java new file mode 100644 index 00000000000..b8fcd97dbce --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FallOfTheThran.java @@ -0,0 +1,127 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.f; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.common.SagaAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DestroyAllEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SagaChapter; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInYourGraveyard; + +/** + * + * @author LevelX2 + */ +public class FallOfTheThran extends CardImpl { + + public FallOfTheThran(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{5}{W}"); + + this.subtype.add(SubType.SAGA); + + // (As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.) + // (As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.) + SagaAbility sagaAbility = new SagaAbility(this, SagaChapter.CHAPTER_III); + // I — Destroy all lands. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I, new DestroyAllEffect(StaticFilters.FILTER_LANDS)); + // II, III — Each player returns two land cards from their graveyard to the battlefield. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_II, SagaChapter.CHAPTER_III, new FallOfTheThranReturnEffect()); + this.addAbility(sagaAbility); + } + + public FallOfTheThran(final FallOfTheThran card) { + super(card); + } + + @Override + public FallOfTheThran copy() { + return new FallOfTheThran(this); + } +} + +class FallOfTheThranReturnEffect extends OneShotEffect { + + public FallOfTheThranReturnEffect() { + super(Outcome.PutLandInPlay); + this.staticText = "each player returns two land cards from their graveyard to the battlefield"; + } + + public FallOfTheThranReturnEffect(final FallOfTheThranReturnEffect effect) { + super(effect); + } + + @Override + public FallOfTheThranReturnEffect copy() { + return new FallOfTheThranReturnEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Map> toBattlefield = new LinkedHashMap<>(); + if (controller != null) { + for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) { + Player player = game.getPlayer(playerId); + TargetCardInYourGraveyard target = new TargetCardInYourGraveyard(2, 2, StaticFilters.FILTER_CARD_LAND); + target.setNotTarget(true); + target.setTargetController(playerId); + if (target.canChoose(source.getSourceId(), playerId, game)) { + player.choose(outcome, target, source.getSourceId(), game); + if (target.getTargets().size() == 2) { + toBattlefield.put(playerId, new CardsImpl(target.getTargets()).getCards(game)); + } + } + } + + for (Map.Entry> entry : toBattlefield.entrySet()) { + Player player = game.getPlayer(entry.getKey()); + if (player != null) { + player.moveCards(entry.getValue(), Zone.BATTLEFIELD, source, game); + } + } + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/Dominaria.java b/Mage.Sets/src/mage/sets/Dominaria.java index 2ade4940c1a..fcafc9bd95a 100644 --- a/Mage.Sets/src/mage/sets/Dominaria.java +++ b/Mage.Sets/src/mage/sets/Dominaria.java @@ -80,6 +80,7 @@ public class Dominaria extends ExpansionSet { cards.add(new SetCardInfo("Divination", 52, Rarity.COMMON, mage.cards.d.Divination.class)); cards.add(new SetCardInfo("Drudge Sentinel", 89, Rarity.COMMON, mage.cards.d.DrudgeSentinel.class)); cards.add(new SetCardInfo("Dub", 15, Rarity.SPECIAL, mage.cards.d.Dub.class)); + cards.add(new SetCardInfo("Fall of the Thran", 18, Rarity.RARE, mage.cards.f.FallOfTheThran.class)); cards.add(new SetCardInfo("Fire Elemental", 120, Rarity.COMMON, mage.cards.f.FireElemental.class)); cards.add(new SetCardInfo("Forebear's Blade", 214, Rarity.RARE, mage.cards.f.ForebearsBlade.class)); cards.add(new SetCardInfo("Forest", 266, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); @@ -93,7 +94,7 @@ public class Dominaria extends ExpansionSet { cards.add(new SetCardInfo("Goblin Chainwhirler", 129, Rarity.RARE, mage.cards.g.GoblinChainwhirler.class)); cards.add(new SetCardInfo("Goblin Warchief", 130, Rarity.UNCOMMON, mage.cards.g.GoblinWarchief.class)); cards.add(new SetCardInfo("Grunn, the Lonely King", 165, Rarity.UNCOMMON, mage.cards.g.GrunnTheLonelyKing.class)); - cards.add(new SetCardInfo("Guardians of Koilos", 216, Rarity.COMMON, mage.cards.g.GuardiansOfKoilos.class)); + cards.add(new SetCardInfo("Guardians of Koilos", 216, Rarity.COMMON, mage.cards.g.GuardiansOfKoilos.class)); cards.add(new SetCardInfo("Helm of the Host", 217, Rarity.RARE, mage.cards.h.HelmOfTheHost.class)); cards.add(new SetCardInfo("Hinterland Harbor", 240, Rarity.RARE, mage.cards.h.HinterlandHarbor.class)); cards.add(new SetCardInfo("Homarid Explorer", 53, Rarity.UNCOMMON, mage.cards.h.HomaridExplorer.class)); diff --git a/Mage/src/main/java/mage/abilities/common/SagaAbility.java b/Mage/src/main/java/mage/abilities/common/SagaAbility.java new file mode 100644 index 00000000000..c72598114e1 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/SagaAbility.java @@ -0,0 +1,216 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.abilities.common; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.constants.SagaChapter; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.GameEvent.EventType; +import mage.game.permanent.Permanent; +import mage.game.stack.StackAbility; +import mage.game.stack.StackObject; + +/** + * + * @author LevelX2 + */ +public class SagaAbility extends TriggeredAbilityImpl { + + private SagaChapter maxChapter; + + public SagaAbility(Card card, SagaChapter maxChapter) { + super(Zone.ALL, new AddCountersSourceEffect(CounterType.LORE.createInstance()), false); + this.maxChapter = maxChapter; + this.setRuleVisible(false); + ((CardImpl) card).addAbility(new SagaAddCounterAbility(maxChapter)); + + } + + public SagaAbility(final SagaAbility ability) { + super(ability); + this.maxChapter = ability.maxChapter; + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.getTargetId().equals(getSourceId()); + } + + @Override + public String getRule() { + return "When {this} enters the battlefield, " + super.getRule(); + } + + public Ability addChapterEffect(Card card, SagaChapter chapter, Effect effect) { + return addChapterEffect(card, chapter, chapter, effect); + } + + public Ability addChapterEffect(Card card, SagaChapter fromChapter, SagaChapter toChapter, Effect effect) { + ChapterTriggeredAbility ability = new ChapterTriggeredAbility(effect, fromChapter, toChapter); + ((CardImpl) card).addAbility(ability); + if (maxChapter == null || toChapter.getNumber() > maxChapter.getNumber()) { + maxChapter = toChapter; + } + return ability; + } + + public SagaChapter getMaxChapter() { + return maxChapter; + } + + @Override + public SagaAbility copy() { + return new SagaAbility(this); + } + + public static boolean isChapterAbility(StackObject stackObject) { + if (stackObject instanceof StackAbility) { + return ((StackAbility) stackObject).getStackAbility() instanceof ChapterTriggeredAbility; + } + return false; + } +} + +class ChapterTriggeredAbility extends TriggeredAbilityImpl { + + SagaChapter chapterFrom, chapterTo; + + public ChapterTriggeredAbility(Effect effect, SagaChapter chapterFrom, SagaChapter chapterTo) { + super(Zone.BATTLEFIELD, effect, false); + this.chapterFrom = chapterFrom; + this.chapterTo = chapterTo; + } + + public ChapterTriggeredAbility(final ChapterTriggeredAbility ability) { + super(ability); + this.chapterFrom = ability.chapterFrom; + this.chapterTo = ability.chapterTo; + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == EventType.COUNTER_ADDED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (event.getTargetId().equals(getSourceId()) && event.getData().equals(CounterType.LORE.getName())) { + Permanent sourceSaga = game.getPermanentOrLKIBattlefield(getSourceId()); + if (sourceSaga != null) { + int loreCounters = sourceSaga.getCounters(game).getCount(CounterType.LORE); + return chapterFrom.getNumber() <= loreCounters + && chapterTo.getNumber() >= loreCounters; + } + } + return false; + } + + @Override + public ChapterTriggeredAbility copy() { + return new ChapterTriggeredAbility(this); + } + + public String getChapterRule() { + String rule = super.getRule(); + return Character.toUpperCase(rule.charAt(0)) + rule.substring(1); + } + + public SagaChapter getChapterFrom() { + return chapterFrom; + } + + public SagaChapter getChapterTo() { + return chapterTo; + } + + @Override + public String getRule() { + StringBuilder sb = new StringBuilder(); + for (SagaChapter chapter : SagaChapter.values()) { + if (chapter.getNumber() >= getChapterFrom().getNumber() + && chapter.getNumber() < getChapterTo().getNumber()) { + sb.append(chapter.toString()).append(", "); + } else if (chapter.equals(getChapterTo())) { + sb.append(chapter.toString()); + } + } + String text = super.getRule(); + sb.append(": ").append(Character.toUpperCase(text.charAt(0)) + text.substring(1)); + return sb.toString(); + } +} + +class SagaAddCounterAbility extends TriggeredAbilityImpl { + + SagaChapter maxChapter; + + SagaAddCounterAbility(SagaChapter maxChapter) { + super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.LORE.createInstance()), false); + this.usesStack = false; + this.maxChapter = maxChapter; + } + + SagaAddCounterAbility(final SagaAddCounterAbility ability) { + super(ability); + this.maxChapter = ability.maxChapter; + } + + @Override + public SagaAddCounterAbility copy() { + return new SagaAddCounterAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == EventType.PRECOMBAT_MAIN_PHASE_PRE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.getPlayerId().equals(this.controllerId); + } + + @Override + public String getRule() { + return "(As this Saga enters and after your draw step, add a lore counter. Sacrifice after " + maxChapter.toString() + ".) "; + } + +} diff --git a/Mage/src/main/java/mage/constants/SagaChapter.java b/Mage/src/main/java/mage/constants/SagaChapter.java new file mode 100644 index 00000000000..8eb2487abb8 --- /dev/null +++ b/Mage/src/main/java/mage/constants/SagaChapter.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.constants; + +/** + * + * @author LevelX2 + */ +public enum SagaChapter { + CHAPTER_I(1, "I"), + CHAPTER_II(2, "II"), + CHAPTER_III(3, "III"); + + private final String text; + private final int number; + + SagaChapter(int number, String text) { + this.number = number; + this.text = text; + } + + @Override + public String toString() { + return text; + } + + public int getNumber() { + return number; + } + +} diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 2d49f4a51fa..c59cf8dbd3e 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -36,6 +36,7 @@ import mage.MageObject; import mage.abilities.*; import mage.abilities.common.AttachableToRestrictedAbility; import mage.abilities.common.CantHaveMoreThanAmountCountersSourceAbility; +import mage.abilities.common.SagaAbility; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffects; import mage.abilities.effects.Effect; @@ -1914,7 +1915,7 @@ public abstract class GameImpl implements Game, Serializable { if (perm.isWorld()) { worldEnchantment.add(perm); } - if (StaticFilters.FILTER_PERMANENT_AURA.match(perm, this)) { + if (perm.hasSubtype(SubType.AURA, this)) { //20091005 - 704.5n, 702.14c if (perm.getAttachedTo() == null) { Card card = this.getCard(perm.getId()); @@ -2004,6 +2005,30 @@ public abstract class GameImpl implements Game, Serializable { } } } + // Remove Saga enchantment if last chapter is reached and chapter ability has left the stack + if (perm.hasSubtype(SubType.SAGA, this)) { + for (Ability sagaAbility : perm.getAbilities()) { + if (sagaAbility instanceof SagaAbility) { + int maxChapter = ((SagaAbility) sagaAbility).getMaxChapter().getNumber(); + if (maxChapter <= perm.getCounters(this).getCount(CounterType.LORE)) { + boolean noChapterAbilityOnStack = true; + // Check chapter abilities on stack + for (StackObject stackObject : getStack()) { + if (stackObject.getSourceId().equals(perm.getId()) && SagaAbility.isChapterAbility(stackObject)) { + noChapterAbilityOnStack = false; + break; + } + } + if (noChapterAbilityOnStack) { + // After the last chapter ability has left the stack, you'll sacrifice the Saga + perm.sacrifice(perm.getId(), this); + somethingHappened = true; + } + } + + } + } + } if (this.getState().isLegendaryRuleActive() && StaticFilters.FILTER_PERMANENT_LEGENDARY.match(perm, this)) { legendary.add(perm); }