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 4708905b36..a71a8432fb 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 @@ -1267,7 +1267,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { //play a land that will allow us to play an unplayable for (Mana mana : unplayable.keySet()) { for (Card card : lands) { - for (ActivatedManaAbilityImpl ability : card.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { + for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.BATTLEFIELD)) { for (Mana netMana : ability.getNetMana(game)) { if (netMana.enough(mana)) { this.playLand(card, game, false); @@ -1281,7 +1281,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { //play a land that will get us closer to playing an unplayable for (Mana mana : unplayable.keySet()) { for (Card card : lands) { - for (ActivatedManaAbilityImpl ability : card.getAbilities().getActivatedManaAbilities(Zone.BATTLEFIELD)) { + for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.BATTLEFIELD)) { for (Mana netMana : ability.getNetMana(game)) { if (mana.contains(netMana)) { this.playLand(card, game, false); @@ -1321,7 +1321,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { if (ability != null && ability.canActivate(playerId, game).canActivate() && !game.getContinuousEffects().preventedByRuleModification(GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getSourceId(), ability.getSourceId(), playerId), ability, game, true)) { if (card.getCardType().contains(CardType.INSTANT) - || card.hasAbility(FlashAbility.getInstance().getId(), game)) { + || card.hasAbility(FlashAbility.getInstance(), game)) { playableInstant.add(card); } else { playableNonInstant.add(card); @@ -1362,7 +1362,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { } } for (Card card : graveyard.getCards(game)) { - for (ActivatedAbility ability : card.getAbilities().getActivatedAbilities(Zone.GRAVEYARD)) { + for (ActivatedAbility ability : card.getAbilities(game).getActivatedAbilities(Zone.GRAVEYARD)) { if (ability.canActivate(playerId, game).canActivate()) { ManaOptions abilityOptions = ability.getManaCosts().getOptions(); if (abilityOptions.isEmpty()) { diff --git a/Mage.Sets/src/mage/cards/a/AnimateDead.java b/Mage.Sets/src/mage/cards/a/AnimateDead.java index 0f833cc16e..4a2f98ff4f 100644 --- a/Mage.Sets/src/mage/cards/a/AnimateDead.java +++ b/Mage.Sets/src/mage/cards/a/AnimateDead.java @@ -225,9 +225,7 @@ class AnimateDeadChangeAbilityEffect extends ContinuousEffectImpl implements Sou abilityToRemove = ability; } } - if (abilityToRemove != null) { - permanent.getAbilities().remove(abilityToRemove); - } + permanent.removeAbility(abilityToRemove, source.getSourceId(), game); permanent.addAbility(newAbility, source.getSourceId(), game); return true; } diff --git a/Mage.Sets/src/mage/cards/a/AnjeFalkenrath.java b/Mage.Sets/src/mage/cards/a/AnjeFalkenrath.java index 9d0e2834dd..70c8b6c5a0 100644 --- a/Mage.Sets/src/mage/cards/a/AnjeFalkenrath.java +++ b/Mage.Sets/src/mage/cards/a/AnjeFalkenrath.java @@ -83,7 +83,7 @@ class AnjeFalkenrathTriggeredAbility extends TriggeredAbilityImpl { if (card == null) { return false; } - return card.getAbilities(game).stream().anyMatch(ability -> ability instanceof MadnessAbility); + return card.getAbilities(game).containsClass(MadnessAbility.class); } @Override diff --git a/Mage.Sets/src/mage/cards/a/ArtisanOfForms.java b/Mage.Sets/src/mage/cards/a/ArtisanOfForms.java index 1d8a35af4a..a6848d62d5 100644 --- a/Mage.Sets/src/mage/cards/a/ArtisanOfForms.java +++ b/Mage.Sets/src/mage/cards/a/ArtisanOfForms.java @@ -63,7 +63,7 @@ class ArtisanOfFormsApplyToPermanent extends ApplyToPermanent { @Override public boolean apply(Game game, Permanent permanent, Ability source, UUID copyToObjectId) { - permanent.addAbility(ArtisanOfForms.createAbility(), game); + permanent.addAbility(ArtisanOfForms.createAbility(), source.getSourceId(), game); return true; } } diff --git a/Mage.Sets/src/mage/cards/b/BloodSun.java b/Mage.Sets/src/mage/cards/b/BloodSun.java index 227b656eaf..0a9e2dd853 100644 --- a/Mage.Sets/src/mage/cards/b/BloodSun.java +++ b/Mage.Sets/src/mage/cards/b/BloodSun.java @@ -1,6 +1,8 @@ package mage.cards.b; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; @@ -70,7 +72,13 @@ class BloodSunEffect extends ContinuousEffectImpl { for (Permanent permanent : game.getState().getBattlefield().getActivePermanents(StaticFilters.FILTER_LANDS, player.getId(), source.getSourceId(), game)) { switch (layer) { case AbilityAddingRemovingEffects_6: - permanent.getAbilities().removeIf(ability -> ability.getAbilityType() != AbilityType.MANA); + List toRemove = new ArrayList<>(); + permanent.getAbilities().forEach(a -> { + if (a.getAbilityType() != AbilityType.MANA) { + toRemove.add(a); + } + }); + permanent.removeAbilities(toRemove, source.getSourceId(), game); break; } } diff --git a/Mage.Sets/src/mage/cards/b/BludgeonBrawl.java b/Mage.Sets/src/mage/cards/b/BludgeonBrawl.java index 5979a3dca0..71166b33c8 100644 --- a/Mage.Sets/src/mage/cards/b/BludgeonBrawl.java +++ b/Mage.Sets/src/mage/cards/b/BludgeonBrawl.java @@ -122,8 +122,8 @@ class BludgeonBrawlGainAbilityEffect extends ContinuousEffectImpl { Permanent permanent = game.getPermanent(permanentId); if (permanent != null) { int convertedManaCost = permanent.getConvertedManaCost(); - permanent.addAbility(new EquipAbility(Outcome.BoostCreature, new GenericManaCost(convertedManaCost)), game); - permanent.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostEquippedEffect(convertedManaCost, 0)), game); + permanent.addAbility(new EquipAbility(Outcome.BoostCreature, new GenericManaCost(convertedManaCost)), source.getSourceId(), game); + permanent.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new BoostEquippedEffect(convertedManaCost, 0)), source.getSourceId(), game); } } return true; diff --git a/Mage.Sets/src/mage/cards/b/BronzehideLion.java b/Mage.Sets/src/mage/cards/b/BronzehideLion.java index a1f7fa245b..5726a08cb0 100644 --- a/Mage.Sets/src/mage/cards/b/BronzehideLion.java +++ b/Mage.Sets/src/mage/cards/b/BronzehideLion.java @@ -133,17 +133,14 @@ class BronzehideLionContinuousEffect extends ContinuousEffectImpl { if (game.getState().getZoneChangeCounter(source.getSourceId()) > zoneChangeCounter) { discard(); } - MageObject sourceObject = game.getPermanent(source.getSourceId()); + Permanent sourceObject = game.getPermanent(source.getSourceId()); if (sourceObject == null) { sourceObject = game.getPermanentEntering(source.getSourceId()); } if (sourceObject == null) { return false; } - if (!(sourceObject instanceof Permanent)) { - return true; - } - Permanent lion = (Permanent) sourceObject; + Permanent lion = sourceObject; switch (layer) { case TypeChangingEffects_4: lion.getCardType().clear(); @@ -158,7 +155,7 @@ class BronzehideLionContinuousEffect extends ContinuousEffectImpl { toRemove.add(ability); } } - lion.getAbilities(game).removeAll(toRemove); + lion.removeAbilities(toRemove, source.getSourceId(), game); lion.getSpellAbility().getTargets().clear(); lion.getSpellAbility().getEffects().clear(); diff --git a/Mage.Sets/src/mage/cards/c/CastThroughTime.java b/Mage.Sets/src/mage/cards/c/CastThroughTime.java index 1693ae89d5..14b0171384 100644 --- a/Mage.Sets/src/mage/cards/c/CastThroughTime.java +++ b/Mage.Sets/src/mage/cards/c/CastThroughTime.java @@ -89,7 +89,7 @@ class GainReboundEffect extends ContinuousEffectImpl { private void addReboundAbility(Card card, Game game) { if (CastThroughTime.filter.match(card, game)) { - boolean found = card.getAbilities(game).stream().anyMatch(ability -> ability instanceof ReboundAbility); + boolean found = card.getAbilities(game).containsClass(ReboundAbility.class); if (!found) { Ability ability = new ReboundAbility(); game.getState().addOtherAbility(card, ability); diff --git a/Mage.Sets/src/mage/cards/c/Chaosphere.java b/Mage.Sets/src/mage/cards/c/Chaosphere.java index 12511380fc..73ed95605d 100644 --- a/Mage.Sets/src/mage/cards/c/Chaosphere.java +++ b/Mage.Sets/src/mage/cards/c/Chaosphere.java @@ -82,7 +82,7 @@ class ChaosphereEffect extends RestrictionEffect { if (attacker == null) { return true; } - return attacker.hasAbility(FlyingAbility.getInstance().getId(), game); + return attacker.hasAbility(FlyingAbility.getInstance(), game); } @Override diff --git a/Mage.Sets/src/mage/cards/c/CrimsonRoc.java b/Mage.Sets/src/mage/cards/c/CrimsonRoc.java index 6b7d68697c..1dab5eb0ff 100644 --- a/Mage.Sets/src/mage/cards/c/CrimsonRoc.java +++ b/Mage.Sets/src/mage/cards/c/CrimsonRoc.java @@ -71,7 +71,7 @@ class CrimsonRocTriggeredAbility extends TriggeredAbilityImpl { Permanent permanent = game.getPermanent(event.getTargetId()); return permanent != null && permanent.isCreature() - && !permanent.getAbilities(game).contains(FlyingAbility.getInstance()); + && !permanent.hasAbility(FlyingAbility.getInstance(), game); } @Override diff --git a/Mage.Sets/src/mage/cards/d/DacksDuplicate.java b/Mage.Sets/src/mage/cards/d/DacksDuplicate.java index 646597f79c..ef7b040837 100644 --- a/Mage.Sets/src/mage/cards/d/DacksDuplicate.java +++ b/Mage.Sets/src/mage/cards/d/DacksDuplicate.java @@ -56,8 +56,8 @@ class DacksDuplicateApplyToPermanent extends ApplyToPermanent { * 29/05/2014 The ability of Dack's Duplicate doesn't target the * creature. */ - permanent.addAbility(new DethroneAbility(), game); - permanent.addAbility(HasteAbility.getInstance(), game); + permanent.addAbility(new DethroneAbility(), source.getSourceId(), game); + permanent.addAbility(HasteAbility.getInstance(), source.getSourceId(), game); return true; } diff --git a/Mage.Sets/src/mage/cards/d/DanceOfTheDead.java b/Mage.Sets/src/mage/cards/d/DanceOfTheDead.java index e5c70512b6..53dd4fb364 100644 --- a/Mage.Sets/src/mage/cards/d/DanceOfTheDead.java +++ b/Mage.Sets/src/mage/cards/d/DanceOfTheDead.java @@ -240,9 +240,7 @@ class DanceOfTheDeadChangeAbilityEffect extends ContinuousEffectImpl implements abilityToRemove = ability; } } - if (abilityToRemove != null) { - permanent.getAbilities().remove(abilityToRemove); - } + permanent.removeAbility(abilityToRemove, source.getSourceId(), game); permanent.addAbility(newAbility, source.getSourceId(), game); return true; } diff --git a/Mage.Sets/src/mage/cards/d/DeadlyRecluse.java b/Mage.Sets/src/mage/cards/d/DeadlyRecluse.java index b39c37d9dc..4c5d0420b4 100644 --- a/Mage.Sets/src/mage/cards/d/DeadlyRecluse.java +++ b/Mage.Sets/src/mage/cards/d/DeadlyRecluse.java @@ -22,8 +22,12 @@ public final class DeadlyRecluse extends CardImpl { this.subtype.add(SubType.SPIDER); this.power = new MageInt(1); - this.toughness = new MageInt(2); + this.toughness = new MageInt(2); + + // Reach (This creature can block creatures with flying.) this.addAbility(ReachAbility.getInstance()); + + // Deathtouch (Any amount of damage this deals to a creature is enough to destroy it.) this.addAbility(DeathtouchAbility.getInstance()); } diff --git a/Mage.Sets/src/mage/cards/d/DenseCanopy.java b/Mage.Sets/src/mage/cards/d/DenseCanopy.java index 9a43da5da0..3226e49efa 100644 --- a/Mage.Sets/src/mage/cards/d/DenseCanopy.java +++ b/Mage.Sets/src/mage/cards/d/DenseCanopy.java @@ -66,7 +66,7 @@ class DenseCanopyCantBlockEffect extends RestrictionEffect { if (attacker == null) { return true; } - return attacker.hasAbility(FlyingAbility.getInstance().getId(), game); + return attacker.hasAbility(FlyingAbility.getInstance(), game); } @Override diff --git a/Mage.Sets/src/mage/cards/e/EvilTwin.java b/Mage.Sets/src/mage/cards/e/EvilTwin.java index cda27f37ff..f08e6c3402 100644 --- a/Mage.Sets/src/mage/cards/e/EvilTwin.java +++ b/Mage.Sets/src/mage/cards/e/EvilTwin.java @@ -69,7 +69,7 @@ class EvilTwinApplyToPermanent extends ApplyToPermanent { Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DestroyTargetEffect(), new ManaCostsImpl("{U}{B}")); ability.addCost(new TapSourceCost()); ability.addTarget(new TargetCreaturePermanent(filter)); - permanent.addAbility(ability, game); + permanent.addAbility(ability, source.getSourceId(), game); return true; } diff --git a/Mage.Sets/src/mage/cards/e/Excavator.java b/Mage.Sets/src/mage/cards/e/Excavator.java index 827f0d2c21..9516f6212c 100644 --- a/Mage.Sets/src/mage/cards/e/Excavator.java +++ b/Mage.Sets/src/mage/cards/e/Excavator.java @@ -112,7 +112,7 @@ class ExcavatorEffect extends ContinuousEffectImpl implements SourceEffect { if (permanent != null) { for(Ability ability : abilities) { - permanent.addAbility(ability, source.getSourceId(), game, false); + permanent.addAbility(ability, source.getSourceId(), game); } } } diff --git a/Mage.Sets/src/mage/cards/f/FlameSweep.java b/Mage.Sets/src/mage/cards/f/FlameSweep.java index 5fb3d7b6c2..6b27948005 100644 --- a/Mage.Sets/src/mage/cards/f/FlameSweep.java +++ b/Mage.Sets/src/mage/cards/f/FlameSweep.java @@ -51,8 +51,6 @@ enum FlameSweepPredicate implements ObjectPlayerPredicate ability.getClass().equals(FlyingAbility.class) - )); + && object.getAbilities(game).containsClass(FlyingAbility.class)); } } \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/g/GargoyleSentinel.java b/Mage.Sets/src/mage/cards/g/GargoyleSentinel.java index a3240598e9..5e7f34fc2c 100644 --- a/Mage.Sets/src/mage/cards/g/GargoyleSentinel.java +++ b/Mage.Sets/src/mage/cards/g/GargoyleSentinel.java @@ -66,7 +66,7 @@ class GargoyleSentinelEffect extends ContinuousEffectImpl { switch (layer) { case AbilityAddingRemovingEffects_6: if (sublayer == SubLayer.NA) { - permanent.getAbilities().removeIf(entry -> entry.getId().equals(DefenderAbility.getInstance().getId())); + permanent.removeAbility(DefenderAbility.getInstance(), source.getSourceId(), game); permanent.getAbilities().add(FlyingAbility.getInstance()); } break; diff --git a/Mage.Sets/src/mage/cards/g/GravityWell.java b/Mage.Sets/src/mage/cards/g/GravityWell.java index 00f083c6d7..0efefec910 100644 --- a/Mage.Sets/src/mage/cards/g/GravityWell.java +++ b/Mage.Sets/src/mage/cards/g/GravityWell.java @@ -100,7 +100,7 @@ class GravityWellEffect extends ContinuousEffectImpl { switch (layer) { case AbilityAddingRemovingEffects_6: if (sublayer == SubLayer.NA) { - permanent.getAbilities().removeIf(entry -> entry.getId().equals(FlyingAbility.getInstance().getId())); + permanent.removeAbility(FlyingAbility.getInstance(), source.getSourceId(), game); } break; } diff --git a/Mage.Sets/src/mage/cards/h/HisokasGuard.java b/Mage.Sets/src/mage/cards/h/HisokasGuard.java index 0d11887e2a..886b4aea6d 100644 --- a/Mage.Sets/src/mage/cards/h/HisokasGuard.java +++ b/Mage.Sets/src/mage/cards/h/HisokasGuard.java @@ -100,7 +100,7 @@ class HisokasGuardGainAbilityTargetEffect extends ContinuousEffectImpl { if (hisokasGuard != null && !hisokasGuard.getConnectedCards("HisokasGuard").isEmpty()) { Permanent guardedCreature = game.getPermanent(hisokasGuard.getConnectedCards("HisokasGuard").get(0)); if (guardedCreature != null && hisokasGuard.isTapped()) { - guardedCreature.addAbility(ability, game); + guardedCreature.addAbility(ability, source.getSourceId(), game); return true; } else { // if guard isn't tapped, the effect is no more valid diff --git a/Mage.Sets/src/mage/cards/m/Martyrdom.java b/Mage.Sets/src/mage/cards/m/Martyrdom.java index 067ad07278..a693c43277 100644 --- a/Mage.Sets/src/mage/cards/m/Martyrdom.java +++ b/Mage.Sets/src/mage/cards/m/Martyrdom.java @@ -66,7 +66,7 @@ class MartyrdomGainAbilityTargetEffect extends ContinuousEffectImpl { if (permanent != null) { ActivatedAbilityImpl ability = new MartyrdomActivatedAbility(source.getControllerId()); ability.setMayActivate(TargetController.ANY); - permanent.addAbility(ability, source.getSourceId(), game, false); + permanent.addAbility(ability, source.getSourceId(), game); return true; } return false; diff --git a/Mage.Sets/src/mage/cards/m/MeliraSylvokOutcast.java b/Mage.Sets/src/mage/cards/m/MeliraSylvokOutcast.java index 3c6124c946..7565cfdca9 100644 --- a/Mage.Sets/src/mage/cards/m/MeliraSylvokOutcast.java +++ b/Mage.Sets/src/mage/cards/m/MeliraSylvokOutcast.java @@ -149,7 +149,7 @@ class MeliraSylvokOutcastEffect3 extends ContinuousEffectImpl { Set opponents = game.getOpponents(source.getControllerId()); for (Permanent perm : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), game)) { if (opponents.contains(perm.getControllerId())) { - perm.getAbilities().remove(InfectAbility.getInstance()); + perm.removeAbility(InfectAbility.getInstance(), source.getSourceId(), game); } } return true; diff --git a/Mage.Sets/src/mage/cards/m/MomentumRumbler.java b/Mage.Sets/src/mage/cards/m/MomentumRumbler.java index e8d8087f9f..b6875666aa 100644 --- a/Mage.Sets/src/mage/cards/m/MomentumRumbler.java +++ b/Mage.Sets/src/mage/cards/m/MomentumRumbler.java @@ -75,6 +75,6 @@ enum MomentumRumblerCondition implements Condition { if (permanent == null) { return false; } - return hasAbility == permanent.getAbilities(game).containsKey(FirstStrikeAbility.getInstance().getId()); + return hasAbility == permanent.hasAbility(FirstStrikeAbility.getInstance(), game); } } \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/MuragandaPetroglyphs.java b/Mage.Sets/src/mage/cards/m/MuragandaPetroglyphs.java index 257e555abf..6271916674 100644 --- a/Mage.Sets/src/mage/cards/m/MuragandaPetroglyphs.java +++ b/Mage.Sets/src/mage/cards/m/MuragandaPetroglyphs.java @@ -1,8 +1,5 @@ - package mage.cards.m; -import java.util.Objects; -import java.util.UUID; import mage.MageObject; import mage.abilities.Abilities; import mage.abilities.Ability; @@ -20,8 +17,10 @@ import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.Predicate; import mage.game.Game; +import java.util.Objects; +import java.util.UUID; + /** - * * @author anonymous */ public final class MuragandaPetroglyphs extends CardImpl { @@ -54,6 +53,12 @@ public final class MuragandaPetroglyphs extends CardImpl { class NoAbilityPredicate implements Predicate { + // Muraganda Petroglyphs gives a bonus only to creatures that have no rules text at all. This includes true vanilla + // creatures (such as Grizzly Bears), face-down creatures, many tokens, and creatures that have lost their abilities + // (due to Ovinize, for example). Any ability of any kind, whether or not the ability functions in the on the + // battlefield zone, including things like “Cycling {2}” means the creature doesn’t get the bonus. + // (2007-05-01) + @Override public boolean apply(MageObject input, Game game) { boolean isFaceDown = false; @@ -65,8 +70,20 @@ class NoAbilityPredicate implements Predicate { abilities = input.getAbilities(); } if (isFaceDown) { + // Some Auras and Equipment grant abilities to creatures, meaning the affected creature would no longer + // get the +2/+2 bonus. For example, Flight grants flying to the enchanted creature. Other Auras and + // Equipment do not, meaning the affected creature would continue to get the +2/+2 bonus. For example, + // Dehydration states something now true about the enchanted creature, but doesn’t give it any abilities. + // Auras and Equipment that grant abilities will use the words “gains” or “has,” and they’ll list a keyword + // ability or an ability in quotation marks. + // (2007-05-01) + for (Ability ability : abilities) { - if (!ability.getSourceId().equals(input.getId()) && !ability.getClass().equals(JohanVigilanceAbility.class)) { + if (ability.getWorksFaceDown()) { + // inner face down abilities like turn up and becomes creature + continue; + } + if (!Objects.equals(ability.getClass(), SpellAbility.class) && !ability.getClass().equals(JohanVigilanceAbility.class)) { return false; } } diff --git a/Mage.Sets/src/mage/cards/p/ProteanThaumaturge.java b/Mage.Sets/src/mage/cards/p/ProteanThaumaturge.java index d8ecd23dac..337bbe5b63 100644 --- a/Mage.Sets/src/mage/cards/p/ProteanThaumaturge.java +++ b/Mage.Sets/src/mage/cards/p/ProteanThaumaturge.java @@ -72,7 +72,7 @@ class ProteanThaumaturgeApplyToPermanent extends ApplyToPermanent { @Override public boolean apply(Game game, Permanent permanent, Ability source, UUID copyToObjectId) { - permanent.addAbility(ProteanThaumaturge.createAbility(), game); + permanent.addAbility(ProteanThaumaturge.createAbility(), source.getSourceId(), game); return true; } } diff --git a/Mage.Sets/src/mage/cards/q/QuartzwoodCrasher.java b/Mage.Sets/src/mage/cards/q/QuartzwoodCrasher.java index 01f05f155e..e83513d223 100644 --- a/Mage.Sets/src/mage/cards/q/QuartzwoodCrasher.java +++ b/Mage.Sets/src/mage/cards/q/QuartzwoodCrasher.java @@ -82,7 +82,7 @@ class QuartzwoodCrasherTriggeredAbility extends TriggeredAbilityImpl { && ((DamagedPlayerEvent) event).isCombatDamage()) { Permanent creature = game.getPermanent(event.getSourceId()); if (creature != null && creature.isControlledBy(controllerId) - && creature.hasAbility(TrampleAbility.getInstance().getId(), game) + && creature.hasAbility(TrampleAbility.getInstance(), game) && !damagedPlayerIds.contains(event.getTargetId())) { damagedPlayerIds.add(event.getTargetId()); this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game)); @@ -143,7 +143,7 @@ class QuartzwoodCrasherWatcher extends Watcher { return; } Permanent creature = game.getPermanent(event.getSourceId()); - if (creature == null || !creature.getAbilities(game).containsKey(TrampleAbility.getInstance().getId())) { + if (creature == null || !creature.hasAbility(TrampleAbility.getInstance(), game)) { return; } damageMap diff --git a/Mage.Sets/src/mage/cards/r/ReturnFromExtinction.java b/Mage.Sets/src/mage/cards/r/ReturnFromExtinction.java index f6decd52d6..6a3f7ce162 100644 --- a/Mage.Sets/src/mage/cards/r/ReturnFromExtinction.java +++ b/Mage.Sets/src/mage/cards/r/ReturnFromExtinction.java @@ -88,7 +88,7 @@ class ReturnFromExtinctionTarget extends TargetCardInYourGraveyard { return false; } for (Card card : player.getGraveyard().getCards(filter, sourceId, sourceControllerId, game)) { - if (card.isAllCreatureTypes() || card.getAbilities(game).contains(ChangelingAbility.getInstance())) { + if (card.isAllCreatureTypes() || card.hasAbility(ChangelingAbility.getInstance(), game)) { if (!subTypes.isEmpty()) { return true; } else { diff --git a/Mage.Sets/src/mage/cards/s/SakashimaTheImpostor.java b/Mage.Sets/src/mage/cards/s/SakashimaTheImpostor.java index 8fed936baa..a0e23f4e3d 100644 --- a/Mage.Sets/src/mage/cards/s/SakashimaTheImpostor.java +++ b/Mage.Sets/src/mage/cards/s/SakashimaTheImpostor.java @@ -64,7 +64,7 @@ class SakashimaTheImpostorApplier extends ApplyToPermanent { permanent.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new CreateDelayedTriggeredAbilityEffect(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(new ReturnToHandSourceEffect(true)), false), new ManaCostsImpl("{2}{U}{U}") - ), game); + ), source.getSourceId(), game); return true; } diff --git a/Mage.Sets/src/mage/cards/s/ShieldsOfVelisVel.java b/Mage.Sets/src/mage/cards/s/ShieldsOfVelisVel.java index 5afe8b026c..edc7d925e2 100644 --- a/Mage.Sets/src/mage/cards/s/ShieldsOfVelisVel.java +++ b/Mage.Sets/src/mage/cards/s/ShieldsOfVelisVel.java @@ -78,7 +78,7 @@ class ShieldsOfVelisVelGainEffect extends ContinuousEffectImpl { for (Iterator it = affectedObjectList.iterator(); it.hasNext();) { Permanent permanent = it.next().getPermanent(game); if (permanent != null) { - permanent.addAbility(ChangelingAbility.getInstance(), source.getSourceId(), game, false); + permanent.addAbility(ChangelingAbility.getInstance(), source.getSourceId(), game); } else { it.remove(); } diff --git a/Mage.Sets/src/mage/cards/s/ShoalSerpent.java b/Mage.Sets/src/mage/cards/s/ShoalSerpent.java index 8769d56081..73a060cff2 100644 --- a/Mage.Sets/src/mage/cards/s/ShoalSerpent.java +++ b/Mage.Sets/src/mage/cards/s/ShoalSerpent.java @@ -67,7 +67,7 @@ class ShoalSerpentEffect extends ContinuousEffectImpl { switch (layer) { case AbilityAddingRemovingEffects_6: if (sublayer == SubLayer.NA) { - permanent.getAbilities().removeIf(entry -> entry.getId().equals(DefenderAbility.getInstance().getId())); + permanent.removeAbility(DefenderAbility.getInstance(), source.getSourceId(), game); } break; } diff --git a/Mage.Sets/src/mage/cards/s/Showstopper.java b/Mage.Sets/src/mage/cards/s/Showstopper.java index b4c5fc75e8..c8b70747a2 100644 --- a/Mage.Sets/src/mage/cards/s/Showstopper.java +++ b/Mage.Sets/src/mage/cards/s/Showstopper.java @@ -1,4 +1,3 @@ - package mage.cards.s; import java.util.UUID; @@ -34,8 +33,6 @@ public final class Showstopper extends CardImpl { public Showstopper (UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{1}{B}{R}"); - - // Until end of turn, creatures you control gain "When this creature dies, it deals 2 damage to target creature an opponent controls." TriggeredAbility ability = new DiesTriggeredAbility(new DamageTargetEffect(2, "it"), false); Target target = new TargetCreaturePermanent(filter2); diff --git a/Mage.Sets/src/mage/cards/s/SidarKondoOfJamuraa.java b/Mage.Sets/src/mage/cards/s/SidarKondoOfJamuraa.java index f3db71d472..6bbd1ab410 100644 --- a/Mage.Sets/src/mage/cards/s/SidarKondoOfJamuraa.java +++ b/Mage.Sets/src/mage/cards/s/SidarKondoOfJamuraa.java @@ -80,8 +80,8 @@ class SidarKondoOfJamuraaCantBlockCreaturesSourceEffect extends RestrictionEffec @Override public boolean applies(Permanent permanent, Ability source, Game game) { - if (permanent.hasAbility(FlyingAbility.getInstance().getId(), game) - || permanent.hasAbility(ReachAbility.getInstance().getId(), game)) { + if (permanent.hasAbility(FlyingAbility.getInstance(), game) + || permanent.hasAbility(ReachAbility.getInstance(), game)) { return false; } return game.getOpponents(source.getControllerId()).contains(permanent.getControllerId()); diff --git a/Mage.Sets/src/mage/cards/s/SlingbowTrap.java b/Mage.Sets/src/mage/cards/s/SlingbowTrap.java index 87f3856419..8fcc5cd8a9 100644 --- a/Mage.Sets/src/mage/cards/s/SlingbowTrap.java +++ b/Mage.Sets/src/mage/cards/s/SlingbowTrap.java @@ -61,7 +61,7 @@ enum SlingbowTrapCondition implements Condition { for (UUID attackingCreatureId : game.getCombat().getAttackers()) { Permanent attackingCreature = game.getPermanent(attackingCreatureId); if (attackingCreature != null) { - if (attackingCreature.getColor(game).isBlack() && attackingCreature.hasAbility(FlyingAbility.getInstance().getId(), game)) { + if (attackingCreature.getColor(game).isBlack() && attackingCreature.hasAbility(FlyingAbility.getInstance(), game)) { return true; } } diff --git a/Mage.Sets/src/mage/cards/s/StormtideLeviathan.java b/Mage.Sets/src/mage/cards/s/StormtideLeviathan.java index 40e76151ee..0c46713eaf 100644 --- a/Mage.Sets/src/mage/cards/s/StormtideLeviathan.java +++ b/Mage.Sets/src/mage/cards/s/StormtideLeviathan.java @@ -91,7 +91,7 @@ public final class StormtideLeviathan extends CardImpl { // land abilities are intrinsic, so add them here, not in layer 6 if (!land.hasSubtype(SubType.ISLAND, game)) { land.getSubtype(game).add(SubType.ISLAND); - if (!land.getAbilities(game).contains(new BlueManaAbility())) { + if (!land.getAbilities(game).containsClass(BlueManaAbility.class)) { land.addAbility(new BlueManaAbility(), source.getSourceId(), game); } } diff --git a/Mage.Sets/src/mage/cards/t/TaigamOjutaiMaster.java b/Mage.Sets/src/mage/cards/t/TaigamOjutaiMaster.java index 4aeaa23049..236ce4ff01 100644 --- a/Mage.Sets/src/mage/cards/t/TaigamOjutaiMaster.java +++ b/Mage.Sets/src/mage/cards/t/TaigamOjutaiMaster.java @@ -150,7 +150,7 @@ class TaigamOjutaiMasterGainReboundEffect extends ContinuousEffectImpl { } private void addReboundAbility(Card card, Ability source, Game game) { - boolean found = card.getAbilities(game).stream().anyMatch(ability -> ability instanceof ReboundAbility); + boolean found = card.getAbilities(game).containsClass(ReboundAbility.class); if (!found) { Ability ability = new ReboundAbility(); game.getState().addOtherAbility(card, ability); diff --git a/Mage.Sets/src/mage/cards/t/TestamentOfFaith.java b/Mage.Sets/src/mage/cards/t/TestamentOfFaith.java index 0e99542953..a0980c9bec 100644 --- a/Mage.Sets/src/mage/cards/t/TestamentOfFaith.java +++ b/Mage.Sets/src/mage/cards/t/TestamentOfFaith.java @@ -111,7 +111,7 @@ class TestamentOfFaithBecomesCreatureSourceEffect extends ContinuousEffectImpl i if (sublayer == SubLayer.NA) { if (!token.getAbilities().isEmpty()) { for (Ability ability: token.getAbilities()) { - permanent.addAbility(ability, source.getSourceId(), game, false); + permanent.addAbility(ability, source.getSourceId(), game); } } } diff --git a/Mage.Sets/src/mage/cards/u/UncheckedGrowth.java b/Mage.Sets/src/mage/cards/u/UncheckedGrowth.java index bcac28c4e4..6ca3ce9e35 100644 --- a/Mage.Sets/src/mage/cards/u/UncheckedGrowth.java +++ b/Mage.Sets/src/mage/cards/u/UncheckedGrowth.java @@ -63,7 +63,7 @@ public final class UncheckedGrowth extends CardImpl { for (UUID permanentId : targetPointer.getTargets(game, source)) { Permanent permanent = game.getPermanent(permanentId); if (permanent != null && permanent.hasSubtype(SubType.SPIRIT, game)) { - permanent.addAbility(TrampleAbility.getInstance(), game); + permanent.addAbility(TrampleAbility.getInstance(), source.getSourceId(), game); affectedTargets++; } } diff --git a/Mage.Sets/src/mage/cards/u/UnstableShapeshifter.java b/Mage.Sets/src/mage/cards/u/UnstableShapeshifter.java index d91b45043f..fff1570c3f 100644 --- a/Mage.Sets/src/mage/cards/u/UnstableShapeshifter.java +++ b/Mage.Sets/src/mage/cards/u/UnstableShapeshifter.java @@ -77,7 +77,7 @@ class UnstableShapeshifterEffect extends OneShotEffect { if (targetCreature != null && permanent != null) { Permanent blueprintPermanent = game.copyPermanent(Duration.Custom, targetCreature, permanent.getId(), source, new EmptyApplyToPermanent()); blueprintPermanent.addAbility(new EntersBattlefieldAllTriggeredAbility(Zone.BATTLEFIELD, - new UnstableShapeshifterEffect(), filterAnotherCreature, false, SetTargetPointer.PERMANENT, ""), game); + new UnstableShapeshifterEffect(), filterAnotherCreature, false, SetTargetPointer.PERMANENT, ""), source.getSourceId(), game); return true; } return false; diff --git a/Mage.Sets/src/mage/cards/v/VraskaTheUnseen.java b/Mage.Sets/src/mage/cards/v/VraskaTheUnseen.java index 172a230eaa..3cc609ca21 100644 --- a/Mage.Sets/src/mage/cards/v/VraskaTheUnseen.java +++ b/Mage.Sets/src/mage/cards/v/VraskaTheUnseen.java @@ -86,7 +86,7 @@ class VraskaTheUnseenGainAbilityEffect extends ContinuousEffectImpl { public boolean apply(Game game, Ability source) { Permanent permanent = game.getPermanent(source.getSourceId()); if (permanent != null) { - permanent.addAbility(ability, game); + permanent.addAbility(ability, source.getSourceId(), game); return true; } return false; diff --git a/Mage.Sets/src/mage/cards/w/WishfulMerfolk.java b/Mage.Sets/src/mage/cards/w/WishfulMerfolk.java index 09ce7ad911..0badf53c39 100644 --- a/Mage.Sets/src/mage/cards/w/WishfulMerfolk.java +++ b/Mage.Sets/src/mage/cards/w/WishfulMerfolk.java @@ -68,7 +68,7 @@ class WishfulMerfolkEffect extends ContinuousEffectImpl { switch (layer) { case AbilityAddingRemovingEffects_6: if (sublayer == SubLayer.NA) { - permanent.getAbilities().removeIf(entry -> entry.getId().equals(DefenderAbility.getInstance().getId())); + permanent.removeAbility(DefenderAbility.getInstance(), source.getSourceId(), game); } break; case TypeChangingEffects_4: diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java index 850580b7b4..82a81887b7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java @@ -274,9 +274,12 @@ public class ManifestTest extends CardTestPlayerBase { // Check if a Megamorph card is manifested and turned face up by their megamorph ability // it gets the +1/+1 counter. + // 701.33c + // If a card with morph is manifested, its controller may turn that card face up using + // either the procedure described in rule 702.36e to turn a face-down permanent with morph face up + // or the procedure described above to turn a manifested permanent face up. @Test - public void testManifestMegamorph() { - + public void testManifestMegamorph_TurnUpByMegamorphCost() { addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); addCard(Zone.BATTLEFIELD, playerB, "Forest", 6); // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. @@ -295,6 +298,7 @@ public class ManifestTest extends CardTestPlayerBase { activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{5}{G}: Turn"); + setStrictChooseMode(true); setStopAt(2, PhaseStep.END_TURN); execute(); assertAllCommandsUsed(); @@ -310,7 +314,45 @@ public class ManifestTest extends CardTestPlayerBase { assertPowerToughness(playerB, "Aerie Bowmasters", 4, 5); // 3/4 and the +1/+1 counter from Megamorph Permanent aerie = getPermanent("Aerie Bowmasters", playerB); Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); + } + @Test + public void testManifestMegamorph_TurnUpBySimpleCost() { + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerB, "Forest", 4); + // {1}{B}, {T}, Sacrifice another creature: Manifest the top card of your library. + addCard(Zone.BATTLEFIELD, playerB, "Qarsi High Priest", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + // {2}{G}{G} + // Reach (This creature can block creatures with flying.) + // Megamorph {5}{G} + addCard(Zone.LIBRARY, playerB, "Aerie Bowmasters", 1); + addCard(Zone.LIBRARY, playerB, "Mountain", 1); + + skipInitShuffling(); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{B}, {T}, Sacrifice another creature"); + setChoice(playerB, "Silvercoat Lion"); + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "{2}{G}{G}: Turn"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // no life gain + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerB, "Silvercoat Lion", 1); + + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + assertPermanentCount(playerB, "Aerie Bowmasters", 1); + assertPowerToughness(playerB, "Aerie Bowmasters", 3, 4); // 3/4 without counter (megamorph not used) + Permanent aerie = getPermanent("Aerie Bowmasters", playerB); + Assert.assertTrue("Aerie Bowmasters has to be green", aerie != null && aerie.getColor(currentGame).isGreen()); } /** diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/GainAbilitiesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/GainAbilitiesTest.java new file mode 100644 index 0000000000..03a690f696 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/other/GainAbilitiesTest.java @@ -0,0 +1,70 @@ +package org.mage.test.cards.abilities.other; + +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.VigilanceAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class GainAbilitiesTest extends CardTestPlayerBase { + + @Test + public void test_AttachmentSingleton() { + // {2}{W} + // Enchanted creature gets +2/+2. + // Enchanted creature has vigilance as long as you control a black or green permanent. + addCard(Zone.HAND, playerA, "Abzan Runemark@attach", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears@bear", 1); // 2/2 + + // attach all + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "@attach.1", "@bear"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "@attach.2", "@bear"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "@bear", VigilanceAbility.class, true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + Permanent permanent = getPermanent("Balduvian Bears"); + Assert.assertEquals("must have only 1 singleton ability instance from two attachments", + 1, permanent.getAbilities(currentGame).stream().filter(a -> a instanceof VigilanceAbility).count()); + } + + @Test + public void test_AttachmentUnique() { + // {R} + // Enchanted creature has "{R}, {T}, Discard a card: Draw a card." + addCard(Zone.HAND, playerA, "Epiphany Storm@attach", 2); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears@bear", 1); // 2/2 + + // attach all + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "@attach.1", "@bear"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "@attach.2", "@bear"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + //checkAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "@bear", VigilanceAbility.class, true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + Permanent permanent = getPermanent("Balduvian Bears"); + Assert.assertEquals("must have 2 dynamic ability instances from two attachments", + 2, permanent.getAbilities(currentGame).stream().filter( + a -> a.getEffects().stream().anyMatch(e -> e instanceof DrawCardSourceControllerEffect) + ).count()); + } + +} 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 4caf5347b1..b217f12dda 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 @@ -113,7 +113,6 @@ public class CommandersCastTest extends CardTestCommander4Players { 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(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LandTypeChangingEffectsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LandTypeChangingEffectsTest.java index 224c316f9a..d011d952b3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LandTypeChangingEffectsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/LandTypeChangingEffectsTest.java @@ -14,20 +14,17 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class LandTypeChangingEffectsTest extends CardTestPlayerBase { /** - * * Playing a commander game. Opponent had a Magus of the Moon, and I later * dropped a Chromatic Lantern. - * + *

* I was not allowed to use the Chromatic Lantern's ability. Since layers * are tricky I asked on the Judge's chat to confirm and the user "luma" * said it should work on this scenario. - * */ @Test public void testMagusOfTheMoonAndChromaticLantern() { @@ -42,8 +39,10 @@ public class LandTypeChangingEffectsTest extends CardTestPlayerBase { castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Chromatic Lantern"); + setStrictChooseMode(true); setStopAt(2, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerB, "Chromatic Lantern", 1); @@ -66,8 +65,11 @@ public class LandTypeChangingEffectsTest extends CardTestPlayerBase { castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Chromatic Lantern"); castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Magus of the Moon"); + + setStrictChooseMode(true); setStopAt(3, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerB, "Chromatic Lantern", 1); assertPermanentCount(playerA, "Magus of the Moon", 1); @@ -97,8 +99,11 @@ public class LandTypeChangingEffectsTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aquitect's Will", "Forbidding Watchtower"); activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{W}:"); + + setStrictChooseMode(true); setStopAt(2, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Aquitect's Will", 1); @@ -128,8 +133,10 @@ public class LandTypeChangingEffectsTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bloodmoon); playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, urborgtoy); + setStrictChooseMode(true); setStopAt(2, PhaseStep.PRECOMBAT_MAIN); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, bloodmoon, 1); assertPermanentCount(playerA, urborgtoy, 1); @@ -157,8 +164,10 @@ public class LandTypeChangingEffectsTest extends CardTestPlayerBase { playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, urborgtoy); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, bloodmoon); + setStrictChooseMode(true); setStopAt(2, PhaseStep.PRECOMBAT_MAIN); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, bloodmoon, 1); assertPermanentCount(playerA, urborgtoy, 1); @@ -176,7 +185,7 @@ public class LandTypeChangingEffectsTest extends CardTestPlayerBase { In terms of time-stamp order, Urborg was down first, then Kormus Bell, then Quicksilver. When I put a flood counter on a basic swamp, it would become a 0/0 instead of a 1/1 and die. */ - + @Test public void testCormusBellAfterUrborg() { // Land - Legendary @@ -198,14 +207,14 @@ public class LandTypeChangingEffectsTest extends CardTestPlayerBase { castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Kormus Bell"); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Quicksilver Fountain"); - + addTarget(playerA, "Mountain"); - + setStrictChooseMode(true); setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); execute(); assertAllCommandsUsed(); - + assertPermanentCount(playerA, urborgtoy, 1); assertPermanentCount(playerA, "Kormus Bell", 1); assertPermanentCount(playerB, "Quicksilver Fountain", 1); @@ -245,8 +254,10 @@ public class LandTypeChangingEffectsTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Stormtide Leviathan"); // all lands are islands in addition to their other types addCard(Zone.BATTLEFIELD, playerA, "Darksteel Citadel"); // land has indestructible ability + setStrictChooseMode(true); setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); execute(); + assertAllCommandsUsed(); Permanent darksteel = getPermanent("Darksteel Citadel", playerA.getId()); Assert.assertNotNull(darksteel); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/WonderTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/WonderTest.java index 440074c3e2..d4c13331a8 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/WonderTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/WonderTest.java @@ -40,7 +40,7 @@ public class WonderTest extends CardTestPlayerBase { // check no flying in graveyard for (Card card : playerA.getGraveyard().getCards(currentGame)) { if (card.getName().equals("Runeclaw Bear")) { - Assert.assertFalse(card.getAbilities().contains(FlyingAbility.getInstance())); + Assert.assertFalse(card.hasAbility(FlyingAbility.getInstance(), currentGame)); } } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/GideonTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/GideonTest.java index 0363ca6c1e..a8302a41b6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/GideonTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/planeswalker/GideonTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.planeswalker; import mage.abilities.keyword.IndestructibleAbility; @@ -11,7 +10,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class GideonTest extends CardTestPlayerBase { @@ -116,16 +114,23 @@ public class GideonTest extends CardTestPlayerBase { // Equip {2} addCard(Zone.BATTLEFIELD, playerB, "Stitcher's Graft", 1); + // transform attack(2, playerB, "Kytheon, Hero of Akros"); attack(2, playerB, "Silvercoat Lion"); attack(2, playerB, "Pillarfield Ox"); + checkPermanentCount("after transform", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Gideon, Battle-Forged", 1); + // become creature and equip activateAbility(4, PhaseStep.PRECOMBAT_MAIN, playerB, "0: Until "); + waitStackResolved(4, PhaseStep.PRECOMBAT_MAIN); activateAbility(4, PhaseStep.PRECOMBAT_MAIN, playerB, "Equip {2}", "Gideon, Battle-Forged"); + attack(4, playerB, "Gideon, Battle-Forged"); // 7 damage + setStrictChooseMode(true); setStopAt(5, PhaseStep.PRECOMBAT_MAIN); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerB, "Silvercoat Lion", 1); assertLife(playerA, 7); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/fut/MuragandaPetroglyphsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/fut/MuragandaPetroglyphsTest.java index a7856156d9..4345d94086 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/fut/MuragandaPetroglyphsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/fut/MuragandaPetroglyphsTest.java @@ -1,5 +1,6 @@ package org.mage.test.cards.single.fut; +import mage.abilities.keyword.HasteAbility; import mage.constants.EmptyNames; import mage.constants.PhaseStep; import mage.constants.Zone; @@ -31,8 +32,10 @@ public class MuragandaPetroglyphsTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Muraganda Petroglyphs", 1); + setStrictChooseMode(true); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); + assertAllCommandsUsed(); assertPowerToughness(playerA, "Grizzly Bears", 4, 4, Filter.ComparisonScope.Any); assertPowerToughness(playerB, "Grizzly Bears", 4, 4, Filter.ComparisonScope.Any); @@ -41,15 +44,20 @@ public class MuragandaPetroglyphsTest extends CardTestPlayerBase { @Test public void faceDownCreaturesTest() { + // Morph {4}{G} addCard(Zone.HAND, playerA, "Pine Walker"); addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + // + // Creatures with no abilities get +2/+2. addCard(Zone.BATTLEFIELD, playerA, "Muraganda Petroglyphs", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pine Walker"); setChoice(playerA, "Yes"); // cast it face down as 2/2 creature + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 4, 4); @@ -57,21 +65,26 @@ public class MuragandaPetroglyphsTest extends CardTestPlayerBase { @Test public void faceDownGainedAbilityTest() { + // Morph {4}{G} addCard(Zone.HAND, playerA, "Pine Walker"); addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); addCard(Zone.BATTLEFIELD, playerA, "Mass Hysteria"); // All creatures have haste. + // Creatures with no abilities get +2/+2. addCard(Zone.BATTLEFIELD, playerA, "Muraganda Petroglyphs", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pine Walker"); setChoice(playerA, "Yes"); // cast it face down as 2/2 creature + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); - assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); + //assertPowerToughness(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2, 2); // no boost (permanent have haste) + assertAbility(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), HasteAbility.getInstance(), true); } @Test @@ -83,8 +96,10 @@ public class MuragandaPetroglyphsTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Raise the Alarm"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPowerToughness(playerA, "Soldier", 3, 3); } @@ -99,8 +114,10 @@ public class MuragandaPetroglyphsTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ovinize", "Goblin Guide"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPowerToughness(playerB, "Goblin Guide", 2, 3); } @@ -110,8 +127,10 @@ public class MuragandaPetroglyphsTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Hundroog", 1); // Cycling {3}, 4/7 addCard(Zone.BATTLEFIELD, playerA, "Muraganda Petroglyphs", 1); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPowerToughness(playerA, "Hundroog", 4, 7); } @@ -126,11 +145,12 @@ public class MuragandaPetroglyphsTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Muraganda Petroglyphs", 1); addCard(Zone.HAND, playerA, "Vastwood Zendikon"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Vastwood Zendikon", "Forest"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPowerToughness(playerA, "Forest", 6, 4); @@ -160,8 +180,10 @@ public class MuragandaPetroglyphsTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dehydration", "Runeclaw Bear"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPowerToughness(playerA, "Grizzly Bears", 4, 2); @@ -179,11 +201,14 @@ public class MuragandaPetroglyphsTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); addCard(Zone.HAND, playerA, "Shadow Slice"); // {4}{B} - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shadow Slice"); - setChoice(playerA, "Grizzly Bears"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shadow Slice", playerB); + setChoice(playerA, "Yes"); // do cipher + addTarget(playerA, "Grizzly Bears"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPowerToughness(playerA, "Grizzly Bears", 2, 2); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/ShowstopperTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/ShowstopperTest.java index fbbbcf4275..23e9e34c1e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/ShowstopperTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/dies/ShowstopperTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.triggers.dies; import mage.constants.PhaseStep; @@ -7,7 +6,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ @@ -16,10 +14,9 @@ public class ShowstopperTest extends CardTestPlayerBase { /** * Tests that the dies triggered ability of silvercoat lion (gained by Showstopper) * triggers as he dies from Lightning Bolt - * */ @Test - public void testDiesTriggeredAbility() { + public void test_OneTrigger() { // Showstopper Instant {1}{B}{R} // Until end of turn, creatures you control gain "When this creature dies, it deals 2 damage to target creature an opponent controls." addCard(Zone.HAND, playerA, "Showstopper"); @@ -30,10 +27,14 @@ public class ShowstopperTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Ornithopter", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Showstopper"); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", "Silvercoat Lion"); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", "Silvercoat Lion"); + addTarget(playerA, "Ornithopter"); + + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); + assertAllCommandsUsed(); assertLife(playerA, 20); assertLife(playerB, 20); @@ -43,12 +44,13 @@ public class ShowstopperTest extends CardTestPlayerBase { assertGraveyardCount(playerB, "Ornithopter", 1); } + /** * Test if Showstopper is called twice */ @Test - public void testTwoDiesTriggeredAbilities() { + public void test_TwoTriggers() { // Showstopper Instant {1}{B}{R} // Until end of turn, creatures you control gain "When this creature dies, it deals 2 damage to target creature an opponent controls." addCard(Zone.HAND, playerA, "Showstopper", 2); @@ -62,12 +64,16 @@ public class ShowstopperTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Showstopper"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Showstopper"); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); + setChoice(playerA, "When {this} dies"); // choose from two triggers addTarget(playerA, "Ornithopter"); addTarget(playerA, "Grizzly Bears"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); + assertAllCommandsUsed(); assertLife(playerA, 20); assertLife(playerB, 20); @@ -79,4 +85,65 @@ public class ShowstopperTest extends CardTestPlayerBase { assertGraveyardCount(playerB, "Ornithopter", 1); } + @Test + public void test_TwoTriggersAndCopies() { + // Showstopper Instant {1}{B}{R} + // Until end of turn, creatures you control gain "When this creature dies, it deals 2 damage to target creature an opponent controls." + addCard(Zone.HAND, playerA, "Showstopper", 2); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); + addCard(Zone.HAND, playerB, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerB, "Ornithopter", 1); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1); + addCard(Zone.BATTLEFIELD, playerB, "Alchemist's Apprentice", 1); + addCard(Zone.BATTLEFIELD, playerB, "Augmenting Automaton", 1); + // + // When you next cast an instant spell, cast a sorcery spell, or activate a loyalty ability this turn, copy that spell or ability twice. + // You may choose new targets for the copies. + addCard(Zone.HAND, playerA, "Repeated Reverberation", 1); // {2}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + // first spell + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 2); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Showstopper"); + + // prepare copy + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 4); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Repeated Reverberation"); + + // second spell with 2x copy + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Showstopper"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Silvercoat Lion"); + setChoice(playerA, "When {this} dies"); // choose from 4 triggers + setChoice(playerA, "When {this} dies"); // choose from 4 triggers + setChoice(playerA, "When {this} dies"); // choose from 4 triggers + addTarget(playerA, "Ornithopter"); + addTarget(playerA, "Grizzly Bears"); + addTarget(playerA, "Alchemist's Apprentice"); + addTarget(playerA, "Augmenting Automaton"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertGraveyardCount(playerA, "Showstopper", 2); + assertGraveyardCount(playerA, "Silvercoat Lion", 1); + + assertGraveyardCount(playerB, "Grizzly Bears", 1); + assertGraveyardCount(playerB, "Ornithopter", 1); + assertGraveyardCount(playerB, "Alchemist's Apprentice", 1); + assertGraveyardCount(playerB, "Augmenting Automaton", 1); + } + } \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/performance/SerializationTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/performance/SerializationTest.java index 1720bdcf10..ecb308c9a8 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/performance/SerializationTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/performance/SerializationTest.java @@ -40,7 +40,7 @@ public class SerializationTest extends CardTestPlayerBase { currentGame.addPermanent(permanent); // mark damage from infected ability - permanent.addAbility(InfectAbility.getInstance(), currentGame); + permanent.addAbility(InfectAbility.getInstance(), null, currentGame); permanent.markDamage(1, permanent.getId(), currentGame, false, false); // test compress (it uses default java serialization) diff --git a/Mage/src/main/java/mage/MageObject.java b/Mage/src/main/java/mage/MageObject.java index 744ec43f07..b2c21ab27d 100644 --- a/Mage/src/main/java/mage/MageObject.java +++ b/Mage/src/main/java/mage/MageObject.java @@ -41,9 +41,13 @@ public interface MageObject extends MageItem, Serializable { Set getSuperType(); + /** + * For cards: return basic abilities (without dynamic added) + * For permanents: return all abilities (dynamic ability inserts into permanent) + */ Abilities getAbilities(); - boolean hasAbility(UUID abilityId, Game game); + boolean hasAbility(Ability ability, Game game); ObjectColor getColor(Game game); @@ -180,9 +184,9 @@ public interface MageObject extends MageItem, Serializable { } if (this.isCreature() && otherCard.isCreature()) { - if (this.getAbilities().contains(ChangelingAbility.getInstance()) + if (this.hasAbility(ChangelingAbility.getInstance(), game) || this.isAllCreatureTypes() - || otherCard.getAbilities().contains(ChangelingAbility.getInstance()) + || otherCard.hasAbility(ChangelingAbility.getInstance(), game) || otherCard.isAllCreatureTypes()) { return true; } diff --git a/Mage/src/main/java/mage/MageObjectImpl.java b/Mage/src/main/java/mage/MageObjectImpl.java index b68592bb6b..f25762695f 100644 --- a/Mage/src/main/java/mage/MageObjectImpl.java +++ b/Mage/src/main/java/mage/MageObjectImpl.java @@ -132,12 +132,12 @@ public abstract class MageObjectImpl implements MageObject { } @Override - public boolean hasAbility(UUID abilityId, Game game) { - if (this.getAbilities().containsKey(abilityId)) { + public boolean hasAbility(Ability ability, Game game) { + if (this.getAbilities().contains(ability)) { return true; } Abilities otherAbilities = game.getState().getAllOtherAbilities(getId()); - return otherAbilities != null && otherAbilities.containsKey(abilityId); + return otherAbilities != null && otherAbilities.contains(ability); } @Override diff --git a/Mage/src/main/java/mage/abilities/Abilities.java b/Mage/src/main/java/mage/abilities/Abilities.java index 0d1000d172..cbceb72bc3 100644 --- a/Mage/src/main/java/mage/abilities/Abilities.java +++ b/Mage/src/main/java/mage/abilities/Abilities.java @@ -2,9 +2,12 @@ package mage.abilities; import java.io.Serializable; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.function.Predicate; + import mage.abilities.keyword.ProtectionAbility; import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.constants.Zone; @@ -255,7 +258,8 @@ public interface Abilities extends List, Serializable { boolean containsAll(Abilities abilities); /** - * Searches this set of abilities for the existence of the give class + * Searches this set of abilities for the existence of the given class + * Warning, it doesn't work with inherited classes (e.g. it's not equal to instanceOf command) * * @param classObject * @return True if the passed in class is also in this set of abilities. @@ -271,4 +275,13 @@ public interface Abilities extends List, Serializable { Abilities copy(); String getValue(); + + @Deprecated // use permanent.removeAbility instead + boolean remove(Object o); + + @Deprecated // use permanent.removeAbility instead + boolean removeAll(Collection c); + + @Deprecated // use permanent.removeAbility instead + boolean removeIf(Predicate filter); } diff --git a/Mage/src/main/java/mage/abilities/AbilitiesImpl.java b/Mage/src/main/java/mage/abilities/AbilitiesImpl.java index 15c1da8270..d46348ac00 100644 --- a/Mage/src/main/java/mage/abilities/AbilitiesImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilitiesImpl.java @@ -232,7 +232,8 @@ public class AbilitiesImpl extends ArrayList implements Ab if (ability.getId().equals(test.getId())) { return true; } - if (ability.getOriginalId().equals(test.getId())) { + if (ability.getOriginalId().equals(test.getOriginalId())) { + // on ability resolve: engine creates ability's copy and generates newId(), so you must use originalId to find that ability in card later return true; } if (ability instanceof MageSingleton && test instanceof MageSingleton && ability.getRule().equals(test.getRule())) { @@ -243,7 +244,7 @@ public class AbilitiesImpl extends ArrayList implements Ab } @Override - public boolean containsRule(T ability) { + public boolean containsRule(T ability) { // TODO: remove return stream().anyMatch(rule -> rule.getRule().equals(ability.getRule())); } @@ -262,7 +263,7 @@ public class AbilitiesImpl extends ArrayList implements Ab } @Override - public boolean containsKey(UUID abilityId) { + public boolean containsKey(UUID abilityId) { // TODO: remove return stream().anyMatch(ability -> abilityId.equals(ability.getId())); } diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index dd5b811f84..a1ebe909d0 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -522,4 +522,11 @@ public interface Ability extends Controllable, Serializable { Ability addCustomOutcome(Outcome customOutcome); Outcome getCustomOutcome(); + + /** + * For mtg's instances search, see rules example in 112.10b + * @param ability + * @return + */ + boolean isSameInstance(Ability ability); } diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 6edcbfc98a..f09b94f616 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -938,6 +938,10 @@ public abstract class AbilityImpl implements Ability { @Override public boolean hasSourceObjectAbility(Game game, MageObject source, GameEvent event) { + // if source object have this ability + // uses for ability.isInUseableZone + // replacement and other continues effects can be without source, but active (must return true) + MageObject object = source; // for singleton abilities like Flying we can't rely on abilities' source because it's only once in continuous effects // so will use the sourceId of the object itself that came as a parameter if it is not null @@ -949,16 +953,10 @@ public abstract class AbilityImpl implements Ability { } if (object != null) { if (object instanceof Permanent) { - if (!((Permanent) object).getAbilities(game).contains(this)) { - return false; - } - return ((Permanent) object).isPhasedIn(); - } else if (object instanceof Card) { - 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()); - return otherAbilities != null && otherAbilities.contains(this); + return object.hasAbility(this, game) && ((Permanent) object).isPhasedIn(); + } else { + // cards and other objects + return object.hasAbility(this, game); } } return true; @@ -1264,4 +1262,17 @@ public abstract class AbilityImpl implements Ability { public Outcome getCustomOutcome() { return this.customOutcome; } + + @Override + public boolean isSameInstance(Ability ability) { + // same instance (by mtg rules) = same object, ID or class+text (you can't check class only cause it can be different by params/text) + if (ability == null) { + return false; + } + + return (this == ability) + || (this.getId().equals(ability.getId())) + || (this.getOriginalId().equals(ability.getOriginalId())) + || (this.getClass() == ability.getClass() && this.getRule(true).equals(ability.getRule(true))); + } } diff --git a/Mage/src/main/java/mage/abilities/SpellAbility.java b/Mage/src/main/java/mage/abilities/SpellAbility.java index 0df442d16c..1978d8e162 100644 --- a/Mage/src/main/java/mage/abilities/SpellAbility.java +++ b/Mage/src/main/java/mage/abilities/SpellAbility.java @@ -71,7 +71,7 @@ public class SpellAbility extends ActivatedAbilityImpl { } return null != game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game) // check this first to allow Offering in main phase || timing == TimingRule.INSTANT - || object.hasAbility(FlashAbility.getInstance().getId(), game) + || object.hasAbility(FlashAbility.getInstance(), game) || game.canPlaySorcery(playerId); } diff --git a/Mage/src/main/java/mage/abilities/common/LicidAbility.java b/Mage/src/main/java/mage/abilities/common/LicidAbility.java index f37f715e26..7963a7808c 100644 --- a/Mage/src/main/java/mage/abilities/common/LicidAbility.java +++ b/Mage/src/main/java/mage/abilities/common/LicidAbility.java @@ -130,7 +130,8 @@ class LicidContinuousEffect extends ContinuousEffectImpl { } } } - licid.getAbilities(game).removeAll(toRemove); + licid.removeAbilities(toRemove, source.getSourceId(), game); + Ability ability = new EnchantAbility("creature"); ability.setRuleAtTheTop(true); licid.addAbility(ability, source.getSourceId(), game); diff --git a/Mage/src/main/java/mage/abilities/common/TurnFaceUpAbility.java b/Mage/src/main/java/mage/abilities/common/TurnFaceUpAbility.java index de4daaa7ef..7a76fc7955 100644 --- a/Mage/src/main/java/mage/abilities/common/TurnFaceUpAbility.java +++ b/Mage/src/main/java/mage/abilities/common/TurnFaceUpAbility.java @@ -40,6 +40,7 @@ public class TurnFaceUpAbility extends SpecialAction { this.usesStack = false; this.abilityType = AbilityType.SPECIAL_ACTION; this.setRuleVisible(false); // will be made visible only to controller in CardView + this.setWorksFaceDown(true); } public TurnFaceUpAbility(final TurnFaceUpAbility ability) { diff --git a/Mage/src/main/java/mage/abilities/condition/common/BuybackCondition.java b/Mage/src/main/java/mage/abilities/condition/common/BuybackCondition.java index f61f4ead82..e565ea72f3 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/BuybackCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/BuybackCondition.java @@ -17,7 +17,7 @@ public enum BuybackCondition implements Condition { public boolean apply(Game game, Ability source) { Card card = game.getCard(source.getSourceId()); if (card != null) { - return card.getAbilities().stream() + return card.getAbilities(game).stream() .filter(a -> a instanceof BuybackAbility) .anyMatch(a -> ((BuybackAbility) a).isBuybackActivated(game)); } diff --git a/Mage/src/main/java/mage/abilities/condition/common/DashedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/DashedCondition.java index 72b75e44c8..dd7c1e8790 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/DashedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/DashedCondition.java @@ -19,9 +19,9 @@ public enum DashedCondition implements Condition { public boolean apply(Game game, Ability source) { Card card = game.getCard(source.getSourceId()); if (card != null) { - return card.getAbilities().stream() + return card.getAbilities(game).stream() .filter(a -> a instanceof DashAbility) - .anyMatch(d -> ((DashAbility)d).isActivated(source, game)); + .anyMatch(d -> ((DashAbility) d).isActivated(source, game)); } return false; diff --git a/Mage/src/main/java/mage/abilities/condition/common/EvokedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/EvokedCondition.java index 82103a9f05..7733f475f7 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/EvokedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/EvokedCondition.java @@ -22,7 +22,7 @@ public enum EvokedCondition implements Condition { public boolean apply(Game game, Ability source) { Card card = game.getCard(source.getSourceId()); if (card != null) { - return card.getAbilities().stream() + return card.getAbilities(game).stream() .filter(ab -> ab instanceof EvokeAbility) .anyMatch(evoke -> ((EvokeAbility) evoke).isActivated(source, game)); } diff --git a/Mage/src/main/java/mage/abilities/condition/common/SuspendedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/SuspendedCondition.java index dcdd6a8162..ba7dc69d1b 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/SuspendedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/SuspendedCondition.java @@ -27,10 +27,10 @@ public enum SuspendedCondition implements Condition { public boolean apply(Game game, Ability source) { Card card = game.getCard(source.getSourceId()); if (card != null) { - boolean found = card.getAbilities().stream().anyMatch(ability -> ability instanceof SuspendAbility); + boolean found = card.getAbilities(game).containsClass(SuspendAbility.class); if (!found) { - found = game.getState().getAllOtherAbilities(source.getSourceId()).stream().anyMatch(ability -> ability instanceof SuspendAbility); + found = game.getState().getAllOtherAbilities(source.getSourceId()).containsClass(SuspendAbility.class); } if (found) { diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index d0ec027be5..f32f87e537 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -57,7 +57,7 @@ public class ContinuousEffects implements Serializable { // private final PlaneswalkerRedirectionEffect planeswalkerRedirectionEffect; private final AuraReplacementEffect auraReplacementEffect; - private final List previous = new ArrayList<>(); + private final Map> lastEffectsListOnLayer = new HashMap<>(); // helps to find out new effect timestamps // note all effect/abilities that were only added temporary private final Map> temporaryEffects = new HashMap<>(); @@ -178,6 +178,16 @@ public class ContinuousEffects implements Serializable { } public synchronized List getLayeredEffects(Game game) { + return getLayeredEffects(game, "main"); + } + + /** + * Return effects list ordered by timestamps (timestamps are automaticity generates from new/old lists on same layer) + * + * @param timestampGroupName workaround to fix broken timestamps on effect's add/remove between different layers + * @return effects list ordered by timestamp + */ + public synchronized List getLayeredEffects(Game game, String timestampGroupName) { List layerEffects = new ArrayList<>(); for (ContinuousEffect effect : layeredEffects) { switch (effect.getDuration()) { @@ -202,9 +212,14 @@ public class ContinuousEffects implements Serializable { } } - updateTimestamps(layerEffects); + updateTimestamps(timestampGroupName, layerEffects); + layerEffects.sort(Comparator.comparingLong(ContinuousEffect::getOrder)); + /* debug effects apply order: + if (game.getStep() != null) System.out.println("layr - " + game.getTurnNum() + "." + game.getStep().getType() + ": layers " + layerEffects.size() + + " - " + layerEffects.stream().map(l -> l.getClass().getSimpleName()).collect(Collectors.joining(", ")) + + " - " + callName); + //*/ - Collections.sort(layerEffects, Comparator.comparingLong(ContinuousEffect::getOrder)); return layerEffects; } @@ -215,17 +230,23 @@ public class ContinuousEffects implements Serializable { * Ability.#isInUseableZone(Game, boolean) method in * #getLayeredEffects(Game). * + * It must be called with different timestamp group name (otherwise sort order will be changed for add/remove effects, see Urborg and Bloodmoon test) + * * @param layerEffects */ - private synchronized void updateTimestamps(List layerEffects) { + private synchronized void updateTimestamps(String timestampGroupName, List layerEffects) { + if (!lastEffectsListOnLayer.containsKey(timestampGroupName)) { + lastEffectsListOnLayer.put(timestampGroupName, new ArrayList<>()); + } + List prevs = lastEffectsListOnLayer.get(timestampGroupName); for (ContinuousEffect continuousEffect : layerEffects) { // check if it's new, then set order - if (!previous.contains(continuousEffect)) { + if (!prevs.contains(continuousEffect)) { setOrder(continuousEffect); } } - previous.clear(); - previous.addAll(layerEffects); + prevs.clear(); + prevs.addAll(layerEffects); } public void setOrder(ContinuousEffect effect) { @@ -425,12 +446,12 @@ public class ContinuousEffects implements Serializable { return false; } boolean exists = true; - if (!object.getAbilities().contains(ability)) { + if (!object.hasAbility(ability, game)) { exists = false; if (object instanceof PermanentCard) { PermanentCard permanent = (PermanentCard) object; if (permanent.isTransformable() && event.getType() == GameEvent.EventType.TRANSFORMED) { - exists = permanent.getCard().getAbilities().contains(ability); + exists = permanent.getCard().hasAbility(ability, game); } } } else if (object instanceof PermanentCard) { @@ -903,7 +924,7 @@ public class ContinuousEffects implements Serializable { //20091005 - 613 public synchronized void apply(Game game) { removeInactiveEffects(game); - List activeLayerEffects = getLayeredEffects(game); + List activeLayerEffects = getLayeredEffects(game); // main call List layer = filterLayeredEffects(activeLayerEffects, Layer.CopyEffects_1); for (ContinuousEffect effect : layer) { @@ -914,7 +935,7 @@ public class ContinuousEffects implements Serializable { } //Reload layerEffect if copy effects were applied if (!layer.isEmpty()) { - activeLayerEffects = getLayeredEffects(game); + activeLayerEffects = getLayeredEffects(game, "layer_1"); } layer = filterLayeredEffects(activeLayerEffects, Layer.ControlChangingEffects_2); @@ -936,16 +957,16 @@ public class ContinuousEffects implements Serializable { game.getBattlefield().resetPermanentsControl(); } - applyLayer(activeLayerEffects, Layer.TextChangingEffects_3, game); - applyLayer(activeLayerEffects, Layer.TypeChangingEffects_4, game); - applyLayer(activeLayerEffects, Layer.ColorChangingEffects_5, game); + applyLayer(activeLayerEffects, Layer.TextChangingEffects_3, game, "layer_3"); + applyLayer(activeLayerEffects, Layer.TypeChangingEffects_4, game, "layer_4"); + applyLayer(activeLayerEffects, Layer.ColorChangingEffects_5, game, "layer_5"); Map> appliedEffectAbilities = new HashMap<>(); boolean done = false; Map> waitingEffects = new LinkedHashMap<>(); Set appliedEffects = new HashSet<>(); applyCounters.apply(Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, null, game); - activeLayerEffects = getLayeredEffects(game); + activeLayerEffects = getLayeredEffects(game, "layer_6"); while (!done) { // loop needed if a added effect adds again an effect (e.g. Level 5- of Joraga Treespeaker) done = true; @@ -1000,7 +1021,7 @@ public class ContinuousEffects implements Serializable { effect.apply(Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, ability, game); done = false; // list must be updated after each applied effect (eg. if "Turn to Frog" removes abilities) - activeLayerEffects = getLayeredEffects(game); + activeLayerEffects = getLayeredEffects(game, "apply"); } appliedEffects.add(effect.getId()); @@ -1027,7 +1048,7 @@ public class ContinuousEffects implements Serializable { entry.getKey().apply(Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, ability, game); done = false; // list must be updated after each applied effect (eg. if "Turn to Frog" removes abilities) - activeLayerEffects = getLayeredEffects(game); + activeLayerEffects = getLayeredEffects(game, "apply"); } appliedEffects.add(entry.getKey().getId()); iterator.remove(); @@ -1083,10 +1104,10 @@ public class ContinuousEffects implements Serializable { private boolean abilityActive(Ability ability, Game game) { MageObject object = game.getObject(ability.getSourceId()); - return object != null && object.hasAbility(ability.getId(), game); + return object != null && object.hasAbility(ability, game); } - private void applyLayer(List activeLayerEffects, Layer currentLayer, Game game) { + private void applyLayer(List activeLayerEffects, Layer currentLayer, Game game, String timestampGroupName) { List layer = filterLayeredEffects(activeLayerEffects, currentLayer); // layer is a list of all effects at the current layer if (!layer.isEmpty()) { @@ -1109,7 +1130,7 @@ public class ContinuousEffects implements Serializable { applyContinuousEffect(effect, currentLayer, game); // add it to the applied effects list appliedEffects.add(effect.getId()); - layer = getLayeredEffects(game); + layer = getLayeredEffects(game, timestampGroupName); // check waiting effects to see if it has anything to check if (!waitingEffects.isEmpty()) { @@ -1120,7 +1141,7 @@ public class ContinuousEffects implements Serializable { applyContinuousEffect(entry.getKey(), currentLayer, game); // add it to the applied effects list appliedEffects.add(entry.getKey().getId()); - layer = getLayeredEffects(game); + layer = getLayeredEffects(game, timestampGroupName); } } } @@ -1131,7 +1152,7 @@ public class ContinuousEffects implements Serializable { applyContinuousEffect(entry.getKey(), currentLayer, game); // add it to the applied effects list appliedEffects.add(entry.getKey().getId()); - layer = getLayeredEffects(game); + layer = getLayeredEffects(game, timestampGroupName); } } } @@ -1154,7 +1175,7 @@ public class ContinuousEffects implements Serializable { if (!(effect instanceof BecomesFaceDownCreatureEffect) && (effect != null && !effect.getDuration().equals(Duration.Custom))) { // Custom effects do not depend on the creating permanent if (card != null) { - return card.getAbilities(game).contains(ability); + return card.hasAbility(ability, game); } } diff --git a/Mage/src/main/java/mage/abilities/effects/GainAbilitySpellsEffect.java b/Mage/src/main/java/mage/abilities/effects/GainAbilitySpellsEffect.java index 11545d7d27..0d96f2a918 100644 --- a/Mage/src/main/java/mage/abilities/effects/GainAbilitySpellsEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/GainAbilitySpellsEffect.java @@ -64,7 +64,7 @@ public class GainAbilitySpellsEffect extends ContinuousEffectImpl { if (stackObject.isControlledBy(source.getControllerId())) { Card card = game.getCard(stackObject.getSourceId()); if (card != null && filter.match(card, game)) { - if (!card.getAbilities().contains(ability)) { + if (!card.hasAbility(ability, game)) { game.getState().addOtherAbility(card, ability); } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java index d75fda572e..ae3f6c5e86 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java @@ -115,11 +115,11 @@ public class CopyEffect extends ContinuousEffectImpl { permanent.removeAllAbilities(source.getSourceId(), game); if (copyFromObject instanceof Permanent) { for (Ability ability : ((Permanent) copyFromObject).getAbilities(game)) { - permanent.addAbility(ability, getSourceId(), game, false); // no new Id so consumed replacement effects are known while new continuousEffects.apply happen. + permanent.addAbility(ability, getSourceId(), game); } } else { for (Ability ability : copyFromObject.getAbilities()) { - permanent.addAbility(ability, getSourceId(), game, false); // no new Id so consumed replacement effects are known while new continuousEffects.apply happen. + permanent.addAbility(ability, getSourceId(), game); } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopyTokenEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopyTokenEffect.java index 63b3018d23..d5de78070d 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopyTokenEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopyTokenEffect.java @@ -41,7 +41,7 @@ public class CopyTokenEffect extends ContinuousEffectImpl { } permanent.getAbilities().clear(); for (Ability ability : token.getAbilities()) { - permanent.addAbility(ability, game); + permanent.addAbility(ability, source.getSourceId(), game); } permanent.getPower().setValue(token.getPower().getValue()); permanent.getToughness().setValue(token.getToughness().getValue()); diff --git a/Mage/src/main/java/mage/abilities/effects/common/combat/CanBlockOnlyFlyingAttachedEffect.java b/Mage/src/main/java/mage/abilities/effects/common/combat/CanBlockOnlyFlyingAttachedEffect.java index ecb95414c9..a3b94d6f0a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/combat/CanBlockOnlyFlyingAttachedEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/combat/CanBlockOnlyFlyingAttachedEffect.java @@ -33,7 +33,7 @@ public class CanBlockOnlyFlyingAttachedEffect extends RestrictionEffect { if (attacker == null) { return true; } - return attacker.getAbilities().contains(FlyingAbility.getInstance()); + return attacker.hasAbility(FlyingAbility.getInstance(), game); } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/common/combat/CanBlockOnlyFlyingEffect.java b/Mage/src/main/java/mage/abilities/effects/common/combat/CanBlockOnlyFlyingEffect.java index c39e9d3188..4d41916cb4 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/combat/CanBlockOnlyFlyingEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/combat/CanBlockOnlyFlyingEffect.java @@ -33,7 +33,7 @@ public class CanBlockOnlyFlyingEffect extends RestrictionEffect { if (attacker == null) { return true; } - return attacker.getAbilities().contains(FlyingAbility.getInstance()); + return attacker.hasAbility(FlyingAbility.getInstance(), game); } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureAllEffect.java index d91a9a4d3d..202788360b 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureAllEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureAllEffect.java @@ -1,11 +1,5 @@ - package mage.abilities.effects.common.continuous; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; import mage.MageObjectReference; import mage.ObjectColor; import mage.abilities.Ability; @@ -13,23 +7,20 @@ import mage.abilities.common.TurnFaceUpAbility; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.keyword.MorphAbility; import mage.cards.Card; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; +import mage.constants.*; import mage.filter.FilterPermanent; import mage.game.Game; import mage.game.permanent.Permanent; +import java.util.*; + /** - * * @author LevelX2 */ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl implements SourceEffect { - protected Map turnFaceUpAbilityMap = new HashMap<>(); + protected Map turnFaceUpAbilityMap = new HashMap<>(); protected FilterPermanent filter; public BecomesFaceDownCreatureAllEffect(FilterPermanent filter) { @@ -40,7 +31,7 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl imple public BecomesFaceDownCreatureAllEffect(final BecomesFaceDownCreatureAllEffect effect) { super(effect); - for (Map.Entry entry: effect.turnFaceUpAbilityMap.entrySet()) { + for (Map.Entry entry : effect.turnFaceUpAbilityMap.entrySet()) { this.turnFaceUpAbilityMap.put(entry.getKey(), entry.getValue()); } this.filter = effect.filter.copy(); @@ -54,16 +45,16 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl imple @Override public void init(Ability source, Game game) { super.init(source, game); - for (Permanent perm: game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source.getSourceId(), game)) { + for (Permanent perm : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source.getSourceId(), game)) { if (!perm.isFaceDown(game) && !perm.isTransformable()) { affectedObjectList.add(new MageObjectReference(perm, game)); perm.setFaceDown(true, game); // check for Morph Card card = game.getCard(perm.getId()); if (card != null) { - for (Ability ability: card.getAbilities()) { + for (Ability ability : card.getAbilities()) { if (ability instanceof MorphAbility) { - this.turnFaceUpAbilityMap.put(card.getId(), new TurnFaceUpAbility(((MorphAbility)ability).getMorphCosts())); + this.turnFaceUpAbilityMap.put(card.getId(), new TurnFaceUpAbility(((MorphAbility) ability).getMorphCosts())); } } } @@ -74,7 +65,7 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl imple @Override public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { boolean targetExists = false; - for (MageObjectReference mor: affectedObjectList) { + for (MageObjectReference mor : affectedObjectList) { Permanent permanent = mor.getPermanent(game); if (permanent != null && permanent.isFaceDown(game)) { targetExists = true; @@ -92,27 +83,35 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl imple break; case AbilityAddingRemovingEffects_6: Card card = game.getCard(permanent.getId()); // - List abilities = new ArrayList<>(); + List abilitiesToRemove = new ArrayList<>(); for (Ability ability : permanent.getAbilities()) { + + // keep gained abilities from other sources, removes only own (card text) if (card != null && !card.getAbilities().contains(ability)) { - // gained abilities from other sources won't be removed continue; } - // TODO: Add flag "works also face down" to ability and use it to control ability removement instead of instanceof check + + // 701.33c + // If a card with morph is manifested, its controller may turn that card face up using + // either the procedure described in rule 702.36e to turn a face-down permanent with morph face up + // or the procedure described above to turn a manifested permanent face up. + // + // so keep all tune face up abilities and other face down compatible if (ability.getWorksFaceDown()) { ability.setRuleVisible(false); continue; } + if (!ability.getRuleVisible() && !ability.getEffects().isEmpty()) { if (ability.getEffects().get(0) instanceof BecomesFaceDownCreatureAllEffect) { continue; } } - abilities.add(ability); + abilitiesToRemove.add(ability); } - permanent.getAbilities().removeAll(abilities); + permanent.removeAbilities(abilitiesToRemove, source.getSourceId(), game); if (turnFaceUpAbilityMap.containsKey(permanent.getId())) { - permanent.addAbility(turnFaceUpAbilityMap.get(permanent.getId()), game); + permanent.addAbility(turnFaceUpAbilityMap.get(permanent.getId()), source.getSourceId(), game); } break; case PTChangingEffects_7: diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureEffect.java index 3471377788..06e5f1c012 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureEffect.java @@ -1,8 +1,5 @@ - package mage.abilities.effects.common.continuous; -import java.util.ArrayList; -import java.util.List; import mage.MageObjectReference; import mage.ObjectColor; import mage.abilities.Ability; @@ -12,14 +9,13 @@ import mage.abilities.costs.Costs; import mage.abilities.costs.CostsImpl; import mage.abilities.effects.ContinuousEffectImpl; import mage.cards.Card; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Layer; -import mage.constants.Outcome; -import mage.constants.SubLayer; +import mage.constants.*; import mage.game.Game; import mage.game.permanent.Permanent; +import java.util.ArrayList; +import java.util.List; + /** * This effect lets the card be a 2/2 face-down creature, with no text, no name, * no subtypes, and no mana cost, if it's face down on the battlefield. And it @@ -149,21 +145,31 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl implemen Card card = game.getCard(permanent.getId()); // List abilitiesToRemove = new ArrayList<>(); for (Ability ability : permanent.getAbilities()) { + + // keep gained abilities from other sources, removes only own (card text) if (card != null && !card.getAbilities().contains(ability)) { - // gained abilities from other sources won't be removed continue; } + + // 701.33c + // If a card with morph is manifested, its controller may turn that card face up using + // either the procedure described in rule 702.36e to turn a face-down permanent with morph face up + // or the procedure described above to turn a manifested permanent face up. + // + // so keep all tune face up abilities and other face down compatible if (ability.getWorksFaceDown()) { ability.setRuleVisible(false); continue; - } else if (!ability.getRuleVisible() && !ability.getEffects().isEmpty()) { + } + + if (!ability.getRuleVisible() && !ability.getEffects().isEmpty()) { if (ability.getEffects().get(0) instanceof BecomesFaceDownCreatureEffect) { continue; } } abilitiesToRemove.add(ability); } - permanent.getAbilities().removeAll(abilitiesToRemove); + permanent.removeAbilities(abilitiesToRemove, source.getSourceId(), game); if (turnFaceUpAbility != null) { permanent.addAbility(turnFaceUpAbility, source.getSourceId(), game); } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/CreaturesCantGetOrHaveAbilityEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/CreaturesCantGetOrHaveAbilityEffect.java index 51fb06870d..6ea1aabfbe 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/CreaturesCantGetOrHaveAbilityEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/CreaturesCantGetOrHaveAbilityEffect.java @@ -45,9 +45,7 @@ public class CreaturesCantGetOrHaveAbilityEffect extends ContinuousEffectImpl { if (controller != null) { for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source.getSourceId(), game)) { if (permanent != null) { - while (permanent.getAbilities().remove(ability)) { - // repeat as long as ability can be removed - } + permanent.removeAbility(ability, source.getSourceId(), game); } } return true; diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAllEffect.java index fca600ed88..36d96f04b6 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAllEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAllEffect.java @@ -87,7 +87,7 @@ public class GainAbilityAllEffect extends ContinuousEffectImpl { for (Iterator it = affectedObjectList.iterator(); it.hasNext(); ) { // filter may not be used again, because object can have changed filter relevant attributes but still geets boost Permanent permanent = it.next().getPermanentOrLKIBattlefield(game); //LKI is neccessary for "dies triggered abilities" to work given to permanets (e.g. Showstopper) if (permanent != null) { - permanent.addAbility(ability, source.getSourceId(), game, false); + permanent.addAbility(ability, source.getSourceId(), game); } else { it.remove(); // no longer on the battlefield, remove reference to object if (affectedObjectList.isEmpty()) { @@ -99,7 +99,7 @@ public class GainAbilityAllEffect extends ContinuousEffectImpl { setRuntimeData(source, game); for (Permanent perm : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source.getSourceId(), game)) { if (!(excludeSource && perm.getId().equals(source.getSourceId())) && selectedByRuntimeData(perm, source, game)) { - perm.addAbility(ability, source.getSourceId(), game, false); + perm.addAbility(ability, source.getSourceId(), game); } } // still as long as the prev. permanent is known to the LKI (e.g. Mikaeus, the Unhallowed) so gained dies triggered ability will trigger @@ -109,7 +109,7 @@ public class GainAbilityAllEffect extends ContinuousEffectImpl { Permanent perm = (Permanent) mageObject; if (!(excludeSource && perm.getId().equals(source.getSourceId())) && selectedByRuntimeData(perm, source, game)) { if (filter.match(perm, source.getSourceId(), source.getControllerId(), game)) { - perm.addAbility(ability, source.getSourceId(), game, false); + perm.addAbility(ability, source.getSourceId(), game); } } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAttachedEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAttachedEffect.java index b9fc862152..959f3cddd5 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAttachedEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityAttachedEffect.java @@ -88,7 +88,7 @@ public class GainAbilityAttachedEffect extends ContinuousEffectImpl { } } if (permanent != null) { - permanent.addAbility(ability, source.getSourceId(), game, false); + permanent.addAbility(ability, source.getSourceId(), game); } return true; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledEffect.java index 2d96eaf08e..1dcc227e8d 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledEffect.java @@ -87,7 +87,7 @@ public class GainAbilityControlledEffect extends ContinuousEffectImpl { Permanent perm = it.next().getPermanentOrLKIBattlefield(game); //LKI is neccessary for "dies triggered abilities" to work given to permanets (e.g. Showstopper) if (perm != null) { for (Ability abilityToAdd : ability) { - perm.addAbility(abilityToAdd, source.getSourceId(), game, false); + perm.addAbility(abilityToAdd, source.getSourceId(), game); } } else { it.remove(); @@ -100,7 +100,7 @@ public class GainAbilityControlledEffect extends ContinuousEffectImpl { for (Permanent perm : game.getBattlefield().getAllActivePermanents(filter, source.getControllerId(), game)) { if (!(excludeSource && perm.getId().equals(source.getSourceId()))) { for (Ability abilityToAdd : ability) { - perm.addAbility(abilityToAdd, source.getSourceId(), game, false); + perm.addAbility(abilityToAdd, source.getSourceId(), game); } } } @@ -112,7 +112,7 @@ public class GainAbilityControlledEffect extends ContinuousEffectImpl { if (!(excludeSource && perm.getId().equals(source.getSourceId()))) { if (filter.match(perm, source.getSourceId(), source.getControllerId(), game)) { for (Ability abilityToAdd : ability) { - perm.addAbility(abilityToAdd, source.getSourceId(), game, false); + perm.addAbility(abilityToAdd, source.getSourceId(), game); } } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java index e58f88cda6..424c996c1a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java @@ -51,7 +51,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl { if ((stackObject instanceof Spell) && !stackObject.isCopy() && stackObject.isControlledBy(source.getControllerId())) { Spell spell = (Spell) stackObject; if (filter.match(spell, game)) { - if (!spell.getAbilities().contains(ability)) { + if (!spell.hasAbility(ability, game)) { game.getState().addOtherAbility(spell.getCard(), ability); } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityPairedEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityPairedEffect.java index 6c8c4b0daa..a1c5554330 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityPairedEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityPairedEffect.java @@ -43,7 +43,7 @@ public class GainAbilityPairedEffect extends ContinuousEffectImpl { Permanent paired = permanent.getPairedCard().getPermanent(game); if (paired != null && paired.getPairedCard() != null && paired.getPairedCard().equals(new MageObjectReference(permanent, game))) { permanent.addAbility(ability, source.getSourceId(), game); - paired.addAbility(ability, source.getSourceId(), game, false); + paired.addAbility(ability, source.getSourceId(), game); return true; } else { diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java index ddf5809187..c3a3507f5e 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilitySourceEffect.java @@ -103,7 +103,7 @@ public class GainAbilitySourceEffect extends ContinuousEffectImpl implements Sou permanent = game.getPermanent(source.getSourceId()); } if (permanent != null) { - permanent.addAbility(ability, source.getSourceId(), game, false); + permanent.addAbility(ability, source.getSourceId(), game); return true; } } 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 5647423c5d..adff4f92a4 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 @@ -112,7 +112,7 @@ public class GainAbilityTargetEffect extends ContinuousEffectImpl { for (UUID permanentId : targetPointer.getTargets(game, source)) { Permanent permanent = game.getPermanentOrLKIBattlefield(permanentId); if (permanent != null) { - permanent.addAbility(ability, source.getSourceId(), game, false); + permanent.addAbility(ability, source.getSourceId(), game); affectedTargets++; } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainProtectionFromTypeTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainProtectionFromTypeTargetEffect.java index d2e00db536..c620c0c4b3 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainProtectionFromTypeTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainProtectionFromTypeTargetEffect.java @@ -38,7 +38,7 @@ public class GainProtectionFromTypeTargetEffect extends GainAbilityTargetEffect public boolean apply(Game game, Ability source) { Permanent creature = game.getPermanent(source.getFirstTarget()); if (creature != null) { - creature.addAbility(ability, game); + creature.addAbility(ability, source.getSourceId(), game); return true; } return false; diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityAllEffect.java index f9a6f40117..dc8daf168c 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityAllEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityAllEffect.java @@ -85,9 +85,7 @@ public class LoseAbilityAllEffect extends ContinuousEffectImpl { for (Iterator it = affectedObjectList.iterator(); it.hasNext();) { // filter may not be used again, because object can have changed filter relevant attributes but still geets boost Permanent perm = it.next().getPermanentOrLKIBattlefield(game); //LKI is neccessary for "dies triggered abilities" to work given to permanets (e.g. Showstopper) if (perm != null) { - for (Ability ability : ability) { - perm.getAbilities().removeIf(entry -> entry.getId().equals(ability.getId())); - } + perm.removeAbilities(ability, source.getSourceId(), game); } else { it.remove(); if (affectedObjectList.isEmpty()) { @@ -99,9 +97,7 @@ public class LoseAbilityAllEffect extends ContinuousEffectImpl { for (Permanent perm : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source.getSourceId(), game)) { if (!(excludeSource && perm.getId().equals(source.getSourceId()))) { System.out.println(game.getTurn() + ", " + game.getPhase() + ": " + "remove from size " + perm.getAbilities().size()); - for (Ability ability : ability) { - perm.getAbilities().removeIf(entry -> entry.getId().equals(ability.getId())); - } + perm.removeAbilities(ability, source.getSourceId(), game); } } // still as long as the prev. permanent is known to the LKI (e.g. Mikaeus, the Unhallowed) so gained dies triggered ability will trigger @@ -111,9 +107,7 @@ public class LoseAbilityAllEffect extends ContinuousEffectImpl { Permanent perm = (Permanent) mageObject; if (!(excludeSource && perm.getId().equals(source.getSourceId()))) { if (filter.match(perm, source.getSourceId(), source.getControllerId(), game)) { - for (Ability ability : ability) { - perm.getAbilities().removeIf(entry -> entry.getId().equals(ability.getId())); - } + perm.removeAbilities(ability, source.getSourceId(), game); } } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityAttachedEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityAttachedEffect.java index 9c43b5c4c3..5007c45494 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityAttachedEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityAttachedEffect.java @@ -43,12 +43,7 @@ public class LoseAbilityAttachedEffect extends ContinuousEffectImpl { if (equipment != null && equipment.getAttachedTo() != null) { Permanent creature = game.getPermanent(equipment.getAttachedTo()); if (creature != null) { - while (creature.getAbilities().contains(ability)) { - if (!creature.getAbilities().remove(ability)) { - // Something went wrong - ability has an other id? - logger.warn("ability" + ability.getRule() + "couldn't be removed."); - } - } + creature.removeAbility(ability, source.getSourceId(), game); } } return true; diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityOrAnotherAbilityTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityOrAnotherAbilityTargetEffect.java index 44f6031387..108f6f9054 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityOrAnotherAbilityTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityOrAnotherAbilityTargetEffect.java @@ -63,13 +63,9 @@ public class LoseAbilityOrAnotherAbilityTargetEffect extends LoseAbilityTargetEf if (player.choose(outcome, chooseAbility, game)) { String chosenAbility = chooseAbility.getChoice(); if (chosenAbility.equals(ability.getRule())) { - while (permanent.getAbilities().contains(ability)) { - permanent.getAbilities().remove(ability); - } + permanent.removeAbility(ability, source.getSourceId(), game); } else if (chosenAbility.equals(ability2.getRule())) { - while (permanent.getAbilities().contains(ability2)) { - permanent.getAbilities().remove(ability2); - } + permanent.removeAbility(ability2, source.getSourceId(), game); } } else { return false; diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilitySourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilitySourceEffect.java index 6f5d03f33b..f55e2fea5a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilitySourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilitySourceEffect.java @@ -56,10 +56,7 @@ public class LoseAbilitySourceEffect extends ContinuousEffectImpl { public boolean apply(Game game, Ability source) { Permanent permanent = game.getPermanent(source.getSourceId()); if (permanent != null) { - // 112.10 - while (permanent.getAbilities().remove(ability)) { - - } + permanent.removeAbility(ability, source.getSourceId(), game); } return true; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityTargetEffect.java index f8fa800f08..a3195035c3 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/LoseAbilityTargetEffect.java @@ -45,18 +45,7 @@ public class LoseAbilityTargetEffect extends ContinuousEffectImpl { public boolean apply(Game game, Ability source) { Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); if (permanent != null) { - if (ability instanceof MageSingleton) { - while (permanent.getAbilities().contains(ability)) { - permanent.getAbilities().remove(ability); - } - } else { - for (Iterator iter = permanent.getAbilities().iterator(); iter.hasNext();) { - Ability ab = iter.next(); - if (ab.getClass().equals(ability.getClass())) { - iter.remove(); - } - } - } + permanent.removeAbility(ability, source.getSourceId(), game); } return true; } diff --git a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java index 379d81027b..2bca55f26d 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java @@ -213,7 +213,7 @@ public class SuspendAbility extends SpecialAction { } MageObject object = game.getObject(sourceId); return new ActivationStatus(object.isInstant() - || object.hasAbility(FlashAbility.getInstance().getId(), game) + || object.hasAbility(FlashAbility.getInstance(), game) || null != game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game) || game.canPlaySorcery(playerId), null); @@ -356,6 +356,13 @@ class SuspendPlayCardEffect extends OneShotEffect { } } // remove the abilities from the card + // TODO: will not work with Adventure Cards and another auto-generated abilities list + // TODO: is it work after blink or return to hand? + /* + bug example: + Epochrasite bug: It comes out of suspend, is cast and enters the battlefield. THEN if it's returned to + its owner's hand from battlefield, the bounced Epochrasite can't be cast for the rest of the game. + */ card.getAbilities().removeAll(abilitiesToRemove); } // cast the card for free diff --git a/Mage/src/main/java/mage/abilities/keyword/TransformAbility.java b/Mage/src/main/java/mage/abilities/keyword/TransformAbility.java index fb90104d2d..8c1022e122 100644 --- a/Mage/src/main/java/mage/abilities/keyword/TransformAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/TransformAbility.java @@ -38,7 +38,7 @@ public class TransformAbility extends SimpleStaticAbility { return ""; } - public static void transform(Permanent permanent, Card sourceCard, Game game) { + public static void transform(Permanent permanent, Card sourceCard, Game game, Ability source) { if (sourceCard == null) { return; @@ -63,7 +63,7 @@ public class TransformAbility extends SimpleStaticAbility { permanent.setExpansionSetCode(sourceCard.getExpansionSetCode()); permanent.getAbilities().clear(); for (Ability ability : sourceCard.getAbilities()) { - permanent.addAbility(ability, game); + permanent.addAbility(ability, source == null ? null : source.getSourceId(), game); } permanent.getPower().modifyBaseValue(sourceCard.getPower().getValue()); permanent.getToughness().modifyBaseValue(sourceCard.getToughness().getValue()); @@ -105,7 +105,7 @@ class TransformEffect extends ContinuousEffectImpl { return false; } - TransformAbility.transform(permanent, card, game); + TransformAbility.transform(permanent, card, game, source); return true; diff --git a/Mage/src/main/java/mage/cards/Card.java b/Mage/src/main/java/mage/cards/Card.java index 81a6877abb..3bb996652b 100644 --- a/Mage/src/main/java/mage/cards/Card.java +++ b/Mage/src/main/java/mage/cards/Card.java @@ -26,6 +26,10 @@ public interface Card extends MageObject { void setOwnerId(UUID ownerId); + /** + * For cards: return all basic and dynamic abilities + * For permanents: return all basic and dynamic abilities + */ Abilities getAbilities(Game game); void setSpellAbility(SpellAbility ability); diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index d5daa56d42..f7d168227b 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -285,7 +285,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card { /** * Gets all current abilities - includes additional abilities added by other - * cards or effects + * cards or effects. Warning, you can't modify that list. * * @param game * @return A list of {@link Ability} - this collection is not modifiable @@ -295,15 +295,23 @@ public abstract class CardImpl extends MageObjectImpl implements Card { if (game == null) { return abilities; // deck editor with empty game } + CardState cardState = game.getState().getCardState(this.getId()); - if (!cardState.hasLostAllAbilities() && (cardState.getAbilities() == null || cardState.getAbilities().isEmpty())) { + if (cardState == null) { return abilities; } + + // collects all abilities Abilities all = new AbilitiesImpl<>(); + + // basic if (!cardState.hasLostAllAbilities()) { all.addAll(abilities); } + + // dynamic all.addAll(cardState.getAbilities()); + return all; } @@ -314,6 +322,12 @@ public abstract class CardImpl extends MageObjectImpl implements Card { cardState.getAbilities().clear(); } + @Override + public boolean hasAbility(Ability ability, Game game) { + // getAbilities(game) searches all abilities from base and dynamic lists (other) + return this.getAbilities(game).contains(ability); + } + /** * Public in order to support adding abilities to SplitCardHalf's * diff --git a/Mage/src/main/java/mage/designations/Designation.java b/Mage/src/main/java/mage/designations/Designation.java index 3554e68c50..4537abe3b0 100644 --- a/Mage/src/main/java/mage/designations/Designation.java +++ b/Mage/src/main/java/mage/designations/Designation.java @@ -171,8 +171,8 @@ public abstract class Designation implements MageObject { } @Override - public boolean hasAbility(UUID abilityId, Game game) { - return abilites.containsKey(abilityId); + public boolean hasAbility(Ability ability, Game game) { + return this.getAbilities().contains(ability); } @Override diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index d0930ad3f0..612783bf0a 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1647,7 +1647,7 @@ public abstract class GameImpl implements Game, Serializable { } newBluePrint.assignNewId(); if (copyFromPermanent.isTransformed()) { - TransformAbility.transform(newBluePrint, newBluePrint.getSecondCardFace(), this); + TransformAbility.transform(newBluePrint, newBluePrint.getSecondCardFace(), this, source); } } if (applier != null) { diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index c502dae73b..32d834b1a1 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -306,7 +306,7 @@ public class Combat implements Serializable, Copyable { || getAttackers().size() <= 1) { return; } - boolean canBand = attacker.getAbilities().containsKey(BandingAbility.getInstance().getId()); + boolean canBand = attacker.hasAbility(BandingAbility.getInstance(), game); List bandsWithOther = new ArrayList<>(); for (Ability ability : attacker.getAbilities()) { if (ability.getClass().equals(BandsWithOtherAbility.class)) { @@ -390,7 +390,7 @@ public class Combat implements Serializable, Copyable { permanent.addBandedCard(creatureId); attacker.addBandedCard(targetId); if (canBand) { - if (!permanent.getAbilities().containsKey(BandingAbility.getInstance().getId())) { + if (!permanent.hasAbility(BandingAbility.getInstance(), game)) { filter.add(new AbilityPredicate(BandingAbility.class)); canBandWithOther = false; } @@ -1289,7 +1289,8 @@ public class Combat implements Serializable, Copyable { if (attacker != null) { if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_ATTACKER, defenderId, creatureId, playerId))) { if (addAttackerToCombat(creatureId, defenderId, game)) { - if (!attacker.getAbilities().containsKey(VigilanceAbility.getInstance().getId()) && !attacker.getAbilities().containsKey(JohanVigilanceAbility.getInstance().getId())) { + if (!attacker.hasAbility(VigilanceAbility.getInstance(), game) + && !attacker.hasAbility(JohanVigilanceAbility.getInstance(), game)) { if (!attacker.isTapped()) { attacker.setTapped(true); attackersTappedByAttack.add(attacker.getId()); diff --git a/Mage/src/main/java/mage/game/command/Commander.java b/Mage/src/main/java/mage/game/command/Commander.java index e8a34c3a7d..c2cd0491bf 100644 --- a/Mage/src/main/java/mage/game/command/Commander.java +++ b/Mage/src/main/java/mage/game/command/Commander.java @@ -158,12 +158,12 @@ public class Commander implements CommandObject { } @Override - public boolean hasAbility(UUID abilityId, Game game) { - if (this.getAbilities().containsKey(abilityId)) { + public boolean hasAbility(Ability ability, Game game) { + if (this.getAbilities().contains(ability)) { return true; } Abilities otherAbilities = game.getState().getAllOtherAbilities(getId()); - return otherAbilities != null && otherAbilities.containsKey(abilityId); + return otherAbilities != null && otherAbilities.contains(ability); } @Override diff --git a/Mage/src/main/java/mage/game/command/Emblem.java b/Mage/src/main/java/mage/game/command/Emblem.java index 7a4a8896aa..b0de304a98 100644 --- a/Mage/src/main/java/mage/game/command/Emblem.java +++ b/Mage/src/main/java/mage/game/command/Emblem.java @@ -173,8 +173,8 @@ public class Emblem implements CommandObject { } @Override - public boolean hasAbility(UUID abilityId, Game game) { - return abilites.containsKey(abilityId); + public boolean hasAbility(Ability ability, Game game) { + return getAbilities().contains(ability); } @Override diff --git a/Mage/src/main/java/mage/game/command/Plane.java b/Mage/src/main/java/mage/game/command/Plane.java index 070a55c6ab..be457ec6ed 100644 --- a/Mage/src/main/java/mage/game/command/Plane.java +++ b/Mage/src/main/java/mage/game/command/Plane.java @@ -182,8 +182,8 @@ public class Plane implements CommandObject { } @Override - public boolean hasAbility(UUID abilityId, Game game) { - return abilites.containsKey(abilityId); + public boolean hasAbility(Ability ability, Game game) { + return getAbilities().contains(ability); } @Override diff --git a/Mage/src/main/java/mage/game/command/planes/TrailOfTheMageRingsPlane.java b/Mage/src/main/java/mage/game/command/planes/TrailOfTheMageRingsPlane.java index 6a5371e12f..28e32d4519 100644 --- a/Mage/src/main/java/mage/game/command/planes/TrailOfTheMageRingsPlane.java +++ b/Mage/src/main/java/mage/game/command/planes/TrailOfTheMageRingsPlane.java @@ -120,7 +120,7 @@ class TrailOfTheMageRingsReboundEffect extends ContinuousEffectImpl { private void addReboundAbility(Card card, Ability source, Game game) { if (filter.match(card, game)) { - boolean found = card.getAbilities().stream().anyMatch(ability -> ability instanceof ReboundAbility); + boolean found = card.getAbilities(game).containsClass(ReboundAbility.class); if (!found) { Ability ability = new ReboundAbility(); game.getState().addOtherAbility(card, ability); diff --git a/Mage/src/main/java/mage/game/permanent/Battlefield.java b/Mage/src/main/java/mage/game/permanent/Battlefield.java index 6aacd3e45f..d2e11c2c48 100644 --- a/Mage/src/main/java/mage/game/permanent/Battlefield.java +++ b/Mage/src/main/java/mage/game/permanent/Battlefield.java @@ -327,16 +327,16 @@ public class Battlefield implements Serializable { } } - public List getPhasedIn(UUID controllerId) { + public List getPhasedIn(Game game, UUID controllerId) { return field.values() .stream() - .filter(perm -> perm.getAbilities().containsKey(PhasingAbility.getInstance().getId()) + .filter(perm -> perm.hasAbility(PhasingAbility.getInstance(), game) && perm.isPhasedIn() && perm.isControlledBy(controllerId)) .collect(Collectors.toList()); } - public List getPhasedOut(UUID controllerId) { + public List getPhasedOut(Game game, UUID controllerId) { return field.values() .stream() .filter(perm -> !perm.isPhasedIn() && perm.isControlledBy(controllerId)) diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index da45f0341b..7999673d27 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -154,15 +154,14 @@ public interface Permanent extends Card, Controllable { String getValue(GameState state); - @Deprecated - void addAbility(Ability ability, Game game); - void addAbility(Ability ability, UUID sourceId, Game game); - void addAbility(Ability ability, UUID sourceId, Game game, boolean createNewId); - void removeAllAbilities(UUID sourceId, Game game); + void removeAbility(Ability abilityToRemove, UUID sourceId, Game game); + + void removeAbilities(List abilitiesToRemove, UUID sourceId, Game game); + void addLoyaltyUsed(); boolean canLoyaltyBeUsed(Game game); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentCard.java b/Mage/src/main/java/mage/game/permanent/PermanentCard.java index bb387139e6..f66b260eea 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentCard.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentCard.java @@ -53,7 +53,7 @@ public class PermanentCard extends PermanentImpl { if (game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getId()) != null) { game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + getId(), null); setTransformed(true); - TransformAbility.transform(this, getSecondCardFace(), game); + TransformAbility.transform(this, getSecondCardFace(), game, null); } } } diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index cee725a145..e093335625 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -351,62 +351,61 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { @Override public Abilities getAbilities() { - return abilities; + return super.getAbilities(); } @Override public Abilities getAbilities(Game game) { - return abilities; - } - - /** - * @param ability - * @param game - */ - @Override - public void addAbility(Ability ability, Game game) { - if (!abilities.containsKey(ability.getId())) { - Ability copyAbility = ability.copy(); - copyAbility.setControllerId(controllerId); - copyAbility.setSourceId(objectId); - if (game != null) { - game.getState().addAbility(copyAbility, this); - } - abilities.add(copyAbility); - } + return super.getAbilities(game); } @Override public void addAbility(Ability ability, UUID sourceId, Game game) { - addAbility(ability, sourceId, game, true); - } - - @Override - public void addAbility(Ability ability, UUID sourceId, Game game, boolean createNewId) { + // singleton abilities -- only one instance + // other abilities -- any amount of instances if (!abilities.containsKey(ability.getId())) { Ability copyAbility = ability.copy(); - if (createNewId) { - copyAbility.newId(); // needed so that source can get an ability multiple times (e.g. Raging Ravine) - } + copyAbility.newId(); // needed so that source can get an ability multiple times (e.g. Raging Ravine) copyAbility.setControllerId(controllerId); copyAbility.setSourceId(objectId); + // triggered abilities must be added to the state().triggers + // still as long as the prev. permanent is known to the LKI (e.g. Showstopper) so gained dies triggered ability will trigger game.getState().addAbility(copyAbility, sourceId, this); abilities.add(copyAbility); - } else if (!createNewId) { - // triggered abilities must be added to the state().triggerdAbilities - // still as long as the prev. permanent is known to the LKI (e.g. Showstopper) so gained dies triggered ability will trigger - if (!game.getBattlefield().containsPermanent(this.getId())) { - Ability copyAbility = ability.copy(); - copyAbility.setControllerId(controllerId); - copyAbility.setSourceId(objectId); - game.getState().addAbility(copyAbility, sourceId, this); - } } } @Override public void removeAllAbilities(UUID sourceId, Game game) { - getAbilities().clear(); + // TODO: what about triggered abilities? See addAbility above -- triggers adds to GameState + abilities.clear(); + } + + @Override + public void removeAbility(Ability abilityToRemove, UUID sourceId, Game game) { + if (abilityToRemove == null) { + return; + } + + // 112.10b Effects that remove an ability remove all instances of it. + List toRemove = new ArrayList<>(); + abilities.forEach(a -> { + if (a.isSameInstance(abilityToRemove)) { + toRemove.add(a); + } + }); + + // TODO: what about triggered abilities? See addAbility above -- triggers adds to GameState + toRemove.forEach(r -> abilities.remove(r)); + } + + @Override + public void removeAbilities(List abilitiesToRemove, UUID sourceId, Game game){ + if (abilitiesToRemove == null) { + return; + } + + abilitiesToRemove.forEach(a -> removeAbility(a, sourceId, game)); } @Override @@ -728,7 +727,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { game.fireEvent(new GameEvent(EventType.GAINED_CONTROL, objectId, objectId, controllerId)); return true; - } else if (isCopy()) {// Because the previous copied abilities can be from another controller chnage controller in any case for abilities + } else if (isCopy()) {// Because the previous copied abilities can be from another controller - change controller in any case for abilities this.getAbilities(game).setControllerId(controllerId); game.getContinuousEffects().setController(objectId, controllerId); } diff --git a/Mage/src/main/java/mage/game/permanent/PermanentToken.java b/Mage/src/main/java/mage/game/permanent/PermanentToken.java index ba809c2056..48c4f4bae6 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentToken.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentToken.java @@ -59,7 +59,7 @@ public class PermanentToken extends PermanentImpl { } else { // first time -> create ContinuousEffects only once for (Ability ability : token.getAbilities()) { - this.addAbility(ability, game); + this.addAbility(ability, null, game); } } this.abilities.setControllerId(this.controllerId); diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index e9e262711a..42c6ba27ec 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -524,8 +524,8 @@ public class Spell extends StackObjImpl implements Card { } @Override - public boolean hasAbility(UUID abilityId, Game game) { - return card.hasAbility(abilityId, game); + public boolean hasAbility(Ability ability, Game game) { + return card.hasAbility(ability, game); } @Override diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index cb4149bcfe..d7f64cac95 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -178,7 +178,7 @@ public class StackAbility extends StackObjImpl implements Ability { } @Override - public boolean hasAbility(UUID abilityId, Game game) { + public boolean hasAbility(Ability ability, Game game) { return false; } @@ -672,4 +672,17 @@ public class StackAbility extends StackObjImpl implements Ability { public Outcome getCustomOutcome() { return this.ability.getCustomOutcome(); } + + @Override + public boolean isSameInstance(Ability ability) { + // same instance (by mtg rules) = same object, ID or class+text (you can't check class only cause it can be different by params/text) + if (ability == null) { + return false; + } + + return (this == ability) + || (this.getId().equals(ability.getId())) + || (this.getOriginalId().equals(ability.getOriginalId())) + || (this.getClass() == ability.getClass() && this.getRule().equals(ability.getRule())); + } } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index f79bc6d165..5c8dd00ea5 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1759,8 +1759,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public void phasing(Game game) { //20091005 - 502.1 - List phasedOut = game.getBattlefield().getPhasedOut(playerId); - for (Permanent permanent : game.getBattlefield().getPhasedIn(playerId)) { + List phasedOut = game.getBattlefield().getPhasedOut(game, playerId); + for (Permanent permanent : game.getBattlefield().getPhasedIn(game, playerId)) { // 502.15i When a permanent phases out, any local enchantments or Equipment // attached to that permanent phase out at the same time. This alternate way of // phasing out is known as phasing out "indirectly." An enchantment or Equipment diff --git a/Mage/src/main/java/mage/util/functions/AbilityApplier.java b/Mage/src/main/java/mage/util/functions/AbilityApplier.java index abbc57d354..215ba18200 100644 --- a/Mage/src/main/java/mage/util/functions/AbilityApplier.java +++ b/Mage/src/main/java/mage/util/functions/AbilityApplier.java @@ -21,7 +21,7 @@ public class AbilityApplier extends ApplyToPermanent { @Override public boolean apply(Game game, Permanent permanent, Ability source, UUID copyToObjectId) { - permanent.addAbility(ability, game); + permanent.addAbility(ability, source.getSourceId(), game); return true; }