diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java index 4ee2440d33..d8ee5251a5 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java @@ -264,6 +264,8 @@ public class SimulatedPlayer2 extends ComputerPlayer { Iterator iterator = options.iterator(); boolean bad = true; boolean good = true; + + // TODO: add custom outcome from ability? for (Effect effect : ability.getEffects()) { if (effect.getOutcome().isGood()) { bad = false; diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/optimizers/impl/OutcomeOptimizer.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/optimizers/impl/OutcomeOptimizer.java index c80f585cc7..c6aed09cfa 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/optimizers/impl/OutcomeOptimizer.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ma/optimizers/impl/OutcomeOptimizer.java @@ -6,24 +6,25 @@ package mage.player.ai.ma.optimizers.impl; -import java.util.List; import mage.abilities.Ability; import mage.abilities.effects.Effect; import mage.constants.Outcome; import mage.game.Game; +import java.util.List; + /** * Removes abilities that require only discard a card for activation. * - * @author LevelX2 + * @author LevelX2 */ public class OutcomeOptimizer extends BaseTreeOptimizer { @Override public void filter(Game game, List actions) { for (Ability ability : actions) { - for (Effect effect: ability.getEffects()) { - if (effect.getOutcome() == Outcome.AIDontUseIt) { + for (Effect effect : ability.getEffects()) { + if (ability.getCustomOutcome() == Outcome.AIDontUseIt || effect.getOutcome() == Outcome.AIDontUseIt) { removeAbility(ability); break; } 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 4b27f6bfd8..bae4133820 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 @@ -1160,12 +1160,12 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (!playableAbilities.isEmpty()) { for (ActivatedAbility ability : playableAbilities) { if (ability.canActivate(playerId, game).canActivate()) { - if (ability.getEffects().hasOutcome(Outcome.PutLandInPlay)) { + if (ability.getEffects().hasOutcome(ability, Outcome.PutLandInPlay)) { if (this.activateAbility(ability, game)) { return true; } } - if (ability.getEffects().hasOutcome(Outcome.PutCreatureInPlay)) { + if (ability.getEffects().hasOutcome(ability, Outcome.PutCreatureInPlay)) { if (getOpponentBlockers(opponentId, game).size() <= 1) { if (this.activateAbility(ability, game)) { return true; diff --git a/Mage.Sets/src/mage/cards/k/KaaliaOfTheVast.java b/Mage.Sets/src/mage/cards/k/KaaliaOfTheVast.java index 3615a8eeba..e9d90de7c4 100644 --- a/Mage.Sets/src/mage/cards/k/KaaliaOfTheVast.java +++ b/Mage.Sets/src/mage/cards/k/KaaliaOfTheVast.java @@ -1,7 +1,5 @@ - package mage.cards.k; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; @@ -20,8 +18,9 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInHand; +import java.util.UUID; + /** - * * @author Backfir3 */ public final class KaaliaOfTheVast extends CardImpl { @@ -73,9 +72,7 @@ class KaaliaOfTheVastAttacksAbility extends TriggeredAbilityImpl { public boolean checkTrigger(GameEvent event, Game game) { if (event.getSourceId().equals(this.getSourceId())) { Player opponent = game.getPlayer(event.getTargetId()); - if (opponent != null) { - return true; - } + return opponent != null; } return false; } @@ -123,7 +120,7 @@ class KaaliaOfTheVastEffect extends OneShotEffect { return false; } TargetCardInHand target = new TargetCardInHand(filter); - if (target.canChoose(controller.getId(), game) && target.choose(getOutcome(), controller.getId(), source.getSourceId(), game)) { + if (target.canChoose(controller.getId(), game) && target.choose(outcome, controller.getId(), source.getSourceId(), game)) { if (!target.getTargets().isEmpty()) { UUID cardId = target.getFirstTarget(); Card card = game.getCard(cardId); diff --git a/Mage.Sets/src/mage/cards/p/PreeminentCaptain.java b/Mage.Sets/src/mage/cards/p/PreeminentCaptain.java index c17a7dc82a..421c4d5ca1 100644 --- a/Mage.Sets/src/mage/cards/p/PreeminentCaptain.java +++ b/Mage.Sets/src/mage/cards/p/PreeminentCaptain.java @@ -1,6 +1,5 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; @@ -19,8 +18,9 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInHand; +import java.util.UUID; + /** - * * @author Rafbill */ public final class PreeminentCaptain extends CardImpl { @@ -71,7 +71,7 @@ class PreeminentCaptainEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); TargetCardInHand target = new TargetCardInHand(filter); if (controller != null && target.canChoose(controller.getId(), game) - && target.choose(getOutcome(), controller.getId(), source.getSourceId(), game)) { + && target.choose(outcome, controller.getId(), source.getSourceId(), game)) { if (!target.getTargets().isEmpty()) { UUID cardId = target.getFirstTarget(); Card card = controller.getHand().get(cardId, game); diff --git a/Mage.Sets/src/mage/cards/r/RescueFromTheUnderworld.java b/Mage.Sets/src/mage/cards/r/RescueFromTheUnderworld.java index 1a8f126c8d..b5041281c0 100644 --- a/Mage.Sets/src/mage/cards/r/RescueFromTheUnderworld.java +++ b/Mage.Sets/src/mage/cards/r/RescueFromTheUnderworld.java @@ -1,7 +1,5 @@ - package mage.cards.r; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.Mode; @@ -29,34 +27,34 @@ import mage.target.common.TargetCardInGraveyard; import mage.target.common.TargetCardInYourGraveyard; import mage.target.common.TargetControlledCreaturePermanent; +import java.util.UUID; + /** - * * Once you announce you're casting Rescue from the Underworld, no player may * attempt to stop you from casting the spell by removing the creature you want * to sacrifice. - * + *

* If you sacrifice a creature token to cast Rescue from the Underworld, it * won't return to the battlefield, although the target creature card will. - * + *

* If either the sacrificed creature or the target creature card leaves the * graveyard before the delayed triggered ability resolves during your next * upkeep, it won't return. - * + *

* However, if the sacrificed creature is put into another public zone instead * of the graveyard, perhaps because it's your commander or because of another * replacement effect, it will return to the battlefield from the zone it went * to. - * + *

* Rescue from the Underworld is exiled as it resolves, not later as its delayed * trigger resolves. * - * * @author LevelX2 */ public final class RescueFromTheUnderworld extends CardImpl { public RescueFromTheUnderworld(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{4}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{4}{B}"); // As an additional cost to cast Rescue from the Underworld, sacrifice a creature. this.getSpellAbility().addCost(new SacrificeTargetCost(new TargetControlledCreaturePermanent(1, 1, new FilterControlledCreaturePermanent("a creature"), false))); @@ -106,7 +104,7 @@ class RescueFromTheUnderworldCreateDelayedTriggeredAbilityEffect extends OneShot protected DelayedTriggeredAbility ability; public RescueFromTheUnderworldCreateDelayedTriggeredAbilityEffect(DelayedTriggeredAbility ability) { - super(ability.getEffects().get(0).getOutcome()); + super(ability.getEffects().getOutcome(ability)); this.ability = ability; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/OutcomesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/OutcomesTest.java new file mode 100644 index 0000000000..1e142b2e5f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/OutcomesTest.java @@ -0,0 +1,99 @@ +package org.mage.test.cards.abilities; + +import mage.abilities.Ability; +import mage.abilities.common.LeavesBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.constants.Duration; +import mage.constants.Outcome; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class OutcomesTest extends CardTestPlayerBaseWithAIHelps { + + /** + * Normal outcome from effects + */ + + @Test + public void test_FromEffects_Single() { + Ability abilityGood = new SimpleStaticAbility(new GainLifeEffect(10)); + Assert.assertEquals(1, abilityGood.getEffects().getOutcomeScore(abilityGood)); + + Ability abilityBad = new SimpleStaticAbility(new DamageTargetEffect(10)); + Assert.assertEquals(-1, abilityBad.getEffects().getOutcomeScore(abilityBad)); + } + + @Test + public void test_FromEffects_Multi() { + Ability abilityGood = new SimpleStaticAbility(new GainLifeEffect(10)); + abilityGood.addEffect(new BoostSourceEffect(10, 10, Duration.EndOfTurn)); + Assert.assertEquals(1 + 1, abilityGood.getEffects().getOutcomeScore(abilityGood)); + + Ability abilityBad = new SimpleStaticAbility(new DamageTargetEffect(10)); + abilityBad.addEffect(new ExileTargetEffect()); + Assert.assertEquals(-1 + -1, abilityBad.getEffects().getOutcomeScore(abilityBad)); + } + + @Test + public void test_FromEffects_MultiCombine() { + Ability ability = new SimpleStaticAbility(new GainLifeEffect(10)); + ability.addEffect(new BoostSourceEffect(10, 10, Duration.EndOfTurn)); + ability.addEffect(new ExileTargetEffect()); + Assert.assertEquals(1 + 1 + -1, ability.getEffects().getOutcomeScore(ability)); + } + + @Test + public void test_FromEffects_Default() { + Ability ability = new LeavesBattlefieldTriggeredAbility(null, false); + Assert.assertEquals(0, ability.getEffects().getOutcomeScore(ability)); + Assert.assertEquals(Outcome.Detriment, ability.getEffects().getOutcome(ability)); + Assert.assertEquals(Outcome.BoostCreature, ability.getEffects().getOutcome(ability, Outcome.BoostCreature)); + } + + /** + * Special outcome from ability (AI activates only good abilities) + */ + + @Test + public void test_FromAbility_Single() { + Ability abilityGood = new SimpleStaticAbility(new GainLifeEffect(10)); + abilityGood.addCustomOutcome(Outcome.Detriment); + Assert.assertEquals(-1, abilityGood.getEffects().getOutcomeScore(abilityGood)); + Assert.assertEquals(Outcome.Detriment, abilityGood.getEffects().getOutcome(abilityGood)); + + Ability abilityBad = new SimpleStaticAbility(new DamageTargetEffect(10)); + abilityBad.addCustomOutcome(Outcome.Neutral); + Assert.assertEquals(1, abilityBad.getEffects().getOutcomeScore(abilityBad)); + Assert.assertEquals(Outcome.Neutral, abilityBad.getEffects().getOutcome(abilityBad)); + } + + @Test + public void test_FromAbility_Multi() { + Ability abilityGood = new SimpleStaticAbility(new GainLifeEffect(10)); + abilityGood.addEffect(new BoostSourceEffect(10, 10, Duration.EndOfTurn)); + abilityGood.addCustomOutcome(Outcome.Detriment); + Assert.assertEquals(-1 + -1, abilityGood.getEffects().getOutcomeScore(abilityGood)); + + Ability abilityBad = new SimpleStaticAbility(new DamageTargetEffect(10)); + abilityBad.addEffect(new ExileTargetEffect()); + abilityBad.addCustomOutcome(Outcome.Neutral); + Assert.assertEquals(1 + 1, abilityBad.getEffects().getOutcomeScore(abilityBad)); + } + + @Test + public void test_FromAbility_MultiCombine() { + Ability ability = new SimpleStaticAbility(new GainLifeEffect(10)); + ability.addEffect(new BoostSourceEffect(10, 10, Duration.EndOfTurn)); + ability.addEffect(new ExileTargetEffect()); + ability.addCustomOutcome(Outcome.Neutral); // must "convert" all effects to good + Assert.assertEquals(1 + 1 + 1, ability.getEffects().getOutcomeScore(ability)); + } +} diff --git a/Mage/src/main/java/mage/abilities/AbilitiesImpl.java b/Mage/src/main/java/mage/abilities/AbilitiesImpl.java index 16ddbfbeac..d389a5ac15 100644 --- a/Mage/src/main/java/mage/abilities/AbilitiesImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilitiesImpl.java @@ -1,8 +1,5 @@ - package mage.abilities; -import java.util.*; -import java.util.stream.Collectors; import mage.abilities.common.ZoneChangeTriggeredAbility; import mage.abilities.costs.Cost; import mage.abilities.keyword.ProtectionAbility; @@ -13,6 +10,9 @@ import mage.game.Game; import mage.util.ThreadLocalStringBuilder; import org.apache.log4j.Logger; +import java.util.*; +import java.util.stream.Collectors; + /** * @param * @author BetaSteward_at_googlemail.com @@ -220,7 +220,7 @@ public class AbilitiesImpl extends ArrayList implements Ab @Override public boolean contains(T ability) { - for (Iterator iterator = this.iterator(); iterator.hasNext();) { // simple loop can cause java.util.ConcurrentModificationException + for (Iterator iterator = this.iterator(); iterator.hasNext(); ) { // simple loop can cause java.util.ConcurrentModificationException T test = iterator.next(); // Checking also by getRule() without other restrictions is a problem when a triggered ability will be copied to a permanent that had the same ability // already before the copy. Because then it keeps the triggered ability twice and it triggers twice. @@ -273,7 +273,7 @@ public class AbilitiesImpl extends ArrayList implements Ab @Override public int getOutcomeTotal() { - return stream().mapToInt(ability -> ability.getEffects().getOutcomeTotal()).sum(); + return stream().mapToInt(ability -> ability.getEffects().getOutcomeScore(ability)).sum(); } @Override diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index f14c95b634..dd5b811f84 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -1,8 +1,5 @@ package mage.abilities; -import java.io.Serializable; -import java.util.List; -import java.util.UUID; import mage.MageObject; import mage.abilities.costs.Cost; import mage.abilities.costs.CostAdjuster; @@ -12,10 +9,7 @@ import mage.abilities.costs.mana.ManaCosts; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; import mage.abilities.hint.Hint; -import mage.constants.AbilityType; -import mage.constants.AbilityWord; -import mage.constants.EffectType; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Controllable; import mage.game.Game; import mage.game.events.GameEvent; @@ -26,6 +20,10 @@ import mage.target.Targets; import mage.target.targetadjustment.TargetAdjuster; import mage.watchers.Watcher; +import java.io.Serializable; +import java.util.List; +import java.util.UUID; + /** * Practically everything in the game is started from an Ability. This interface * describes what an Ability is composed of at the highest level. @@ -46,10 +44,8 @@ public interface Ability extends Controllable, Serializable { * * @see mage.players.PlayerImpl#playAbility(mage.abilities.ActivatedAbility, * mage.game.Game) - * @see - * mage.game.GameImpl#addTriggeredAbility(mage.abilities.TriggeredAbility) - * @see - * mage.game.GameImpl#addDelayedTriggeredAbility(mage.abilities.DelayedTriggeredAbility) + * @see mage.game.GameImpl#addTriggeredAbility(mage.abilities.TriggeredAbility) + * @see mage.game.GameImpl#addDelayedTriggeredAbility(mage.abilities.DelayedTriggeredAbility) */ void newId(); @@ -58,10 +54,8 @@ public interface Ability extends Controllable, Serializable { * * @see mage.players.PlayerImpl#playAbility(mage.abilities.ActivatedAbility, * mage.game.Game) - * @see - * mage.game.GameImpl#addTriggeredAbility(mage.abilities.TriggeredAbility) - * @see - * mage.game.GameImpl#addDelayedTriggeredAbility(mage.abilities.DelayedTriggeredAbility) + * @see mage.game.GameImpl#addTriggeredAbility(mage.abilities.TriggeredAbility) + * @see mage.game.GameImpl#addDelayedTriggeredAbility(mage.abilities.DelayedTriggeredAbility) */ void newOriginalId(); @@ -267,16 +261,15 @@ public interface Ability extends Controllable, Serializable { /** * Activates this ability prompting the controller to pay any mandatory * - * @param game A reference the {@link Game} for which this ability should be - * activated within. + * @param game A reference the {@link Game} for which this ability should be + * activated within. * @param noMana Whether or not {@link ManaCosts} have to be paid. * @return True if this ability was successfully activated. * @see mage.players.PlayerImpl#cast(mage.abilities.SpellAbility, * mage.game.Game, boolean) * @see mage.players.PlayerImpl#playAbility(mage.abilities.ActivatedAbility, * mage.game.Game) - * @see - * mage.players.PlayerImpl#triggerAbility(mage.abilities.TriggeredAbility, + * @see mage.players.PlayerImpl#triggerAbility(mage.abilities.TriggeredAbility, * mage.game.Game) */ boolean activate(Game game, boolean noMana); @@ -290,8 +283,7 @@ public interface Ability extends Controllable, Serializable { * * @param game The {@link Game} for which this ability resolves within. * @return Whether or not this ability successfully resolved. - * @see - * mage.players.PlayerImpl#playManaAbility(mage.abilities.mana.ManaAbility, + * @see mage.players.PlayerImpl#playManaAbility(mage.abilities.mana.ManaAbility, * mage.game.Game) * @see mage.players.PlayerImpl#specialAction(mage.abilities.SpecialAction, * mage.game.Game) @@ -526,4 +518,8 @@ public interface Ability extends Controllable, Serializable { List getHints(); Ability addHint(Hint hint); + + Ability addCustomOutcome(Outcome customOutcome); + + Outcome getCustomOutcome(); } diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 43c53e8820..7fcb21e585 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1,9 +1,5 @@ package mage.abilities; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; import mage.MageObject; import mage.abilities.costs.*; import mage.abilities.costs.common.PayLifeCost; @@ -33,6 +29,11 @@ import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import org.apache.log4j.Logger; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -68,6 +69,7 @@ public abstract class AbilityImpl implements Ability { protected TargetAdjuster targetAdjuster = null; protected CostAdjuster costAdjuster = null; protected List hints = new ArrayList<>(); + protected Outcome customOutcome = null; // uses for AI decisions instead effects public AbilityImpl(AbilityType abilityType, Zone zone) { this.id = UUID.randomUUID(); @@ -117,6 +119,7 @@ public abstract class AbilityImpl implements Ability { for (Hint hint : ability.getHints()) { this.hints.add(hint.copy()); } + this.customOutcome = ability.customOutcome; } @Override @@ -321,7 +324,7 @@ public abstract class AbilityImpl implements Ability { sourceObject.adjustTargets(this, game); } if (!getTargets().isEmpty()) { - Outcome outcome = getEffects().isEmpty() ? Outcome.Detriment : getEffects().get(0).getOutcome(); + Outcome outcome = getEffects().getOutcome(this); // only activated abilities can be canceled by user (not triggered) if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, this instanceof ActivatedAbility)) { // was canceled during targer selection @@ -948,10 +951,7 @@ public abstract class AbilityImpl implements Ability { } return ((Permanent) object).isPhasedIn(); } else if (object instanceof Card) { - if (!((Card) object).getAbilities(game).contains(this)) { - return false; - } - return true; + return ((Card) object).getAbilities(game).contains(this); } else if (!object.getAbilities().contains(this)) { // not sure which object it can still be // check if it's an ability that is temporary gained to a card Abilities otherAbilities = game.getState().getAllOtherAbilities(this.getSourceId()); @@ -1250,4 +1250,15 @@ public abstract class AbilityImpl implements Ability { this.hints.add(hint); return this; } + + @Override + public Ability addCustomOutcome(Outcome customOutcome) { + this.customOutcome = customOutcome; + return this; + } + + @Override + public Outcome getCustomOutcome() { + return this.customOutcome; + } } diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java index 62cf4f0ea3..e037461998 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java @@ -3,7 +3,6 @@ package mage.abilities; import mage.MageObject; import mage.abilities.effects.Effect; import mage.constants.AbilityType; -import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; @@ -61,7 +60,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge MageObject object = game.getObject(getSourceId()); Player player = game.getPlayer(this.getControllerId()); if (player != null && object != null) { - if (!player.chooseUse(getEffects().isEmpty() ? Outcome.Detriment : getEffects().get(0).getOutcome(), this.getRule(object.getLogName()), this, game)) { + if (!player.chooseUse(getEffects().getOutcome(this), this.getRule(object.getLogName()), this, game)) { return false; } } else { diff --git a/Mage/src/main/java/mage/abilities/effects/Effects.java b/Mage/src/main/java/mage/abilities/effects/Effects.java index 3eaf378be5..8d3d81db54 100644 --- a/Mage/src/main/java/mage/abilities/effects/Effects.java +++ b/Mage/src/main/java/mage/abilities/effects/Effects.java @@ -1,13 +1,11 @@ package mage.abilities.effects; +import mage.abilities.Ability; import mage.abilities.Mode; import mage.constants.Outcome; import mage.target.targetpointer.TargetPointer; import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; /** * @author BetaSteward_at_googlemail.com @@ -99,7 +97,11 @@ public class Effects extends ArrayList { return sbText.toString(); } - public boolean hasOutcome(Outcome outcome) { + public boolean hasOutcome(Ability source, Outcome outcome) { + Outcome realOutcome = (source == null ? null : source.getCustomOutcome()); + if (realOutcome != null) { + return realOutcome == outcome; + } for (Effect effect : this) { if (effect.getOutcome() == outcome) { return true; @@ -108,18 +110,40 @@ public class Effects extends ArrayList { return false; } - public List getOutcomes() { - Set outcomes = new HashSet<>(); - for (Effect effect : this) { - outcomes.add(effect.getOutcome()); - } - return new ArrayList<>(outcomes); + /** + * @param source source ability for effects + * @return real outcome of ability + */ + public Outcome getOutcome(Ability source) { + return getOutcome(source, Outcome.Detriment); } - public int getOutcomeTotal() { + public Outcome getOutcome(Ability source, Outcome defaultOutcome) { + Outcome realOutcome = (source == null ? null : source.getCustomOutcome()); + if (realOutcome != null) { + return realOutcome; + } + + if (!this.isEmpty()) { + return this.get(0).getOutcome(); + } + + return defaultOutcome; + } + + /** + * @param source source ability for effects + * @return total score of outcome effects (plus/minus) + */ + public int getOutcomeScore(Ability source) { int total = 0; for (Effect effect : this) { - if (effect.getOutcome().isGood()) { + // custom ability outcome must "rewrite" effect's outcome (it uses for AI desisions and card score... hmm, getOutcomeTotal used on 28.01.2020) + Outcome realOutcome = (source == null ? null : source.getCustomOutcome()); + if (realOutcome == null) { + realOutcome = effect.getOutcome(); + } + if (realOutcome.isGood()) { total++; } else { total--; diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java index c8aeb347db..3485c7c4f9 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java @@ -1,4 +1,3 @@ - package mage.abilities.effects.common; import mage.MageObject; @@ -103,11 +102,11 @@ public class CopyPermanentEffect extends OneShotEffect { // attach - search effect in spell ability (example: cast Utopia Sprawl, cast Estrid's Invocation on it) for (Ability ability : bluePrintPermanent.getAbilities()) { if (ability instanceof SpellAbility) { + auraOutcome = ability.getEffects().getOutcome(ability); for (Effect effect : ability.getEffects()) { if (effect instanceof AttachEffect) { if (bluePrintPermanent.getSpellAbility().getTargets().size() > 0) { auraTarget = bluePrintPermanent.getSpellAbility().getTargets().get(0); - auraOutcome = effect.getOutcome(); } } } @@ -118,12 +117,9 @@ public class CopyPermanentEffect extends OneShotEffect { if (auraTarget == null) { for (Ability ability : bluePrintPermanent.getAbilities()) { if (ability instanceof EnchantAbility) { + auraOutcome = ability.getEffects().getOutcome(ability); if (ability.getTargets().size() > 0) { // Animate Dead don't have targets auraTarget = ability.getTargets().get(0); - for (Effect effect : ability.getEffects()) { - // first outcome - auraOutcome = effect.getOutcome(); - } } } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java index 646e3bebfa..179a59f736 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java @@ -1,4 +1,3 @@ - package mage.abilities.effects.common; import mage.abilities.Ability; @@ -6,11 +5,9 @@ import mage.abilities.DelayedTriggeredAbility; import mage.abilities.Mode; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; -import mage.constants.Outcome; import mage.game.Game; /** - * * @author BetaSteward_at_googlemail.com */ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect { @@ -28,7 +25,7 @@ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect { } public CreateDelayedTriggeredAbilityEffect(DelayedTriggeredAbility ability, boolean copyTargets, boolean initAbility) { - super(ability.getEffects().isEmpty() ? Outcome.Detriment : ability.getEffects().get(0).getOutcome()); + super(ability.getEffects().getOutcome(ability)); this.ability = ability; this.copyTargets = copyTargets; this.initAbility = initAbility; diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateSpecialActionEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateSpecialActionEffect.java index 26266d0406..3d760dcee4 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateSpecialActionEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateSpecialActionEffect.java @@ -1,15 +1,12 @@ - package mage.abilities.effects.common; import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.SpecialAction; import mage.abilities.effects.OneShotEffect; -import mage.constants.Outcome; import mage.game.Game; /** - * * @author BetaSteward_at_googlemail.com */ public class CreateSpecialActionEffect extends OneShotEffect { @@ -17,7 +14,7 @@ public class CreateSpecialActionEffect extends OneShotEffect { protected SpecialAction action; public CreateSpecialActionEffect(SpecialAction action) { - super(action.getEffects().isEmpty() ? Outcome.Detriment : action.getEffects().get(0).getOutcome()); + super(action.getEffects().getOutcome(action)); this.action = action; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java b/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java index 7277831f43..d231598bf0 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java +++ b/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java @@ -91,7 +91,7 @@ public class DoIfCostPaid extends OneShotEffect { } message = CardUtil.replaceSourceName(message, mageObject.getLogName()); boolean result = true; - Outcome payOutcome = executingEffects.size() > 0 ? executingEffects.get(0).getOutcome() : this.outcome; + Outcome payOutcome = executingEffects.getOutcome(source, this.outcome); if (cost.canPay(source, source.getSourceId(), player.getId(), game) && (!optional || player.chooseUse(payOutcome, message, source, game))) { cost.clearPaid(); diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java index b98e86f8bb..5647423c5d 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java @@ -39,8 +39,7 @@ public class GainAbilityTargetEffect extends ContinuousEffectImpl { } public GainAbilityTargetEffect(Ability ability, Duration duration, String rule, boolean onCard, Layer layer, SubLayer subLayer) { - super(duration, layer, subLayer, - !ability.getEffects().isEmpty() ? ability.getEffects().get(0).getOutcome() : Outcome.AddAbility); + super(duration, layer, subLayer, ability.getEffects().getOutcome(ability, Outcome.AddAbility)); this.ability = ability; staticText = rule; this.onCard = onCard; diff --git a/Mage/src/main/java/mage/game/draft/RateCard.java b/Mage/src/main/java/mage/game/draft/RateCard.java index 87676a9210..d2d3608a85 100644 --- a/Mage/src/main/java/mage/game/draft/RateCard.java +++ b/Mage/src/main/java/mage/game/draft/RateCard.java @@ -132,6 +132,7 @@ public final class RateCard { } private static int isEffectRemoval(Card card, Ability ability, Effect effect) { + // it's effect relates score, do not use custom outcome from ability if (effect.getOutcome() == Outcome.Removal) { // found removal return 1; diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index e6436d7eae..bd37c6436f 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -1,9 +1,5 @@ package mage.game.stack; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import java.util.UUID; import mage.MageInt; import mage.MageObject; import mage.ObjectColor; @@ -34,6 +30,11 @@ import mage.util.GameLog; import mage.util.SubTypeList; import mage.watchers.Watcher; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -578,7 +579,7 @@ public class StackAbility extends StackObjImpl implements Ability { game.getStack().push(newStackAbility); if (chooseNewTargets && !newAbility.getTargets().isEmpty()) { Player controller = game.getPlayer(newControllerId); - Outcome outcome = newAbility.getEffects().isEmpty() ? Outcome.Detriment : newAbility.getEffects().get(0).getOutcome(); + Outcome outcome = newAbility.getEffects().getOutcome(newAbility); if (controller.chooseUse(outcome, "Choose new targets?", source, game)) { newAbility.getTargets().clearChosen(); newAbility.getTargets().chooseTargets(outcome, newControllerId, newAbility, false, game, false); @@ -648,7 +649,16 @@ public class StackAbility extends StackObjImpl implements Ability { @Override public Ability addHint(Hint hint) { - // only abilities supports addhint - return null; + throw new IllegalArgumentException("Stack ability is not supports hint adding"); + } + + @Override + public Ability addCustomOutcome(Outcome customOutcome) { + throw new IllegalArgumentException("Stack ability is not supports custom outcome adding"); + } + + @Override + public Outcome getCustomOutcome() { + return this.ability.getCustomOutcome(); } } diff --git a/Mage/src/main/java/mage/game/stack/StackObjImpl.java b/Mage/src/main/java/mage/game/stack/StackObjImpl.java index 48c32191a9..6b38b76acb 100644 --- a/Mage/src/main/java/mage/game/stack/StackObjImpl.java +++ b/Mage/src/main/java/mage/game/stack/StackObjImpl.java @@ -163,7 +163,7 @@ public abstract class StackObjImpl implements StackObject { targetAmount = " (amount: " + target.getTargetAmount(targetId) + ")"; } // change the target? - Outcome outcome = mode.getEffects().isEmpty() ? Outcome.Detriment : mode.getEffects().get(0).getOutcome(); + Outcome outcome = mode.getEffects().getOutcome(ability); if (targetNames != null && (forceChange || targetController.chooseUse(outcome, "Change this target: " + targetNames + targetAmount + '?', ability, game))) {