From daba01b4930cdc77a48fb077f4f30737cabd53f9 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:24:04 +0200 Subject: [PATCH 01/16] tweak Felix to test if change work --- Mage.Sets/src/mage/cards/f/FelixFiveBoots.java | 16 ++++++++-------- .../triggers/damage/FelixFiveBootsTest.java | 2 -- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Mage.Sets/src/mage/cards/f/FelixFiveBoots.java b/Mage.Sets/src/mage/cards/f/FelixFiveBoots.java index c4e9fb401f08..3ac18d6e2751 100644 --- a/Mage.Sets/src/mage/cards/f/FelixFiveBoots.java +++ b/Mage.Sets/src/mage/cards/f/FelixFiveBoots.java @@ -2,6 +2,8 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.TriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.ReplacementEffectImpl; @@ -91,14 +93,12 @@ public boolean applies(GameEvent event, Ability source, Game game) { if (sourceEvent instanceof DamagedEvent) { return checkDamagedEvent((DamagedEvent) sourceEvent, source.getControllerId(), game); } else if (sourceEvent instanceof BatchEvent) { - for (Object singleEventAsObject : ((BatchEvent) sourceEvent).getEvents()) { - if (singleEventAsObject instanceof DamagedEvent - && checkDamagedEvent((DamagedEvent) singleEventAsObject, source.getControllerId(), game) - ) { - // For batch events, if one of the event inside the condition match the condition, - // the effect applies to the whole batch events. - return true; - } + TriggeredAbility sourceTrigger = numberOfTriggersEvent.getSourceTrigger(); + if (sourceTrigger instanceof BatchTriggeredAbility) { + return ((BatchTriggeredAbility) sourceTrigger) + .filterBatchEvent(sourceEvent, game) + .anyMatch(singleEvent -> singleEvent instanceof DamagedEvent + && checkDamagedEvent((DamagedEvent) singleEvent, source.getControllerId(), game)); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/FelixFiveBootsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/FelixFiveBootsTest.java index 0b53ba6526e1..6176bada3215 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/FelixFiveBootsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/FelixFiveBootsTest.java @@ -3,7 +3,6 @@ import mage.constants.PhaseStep; import mage.constants.Zone; import mage.counters.CounterType; -import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -101,7 +100,6 @@ public void testBatchEvent() { } @Test - @Ignore // see #12095 public void testSelectRightPartOfBatch() { setStrictChooseMode(true); From c553e5e0ac405b4279085cd641a12103d17d118d Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:25:00 +0200 Subject: [PATCH 02/16] refactor a couple Batch TriggerAbility --- .../src/mage/cards/o/OliviasAttendants.java | 19 +++++++++++--- .../mage/abilities/BatchTriggeredAbility.java | 25 +++++++++++++++++++ ...sCombatDamageEquippedTriggeredAbility.java | 21 ++++++++++++---- .../main/java/mage/game/events/GameEvent.java | 21 ++++++++++++++-- 4 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/BatchTriggeredAbility.java diff --git a/Mage.Sets/src/mage/cards/o/OliviasAttendants.java b/Mage.Sets/src/mage/cards/o/OliviasAttendants.java index 294986114fb8..f23c648e6865 100644 --- a/Mage.Sets/src/mage/cards/o/OliviasAttendants.java +++ b/Mage.Sets/src/mage/cards/o/OliviasAttendants.java @@ -2,6 +2,7 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.mana.ManaCostsImpl; @@ -15,11 +16,13 @@ import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchAllEvent; +import mage.game.events.DamagedEvent; import mage.game.events.GameEvent; import mage.game.permanent.token.BloodToken; import mage.target.common.TargetAnyTarget; import java.util.UUID; +import java.util.stream.Stream; /** * @author TheElk801 @@ -55,7 +58,7 @@ public OliviasAttendants copy() { } } -class OliviasAttendantsTriggeredAbility extends TriggeredAbilityImpl { +class OliviasAttendantsTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { OliviasAttendantsTriggeredAbility() { super(Zone.BATTLEFIELD, null); @@ -76,11 +79,19 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - int amount = ((DamagedBatchAllEvent) event) + public Stream filterBatchEvent(GameEvent event, Game game) { + if (!checkEventType(event, game)) { + return Stream.empty(); + } + return ((DamagedBatchAllEvent) event) .getEvents() .stream() - .filter(e -> e.getAttackerId().equals(this.getSourceId())) + .filter(e -> e.getAttackerId().equals(this.getSourceId())); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) .mapToInt(GameEvent::getAmount) .sum(); if (amount < 1) { diff --git a/Mage/src/main/java/mage/abilities/BatchTriggeredAbility.java b/Mage/src/main/java/mage/abilities/BatchTriggeredAbility.java new file mode 100644 index 000000000000..bec0f0d6fcab --- /dev/null +++ b/Mage/src/main/java/mage/abilities/BatchTriggeredAbility.java @@ -0,0 +1,25 @@ + +package mage.abilities; + +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.stream.Stream; + +/** + * Batch triggers (e.g. 'When... one or more ..., ') + * are triggers that require a little more details. + * + * @author Susucr + */ +public interface BatchTriggeredAbility extends TriggeredAbility { + + /** + * filter a batch event into all it's sub events that are relevant. + *

+ * Properly filtering is required for further analysis of trigger + event, + * for instance for complex NUMBER_OF_TRIGGERS triggers. + * e.g. Umezawa's Jitte + Felix Five-Boots. + */ + public Stream filterBatchEvent(GameEvent event, Game game); +} diff --git a/Mage/src/main/java/mage/abilities/common/DealsCombatDamageEquippedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealsCombatDamageEquippedTriggeredAbility.java index 9620d538d1c6..a81ceb14da2d 100644 --- a/Mage/src/main/java/mage/abilities/common/DealsCombatDamageEquippedTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealsCombatDamageEquippedTriggeredAbility.java @@ -1,5 +1,6 @@ package mage.abilities.common; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.Zone; @@ -9,10 +10,12 @@ import mage.game.events.GameEvent; import mage.game.permanent.Permanent; +import java.util.stream.Stream; + /** * @author TheElk801, xenohedron */ -public class DealsCombatDamageEquippedTriggeredAbility extends TriggeredAbilityImpl { +public class DealsCombatDamageEquippedTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public DealsCombatDamageEquippedTriggeredAbility(Effect effect) { this(effect, false); @@ -38,16 +41,24 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { + public Stream filterBatchEvent(GameEvent event, Game game) { + if (!checkEventType(event, game)) { + return Stream.empty(); + } Permanent sourcePermanent = getSourcePermanentOrLKI(game); if (sourcePermanent == null || sourcePermanent.getAttachedTo() == null) { - return false; + return Stream.empty(); } - int amount = ((DamagedBatchAllEvent) event) + return ((DamagedBatchAllEvent) event) .getEvents() .stream() .filter(DamagedEvent::isCombatDamage) - .filter(e -> e.getAttackerId().equals(sourcePermanent.getAttachedTo())) + .filter(e -> e.getAttackerId().equals(sourcePermanent.getAttachedTo())); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) .mapToInt(GameEvent::getAmount) .sum(); if (amount < 1) { diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 24ed059b6b20..36b6ccfdeab5 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -146,7 +146,7 @@ targetId the id of the damaged player (playerId won't work for batch) /* DAMAGED_BATCH_FOR_ALL includes all damage events, both permanent damage and player damage, in single batch event */ - DAMAGED_BATCH_FOR_ALL, + DAMAGED_BATCH_FOR_ALL(true), /* DAMAGED_BATCH_FIRED * Does not contain any info on damage events, and can fire even when all damage is prevented. * Fire any time a DAMAGED_BATCH_FOR_ALL could have fired (combat & noncombat). @@ -663,7 +663,24 @@ playerId owner of the plotted card (the one able to cast the card) */ BECOME_PLOTTED, //custom events - CUSTOM_EVENT + CUSTOM_EVENT; + + private final boolean batch; + + EventType() { + this(false); + } + + EventType(boolean isBatch) { + this.batch = isBatch; + } + + /** + * Is this a batch event type? + */ + public boolean isBatch() { + return this.batch; + } } public GameEvent(EventType type, UUID targetId, Ability source, UUID playerId) { From 1a52025a91614e18276352602cc117a7c39069bc Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:10:25 +0200 Subject: [PATCH 03/16] add verify test --- .../java/mage/verify/VerifyCardDataTest.java | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index a9178b75f165..dfd3016d93a4 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -4,9 +4,7 @@ import mage.MageObject; import mage.Mana; import mage.ObjectColor; -import mage.abilities.Ability; -import mage.abilities.AbilityImpl; -import mage.abilities.Mode; +import mage.abilities.*; import mage.abilities.common.*; import mage.abilities.condition.Condition; import mage.abilities.costs.Cost; @@ -35,6 +33,7 @@ import mage.game.command.Dungeon; import mage.game.command.Plane; import mage.game.draft.DraftCube; +import mage.game.events.GameEvent; import mage.game.permanent.token.Token; import mage.game.permanent.token.TokenImpl; import mage.game.permanent.token.custom.CreatureToken; @@ -62,6 +61,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author JayDi85 @@ -1722,6 +1722,10 @@ private void compareClassRecursive(Object obj1, Object obj2, Card originalCard, } } + if (obj1 instanceof Ability) { + checkAbility(originalCard, (Ability) obj1, msg); + } + //System.out.println(msg); Class class1 = obj1.getClass(); Class class2 = obj2.getClass(); @@ -1856,6 +1860,35 @@ private void compareClassRecursive(Object obj1, Object obj2, Card originalCard, } } + // One (fake) event per batch event type + private static Set fakeBatchEvents; + + static { + fakeBatchEvents = Stream + .of(GameEvent.EventType.values()) + .filter(GameEvent.EventType::isBatch) + .map(eventType -> new GameEvent(eventType, null, null, null)) + .collect(Collectors.toSet()); + } + + /** + * Perform checks on abilities + */ + private void checkAbility(Card originalCard, Ability ability, String msg) { + if (ability instanceof TriggeredAbility) { + // Checks that non-batched triggered ability don't accept batch events. + if (!(ability instanceof BatchTriggeredAbility)) { + for (GameEvent event : fakeBatchEvents) { + if (((TriggeredAbility) ability).checkEventType(event, null)) { + fail(originalCard, "checkAbility", "unexpected non-BatchTriggeredAbility accepting " + + event.getType() + " " + msg + "<" + ability.getClass() + ">"); + } + } + } + } + } + + private void checkSubtypes(Card card, MtgJsonCard ref) { if (skipListHaveName(SKIP_LIST_SUBTYPE, card.getExpansionSetCode(), card.getName())) { return; From de841d561be563b8b4ab561d72918a34955394ab Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:23:06 +0200 Subject: [PATCH 04/16] adjust other DAMAGED_BATCH_FOR_ALL triggers --- .../src/mage/cards/z/ZurgoAndOjutai.java | 25 +++++++++++++------ .../DealsCombatDamageTriggeredAbility.java | 19 +++++++++++--- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java b/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java index e033dbe1a283..bb4d443f73f9 100644 --- a/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java +++ b/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java @@ -2,6 +2,7 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.SourceEnteredThisTurnCondition; @@ -32,6 +33,7 @@ import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author TheElk801 @@ -73,7 +75,7 @@ public ZurgoAndOjutai copy() { } } -class ZurgoAndOjutaiTriggeredAbility extends TriggeredAbilityImpl { +class ZurgoAndOjutaiTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { ZurgoAndOjutaiTriggeredAbility() { super(Zone.BATTLEFIELD, new LookLibraryAndPickControllerEffect(3, 1, PutCards.HAND, PutCards.BOTTOM_ANY)); @@ -96,12 +98,15 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - List permanents = ((DamagedBatchAllEvent) event) + public Stream filterBatchEvent(GameEvent event, Game game) { + if (!(event instanceof DamagedBatchAllEvent)) { + return Stream.empty(); + } + return ((DamagedBatchAllEvent) event) .getEvents() .stream() .filter(DamagedEvent::isCombatDamage) - .map(e -> { + .filter(e -> { Permanent permanent = game.getPermanent(e.getSourceId()); Permanent defender = game.getPermanent(e.getTargetId()); if (permanent != null @@ -109,10 +114,16 @@ public boolean checkTrigger(GameEvent event, Game game) { && permanent.isControlledBy(this.getControllerId()) && ((defender != null && defender.isBattle(game)) || game.getPlayer(e.getTargetId()) != null)) { - return permanent; + return true; } - return null; - }) + return false; + }); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + List permanents = filterBatchEvent(event, game) + .map(e -> game.getPermanent(e.getSourceId())) .filter(Objects::nonNull) .collect(Collectors.toList()); if (permanents.isEmpty()) { diff --git a/Mage/src/main/java/mage/abilities/common/DealsCombatDamageTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealsCombatDamageTriggeredAbility.java index 5d85fbe626a2..690fdbd08eef 100644 --- a/Mage/src/main/java/mage/abilities/common/DealsCombatDamageTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealsCombatDamageTriggeredAbility.java @@ -1,5 +1,6 @@ package mage.abilities.common; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.Zone; @@ -8,6 +9,8 @@ import mage.game.events.DamagedEvent; import mage.game.events.GameEvent; +import java.util.stream.Stream; + /** * This triggers only once for each combat damage step the source creature deals damage. * So a creature blocked by two creatures and dealing damage to both blockers in the same @@ -15,7 +18,7 @@ * * @author LevelX, xenohedron */ -public class DealsCombatDamageTriggeredAbility extends TriggeredAbilityImpl { +public class DealsCombatDamageTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public DealsCombatDamageTriggeredAbility(Effect effect, boolean optional) { super(Zone.BATTLEFIELD, effect, optional); @@ -38,12 +41,20 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - int amount = ((DamagedBatchAllEvent) event) + public Stream filterBatchEvent(GameEvent event, Game game) { + if (!(event instanceof DamagedBatchAllEvent)) { + return Stream.empty(); + } + return ((DamagedBatchAllEvent) event) .getEvents() .stream() .filter(DamagedEvent::isCombatDamage) - .filter(e -> e.getAttackerId().equals(getSourceId())) + .filter(e -> e.getAttackerId().equals(getSourceId())); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) .mapToInt(GameEvent::getAmount) .sum(); if (amount < 1) { From 660d05abdaa02c6caf0cb0aeefd2a7aaa9faa1b7 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:45:39 +0200 Subject: [PATCH 05/16] refactor all batch triggers --- .../mage/cards/a/AegarTheFreezingFlame.java | 25 ++-- .../src/mage/cards/a/AngelheartVial.java | 75 +---------- Mage.Sets/src/mage/cards/a/Arcbond.java | 35 +++-- Mage.Sets/src/mage/cards/b/BindingAgony.java | 16 ++- .../src/mage/cards/b/BlazingSunsteel.java | 96 +++----------- Mage.Sets/src/mage/cards/b/BloodHound.java | 50 +------ .../mage/cards/b/BloodSpatterAnalysis.java | 31 +++-- .../mage/cards/b/BreechesBrazenPlunderer.java | 29 ++-- .../src/mage/cards/c/ChandrasSpitfire.java | 26 +++- .../src/mage/cards/c/ContaminantGrafter.java | 36 +++-- .../src/mage/cards/c/ContestedGameBall.java | 25 +++- .../src/mage/cards/d/DarienKingOfKjeldor.java | 81 ++---------- Mage.Sets/src/mage/cards/d/DonnaNoble.java | 68 ++++++---- Mage.Sets/src/mage/cards/d/DruidsCall.java | 18 ++- .../mage/cards/e/ExpeditedInheritance.java | 37 +++++- .../src/mage/cards/f/FallOfCairAndros.java | 33 +++-- Mage.Sets/src/mage/cards/f/Fiendlash.java | 84 ++---------- Mage.Sets/src/mage/cards/f/FilthyCur.java | 57 ++------ .../src/mage/cards/f/ForthEorlingas.java | 41 +++--- .../src/mage/cards/f/FranticScapegoat.java | 41 ++++-- Mage.Sets/src/mage/cards/f/FrozenSolid.java | 12 +- .../src/mage/cards/h/HordewingSkaab.java | 45 ++++--- Mage.Sets/src/mage/cards/h/HotSoup.java | 63 ++------- .../src/mage/cards/h/HowlpackAvenger.java | 21 ++- .../mage/cards/i/ImodaneThePyrohammer.java | 62 +++------ .../src/mage/cards/i/InnocentBystander.java | 22 ++- .../mage/cards/k/KambalProfiteeringMayor.java | 34 +++-- .../src/mage/cards/k/KayaSpiritsJustice.java | 66 +++++++-- .../mage/cards/k/KazarovSengirPureblood.java | 36 +++-- .../mage/cards/l/LaeliaTheBladeReforged.java | 30 +++-- Mage.Sets/src/mage/cards/l/Lich.java | 81 +++--------- .../src/mage/cards/l/LivingArtifact.java | 83 ++---------- .../src/mage/cards/m/MagmaticGalleon.java | 27 ++-- .../cards/m/MalcolmKeenEyedNavigator.java | 30 +++-- .../src/mage/cards/m/MindbladeRender.java | 51 ++++--- Mage.Sets/src/mage/cards/m/MireBlight.java | 14 +- Mage.Sets/src/mage/cards/m/MortalWound.java | 15 ++- .../mage/cards/o/ObNixilisCaptiveKingpin.java | 56 ++++---- .../src/mage/cards/o/OliviasAttendants.java | 3 - .../src/mage/cards/p/PhyrexianNegator.java | 62 ++------- .../src/mage/cards/p/PhyrexianTotem.java | 110 ++++++--------- Mage.Sets/src/mage/cards/p/PiousWarrior.java | 89 ++----------- .../src/mage/cards/p/PopularEntertainer.java | 36 +++-- Mage.Sets/src/mage/cards/r/RaggedVeins.java | 14 +- Mage.Sets/src/mage/cards/r/Repercussion.java | 22 ++- .../mage/cards/r/RisonaAsariCommander.java | 22 ++- Mage.Sets/src/mage/cards/r/RiteOfPassage.java | 40 ++++-- .../mage/cards/s/SatoruTheInfiltrator.java | 20 +-- Mage.Sets/src/mage/cards/s/SoulLink.java | 16 ++- .../src/mage/cards/s/SoulsOfTheFaultless.java | 62 ++------- .../src/mage/cards/s/SowerOfDiscord.java | 40 ++++-- .../src/mage/cards/s/SpitefulShadows.java | 7 +- .../src/mage/cards/s/SunCrownedHunters.java | 2 +- Mage.Sets/src/mage/cards/s/SunDroplet.java | 51 +------ .../src/mage/cards/s/SwarmbornGiant.java | 22 ++- .../mage/cards/t/TheMillenniumCalendar.java | 37 +++--- .../src/mage/cards/t/TheRavensWarning.java | 42 +++--- .../src/mage/cards/t/ToralfGodOfFury.java | 24 ++-- .../src/mage/cards/v/VengefulPharaoh.java | 125 +++++++----------- Mage.Sets/src/mage/cards/w/WallOfEssence.java | 82 ++---------- Mage.Sets/src/mage/cards/w/WallOfSouls.java | 48 +------ Mage.Sets/src/mage/cards/w/WarElemental.java | 63 ++++----- .../src/mage/cards/w/WildfireElemental.java | 18 ++- .../src/mage/cards/w/WrathfulRaptors.java | 40 ++++-- .../src/mage/cards/w/WrathfulRedDragon.java | 31 ++++- .../src/mage/cards/z/ZurgoAndOjutai.java | 11 +- .../cards/single/cmr/BlazingSunsteelTest.java | 42 ++++++ .../damage/ObNixilisCaptiveKingpinTest.java | 5 +- .../mage/abilities/BatchTriggeredAbility.java | 6 +- ...ecomesTappedOneOrMoreTriggeredAbility.java | 28 +++- ...ombatDamageDealtToYouTriggeredAbility.java | 34 +++-- ...sCombatDamageEquippedTriggeredAbility.java | 3 - .../DealsCombatDamageTriggeredAbility.java | 3 - .../DealtDamageAttachedTriggeredAbility.java | 68 ---------- .../DealtDamageToSourceTriggeredAbility.java | 24 +++- ...DiesOneOrMoreCreatureTriggeredAbility.java | 23 +++- ...altCombatDamageSourceTriggeredAbility.java | 38 ++++++ ...IsDealtDamageAttachedTriggeredAbility.java | 95 +++++++++++++ .../IsDealtDamageYouTriggeredAbility.java | 58 ++++++++ .../OneOrMoreDealDamageTriggeredAbility.java | 28 ++-- ...nalInterveningIfBatchTriggeredAbility.java | 36 +++++ .../java/mage/game/events/BatchEvent.java | 6 + .../main/java/mage/game/events/GameEvent.java | 16 +-- 83 files changed, 1551 insertions(+), 1753 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/BlazingSunsteelTest.java delete mode 100644 Mage/src/main/java/mage/abilities/common/DealtDamageAttachedTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/abilities/common/IsDealtCombatDamageSourceTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/abilities/common/IsDealtDamageAttachedTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/abilities/common/IsDealtDamageYouTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/abilities/decorator/ConditionalInterveningIfBatchTriggeredAbility.java diff --git a/Mage.Sets/src/mage/cards/a/AegarTheFreezingFlame.java b/Mage.Sets/src/mage/cards/a/AegarTheFreezingFlame.java index ab3c5d2ed065..da9dfd275721 100644 --- a/Mage.Sets/src/mage/cards/a/AegarTheFreezingFlame.java +++ b/Mage.Sets/src/mage/cards/a/AegarTheFreezingFlame.java @@ -3,6 +3,7 @@ import mage.MageInt; import mage.MageObject; import mage.MageObjectReference; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.cards.CardImpl; @@ -11,10 +12,12 @@ import mage.game.Game; import mage.game.events.DamagedBatchForOnePermanentEvent; import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.watchers.Watcher; import java.util.*; +import java.util.stream.Stream; /** * @author TheElk801 @@ -44,7 +47,7 @@ public AegarTheFreezingFlame copy() { } } -class AegarTheFreezingFlameTriggeredAbility extends TriggeredAbilityImpl { +class AegarTheFreezingFlameTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { AegarTheFreezingFlameTriggeredAbility() { super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1)); @@ -61,18 +64,20 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event; - - int excess = dEvent.getEvents() + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() .stream() + .filter(e -> e.getExcess() > 0) + .filter(e -> game.getOpponents(getControllerId()).contains(game.getControllerId(e.getTargetId()))); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int excessDamage = filterBatchEvent(event, game) .mapToInt(DamagedEvent::getExcess) .sum(); - - boolean controlledByOpponent = - game.getOpponents(getControllerId()).contains(game.getControllerId(event.getTargetId())); - - if (excess < 1 || !controlledByOpponent) { + if (excessDamage <= 0) { return false; } AegarTheFreezingFlameWatcher watcher = game.getState().getWatcher(AegarTheFreezingFlameWatcher.class); diff --git a/Mage.Sets/src/mage/cards/a/AngelheartVial.java b/Mage.Sets/src/mage/cards/a/AngelheartVial.java index 9c8968ee7ff8..7ed2b9307a90 100644 --- a/Mage.Sets/src/mage/cards/a/AngelheartVial.java +++ b/Mage.Sets/src/mage/cards/a/AngelheartVial.java @@ -2,23 +2,20 @@ package mage.cards.a; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.IsDealtDamageYouTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.RemoveCountersSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.Zone; import mage.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import java.util.UUID; @@ -31,7 +28,9 @@ public AngelheartVial(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{5}"); // Whenever you're dealt damage, you may put that many charge counters on Angelheart Vial. - this.addAbility(new AngelheartVialTriggeredAbility()); + this.addAbility(new IsDealtDamageYouTriggeredAbility( + new AddCountersSourceEffect(CounterType.CHARGE.createInstance(), SavedDamageValue.MANY, true), false + )); // {2}, {tap}, Remove four charge counters from Angelheart Vial: You gain 2 life and draw a card. Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new GainLifeEffect(2), new GenericManaCost(2)); @@ -49,64 +48,4 @@ private AngelheartVial(final AngelheartVial card) { public AngelheartVial copy() { return new AngelheartVial(this); } -} - -class AngelheartVialTriggeredAbility extends TriggeredAbilityImpl { - - public AngelheartVialTriggeredAbility() { - super(Zone.BATTLEFIELD, new AngelheartVialEffect(), true); - } - - private AngelheartVialTriggeredAbility(final AngelheartVialTriggeredAbility ability) { - super(ability); - } - - @Override - public AngelheartVialTriggeredAbility copy() { - return new AngelheartVialTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getTargetId().equals(this.getControllerId())) { - this.getEffects().get(0).setValue("damageAmount", event.getAmount()); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you're dealt damage, you may put that many charge counters on {this}."; - } -} - -class AngelheartVialEffect extends OneShotEffect { - - AngelheartVialEffect() { - super(Outcome.Benefit); - } - - private AngelheartVialEffect(final AngelheartVialEffect effect) { - super(effect); - } - - @Override - public AngelheartVialEffect copy() { - return new AngelheartVialEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent != null) { - permanent.addCounters(CounterType.CHARGE.createInstance((Integer) this.getValue("damageAmount")), source.getControllerId(), source, game); - } - return true; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/Arcbond.java b/Mage.Sets/src/mage/cards/a/Arcbond.java index a7e99622ba0e..0f5ec9f32e03 100644 --- a/Mage.Sets/src/mage/cards/a/Arcbond.java +++ b/Mage.Sets/src/mage/cards/a/Arcbond.java @@ -1,10 +1,10 @@ package mage.cards.a; -import java.util.UUID; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.Effect; @@ -21,18 +21,22 @@ import mage.filter.predicate.Predicates; import mage.filter.predicate.permanent.PermanentIdPredicate; import mage.game.Game; +import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author LevelX2 */ public final class Arcbond extends CardImpl { public Arcbond(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{2}{R}"); + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{R}"); // Choose target creature. Whenever that creature is dealt damage this turn, it deals that much damage to each other creature and each player. this.getSpellAbility().addEffect(new CreateDelayedTriggeredAbilityEffect(new ArcbondDelayedTriggeredAbility())); @@ -49,7 +53,7 @@ public Arcbond copy() { } } -class ArcbondDelayedTriggeredAbility extends DelayedTriggeredAbility { +class ArcbondDelayedTriggeredAbility extends DelayedTriggeredAbility implements BatchTriggeredAbility { MageObjectReference targetObject; @@ -87,16 +91,25 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(e -> e.getTargetId().equals(targetObject.getSourceId()) && targetObject.getPermanentOrLKIBattlefield(game) != null) + .filter(e -> e.getAmount() > 0); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - if (event.getTargetId().equals(targetObject.getSourceId()) - && targetObject.getPermanentOrLKIBattlefield(game) != null) { - for (Effect effect : this.getEffects()) { - effect.setValue("damage", event.getAmount()); - } - return true; + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPermanentEvent::getAmount) + .sum(); + if (amount <= 0) { + return false; } - return false; + getEffects().setValue("damage", amount); + return true; } @Override diff --git a/Mage.Sets/src/mage/cards/b/BindingAgony.java b/Mage.Sets/src/mage/cards/b/BindingAgony.java index c8c3ab3cac33..b9fccebbe28f 100644 --- a/Mage.Sets/src/mage/cards/b/BindingAgony.java +++ b/Mage.Sets/src/mage/cards/b/BindingAgony.java @@ -1,25 +1,27 @@ package mage.cards.b; -import java.util.UUID; -import mage.abilities.common.DealtDamageAttachedTriggeredAbility; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.DamageAttachedControllerEffect; import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * * @author LoneFox */ public final class BindingAgony extends CardImpl { public BindingAgony(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{1}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}"); this.subtype.add(SubType.AURA); // Enchant creature @@ -29,7 +31,9 @@ public BindingAgony(UUID ownerId, CardSetInfo setInfo) { this.addAbility(new EnchantAbility(auraTarget)); // Whenever enchanted creature is dealt damage, Binding Agony deals that much damage to that creature's controller. - this.addAbility(new DealtDamageAttachedTriggeredAbility(new DamageAttachedControllerEffect(SavedDamageValue.MUCH), false)); + this.addAbility(new IsDealtDamageAttachedTriggeredAbility( + new DamageAttachedControllerEffect(SavedDamageValue.MUCH), false, "enchanted" + )); } private BindingAgony(final BindingAgony card) { diff --git a/Mage.Sets/src/mage/cards/b/BlazingSunsteel.java b/Mage.Sets/src/mage/cards/b/BlazingSunsteel.java index 39b2519aa44b..a4842a9d3158 100644 --- a/Mage.Sets/src/mage/cards/b/BlazingSunsteel.java +++ b/Mage.Sets/src/mage/cards/b/BlazingSunsteel.java @@ -1,30 +1,25 @@ package mage.cards.b; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.dynamicvalue.common.OpponentsCount; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.continuous.BoostEquippedEffect; import mage.abilities.keyword.EquipAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; -import mage.game.events.DamagedEvent; -import mage.game.events.DamagedBatchForPermanentsEvent; -import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetAnyTarget; +import mage.target.common.TargetControlledCreaturePermanent; import java.util.UUID; -import mage.abilities.costs.mana.GenericManaCost; -import mage.target.common.TargetControlledCreaturePermanent; /** * @author TheElk801 @@ -32,7 +27,7 @@ public final class BlazingSunsteel extends CardImpl { public BlazingSunsteel(UUID ownerId, CardSetInfo setInfo) { - super(ownerId, setInfo, new CardType[] { CardType.ARTIFACT }, "{1}{R}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{R}"); this.subtype.add(SubType.EQUIPMENT); @@ -40,9 +35,12 @@ public BlazingSunsteel(UUID ownerId, CardSetInfo setInfo) { this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect(OpponentsCount.instance, StaticValue.get(0)) .setText("equipped creature gets +1/+0 for each opponent you have"))); - // Whenever equipped creature is dealt damage, it deals that much damage to any - // target. - this.addAbility(new BlazingSunsteelTriggeredAbility()); + // Whenever equipped creature is dealt damage, it deals that much damage to any target. + Ability ability = new IsDealtDamageAttachedTriggeredAbility( + Zone.BATTLEFIELD, new BlazingSunsteelEffect(), false, "equipped", SetTargetPointer.PERMANENT + ); + ability.addTarget(new TargetAnyTarget()); + this.addAbility(ability); // Equip {4} this.addAbility(new EquipAbility(Outcome.BoostCreature, new GenericManaCost(4), new TargetControlledCreaturePermanent(), false)); @@ -58,70 +56,11 @@ public BlazingSunsteel copy() { } } -class BlazingSunsteelTriggeredAbility extends TriggeredAbilityImpl { - - BlazingSunsteelTriggeredAbility() { - super(Zone.BATTLEFIELD, new BlazingSunsteelEffect(), false); - this.addTarget(new TargetAnyTarget()); - } - - private BlazingSunsteelTriggeredAbility(final BlazingSunsteelTriggeredAbility ability) { - super(ability); - } - - @Override - public BlazingSunsteelTriggeredAbility copy() { - return new BlazingSunsteelTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PERMANENTS; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Permanent equipment = game.getPermanent(this.getSourceId()); - if (equipment == null) { - return false; - } - - UUID attachedCreature = equipment.getAttachedTo(); - if (attachedCreature == null) { - return false; - } - - int damage = 0; - DamagedBatchForPermanentsEvent dEvent = (DamagedBatchForPermanentsEvent) event; - for (DamagedEvent damagedEvent : dEvent.getEvents()) { - UUID targetID = damagedEvent.getTargetId(); - if (targetID == null) { - continue; - } - - if (targetID == attachedCreature) { - damage += damagedEvent.getAmount(); - } - } - - if (damage > 0) { - this.getEffects().setValue("equipped", attachedCreature); - this.getEffects().setValue("damage", damage); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever equipped creature is dealt damage, it deals that much damage to any target."; - } -} - class BlazingSunsteelEffect extends OneShotEffect { BlazingSunsteelEffect() { super(Outcome.Benefit); + staticText = "it deals that much damage to any target"; } private BlazingSunsteelEffect(final BlazingSunsteelEffect effect) { @@ -135,13 +74,12 @@ public BlazingSunsteelEffect copy() { @Override public boolean apply(Game game, Ability source) { - Permanent creature = game.getPermanentOrLKIBattlefield((UUID) getValue("equipped")); - Integer damage = (Integer)getValue("damage"); - - if (creature == null || damage == null || damage < 1) { + Permanent creature = getTargetPointer().getFirstTargetPermanentOrLKI(game, source); + int damage = SavedDamageValue.MUCH.calculate(game, source, this); + if (creature == null || damage <= 0) { return false; } - + Permanent permanent = game.getPermanent(source.getFirstTarget()); if (permanent != null) { permanent.damage(damage, creature.getId(), source, game); diff --git a/Mage.Sets/src/mage/cards/b/BloodHound.java b/Mage.Sets/src/mage/cards/b/BloodHound.java index 942a67c4953a..c02df2d805d0 100644 --- a/Mage.Sets/src/mage/cards/b/BloodHound.java +++ b/Mage.Sets/src/mage/cards/b/BloodHound.java @@ -1,8 +1,9 @@ package mage.cards.b; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.IsDealtDamageYouTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.RemoveAllCountersSourceEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; @@ -10,10 +11,7 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.TargetController; -import mage.constants.Zone; import mage.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; import java.util.UUID; @@ -30,7 +28,9 @@ public BloodHound(UUID ownerId, CardSetInfo setInfo) { this.toughness = new MageInt(1); // Whenever you're dealt damage, you may put that many +1/+1 counters on Blood Hound. - this.addAbility(new BloodHoundTriggeredAbility()); + this.addAbility(new IsDealtDamageYouTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance(), SavedDamageValue.MANY, true), true + )); // At the beginning of your end step, remove all +1/+1 counters from Blood Hound. this.addAbility(new BeginningOfEndStepTriggeredAbility( @@ -48,42 +48,4 @@ private BloodHound(final BloodHound card) { public BloodHound copy() { return new BloodHound(this); } -} - -class BloodHoundTriggeredAbility extends TriggeredAbilityImpl { - - BloodHoundTriggeredAbility() { - super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance()), true); - } - - private BloodHoundTriggeredAbility(final BloodHoundTriggeredAbility ability) { - super(ability); - } - - @Override - public BloodHoundTriggeredAbility copy() { - return new BloodHoundTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getTargetId().equals(this.getControllerId()) && event.getAmount() > 0) { - this.getEffects().clear(); - if (event.getAmount() > 0) { - this.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(event.getAmount()))); - } - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you're dealt damage, you may put that many +1/+1 counters on {this}."; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/b/BloodSpatterAnalysis.java b/Mage.Sets/src/mage/cards/b/BloodSpatterAnalysis.java index 592cb644a0c8..24f2bc6e1321 100644 --- a/Mage.Sets/src/mage/cards/b/BloodSpatterAnalysis.java +++ b/Mage.Sets/src/mage/cards/b/BloodSpatterAnalysis.java @@ -1,6 +1,7 @@ package mage.cards.b; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.delayed.ReflexiveTriggeredAbility; @@ -22,11 +23,12 @@ import mage.game.events.GameEvent; import mage.game.events.ZoneChangeBatchEvent; import mage.game.events.ZoneChangeEvent; -import mage.game.permanent.Permanent; import mage.target.common.TargetCardInYourGraveyard; import mage.target.common.TargetOpponentsCreaturePermanent; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; /** * @author Cguy7777 @@ -57,7 +59,7 @@ public BloodSpatterAnalysis copy() { } } -class BloodSpatterAnalysisTriggeredAbility extends TriggeredAbilityImpl { +class BloodSpatterAnalysisTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { BloodSpatterAnalysisTriggeredAbility() { super(Zone.BATTLEFIELD, new MillCardsControllerEffect(1)); @@ -83,17 +85,24 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((ZoneChangeBatchEvent) event) + .getEvents() + .stream() + .filter(ZoneChangeEvent::isDiesEvent) + .filter(e -> Optional + .of(e) + .map(ZoneChangeEvent::getTargetId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> p.isCreature(game)) + .isPresent() + ); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - for (ZoneChangeEvent zEvent : ((ZoneChangeBatchEvent) event).getEvents()) { - if (zEvent.isDiesEvent()) { - Permanent permanent = game.getPermanentOrLKIBattlefield(zEvent.getTargetId()); - if (permanent != null && permanent.isCreature(game)) { - return true; - } - } - } - return false; + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/b/BreechesBrazenPlunderer.java b/Mage.Sets/src/mage/cards/b/BreechesBrazenPlunderer.java index 088fbd4f3976..1a63cc55187b 100644 --- a/Mage.Sets/src/mage/cards/b/BreechesBrazenPlunderer.java +++ b/Mage.Sets/src/mage/cards/b/BreechesBrazenPlunderer.java @@ -2,6 +2,7 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.MenaceAbility; @@ -11,6 +12,7 @@ import mage.game.Game; import mage.game.events.DamagedBatchForPlayersEvent; import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; @@ -20,6 +22,8 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author TheElk801 @@ -55,7 +59,7 @@ public BreechesBrazenPlunderer copy() { } } -class BreechesBrazenPlundererTriggeredAbility extends TriggeredAbilityImpl { +class BreechesBrazenPlundererTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { BreechesBrazenPlundererTriggeredAbility() { super(Zone.BATTLEFIELD, null); @@ -70,17 +74,24 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PLAYERS; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPlayersEvent) event) + .getEvents() + .stream() + .filter(e -> { + Permanent permanent = game.getPermanent(e.getSourceId()); + return permanent != null + && permanent.isControlledBy(getControllerId()) + && permanent.hasSubtype(SubType.PIRATE, game) + && game.getOpponents(getControllerId()).contains(e.getTargetId()); + }); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { Set opponents = new HashSet<>(); - for (DamagedEvent damagedEvent : ((DamagedBatchForPlayersEvent) event).getEvents()) { - Permanent permanent = game.getPermanent(damagedEvent.getSourceId()); - if (permanent == null - || !permanent.isControlledBy(getControllerId()) - || !permanent.hasSubtype(SubType.PIRATE, game) - || !game.getOpponents(getControllerId()).contains(damagedEvent.getTargetId())) { - continue; - } + for (DamagedEvent damagedEvent : filterBatchEvent(event, game).collect(Collectors.toSet())) { opponents.add(damagedEvent.getTargetId()); } if (opponents.isEmpty()) { diff --git a/Mage.Sets/src/mage/cards/c/ChandrasSpitfire.java b/Mage.Sets/src/mage/cards/c/ChandrasSpitfire.java index 2d3800e8eb76..0ee16eec7482 100644 --- a/Mage.Sets/src/mage/cards/c/ChandrasSpitfire.java +++ b/Mage.Sets/src/mage/cards/c/ChandrasSpitfire.java @@ -2,29 +2,32 @@ package mage.cards.c; -import java.util.UUID; import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; +import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author BetaSteward_at_googlemail.com */ public final class ChandrasSpitfire extends CardImpl { public ChandrasSpitfire(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{R}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); this.subtype.add(SubType.ELEMENTAL); this.power = new MageInt(1); @@ -45,7 +48,7 @@ public ChandrasSpitfire copy() { } -class ChandrasSpitfireAbility extends TriggeredAbilityImpl { +class ChandrasSpitfireAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public ChandrasSpitfireAbility() { super(Zone.BATTLEFIELD, new BoostSourceEffect(3, 0, Duration.EndOfTurn), false); @@ -60,10 +63,19 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(e -> !e.isCombatDamage()) + .filter(e -> e.getAmount() > 0) + .filter(e -> game.getOpponents(getControllerId()).contains(e.getTargetId())); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event; - return !dEvent.isCombatDamage() && dEvent.getAmount() > 0 && game.getOpponents(controllerId).contains(dEvent.getTargetId()); + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/c/ContaminantGrafter.java b/Mage.Sets/src/mage/cards/c/ContaminantGrafter.java index 0c0ec3d56c5d..c7b92ce8f3b5 100644 --- a/Mage.Sets/src/mage/cards/c/ContaminantGrafter.java +++ b/Mage.Sets/src/mage/cards/c/ContaminantGrafter.java @@ -2,6 +2,7 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.BeginningOfYourEndStepTriggeredAbility; import mage.abilities.condition.common.CorruptedCondition; @@ -19,12 +20,13 @@ import mage.constants.Zone; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.events.DamagedEvent; import mage.game.events.DamagedBatchForPlayersEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; /** * @author PurpleCrowbar @@ -69,7 +71,7 @@ public ContaminantGrafter copy() { } } -class ContaminantGrafterTriggeredAbility extends TriggeredAbilityImpl { +class ContaminantGrafterTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { ContaminantGrafterTriggeredAbility() { super(Zone.BATTLEFIELD, new ProliferateEffect(false), false); @@ -85,19 +87,25 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PLAYERS; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPlayersEvent) event) + .getEvents() + .stream() + .filter(DamagedPlayerEvent::isCombatDamage) + .filter(e -> e.getAmount() > 0) + .filter(e -> Optional + .of(e) + .map(DamagedPlayerEvent::getSourceId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> p.isControlledBy(getControllerId())) + .isPresent() + ); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForPlayersEvent dEvent = (DamagedBatchForPlayersEvent) event; - for (DamagedEvent damagedEvent : dEvent.getEvents()) { - if (!damagedEvent.isCombatDamage()) { - continue; - } - Permanent permanent = game.getPermanent(damagedEvent.getSourceId()); - if (permanent != null && permanent.isControlledBy(getControllerId())) { - return true; - } - } - return false; + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/c/ContestedGameBall.java b/Mage.Sets/src/mage/cards/c/ContestedGameBall.java index c9493559c852..23af26e0d39d 100644 --- a/Mage.Sets/src/mage/cards/c/ContestedGameBall.java +++ b/Mage.Sets/src/mage/cards/c/ContestedGameBall.java @@ -1,6 +1,7 @@ package mage.cards.c; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.common.SourceHasCounterCondition; @@ -23,6 +24,7 @@ import mage.counters.CounterType; import mage.game.Game; import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.permanent.token.TreasureToken; @@ -30,6 +32,7 @@ import mage.target.targetpointer.FixedTarget; import java.util.UUID; +import java.util.stream.Stream; /** * @author xenohedron @@ -66,7 +69,7 @@ public ContestedGameBall copy() { } } -class ContestedGameBallTriggeredAbility extends TriggeredAbilityImpl { +class ContestedGameBallTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { ContestedGameBallTriggeredAbility() { super(Zone.BATTLEFIELD, new ContestedGameBallEffect()); @@ -82,14 +85,24 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(DamagedPlayerEvent::isCombatDamage) + .filter(e -> getControllerId().equals(e.getTargetId())) + .filter(e -> e.getAmount() > 0); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - if (((DamagedBatchForOnePlayerEvent) event).isCombatDamage() && event.getTargetId().equals(this.getControllerId())) { - this.getAllEffects().setTargetPointer(new FixedTarget(game.getActivePlayerId())); - // attacking player is active player - return true; + if (!filterBatchEvent(event, game).findAny().isPresent()) { + return false; } - return false; + // attacking player is active player + this.getAllEffects().setTargetPointer(new FixedTarget(game.getActivePlayerId())); + return true; } @Override diff --git a/Mage.Sets/src/mage/cards/d/DarienKingOfKjeldor.java b/Mage.Sets/src/mage/cards/d/DarienKingOfKjeldor.java index ac94d2079153..f82bbc52fcd8 100644 --- a/Mage.Sets/src/mage/cards/d/DarienKingOfKjeldor.java +++ b/Mage.Sets/src/mage/cards/d/DarienKingOfKjeldor.java @@ -1,26 +1,20 @@ package mage.cards.d; -import java.util.UUID; import mage.MageInt; -import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.common.IsDealtDamageYouTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.CreateTokenEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Outcome; import mage.constants.SuperType; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; import mage.game.permanent.token.SoldierToken; -import mage.players.Player; + +import java.util.UUID; /** - * * @author LevelX2 */ public final class DarienKingOfKjeldor extends CardImpl { @@ -35,7 +29,9 @@ public DarienKingOfKjeldor(UUID ownerId, CardSetInfo setInfo) { this.toughness = new MageInt(3); // Whenever you're dealt damage, you may create that many 1/1 white Soldier creature tokens. - this.addAbility(new DarienKingOfKjeldorTriggeredAbility()); + this.addAbility(new IsDealtDamageYouTriggeredAbility( + new CreateTokenEffect(new SoldierToken(), SavedDamageValue.MANY), true + )); } private DarienKingOfKjeldor(final DarienKingOfKjeldor card) { @@ -46,65 +42,4 @@ private DarienKingOfKjeldor(final DarienKingOfKjeldor card) { public DarienKingOfKjeldor copy() { return new DarienKingOfKjeldor(this); } -} - -class DarienKingOfKjeldorTriggeredAbility extends TriggeredAbilityImpl { - - public DarienKingOfKjeldorTriggeredAbility() { - super(Zone.BATTLEFIELD, new DarienKingOfKjeldorEffect(), true); - } - - private DarienKingOfKjeldorTriggeredAbility(final DarienKingOfKjeldorTriggeredAbility ability) { - super(ability); - } - - @Override - public DarienKingOfKjeldorTriggeredAbility copy() { - return new DarienKingOfKjeldorTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if ((event.getTargetId().equals(this.getControllerId()))) { - this.getEffects().get(0).setValue("damageAmount", event.getAmount()); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you're dealt damage, you may create that many 1/1 white Soldier creature tokens."; - } -} - -class DarienKingOfKjeldorEffect extends OneShotEffect { - - DarienKingOfKjeldorEffect() { - super(Outcome.Benefit); - } - - private DarienKingOfKjeldorEffect(final DarienKingOfKjeldorEffect effect) { - super(effect); - } - - @Override - public DarienKingOfKjeldorEffect copy() { - return new DarienKingOfKjeldorEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - int damage = (Integer) this.getValue("damageAmount"); - return new CreateTokenEffect(new SoldierToken(), damage).apply(game, source); - } - return false; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/d/DonnaNoble.java b/Mage.Sets/src/mage/cards/d/DonnaNoble.java index f3ad5f05598e..a8980747b50a 100644 --- a/Mage.Sets/src/mage/cards/d/DonnaNoble.java +++ b/Mage.Sets/src/mage/cards/d/DonnaNoble.java @@ -1,33 +1,38 @@ package mage.cards.d; -import java.util.UUID; import mage.MageInt; -import mage.MageObjectReference; -import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.DamageTargetEffect; -import mage.constants.*; -import mage.abilities.keyword.SoulbondAbility; import mage.abilities.keyword.DoctorsCompanionAbility; +import mage.abilities.keyword.SoulbondAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.target.common.TargetOpponent; -import mage.util.CardUtil; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; /** - * * @author jimga150 */ public final class DonnaNoble extends CardImpl { public DonnaNoble(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); - + this.supertype.add(SuperType.LEGENDARY); this.subtype.add(SubType.HUMAN); this.power = new MageInt(2); @@ -59,9 +64,10 @@ public DonnaNoble copy() { return new DonnaNoble(this); } } + // Based on DealtDamageToSourceTriggeredAbility, except this uses DamagedBatchForOnePermanentEvent, // which batches all damage dealt at the same time on a permanent-by-permanent basis -class DonnaNobleTriggeredAbility extends TriggeredAbilityImpl { +class DonnaNobleTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { DonnaNobleTriggeredAbility() { super(Zone.BATTLEFIELD, new DamageTargetEffect(SavedDamageValue.MUCH)); @@ -84,31 +90,35 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event; - - // check if the permanent is Donna or its paired card - if (!CardUtil.getEventTargets(dEvent).contains(getSourceId())){ - Permanent paired; - Permanent permanent = game.getPermanent(getSourceId()); - if (permanent != null && permanent.getPairedCard() != null) { - paired = permanent.getPairedCard().getPermanent(game); - if (paired == null || paired.getPairedCard() == null || !paired.getPairedCard().equals(new MageObjectReference(permanent, game))) { - return false; - } - } else { - return false; - } - if (!CardUtil.getEventTargets(dEvent).contains(paired.getId())){ - return false; + public Stream filterBatchEvent(GameEvent event, Game game) { + Permanent permanent = game.getPermanentOrLKIBattlefield(getSourceId()); + if (permanent == null) { + return Stream.empty(); + } + Set permanentsLookedFor = new HashSet<>(); + permanentsLookedFor.add(permanent.getId()); + if (permanent.getPairedCard() != null) { + Permanent paired = permanent.getPairedCard().getPermanentOrLKIBattlefield(game); + if (paired != null) { + permanentsLookedFor.add(paired.getId()); } } + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(e -> e.getAmount() > 0) + .filter(e -> permanentsLookedFor.contains(e.getTargetId())); + } - int damage = dEvent.getAmount(); - if (damage < 1) { + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPermanentEvent::getAmount) + .sum(); + if (amount <= 0) { return false; } - this.getEffects().setValue("damage", damage); + this.getEffects().setValue("damage", amount); return true; } } diff --git a/Mage.Sets/src/mage/cards/d/DruidsCall.java b/Mage.Sets/src/mage/cards/d/DruidsCall.java index 939fbb0b6b85..a3cf284a419b 100644 --- a/Mage.Sets/src/mage/cards/d/DruidsCall.java +++ b/Mage.Sets/src/mage/cards/d/DruidsCall.java @@ -1,7 +1,6 @@ package mage.cards.d; -import java.util.UUID; -import mage.abilities.common.DealtDamageAttachedTriggeredAbility; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.Effect; import mage.abilities.effects.common.AttachEffect; @@ -9,23 +8,20 @@ import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Outcome; -import mage.constants.SetTargetPointer; -import mage.constants.Zone; +import mage.constants.*; import mage.game.permanent.token.SquirrelToken; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class DruidsCall extends CardImpl { public DruidsCall(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{1}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{G}"); this.subtype.add(SubType.AURA); // Enchant creature @@ -37,7 +33,9 @@ public DruidsCall(UUID ownerId, CardSetInfo setInfo) { // Whenever enchanted creature is dealt damage, its controller creates that many 1/1 green Squirrel creature tokens. Effect effect = new CreateTokenTargetEffect(new SquirrelToken(), SavedDamageValue.MANY); effect.setText("its controller creates that many 1/1 green Squirrel creature tokens"); - this.addAbility(new DealtDamageAttachedTriggeredAbility(Zone.BATTLEFIELD, effect, false, SetTargetPointer.PLAYER)); + this.addAbility(new IsDealtDamageAttachedTriggeredAbility( + Zone.BATTLEFIELD, effect, false, "enchanted", SetTargetPointer.PLAYER + )); } private DruidsCall(final DruidsCall card) { diff --git a/Mage.Sets/src/mage/cards/e/ExpeditedInheritance.java b/Mage.Sets/src/mage/cards/e/ExpeditedInheritance.java index 9a3a53516046..b9ee85015d7a 100644 --- a/Mage.Sets/src/mage/cards/e/ExpeditedInheritance.java +++ b/Mage.Sets/src/mage/cards/e/ExpeditedInheritance.java @@ -1,9 +1,8 @@ package mage.cards.e; -import java.util.Set; -import java.util.UUID; import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.ContinuousEffect; @@ -14,14 +13,20 @@ import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; +import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author kleese */ public final class ExpeditedInheritance extends CardImpl { @@ -43,7 +48,7 @@ public ExpeditedInheritance copy() { } } -class ExpeditedInheritanceTriggeredAbility extends TriggeredAbilityImpl { +class ExpeditedInheritanceTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { static final String IMPULSE_DRAW_AMOUNT_KEY = "playerDamage"; static final String TRIGGERING_CREATURE_KEY = "triggeringCreature"; @@ -61,13 +66,31 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(e -> e.getAmount() > 0) + .filter(e -> Optional + .of(e) + .map(DamagedPermanentEvent::getTargetId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> p.isCreature(game)) + .isPresent() + ); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { Permanent permanent = game.getPermanent(event.getTargetId()); - if (permanent == null || !permanent.isCreature(game)) { + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPermanentEvent::getAmount) + .sum(); + if (permanent == null || amount <= 0) { return false; } - getEffects().setValue(IMPULSE_DRAW_AMOUNT_KEY, event.getAmount()); + getEffects().setValue(IMPULSE_DRAW_AMOUNT_KEY, amount); getEffects().setValue(TRIGGERING_CREATURE_KEY, new MageObjectReference(event.getTargetId(), game)); return true; } @@ -111,7 +134,7 @@ public boolean apply(Game game, Ability source) { Set cards = player.getLibrary().getTopCards(game, impulseDrawAmount); if (!cards.isEmpty()) { player.moveCards(cards, Zone.EXILED, source, game); - for (Card card:cards){ + for (Card card : cards) { ContinuousEffect effect = new ExpeditedInheritanceMayPlayEffect(playerId); effect.setTargetPointer(new FixedTarget(card.getId(), game)); game.addEffect(effect, source); diff --git a/Mage.Sets/src/mage/cards/f/FallOfCairAndros.java b/Mage.Sets/src/mage/cards/f/FallOfCairAndros.java index 26d3c005debe..82a74d512066 100644 --- a/Mage.Sets/src/mage/cards/f/FallOfCairAndros.java +++ b/Mage.Sets/src/mage/cards/f/FallOfCairAndros.java @@ -1,6 +1,7 @@ package mage.cards.f; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.mana.ManaCostsImpl; @@ -13,13 +14,13 @@ import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchForOnePermanentEvent; -import mage.game.events.DamagedEvent; import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.target.common.TargetCreaturePermanent; import java.util.UUID; +import java.util.stream.Stream; /** * @author TheElk801 @@ -48,7 +49,7 @@ public FallOfCairAndros copy() { } } -class FallOfCairAndrosTriggeredAbility extends TriggeredAbilityImpl { +class FallOfCairAndrosTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { FallOfCairAndrosTriggeredAbility() { super(Zone.BATTLEFIELD, null); @@ -69,23 +70,31 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { + public Stream filterBatchEvent(GameEvent event, Game game) { Permanent permanent = game.getPermanent(event.getTargetId()); - if (permanent == null || !permanent.isCreature(game) - || !game.getOpponents(getControllerId()).contains(permanent.getControllerId())) { - return false; + if (permanent == null + || !permanent.isCreature(game) + || !game.getOpponents(getControllerId()).contains(permanent.getControllerId()) + ) { + return Stream.empty(); } - DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event; - int excessDamage = dEvent.getEvents() + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() .stream() - .mapToInt(DamagedEvent::getExcess) - .sum(); + .filter(DamagedPermanentEvent::isCombatDamage) + .filter(e -> e.getExcess() > 0); + } - if (dEvent.isCombatDamage() || excessDamage < 1) { + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPermanentEvent::getExcess) + .sum(); + if (amount <= 0) { return false; } this.getEffects().clear(); - this.addEffect(new AmassEffect(excessDamage, SubType.ORC)); + this.addEffect(new AmassEffect(amount, SubType.ORC)); return true; } diff --git a/Mage.Sets/src/mage/cards/f/Fiendlash.java b/Mage.Sets/src/mage/cards/f/Fiendlash.java index b7aa24b320ef..b7ee669440fd 100644 --- a/Mage.Sets/src/mage/cards/f/Fiendlash.java +++ b/Mage.Sets/src/mage/cards/f/Fiendlash.java @@ -1,9 +1,7 @@ package mage.cards.f; -import java.util.UUID; - import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; @@ -13,22 +11,16 @@ import mage.abilities.keyword.ReachAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.AttachmentType; -import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; -import mage.game.events.DamagedEvent; -import mage.game.events.DamagedBatchForPermanentsEvent; -import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetPlayerOrPlaneswalker; +import java.util.UUID; + /** - * * @author zeffirojoe */ public final class Fiendlash extends CardImpl { @@ -44,9 +36,12 @@ public Fiendlash(UUID ownerId, CardSetInfo setInfo) { .setText("and has reach")); this.addAbility(staticAbility); - // Whenever equipped creature is dealt damage, it deals damage equal to its - // power to target player or planeswalker. - this.addAbility(new FiendlashTriggeredAbility()); + // Whenever equipped creature is dealt damage, it deals damage equal to its power to target player or planeswalker. + Ability ability = new IsDealtDamageAttachedTriggeredAbility( + Zone.BATTLEFIELD, new FiendlashEffect(), false, "equipped", SetTargetPointer.PERMANENT + ); + ability.addTarget(new TargetPlayerOrPlaneswalker()); + this.addAbility(ability); // Equip {2}{R} this.addAbility(new EquipAbility(Outcome.AddAbility, new ManaCostsImpl<>("{2}{R}"), new TargetControlledCreaturePermanent(), false)); @@ -62,62 +57,6 @@ public Fiendlash copy() { } } -class FiendlashTriggeredAbility extends TriggeredAbilityImpl { - - FiendlashTriggeredAbility() { - super(Zone.BATTLEFIELD, new FiendlashEffect(), false); - this.addTarget(new TargetPlayerOrPlaneswalker()); - } - - private FiendlashTriggeredAbility(final FiendlashTriggeredAbility ability) { - super(ability); - } - - @Override - public FiendlashTriggeredAbility copy() { - return new FiendlashTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PERMANENTS; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Permanent equipment = game.getPermanent(this.getSourceId()); - if (equipment == null) { - return false; - } - - UUID attachedCreature = equipment.getAttachedTo(); - if (attachedCreature == null) { - return false; - } - - game.getState().setValue("Fiendlash" + equipment.getId(), attachedCreature); - - DamagedBatchForPermanentsEvent dEvent = (DamagedBatchForPermanentsEvent) event; - for (DamagedEvent damagedEvent : dEvent.getEvents()) { - UUID targetID = damagedEvent.getTargetId(); - if (targetID == null) { - continue; - } - - if (targetID == attachedCreature) { - return true; - } - } - - return false; - } - - @Override - public String getRule() { - return "Whenever equipped creature is dealt damage, it deals damage equal to its power to target player or planeswalker."; - } -} - class FiendlashEffect extends OneShotEffect { FiendlashEffect() { @@ -135,8 +74,7 @@ public FiendlashEffect copy() { @Override public boolean apply(Game game, Ability source) { - Permanent creature = game - .getPermanentOrLKIBattlefield((UUID) game.getState().getValue("Fiendlash" + source.getSourceId())); + Permanent creature = getTargetPointer().getFirstTargetPermanentOrLKI(game, source); if (creature == null) { return false; } diff --git a/Mage.Sets/src/mage/cards/f/FilthyCur.java b/Mage.Sets/src/mage/cards/f/FilthyCur.java index d983778da5dd..9233e6cc7d99 100644 --- a/Mage.Sets/src/mage/cards/f/FilthyCur.java +++ b/Mage.Sets/src/mage/cards/f/FilthyCur.java @@ -1,36 +1,33 @@ package mage.cards.f; -import java.util.UUID; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; +import mage.abilities.common.DealtDamageToSourceTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.LoseLifeSourceControllerEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; + +import java.util.UUID; /** - * * @author cbt33 */ public final class FilthyCur extends CardImpl { public FilthyCur(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{1}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); this.subtype.add(SubType.DOG); this.power = new MageInt(2); this.toughness = new MageInt(2); // Whenever Filthy Cur is dealt damage, you lose that much life. - this.addAbility(new DealtDamageLoseLifeTriggeredAbility(Zone.BATTLEFIELD, new LoseLifeSourceControllerEffect(0), false)); - + this.addAbility(new DealtDamageToSourceTriggeredAbility( + new LoseLifeSourceControllerEffect(SavedDamageValue.MUCH) + )); } private FilthyCur(final FilthyCur card) { @@ -41,40 +38,4 @@ private FilthyCur(final FilthyCur card) { public FilthyCur copy() { return new FilthyCur(this); } -} - -class DealtDamageLoseLifeTriggeredAbility extends TriggeredAbilityImpl { - - public DealtDamageLoseLifeTriggeredAbility(Zone zone, Effect effect, boolean optional) { - super(zone, effect, optional); - } - - private DealtDamageLoseLifeTriggeredAbility(final DealtDamageLoseLifeTriggeredAbility ability) { - super(ability); - } - - @Override - public DealtDamageLoseLifeTriggeredAbility copy() { - return new DealtDamageLoseLifeTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getTargetId().equals(this.sourceId)) { - this.getEffects().clear(); - this.addEffect(new LoseLifeSourceControllerEffect(event.getAmount())); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever {this} is dealt damage, you lose that much life."; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/f/ForthEorlingas.java b/Mage.Sets/src/mage/cards/f/ForthEorlingas.java index 408f8d26958b..d9300def9372 100644 --- a/Mage.Sets/src/mage/cards/f/ForthEorlingas.java +++ b/Mage.Sets/src/mage/cards/f/ForthEorlingas.java @@ -1,7 +1,6 @@ package mage.cards.f; -import java.util.UUID; - +import mage.abilities.BatchTriggeredAbility; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.dynamicvalue.common.ManacostVariableValue; import mage.abilities.effects.common.BecomesMonarchSourceEffect; @@ -13,14 +12,16 @@ import mage.constants.CardType; import mage.constants.Duration; import mage.game.Game; -import mage.game.events.DamagedEvent; import mage.game.events.DamagedBatchForPlayersEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import mage.game.permanent.token.HumanKnightToken; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author Susucr */ public final class ForthEorlingas extends CardImpl { @@ -50,7 +51,7 @@ public ForthEorlingas copy() { } } -class ForthEorlingasTriggeredAbility extends DelayedTriggeredAbility { +class ForthEorlingasTriggeredAbility extends DelayedTriggeredAbility implements BatchTriggeredAbility { public ForthEorlingasTriggeredAbility() { super(new BecomesMonarchSourceEffect(), Duration.EndOfTurn, false); @@ -67,19 +68,25 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PLAYERS; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPlayersEvent) event) + .getEvents() + .stream() + .filter(DamagedPlayerEvent::isCombatDamage) + .filter(e -> e.getAmount() > 0) + .filter(e -> Optional + .of(e) + .map(DamagedPlayerEvent::getSourceId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> p.isControlledBy(getControllerId())) + .isPresent() + ); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForPlayersEvent dEvent = (DamagedBatchForPlayersEvent) event; - for (DamagedEvent damagedEvent : dEvent.getEvents()) { - if (!damagedEvent.isCombatDamage()) { - continue; - } - Permanent permanent = game.getPermanent(damagedEvent.getSourceId()); - if (permanent != null && permanent.isControlledBy(getControllerId())) { - return true; - } - } - return false; + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/f/FranticScapegoat.java b/Mage.Sets/src/mage/cards/f/FranticScapegoat.java index feb8fe2daeb4..2884814e884b 100644 --- a/Mage.Sets/src/mage/cards/f/FranticScapegoat.java +++ b/Mage.Sets/src/mage/cards/f/FranticScapegoat.java @@ -3,6 +3,7 @@ import mage.MageInt; import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.effects.OneShotEffect; @@ -26,9 +27,11 @@ import mage.target.TargetPermanent; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author notgreat @@ -64,7 +67,7 @@ public FranticScapegoat copy() { } //Based on Lightmine Field -class FranticScapegoatTriggeredAbility extends TriggeredAbilityImpl { +class FranticScapegoatTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { FranticScapegoatTriggeredAbility() { super(Zone.BATTLEFIELD, new FranticScapegoatSuspectEffect(), true); @@ -80,27 +83,39 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkInterveningIfClause(Game game) { - Permanent source = getSourcePermanentIfItStillExists(game); - return (source != null && source.isSuspected()); + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((ZoneChangeBatchEvent) event) + .getEvents() + .stream() + .filter(e -> e.getZone() == Zone.BATTLEFIELD) + .filter(e -> getControllerId().equals(e.getPlayerId())) + .filter(e -> Optional + .of(e) + .map(ZoneChangeEvent::getTarget) + .filter(p -> !p.getId().equals(getSourceId())) + .filter(p -> p.isCreature(game)) + .isPresent() + ); } @Override public boolean checkTrigger(GameEvent event, Game game) { - ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event; - Set enteringCreatures = zEvent.getEvents().stream() - .filter(z -> z.getToZone() == Zone.BATTLEFIELD) - .filter(z -> this.controllerId.equals(z.getPlayerId())) + Set enteringCreatures = filterBatchEvent(event, game) .map(ZoneChangeEvent::getTarget) .filter(Objects::nonNull) - .filter(permanent -> permanent.isCreature(game)) .map(p -> new MageObjectReference(p, game)) .collect(Collectors.toSet()); - if (!enteringCreatures.isEmpty()) { - this.getEffects().setValue("franticScapegoatEnteringCreatures", enteringCreatures); - return true; + if (enteringCreatures.isEmpty()) { + return false; } - return false; + this.getEffects().setValue("franticScapegoatEnteringCreatures", enteringCreatures); + return true; + } + + @Override + public boolean checkInterveningIfClause(Game game) { + Permanent source = getSourcePermanentIfItStillExists(game); + return (source != null && source.isSuspected()); } @Override diff --git a/Mage.Sets/src/mage/cards/f/FrozenSolid.java b/Mage.Sets/src/mage/cards/f/FrozenSolid.java index 1c12dc6461a2..b0e8b8486c80 100644 --- a/Mage.Sets/src/mage/cards/f/FrozenSolid.java +++ b/Mage.Sets/src/mage/cards/f/FrozenSolid.java @@ -1,9 +1,8 @@ package mage.cards.f; -import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.common.DealtDamageAttachedTriggeredAbility; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.DestroyAttachedToEffect; @@ -12,20 +11,21 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * * @author LoneFox */ public final class FrozenSolid extends CardImpl { public FrozenSolid(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{1}{U}{U}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}{U}"); this.subtype.add(SubType.AURA); // Enchant creature @@ -37,7 +37,7 @@ public FrozenSolid(UUID ownerId, CardSetInfo setInfo) { // Enchanted creature doesn't untap during its controller's untap step. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new DontUntapInControllersUntapStepEnchantedEffect())); // When enchanted creature is dealt damage, destroy it. - this.addAbility(new DealtDamageAttachedTriggeredAbility(new DestroyAttachedToEffect("it"), false) + this.addAbility(new IsDealtDamageAttachedTriggeredAbility(new DestroyAttachedToEffect("it"), false, "enchanted") .setTriggerPhrase("When enchanted creature is dealt damage, ")); } diff --git a/Mage.Sets/src/mage/cards/h/HordewingSkaab.java b/Mage.Sets/src/mage/cards/h/HordewingSkaab.java index 1ba5aa99c690..dbf3cee7597d 100644 --- a/Mage.Sets/src/mage/cards/h/HordewingSkaab.java +++ b/Mage.Sets/src/mage/cards/h/HordewingSkaab.java @@ -1,6 +1,7 @@ package mage.cards.h; import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.DrawDiscardControllerEffect; @@ -15,14 +16,15 @@ import mage.filter.FilterPermanent; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; -import mage.game.events.DamagedEvent; import mage.game.events.DamagedBatchForPlayersEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author TheElk801 @@ -61,7 +63,7 @@ public HordewingSkaab copy() { } } -class HordewingSkaabTriggeredAbility extends TriggeredAbilityImpl { +class HordewingSkaabTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { HordewingSkaabTriggeredAbility() { super(Zone.BATTLEFIELD, null, true); @@ -76,23 +78,28 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PLAYERS; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPlayersEvent) event) + .getEvents() + .stream() + .filter(DamagedPlayerEvent::isCombatDamage) + .filter(e -> e.getAmount() > 0) + .filter(e -> Optional + .of(e) + .map(DamagedPlayerEvent::getSourceId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> p.isControlledBy(getControllerId())) + .filter(p -> p.hasSubtype(SubType.ZOMBIE, game)) + .isPresent()) + .filter(e -> game.getOpponents(getControllerId()).contains(e.getTargetId())); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForPlayersEvent dEvent = (DamagedBatchForPlayersEvent) event; - Set opponents = new HashSet<>(); - for (DamagedEvent damagedEvent : dEvent.getEvents()) { - if (!damagedEvent.isCombatDamage()) { - continue; - } - Permanent permanent = game.getPermanent(damagedEvent.getSourceId()); - if (permanent == null - || !permanent.isControlledBy(getControllerId()) - || !permanent.hasSubtype(SubType.ZOMBIE, game) - || !game.getOpponents(getControllerId()).contains(damagedEvent.getTargetId())) { - continue; - } - opponents.add(damagedEvent.getTargetId()); - } + Set opponents = filterBatchEvent(event, game) + .map(DamagedPlayerEvent::getTargetId) + .collect(Collectors.toSet()); if (opponents.size() < 1) { return false; } diff --git a/Mage.Sets/src/mage/cards/h/HotSoup.java b/Mage.Sets/src/mage/cards/h/HotSoup.java index 3f1bc1d21415..99b415a3d104 100644 --- a/Mage.Sets/src/mage/cards/h/HotSoup.java +++ b/Mage.Sets/src/mage/cards/h/HotSoup.java @@ -1,8 +1,6 @@ package mage.cards.h; -import java.util.Objects; -import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.common.DestroyTargetEffect; @@ -10,32 +8,27 @@ import mage.abilities.keyword.EquipAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.AttachmentType; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Outcome; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.target.targetpointer.FixedTarget; +import mage.constants.*; + +import java.util.UUID; /** - * * @author emerald000 */ public final class HotSoup extends CardImpl { public HotSoup(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT},"{1}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}"); this.subtype.add(SubType.EQUIPMENT); // Equipped creature can't be blocked. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new CantBeBlockedAttachedEffect(AttachmentType.EQUIPMENT))); - + // Whenever equipped creature is dealt damage, destroy it. - this.addAbility(new HotSoupTriggeredAbility()); - + this.addAbility(new IsDealtDamageAttachedTriggeredAbility( + Zone.BATTLEFIELD, new DestroyTargetEffect().setText("destroy it"), false, "equipped", SetTargetPointer.PERMANENT + )); + // Equip {3} this.addAbility(new EquipAbility(Outcome.AddAbility, new GenericManaCost(3))); } @@ -48,38 +41,4 @@ private HotSoup(final HotSoup card) { public HotSoup copy() { return new HotSoup(this); } -} - -class HotSoupTriggeredAbility extends TriggeredAbilityImpl { - - HotSoupTriggeredAbility() { - super(Zone.BATTLEFIELD, new DestroyTargetEffect().setText("destroy it"), false); - setTriggerPhrase("Whenever equipped creature is dealt damage, "); - } - - private HotSoupTriggeredAbility(final HotSoupTriggeredAbility ability) { - super(ability); - } - - @Override - public HotSoupTriggeredAbility copy() { - return new HotSoupTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Permanent equipment = game.getPermanent(this.getSourceId()); - if (equipment != null && equipment.getAttachedTo() != null) { - if (Objects.equals(event.getTargetId(), equipment.getAttachedTo())) { - this.getEffects().setTargetPointer(new FixedTarget(equipment.getAttachedTo(), game)); - return true; - } - } - return false; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/h/HowlpackAvenger.java b/Mage.Sets/src/mage/cards/h/HowlpackAvenger.java index de1d6947b172..e5ebd4758079 100644 --- a/Mage.Sets/src/mage/cards/h/HowlpackAvenger.java +++ b/Mage.Sets/src/mage/cards/h/HowlpackAvenger.java @@ -1,6 +1,7 @@ package mage.cards.h; import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.mana.ManaCostsImpl; @@ -16,10 +17,12 @@ import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchForPermanentsEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.target.common.TargetAnyTarget; import java.util.UUID; +import java.util.stream.Stream; /** * @author TheElk801 @@ -57,7 +60,7 @@ public HowlpackAvenger copy() { } } -class HowlpackAvengerTriggeredAbility extends TriggeredAbilityImpl { +class HowlpackAvengerTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { HowlpackAvengerTriggeredAbility() { super(Zone.BATTLEFIELD, new DamageTargetEffect(SavedDamageValue.MUCH)); @@ -80,17 +83,23 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - int damage = ((DamagedBatchForPermanentsEvent) event) + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPermanentsEvent) event) .getEvents() .stream() - .filter(damagedEvent -> isControlledBy(game.getControllerId(damagedEvent.getTargetId()))) + .filter(e -> isControlledBy(game.getControllerId(e.getTargetId()))) + .filter(e -> e.getAmount() > 0); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) .mapToInt(GameEvent::getAmount) .sum(); - if (damage < 1) { + if (amount <= 0) { return false; } - this.getEffects().setValue("damage", damage); + this.getEffects().setValue("damage", amount); return true; } } diff --git a/Mage.Sets/src/mage/cards/i/ImodaneThePyrohammer.java b/Mage.Sets/src/mage/cards/i/ImodaneThePyrohammer.java index 0916091b9929..8a166c28af8c 100644 --- a/Mage.Sets/src/mage/cards/i/ImodaneThePyrohammer.java +++ b/Mage.Sets/src/mage/cards/i/ImodaneThePyrohammer.java @@ -2,10 +2,9 @@ import mage.MageInt; import mage.MageObject; -import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.effects.Effect; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.DamagePlayersEffect; import mage.abilities.hint.Hint; import mage.abilities.hint.ValuePositiveHint; @@ -17,11 +16,13 @@ import mage.filter.predicate.other.HasOnlySingleTargetPermanentPredicate; import mage.game.Game; import mage.game.events.DamagedBatchForPermanentsEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.stack.StackObject; import java.util.UUID; +import java.util.stream.Stream; /** * @author Susucr @@ -51,7 +52,7 @@ public ImodaneThePyrohammer copy() { } } -class ImodaneThePyrohammerTriggeredAbility extends TriggeredAbilityImpl { +class ImodaneThePyrohammerTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { private static final FilterSpell filter = new FilterSpell("instant or sorcery spell you control that targets only a single creature"); @@ -60,10 +61,10 @@ class ImodaneThePyrohammerTriggeredAbility extends TriggeredAbilityImpl { filter.add(new HasOnlySingleTargetPermanentPredicate(StaticFilters.FILTER_PERMANENT_CREATURE)); } - private static final Hint hint = new ValuePositiveHint("Damage dealt to the target", ImodaneThePyrohammerDynamicValue.instance); + private static final Hint hint = new ValuePositiveHint("Damage dealt to the target", SavedDamageValue.MUCH); ImodaneThePyrohammerTriggeredAbility() { - super(Zone.BATTLEFIELD, new DamagePlayersEffect(Outcome.Damage, ImodaneThePyrohammerDynamicValue.instance, TargetController.OPPONENT) + super(Zone.BATTLEFIELD, new DamagePlayersEffect(Outcome.Damage, SavedDamageValue.MUCH, TargetController.OPPONENT) .setText("{this} deals that much damage to each opponent"), false); setTriggerPhrase("Whenever an instant or sorcery spell you control that targets only a single creature deals damage to that creature, "); addHint(hint); @@ -84,9 +85,8 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForPermanentsEvent dEvent = (DamagedBatchForPermanentsEvent) event; - int damage = dEvent + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPermanentsEvent) event) .getEvents() .stream() .filter(damagedEvent -> { @@ -100,45 +100,19 @@ public boolean checkTrigger(GameEvent event, Game game) { && filter.match((StackObject) sourceObject, controllerId, this, game) && target.getId().equals(((StackObject) sourceObject).getStackAbility().getFirstTarget()); }) + .filter(e -> e.getAmount() > 0); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) .mapToInt(GameEvent::getAmount) .sum(); - if (damage < 1) { + if (amount <= 0) { return false; } - this.getEffects().setValue(ImodaneThePyrohammerDynamicValue.IMODANE_VALUE_KEY, damage); + this.getEffects().setValue("damage", amount); return true; } -} - -enum ImodaneThePyrohammerDynamicValue implements DynamicValue { - instance; - - static final String IMODANE_VALUE_KEY = "Imodane-Damage-Amount"; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - StackObject source = game.getStack().getStackObject(sourceAbility.getSourceId()); - if (source == null) { - return 0; - } - - Integer value = (Integer) sourceAbility.getEffects().get(0).getValue(IMODANE_VALUE_KEY); - return value == null ? 0 : value; - } - - @Override - public ImodaneThePyrohammerDynamicValue copy() { - return this; - } - - @Override - public String toString() { - return "X"; - } - - @Override - public String getMessage() { - return "that much damage"; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/i/InnocentBystander.java b/Mage.Sets/src/mage/cards/i/InnocentBystander.java index 6efdbc1f578a..daf8efa0e0b9 100644 --- a/Mage.Sets/src/mage/cards/i/InnocentBystander.java +++ b/Mage.Sets/src/mage/cards/i/InnocentBystander.java @@ -1,6 +1,7 @@ package mage.cards.i; import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.keyword.InvestigateEffect; import mage.cards.CardImpl; @@ -9,9 +10,12 @@ import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; +import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import java.util.UUID; +import java.util.stream.Stream; /** * @author xenohedron @@ -20,7 +24,7 @@ public final class InnocentBystander extends CardImpl { public InnocentBystander(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}"); - + this.subtype.add(SubType.GOBLIN); this.subtype.add(SubType.CITIZEN); this.power = new MageInt(2); @@ -41,7 +45,7 @@ public InnocentBystander copy() { } } -class InnocentBystanderTriggeredAbility extends TriggeredAbilityImpl { +class InnocentBystanderTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { InnocentBystanderTriggeredAbility() { super(Zone.BATTLEFIELD, new InvestigateEffect(), false); @@ -62,8 +66,20 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(e -> e.getTargetId().equals(getSourceId())) + .filter(e -> e.getAmount() > 0); // all the contribution for the 3 damage + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - return event.getTargetId().equals(getSourceId()) && event.getAmount() >= 3; + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPermanentEvent::getAmount) + .sum(); + return amount >= 3; } } diff --git a/Mage.Sets/src/mage/cards/k/KambalProfiteeringMayor.java b/Mage.Sets/src/mage/cards/k/KambalProfiteeringMayor.java index 885dc2d0bf23..24ad173c64d3 100644 --- a/Mage.Sets/src/mage/cards/k/KambalProfiteeringMayor.java +++ b/Mage.Sets/src/mage/cards/k/KambalProfiteeringMayor.java @@ -2,6 +2,7 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldOneOrMoreTriggeredAbility; import mage.abilities.effects.OneShotEffect; @@ -22,11 +23,9 @@ import mage.players.Player; import mage.target.targetpointer.FixedTarget; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Susucr @@ -69,7 +68,7 @@ public KambalProfiteeringMayor copy() { } } -class KambalProfiteeringMayorTriggeredAbility extends TriggeredAbilityImpl { +class KambalProfiteeringMayorTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { KambalProfiteeringMayorTriggeredAbility() { super(Zone.BATTLEFIELD, null); @@ -91,16 +90,27 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event; - Player controller = game.getPlayer(this.controllerId); + public Stream filterBatchEvent(GameEvent event, Game game) { + Player controller = game.getPlayer(getControllerId()); if (controller == null) { - return false; + return Stream.empty(); } - List tokensIds = zEvent.getEvents() + return ((ZoneChangeBatchEvent) event) + .getEvents() .stream() - .filter(zce -> zce.getToZone() == Zone.BATTLEFIELD // keep enter the battlefield - && controller.hasOpponent(zce.getPlayerId(), game)) // & under your opponent's control + .filter(e -> e.getToZone() == Zone.BATTLEFIELD) + .filter(e -> controller.hasOpponent(e.getPlayerId(), game)) + .filter(e -> Optional + .of(e) + .map(ZoneChangeEvent::getTarget) + .filter(p -> p instanceof PermanentToken) + .isPresent() + ); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + List tokensIds = filterBatchEvent(event, game) .map(ZoneChangeEvent::getTarget) .filter(Objects::nonNull) .filter(p -> p instanceof PermanentToken) // collect only tokens diff --git a/Mage.Sets/src/mage/cards/k/KayaSpiritsJustice.java b/Mage.Sets/src/mage/cards/k/KayaSpiritsJustice.java index 9ca1a445f87c..0121ebc495ee 100644 --- a/Mage.Sets/src/mage/cards/k/KayaSpiritsJustice.java +++ b/Mage.Sets/src/mage/cards/k/KayaSpiritsJustice.java @@ -2,6 +2,7 @@ import mage.MageObject; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.LoyaltyAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.OneShotEffect; @@ -34,11 +35,9 @@ import mage.util.CardUtil; import mage.util.functions.CopyApplier; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author DominionSpy @@ -84,13 +83,11 @@ public KayaSpiritsJustice copy() { } } -class KayaSpiritsJusticeTriggeredAbility extends TriggeredAbilityImpl { +class KayaSpiritsJusticeTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { KayaSpiritsJusticeTriggeredAbility() { super(Zone.BATTLEFIELD, new KayaSpiritsJusticeCopyEffect(), false); - setTriggerPhrase("Whenever one or more creatures you control and/or creature cards in your graveyard are put into exile, " + - "you may choose a creature card from among them. Until end of turn, target token you control becomes a copy of it, " + - "except it has flying."); + setTriggerPhrase("Whenever one or more creatures you control and/or creature cards in your graveyard are put into exile, "); } private KayaSpiritsJusticeTriggeredAbility(final KayaSpiritsJusticeTriggeredAbility ability) { @@ -107,12 +104,52 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + // From Battlefield + Stream filteredBattlefield = ((ZoneChangeBatchEvent) event) + .getEvents() + .stream() + .filter(e -> e.getFromZone() == Zone.BATTLEFIELD) + .filter(e -> e.getToZone() == Zone.EXILED) + .filter(e -> Optional + .of(e) + .map(ZoneChangeEvent::getTargetId) + .map(game::getCard) + .filter(card -> { + Permanent permanent = game.getPermanentOrLKIBattlefield(card.getId()); + return StaticFilters.FILTER_PERMANENT_CREATURE + .match(permanent, getControllerId(), this, game); + }) + .isPresent() + ); + + // From Graveyard + Stream filteredGraveyard = ((ZoneChangeBatchEvent) event) + .getEvents() + .stream() + .filter(e -> e.getFromZone() == Zone.GRAVEYARD) + .filter(e -> e.getToZone() == Zone.EXILED) + .filter(e -> Optional + .of(e) + .map(ZoneChangeEvent::getTargetId) + .map(game::getCard) + .filter(card -> StaticFilters.FILTER_CARD_CREATURE.match(card, getControllerId(), this, game)) + .isPresent() + ); + + return Stream.concat(filteredBattlefield, filteredGraveyard); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event; + Stream filteredEvents = filterBatchEvent(event, game); + if (!filteredEvents.findAny().isPresent()) { + return false; + } - Set battlefieldCards = zEvent.getEvents() - .stream() + // From Battlefield + Set battlefieldCards = filteredEvents .filter(e -> e.getFromZone() == Zone.BATTLEFIELD) .filter(e -> e.getToZone() == Zone.EXILED) .map(ZoneChangeEvent::getTargetId) @@ -126,8 +163,8 @@ public boolean checkTrigger(GameEvent event, Game game) { }) .collect(Collectors.toSet()); - Set graveyardCards = zEvent.getEvents() - .stream() + // From Graveyard + Set graveyardCards = filteredEvents .filter(e -> e.getFromZone() == Zone.GRAVEYARD) .filter(e -> e.getToZone() == Zone.EXILED) .map(ZoneChangeEvent::getTargetId) @@ -153,6 +190,9 @@ class KayaSpiritsJusticeCopyEffect extends OneShotEffect { KayaSpiritsJusticeCopyEffect() { super(Outcome.Copy); + staticText = "you may choose a creature card from among them. " + + "Until end of turn, target token you control becomes a copy of it, " + + "except it has flying."; } private KayaSpiritsJusticeCopyEffect(final KayaSpiritsJusticeCopyEffect effect) { diff --git a/Mage.Sets/src/mage/cards/k/KazarovSengirPureblood.java b/Mage.Sets/src/mage/cards/k/KazarovSengirPureblood.java index d09a18e848d3..407da88ec007 100644 --- a/Mage.Sets/src/mage/cards/k/KazarovSengirPureblood.java +++ b/Mage.Sets/src/mage/cards/k/KazarovSengirPureblood.java @@ -1,29 +1,34 @@ package mage.cards.k; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; -import mage.constants.SubType; -import mage.constants.SuperType; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; +import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.target.common.TargetCreaturePermanent; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author TheElk801 */ public final class KazarovSengirPureblood extends CardImpl { @@ -58,7 +63,7 @@ public KazarovSengirPureblood copy() { } } -class KazarovSengirPurebloodTriggeredAbility extends TriggeredAbilityImpl { +class KazarovSengirPurebloodTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public KazarovSengirPurebloodTriggeredAbility() { super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance())); @@ -78,12 +83,25 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(e -> e.getAmount() > 0) + .filter(e -> Optional + .of(e) + .map(DamagedPermanentEvent::getTargetId) + .map(game::getPermanentOrLKIBattlefield) + .filter(Permanent::isCreature) + .filter(p -> game.getOpponents(p.getControllerId()).contains(getControllerId())) + .isPresent() + ); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); - return permanent!=null - && permanent.isCreature(game) - && game.getOpponents(permanent.getControllerId()).contains(this.getControllerId()); + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/l/LaeliaTheBladeReforged.java b/Mage.Sets/src/mage/cards/l/LaeliaTheBladeReforged.java index 5bbad75c84c8..f2fce39e8bf5 100644 --- a/Mage.Sets/src/mage/cards/l/LaeliaTheBladeReforged.java +++ b/Mage.Sets/src/mage/cards/l/LaeliaTheBladeReforged.java @@ -1,6 +1,7 @@ package mage.cards.l; import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.effects.common.ExileTopXMayPlayUntilEffect; @@ -16,8 +17,9 @@ import mage.game.events.ZoneChangeBatchEvent; import mage.game.events.ZoneChangeEvent; -import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; /** * Rules update: 6/18/2021 @@ -58,7 +60,7 @@ public LaeliaTheBladeReforged copy() { } } -class LaeliaTheBladeReforgedAddCountersTriggeredAbility extends TriggeredAbilityImpl { +class LaeliaTheBladeReforgedAddCountersTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { LaeliaTheBladeReforgedAddCountersTriggeredAbility() { super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance()), false); @@ -79,17 +81,25 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event; - return zEvent.getEvents() + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((ZoneChangeBatchEvent) event) + .getEvents() .stream() .filter(e -> e.getFromZone() == Zone.LIBRARY || e.getFromZone() == Zone.GRAVEYARD) .filter(e -> e.getToZone() == Zone.EXILED) - .map(ZoneChangeEvent::getTargetId) - .map(game::getCard) - .filter(Objects::nonNull) - .map(Card::getOwnerId) - .anyMatch(this::isControlledBy); + .filter(e -> Optional + .of(e) + .map(ZoneChangeEvent::getTargetId) + .map(game::getCard) + .map(Card::getOwnerId) + .filter(this::isControlledBy) + .isPresent() + ); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/l/Lich.java b/Mage.Sets/src/mage/cards/l/Lich.java index 4be19bb51ee7..c0592c4ca189 100644 --- a/Mage.Sets/src/mage/cards/l/Lich.java +++ b/Mage.Sets/src/mage/cards/l/Lich.java @@ -1,14 +1,13 @@ package mage.cards.l; -import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.IsDealtDamageYouTriggeredAbility; import mage.abilities.common.PutIntoGraveFromBattlefieldSourceTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.dynamicvalue.common.ControllerLifeCount; -import mage.abilities.effects.Effect; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.LoseGameSourceControllerEffect; @@ -26,31 +25,30 @@ import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; -import mage.target.Target; -import mage.target.common.TargetControlledPermanent; import mage.target.common.TargetSacrifice; +import java.util.UUID; + /** - * * @author emerald000 */ public final class Lich extends CardImpl { public Lich(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{B}{B}{B}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}{B}{B}{B}"); // As Lich enters the battlefield, you lose life equal to your life total. this.addAbility(new EntersBattlefieldAbility(new LoseLifeSourceControllerEffect(ControllerLifeCount.instance), null, "As Lich enters the battlefield, you lose life equal to your life total.", null)); - + // You don't lose the game for having 0 or less life. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new DontLoseByZeroOrLessLifeEffect(Duration.WhileOnBattlefield))); - + // If you would gain life, draw that many cards instead. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new LichLifeGainReplacementEffect())); - + // Whenever you're dealt damage, sacrifice that many nontoken permanents. If you can't, you lose the game. - this.addAbility(new LichDamageTriggeredAbility()); - + this.addAbility(new IsDealtDamageYouTriggeredAbility(new LichDamageEffect(), false)); + // When Lich is put into a graveyard from the battlefield, you lose the game. this.addAbility(new PutIntoGraveFromBattlefieldSourceTriggeredAbility(new LoseGameSourceControllerEffect())); } @@ -101,73 +99,34 @@ public boolean applies(GameEvent event, Ability source, Game game) { } } -class LichDamageTriggeredAbility extends TriggeredAbilityImpl { - - LichDamageTriggeredAbility() { - super(Zone.BATTLEFIELD, new LichDamageEffect(), false); - } - - private LichDamageTriggeredAbility(final LichDamageTriggeredAbility ability) { - super(ability); - } - - @Override - public LichDamageTriggeredAbility copy() { - return new LichDamageTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getTargetId().equals(this.getControllerId())) { - for (Effect effect : this.getEffects()) { - if (effect instanceof LichDamageEffect) { - ((LichDamageEffect) effect).setAmount(event.getAmount()); - } - } - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you're dealt damage, sacrifice that many nontoken permanents. If you can't, you lose the game."; - } -} class LichDamageEffect extends OneShotEffect { - + private static final FilterControlledPermanent filter = new FilterControlledPermanent("nontoken permanent"); + static { filter.add(TokenPredicate.FALSE); } - - private int amount = 0; - + LichDamageEffect() { super(Outcome.Sacrifice); this.staticText = "sacrifice that many nontoken permanents. If you can't, you lose the game"; } - + private LichDamageEffect(final LichDamageEffect effect) { super(effect); - this.amount = effect.amount; } - + @Override public LichDamageEffect copy() { return new LichDamageEffect(this); } - + @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { + int amount = SavedDamageValue.MANY.calculate(game, source, this); + if (controller != null && amount > 0) { TargetSacrifice target = new TargetSacrifice(amount, filter); if (target.canChoose(controller.getId(), source, game)) { if (controller.choose(Outcome.Sacrifice, target, source, game)) { @@ -185,8 +144,4 @@ public boolean apply(Game game, Ability source) { } return false; } - - public void setAmount(int amount) { - this.amount = amount; - } } diff --git a/Mage.Sets/src/mage/cards/l/LivingArtifact.java b/Mage.Sets/src/mage/cards/l/LivingArtifact.java index c4b3c6520d8a..2da87ffa72f0 100644 --- a/Mage.Sets/src/mage/cards/l/LivingArtifact.java +++ b/Mage.Sets/src/mage/cards/l/LivingArtifact.java @@ -1,14 +1,13 @@ package mage.cards.l; -import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.common.IsDealtDamageYouTriggeredAbility; import mage.abilities.condition.common.SourceHasCounterCondition; import mage.abilities.costs.common.RemoveCountersSourceCost; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.GainLifeEffect; @@ -17,24 +16,22 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.TargetController; -import mage.constants.Zone; import mage.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; import mage.target.TargetPermanent; import mage.target.common.TargetArtifactPermanent; +import java.util.UUID; + /** - * * @author LoneFox */ public final class LivingArtifact extends CardImpl { public LivingArtifact(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{G}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{G}"); this.subtype.add(SubType.AURA); // Enchant artifact @@ -43,13 +40,17 @@ public LivingArtifact(UUID ownerId, CardSetInfo setInfo) { this.getSpellAbility().addEffect(new AttachEffect(Outcome.Benefit)); Ability ability = new EnchantAbility(auraTarget); this.addAbility(ability); + // Whenever you're dealt damage, put that many vitality counters on Living Artifact. - this.addAbility(new LivingArtifactTriggeredAbility()); + this.addAbility(new IsDealtDamageYouTriggeredAbility( + new AddCountersSourceEffect(CounterType.VITALITY.createInstance(), SavedDamageValue.MANY, true), false + )); + // At the beginning of your upkeep, you may remove a vitality counter from Living Artifact. If you do, you gain 1 life. //TODO make this a custom ability- it's really not intervening if because you should be able to add counters in response to this trigger this.addAbility(new ConditionalInterveningIfTriggeredAbility(new BeginningOfUpkeepTriggeredAbility(new DoIfCostPaid(new GainLifeEffect(1), - new RemoveCountersSourceCost(CounterType.VITALITY.createInstance(1))), TargetController.YOU, false), - new SourceHasCounterCondition(CounterType.VITALITY, 1), "At the beginning of your upkeep, you may remove a vitality counter from {this}. If you do, you gain 1 life")); + new RemoveCountersSourceCost(CounterType.VITALITY.createInstance(1))), TargetController.YOU, false), + new SourceHasCounterCondition(CounterType.VITALITY, 1), "At the beginning of your upkeep, you may remove a vitality counter from {this}. If you do, you gain 1 life")); } private LivingArtifact(final LivingArtifact card) { @@ -60,60 +61,4 @@ private LivingArtifact(final LivingArtifact card) { public LivingArtifact copy() { return new LivingArtifact(this); } -} - -class LivingArtifactTriggeredAbility extends TriggeredAbilityImpl { - - public LivingArtifactTriggeredAbility() { - super(Zone.BATTLEFIELD, new LivingArtifactEffect(), false); - } - - private LivingArtifactTriggeredAbility(final LivingArtifactTriggeredAbility ability) { - super(ability); - } - - @Override - public LivingArtifactTriggeredAbility copy() { - return new LivingArtifactTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getTargetId().equals(this.getControllerId())) { - this.getEffects().get(0).setValue("damageAmount", event.getAmount()); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you're dealt damage, put that many vitality counters on {this}."; - } -} - -class LivingArtifactEffect extends OneShotEffect { - - LivingArtifactEffect() { - super(Outcome.Benefit); - } - - private LivingArtifactEffect(final LivingArtifactEffect effect) { - super(effect); - } - - @Override - public LivingArtifactEffect copy() { - return new LivingArtifactEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - return new AddCountersSourceEffect(CounterType.VITALITY.createInstance((Integer) this.getValue("damageAmount"))).apply(game, source); - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/MagmaticGalleon.java b/Mage.Sets/src/mage/cards/m/MagmaticGalleon.java index 0b1019cd0476..c0453738b8b9 100644 --- a/Mage.Sets/src/mage/cards/m/MagmaticGalleon.java +++ b/Mage.Sets/src/mage/cards/m/MagmaticGalleon.java @@ -2,6 +2,7 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; @@ -14,13 +15,14 @@ import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchForPermanentsEvent; -import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.permanent.token.TreasureToken; import mage.target.common.TargetOpponentsCreaturePermanent; import java.util.UUID; +import java.util.stream.Stream; /** * @author xenohedron @@ -29,7 +31,7 @@ public final class MagmaticGalleon extends CardImpl { public MagmaticGalleon(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}{R}{R}"); - + this.subtype.add(SubType.VEHICLE); this.power = new MageInt(5); this.toughness = new MageInt(5); @@ -57,7 +59,7 @@ public MagmaticGalleon copy() { } } -class MagmaticGalleonTriggeredAbility extends TriggeredAbilityImpl { +class MagmaticGalleonTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { MagmaticGalleonTriggeredAbility() { super(Zone.BATTLEFIELD, new CreateTokenEffect(new TreasureToken())); @@ -79,21 +81,22 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - int damage = ((DamagedBatchForPermanentsEvent) event) + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPermanentsEvent) event) .getEvents() .stream() + .filter(e -> !e.isCombatDamage()) + .filter(e -> e.getExcess() > 0) .filter(damagedEvent -> { - if (damagedEvent.isCombatDamage()) { - return false; - } Permanent permanent = game.getPermanentOrLKIBattlefield(damagedEvent.getTargetId()); return permanent != null && permanent.isCreature(game) && game.getOpponents(this.getControllerId()).contains(permanent.getControllerId()); - }) - .mapToInt(DamagedEvent::getExcess) - .sum(); - return damage >= 1; + }); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return filterBatchEvent(event, game).findAny().isPresent(); } } diff --git a/Mage.Sets/src/mage/cards/m/MalcolmKeenEyedNavigator.java b/Mage.Sets/src/mage/cards/m/MalcolmKeenEyedNavigator.java index 07944f8d4ca0..6eea5587da9e 100644 --- a/Mage.Sets/src/mage/cards/m/MalcolmKeenEyedNavigator.java +++ b/Mage.Sets/src/mage/cards/m/MalcolmKeenEyedNavigator.java @@ -1,6 +1,7 @@ package mage.cards.m; import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.keyword.FlyingAbility; @@ -14,6 +15,7 @@ import mage.game.Game; import mage.game.events.DamagedEvent; import mage.game.events.DamagedBatchForPlayersEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.permanent.token.TreasureToken; @@ -21,6 +23,8 @@ import java.util.HashSet; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author TheElk801 @@ -56,7 +60,7 @@ public MalcolmKeenEyedNavigator copy() { } } -class MalcolmKeenEyedNavigatorTriggeredAbility extends TriggeredAbilityImpl { +class MalcolmKeenEyedNavigatorTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { MalcolmKeenEyedNavigatorTriggeredAbility() { super(Zone.BATTLEFIELD, null); @@ -71,18 +75,24 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_PLAYERS; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPlayersEvent) event) + .getEvents() + .stream() + .filter(e -> { + Permanent permanent = game.getPermanent(e.getSourceId()); + return permanent != null + && permanent.isControlledBy(getControllerId()) + && permanent.hasSubtype(SubType.PIRATE, game) + && game.getOpponents(getControllerId()).contains(e.getTargetId()); + }); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForPlayersEvent dEvent = (DamagedBatchForPlayersEvent) event; Set opponents = new HashSet<>(); - for (DamagedEvent damagedEvent : dEvent.getEvents()) { - Permanent permanent = game.getPermanent(damagedEvent.getSourceId()); - if (permanent == null - || !permanent.isControlledBy(getControllerId()) - || !permanent.hasSubtype(SubType.PIRATE, game) - || !game.getOpponents(getControllerId()).contains(damagedEvent.getTargetId())) { - continue; - } + for (DamagedEvent damagedEvent : filterBatchEvent(event, game).collect(Collectors.toSet())) { opponents.add(damagedEvent.getTargetId()); } if (opponents.size() < 1) { diff --git a/Mage.Sets/src/mage/cards/m/MindbladeRender.java b/Mage.Sets/src/mage/cards/m/MindbladeRender.java index cd23225d4194..efe8f7b42825 100644 --- a/Mage.Sets/src/mage/cards/m/MindbladeRender.java +++ b/Mage.Sets/src/mage/cards/m/MindbladeRender.java @@ -1,6 +1,7 @@ package mage.cards.m; import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.LoseLifeSourceControllerEffect; @@ -10,15 +11,15 @@ import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; -import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedBatchForPlayersEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.players.Player; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; /** - * * @author TheElk801 */ public final class MindbladeRender extends CardImpl { @@ -45,7 +46,7 @@ public MindbladeRender copy() { } } -class MindbladeRenderTriggeredAbility extends TriggeredAbilityImpl { +class MindbladeRenderTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public MindbladeRenderTriggeredAbility() { super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1)); @@ -67,30 +68,28 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - Player controller = game.getPlayer(getControllerId()); - if (controller == null) { - return false; - } - DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event; - - if (!controller.hasOpponent(dEvent.getTargetId(), game)){ - return false; - } - if (!dEvent.isCombatDamage()) { - return false; - } - - int warriorDamage = dEvent.getEvents() + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPlayersEvent) event) + .getEvents() .stream() - .filter(ev -> { - Permanent attacker = game.getPermanentOrLKIBattlefield(ev.getSourceId()); - return attacker != null && attacker.hasSubtype(SubType.WARRIOR, game); - }) + .filter(DamagedPlayerEvent::isCombatDamage) + .filter(e -> e.getAmount() > 0) + .filter(e -> game.getOpponents(getControllerId()).contains(e.getTargetId())) + .filter(e -> Optional + .of(e) + .map(DamagedPlayerEvent::getSourceId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> p.hasSubtype(SubType.WARRIOR, game)) + .isPresent() + ); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) .mapToInt(GameEvent::getAmount) .sum(); - - return warriorDamage > 0; + return amount > 0; } @Override diff --git a/Mage.Sets/src/mage/cards/m/MireBlight.java b/Mage.Sets/src/mage/cards/m/MireBlight.java index b582557bdcb4..3579919e59d3 100644 --- a/Mage.Sets/src/mage/cards/m/MireBlight.java +++ b/Mage.Sets/src/mage/cards/m/MireBlight.java @@ -1,28 +1,28 @@ package mage.cards.m; -import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.common.DealtDamageAttachedTriggeredAbility; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.DestroyAttachedToEffect; import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * * @author North */ public final class MireBlight extends CardImpl { public MireBlight(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{B}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}"); this.subtype.add(SubType.AURA); @@ -33,7 +33,9 @@ public MireBlight(UUID ownerId, CardSetInfo setInfo) { Ability ability = new EnchantAbility(auraTarget); this.addAbility(ability); // When enchanted creature is dealt damage, destroy it. - this.addAbility(new DealtDamageAttachedTriggeredAbility(new DestroyAttachedToEffect("it"), false).setTriggerPhrase("When enchanted creature is dealt damage, ")); + this.addAbility(new IsDealtDamageAttachedTriggeredAbility( + new DestroyAttachedToEffect("it"), false, "enchanted" + ).setTriggerPhrase("When enchanted creature is dealt damage, ")); } private MireBlight(final MireBlight card) { diff --git a/Mage.Sets/src/mage/cards/m/MortalWound.java b/Mage.Sets/src/mage/cards/m/MortalWound.java index 46c865183d64..951ea004735b 100644 --- a/Mage.Sets/src/mage/cards/m/MortalWound.java +++ b/Mage.Sets/src/mage/cards/m/MortalWound.java @@ -1,27 +1,27 @@ package mage.cards.m; -import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.common.DealtDamageAttachedTriggeredAbility; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.DestroyAttachedToEffect; import mage.abilities.keyword.EnchantAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * * @author LoneFox */ public final class MortalWound extends CardImpl { public MortalWound(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{G}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{G}"); this.subtype.add(SubType.AURA); // Enchant creature @@ -31,8 +31,9 @@ public MortalWound(UUID ownerId, CardSetInfo setInfo) { Ability ability = new EnchantAbility(auraTarget); this.addAbility(ability); // When enchanted creature is dealt damage, destroy it. - this.addAbility(new DealtDamageAttachedTriggeredAbility(new DestroyAttachedToEffect("it"), false) - .setTriggerPhrase("When enchanted creature is dealt damage, ")); + this.addAbility(new IsDealtDamageAttachedTriggeredAbility( + new DestroyAttachedToEffect("it"), false, "enchanted" + ).setTriggerPhrase("When enchanted creature is dealt damage, ")); } private MortalWound(final MortalWound card) { diff --git a/Mage.Sets/src/mage/cards/o/ObNixilisCaptiveKingpin.java b/Mage.Sets/src/mage/cards/o/ObNixilisCaptiveKingpin.java index 13c3eaff2e55..38acafe33bd7 100644 --- a/Mage.Sets/src/mage/cards/o/ObNixilisCaptiveKingpin.java +++ b/Mage.Sets/src/mage/cards/o/ObNixilisCaptiveKingpin.java @@ -1,31 +1,36 @@ package mage.cards.o; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.common.ExileTopXMayPlayUntilEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; -import mage.constants.*; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.*; import mage.counters.CounterType; import mage.game.Game; -import mage.game.events.*; -import mage.util.CardUtil; +import mage.game.events.GameEvent; +import mage.game.events.LifeLostBatchEvent; +import mage.game.events.LifeLostEvent; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; /** - * * @author jimga150 */ public final class ObNixilisCaptiveKingpin extends CardImpl { public ObNixilisCaptiveKingpin(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{R}"); - + this.supertype.add(SuperType.LEGENDARY); this.subtype.add(SubType.DEMON); this.power = new MageInt(4); @@ -39,7 +44,7 @@ public ObNixilisCaptiveKingpin(UUID ownerId, CardSetInfo setInfo) { // Whenever one or more opponents each lose exactly 1 life, put a +1/+1 counter on Ob Nixilis, Captive Kingpin. Exile the top card of your library. Until your next end step, you may play that card. Ability ability = new ObNixilisCaptiveKingpinAbility( - new AddCountersSourceEffect(CounterType.P1P1.createInstance()) + new AddCountersSourceEffect(CounterType.P1P1.createInstance()) ); ability.addEffect(new ExileTopXMayPlayUntilEffect(1, Duration.UntilYourNextEndStep) .withTextOptions("that card", false)); @@ -58,8 +63,8 @@ public ObNixilisCaptiveKingpin copy() { } } -class ObNixilisCaptiveKingpinAbility extends TriggeredAbilityImpl { - +class ObNixilisCaptiveKingpinAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { + ObNixilisCaptiveKingpinAbility(Effect effect) { super(Zone.BATTLEFIELD, effect); setTriggerPhrase("Whenever one or more opponents each lose exactly 1 life, "); @@ -75,27 +80,24 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - + public Stream filterBatchEvent(GameEvent event, Game game) { LifeLostBatchEvent lifeLostBatchEvent = (LifeLostBatchEvent) event; - - boolean opponentLostLife = false; - boolean allis1 = true; - - for (UUID targetPlayer : CardUtil.getEventTargets(lifeLostBatchEvent)) { - // skip controller - if (targetPlayer.equals(getControllerId())) { - continue; - } - opponentLostLife = true; - - int lifeLost = lifeLostBatchEvent.getLifeLostByPlayer(targetPlayer); - if (lifeLost != 1) { - allis1 = false; - break; + Set opponentsThatLost1LifeIds = new HashSet<>(); + for (UUID opponentId : game.getOpponents(getControllerId())) { + if (1 == lifeLostBatchEvent.getLifeLostByPlayer(opponentId)) { + opponentsThatLost1LifeIds.add(opponentId); } } - return opponentLostLife && allis1; + return lifeLostBatchEvent + .getEvents() + .stream() + .filter(e -> e.getAmount() > 0) + .filter(e -> opponentsThatLost1LifeIds.contains(e.getTargetId())); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/o/OliviasAttendants.java b/Mage.Sets/src/mage/cards/o/OliviasAttendants.java index f23c648e6865..b2da929ce2dd 100644 --- a/Mage.Sets/src/mage/cards/o/OliviasAttendants.java +++ b/Mage.Sets/src/mage/cards/o/OliviasAttendants.java @@ -80,9 +80,6 @@ public boolean checkEventType(GameEvent event, Game game) { @Override public Stream filterBatchEvent(GameEvent event, Game game) { - if (!checkEventType(event, game)) { - return Stream.empty(); - } return ((DamagedBatchAllEvent) event) .getEvents() .stream() diff --git a/Mage.Sets/src/mage/cards/p/PhyrexianNegator.java b/Mage.Sets/src/mage/cards/p/PhyrexianNegator.java index f5f462bbf71e..9838a7455a7c 100644 --- a/Mage.Sets/src/mage/cards/p/PhyrexianNegator.java +++ b/Mage.Sets/src/mage/cards/p/PhyrexianNegator.java @@ -1,30 +1,26 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.common.DealtDamageToSourceTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.SacrificeEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; import mage.filter.FilterPermanent; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.players.Player; -import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + /** - * * @author fireshoes */ public final class PhyrexianNegator extends CardImpl { public PhyrexianNegator(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); this.subtype.add(SubType.PHYREXIAN); this.subtype.add(SubType.HORROR); @@ -33,8 +29,11 @@ public PhyrexianNegator(UUID ownerId, CardSetInfo setInfo) { // Trample this.addAbility(TrampleAbility.getInstance()); + // Whenever Phyrexian Negator is dealt damage, sacrifice that many permanents. - this.addAbility(new PhyrexianNegatorTriggeredAbility()); + this.addAbility(new DealtDamageToSourceTriggeredAbility( + new SacrificeEffect(new FilterPermanent(), SavedDamageValue.MANY, "") + )); } private PhyrexianNegator(final PhyrexianNegator card) { @@ -45,45 +44,4 @@ private PhyrexianNegator(final PhyrexianNegator card) { public PhyrexianNegator copy() { return new PhyrexianNegator(this); } -} - -class PhyrexianNegatorTriggeredAbility extends TriggeredAbilityImpl { - PhyrexianNegatorTriggeredAbility() { - super(Zone.BATTLEFIELD, new SacrificeEffect(new FilterPermanent(), 0,"")); - } - - private PhyrexianNegatorTriggeredAbility(final PhyrexianNegatorTriggeredAbility ability) { - super(ability); - } - - @Override - public PhyrexianNegatorTriggeredAbility copy() { - return new PhyrexianNegatorTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getTargetId().equals(this.sourceId)) { - UUID controller = game.getControllerId(event.getTargetId()); - if (controller != null) { - Player player = game.getPlayer(controller); - if (player != null) { - getEffects().get(0).setTargetPointer(new FixedTarget(player.getId())); - ((SacrificeEffect) getEffects().get(0)).setAmount(StaticValue.get(event.getAmount())); - return true; - } - } - } - return false; - } - - @Override - public String getRule() { - return "Whenever {this} is dealt damage, sacrifice that many permanents."; - } } \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/PhyrexianTotem.java b/Mage.Sets/src/mage/cards/p/PhyrexianTotem.java index af89b977e0da..43a777c8f91b 100644 --- a/Mage.Sets/src/mage/cards/p/PhyrexianTotem.java +++ b/Mage.Sets/src/mage/cards/p/PhyrexianTotem.java @@ -1,12 +1,14 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.DealtDamageToSourceTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SourceMatchesFilterCondition; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.decorator.ConditionalInterveningIfBatchTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.SacrificeEffect; import mage.abilities.effects.common.continuous.BecomesCreatureSourceEffect; import mage.abilities.keyword.TrampleAbility; @@ -14,32 +16,37 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; +import mage.constants.SubType; import mage.constants.Zone; -import mage.filter.common.FilterControlledPermanent; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; import mage.game.permanent.token.TokenImpl; -import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; /** - * * @author FenrisulfrX */ public final class PhyrexianTotem extends CardImpl { + private static final Condition condition = new SourceMatchesFilterCondition("it's a creature", StaticFilters.FILTER_PERMANENT_CREATURE); + public PhyrexianTotem(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT},"{3}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); // {tap}: Add {B}. this.addAbility(new BlackManaAbility()); - // {2}{B}: {this} becomes a 5/5 black Horror artifact creature with trample until end of turn. + + // {2}{B}: Phyrexian Totem becomes a 5/5 black Horror artifact creature with trample until end of turn. this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new BecomesCreatureSourceEffect( new PhyrexianTotemToken(), CardType.ARTIFACT, Duration.EndOfTurn), new ManaCostsImpl<>("{2}{B}"))); - // Whenever {this} is dealt damage, if it's a creature, sacrifice that many permanents. - this.addAbility(new PhyrexianTotemTriggeredAbility()); + + // Whenever Phyrexian Totem is dealt damage, if it's a creature, sacrifice that many permanents. + this.addAbility(new ConditionalInterveningIfBatchTriggeredAbility( + new DealtDamageToSourceTriggeredAbility(new SacrificeEffect(new FilterPermanent(), SavedDamageValue.MANY, "")), + condition, "Whenever {this} is dealt damage, if it's a creature, sacrifice that many permanents." + )); } private PhyrexianTotem(final PhyrexianTotem card) { @@ -50,70 +57,27 @@ private PhyrexianTotem(final PhyrexianTotem card) { public PhyrexianTotem copy() { return new PhyrexianTotem(this); } - - private static class PhyrexianTotemToken extends TokenImpl { - PhyrexianTotemToken() { - super("Phyrexian Horror", "5/5 black Phyrexian Horror artifact creature with trample"); - cardType.add(CardType.ARTIFACT); - cardType.add(CardType.CREATURE); - color.setBlack(true); - this.subtype.add(SubType.PHYREXIAN); - this.subtype.add(SubType.HORROR); - power = new MageInt(5); - toughness = new MageInt(5); - this.addAbility(TrampleAbility.getInstance()); - } - private PhyrexianTotemToken(final PhyrexianTotemToken token) { - super(token); - } - public PhyrexianTotemToken copy() { - return new PhyrexianTotemToken(this); - } - } } -class PhyrexianTotemTriggeredAbility extends TriggeredAbilityImpl { - - public PhyrexianTotemTriggeredAbility() { - super(Zone.BATTLEFIELD, new SacrificeEffect(new FilterControlledPermanent(), 0,"")); - } - - private PhyrexianTotemTriggeredAbility(final PhyrexianTotemTriggeredAbility ability) { - super(ability); - } - - @Override - public PhyrexianTotemTriggeredAbility copy() { - return new PhyrexianTotemTriggeredAbility(this); - } - - @Override - public boolean checkInterveningIfClause(Game game) { - Permanent permanent = game.getPermanentOrLKIBattlefield(getSourceId()); - if (permanent != null) { - return permanent.isCreature(game); - } - return false; +class PhyrexianTotemToken extends TokenImpl { + PhyrexianTotemToken() { + super("Phyrexian Horror", "5/5 black Phyrexian Horror artifact creature with trample"); + cardType.add(CardType.ARTIFACT); + cardType.add(CardType.CREATURE); + color.setBlack(true); + this.subtype.add(SubType.PHYREXIAN); + this.subtype.add(SubType.HORROR); + power = new MageInt(5); + toughness = new MageInt(5); + this.addAbility(TrampleAbility.getInstance()); } - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; + private PhyrexianTotemToken(final PhyrexianTotemToken token) { + super(token); } - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getTargetId().equals(getSourceId())) { - getEffects().get(0).setTargetPointer(new FixedTarget(getControllerId())); - ((SacrificeEffect) getEffects().get(0)).setAmount(StaticValue.get(event.getAmount())); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever {this} is dealt damage, if it's a creature, sacrifice that many permanents."; + public PhyrexianTotemToken copy() { + return new PhyrexianTotemToken(this); } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/PiousWarrior.java b/Mage.Sets/src/mage/cards/p/PiousWarrior.java index 892927fc46ec..3a69a0714570 100644 --- a/Mage.Sets/src/mage/cards/p/PiousWarrior.java +++ b/Mage.Sets/src/mage/cards/p/PiousWarrior.java @@ -1,30 +1,24 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; -import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.common.IsDealtCombatDamageSourceTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; +import mage.abilities.effects.common.GainLifeEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Outcome; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.DamagedBatchForOnePermanentEvent; -import mage.game.events.GameEvent; -import mage.players.Player; + +import java.util.UUID; /** - * * @author Backfir3 */ public final class PiousWarrior extends CardImpl { public PiousWarrior(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{W}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.REBEL); this.subtype.add(SubType.WARRIOR); @@ -32,8 +26,10 @@ public PiousWarrior(UUID ownerId, CardSetInfo setInfo) { this.power = new MageInt(2); this.toughness = new MageInt(3); - // Whenever Pious Warrior is dealt combat damage, you gain that much life. - this.addAbility(new PiousWarriorTriggeredAbility()); + // Whenever Pious Warrior is dealt combat damage, you gain that much life. + this.addAbility(new IsDealtCombatDamageSourceTriggeredAbility( + new GainLifeEffect(SavedDamageValue.MUCH) + )); } private PiousWarrior(final PiousWarrior card) { @@ -44,67 +40,4 @@ private PiousWarrior(final PiousWarrior card) { public PiousWarrior copy() { return new PiousWarrior(this); } -} - -class PiousWarriorTriggeredAbility extends TriggeredAbilityImpl { - - public PiousWarriorTriggeredAbility() { - super(Zone.BATTLEFIELD, new PiousWarriorGainLifeEffect()); - setTriggerPhrase("Whenever {this} is dealt combat damage, "); - } - - private PiousWarriorTriggeredAbility(final PiousWarriorTriggeredAbility effect) { - super(effect); - } - - @Override - public PiousWarriorTriggeredAbility copy() { - return new PiousWarriorTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - - DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event; - int damage = dEvent.getAmount(); - - if (event.getTargetId().equals(this.sourceId) && dEvent.isCombatDamage() && damage > 0) { - this.getEffects().setValue("damageAmount", damage); - return true; - } - return false; - } -} - - -class PiousWarriorGainLifeEffect extends OneShotEffect { - - public PiousWarriorGainLifeEffect() { - super(Outcome.GainLife); - staticText = "you gain that much life"; - } - - private PiousWarriorGainLifeEffect(final PiousWarriorGainLifeEffect effect) { - super(effect); - } - - @Override - public PiousWarriorGainLifeEffect copy() { - return new PiousWarriorGainLifeEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - if (player != null) { - player.gainLife((Integer) this.getValue("damageAmount"), game, source); - } - return true; - } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/PopularEntertainer.java b/Mage.Sets/src/mage/cards/p/PopularEntertainer.java index 3ff04e514777..c205df6bca42 100644 --- a/Mage.Sets/src/mage/cards/p/PopularEntertainer.java +++ b/Mage.Sets/src/mage/cards/p/PopularEntertainer.java @@ -1,19 +1,22 @@ package mage.cards.p; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.DealCombatDamageControlledTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.combat.GoadTargetEffect; import mage.abilities.effects.common.continuous.GainAbilityAllEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.permanent.ControllerIdPredicate; import mage.game.Game; -import mage.game.events.DamagedBatchForOnePlayerEvent; import mage.game.events.GameEvent; +import mage.players.Player; import mage.target.TargetPermanent; import java.util.UUID; @@ -46,10 +49,10 @@ public PopularEntertainer copy() { } } -class PopularEntertainerAbility extends TriggeredAbilityImpl { +class PopularEntertainerAbility extends DealCombatDamageControlledTriggeredAbility { PopularEntertainerAbility() { - super(Zone.BATTLEFIELD, new GoadTargetEffect(), false); + super(new GoadTargetEffect()); } private PopularEntertainerAbility(final PopularEntertainerAbility ability) { @@ -61,28 +64,19 @@ public PopularEntertainerAbility copy() { return new PopularEntertainerAbility(this); } - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; - } - @Override public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event; - - int damage = dEvent.getEvents() - .stream() - .filter(ev -> ev.getSourceId().equals(controllerId)) - .mapToInt(GameEvent::getAmount) - .sum(); - - if (!dEvent.isCombatDamage() || damage < 1){ + if (!super.checkTrigger(event, game)) { + return false; + } + Player player = game.getPlayer(event.getTargetId()); + if (player == null) { return false; } FilterPermanent filter = new FilterCreaturePermanent( - "creature controlled by " + game.getPlayer(dEvent.getTargetId()).getName() + "creature controlled by " + player.getName() ); - filter.add(new ControllerIdPredicate(dEvent.getTargetId())); + filter.add(new ControllerIdPredicate(player.getId())); this.getTargets().clear(); this.addTarget(new TargetPermanent(filter)); return true; diff --git a/Mage.Sets/src/mage/cards/r/RaggedVeins.java b/Mage.Sets/src/mage/cards/r/RaggedVeins.java index a468b0cd46f0..d70906fb2609 100644 --- a/Mage.Sets/src/mage/cards/r/RaggedVeins.java +++ b/Mage.Sets/src/mage/cards/r/RaggedVeins.java @@ -1,7 +1,6 @@ package mage.cards.r; -import java.util.UUID; -import mage.abilities.common.DealtDamageAttachedTriggeredAbility; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.LoseLifeControllerAttachedEffect; @@ -10,19 +9,20 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class RaggedVeins extends CardImpl { public RaggedVeins(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{1}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}"); this.subtype.add(SubType.AURA); // Flash @@ -35,9 +35,9 @@ public RaggedVeins(UUID ownerId, CardSetInfo setInfo) { this.addAbility(new EnchantAbility(auraTarget)); // Whenever enchanted creature is dealt damage, its controller loses that much life. - this.addAbility(new DealtDamageAttachedTriggeredAbility( + this.addAbility(new IsDealtDamageAttachedTriggeredAbility( new LoseLifeControllerAttachedEffect(SavedDamageValue.MUCH), - false + false, "enchanted" )); } diff --git a/Mage.Sets/src/mage/cards/r/Repercussion.java b/Mage.Sets/src/mage/cards/r/Repercussion.java index 2f7c4e51c304..fb4d7a782519 100644 --- a/Mage.Sets/src/mage/cards/r/Repercussion.java +++ b/Mage.Sets/src/mage/cards/r/Repercussion.java @@ -2,6 +2,7 @@ import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; @@ -11,11 +12,14 @@ import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; +import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import java.util.UUID; +import java.util.stream.Stream; /** * @author cbrianhill @@ -39,7 +43,7 @@ public Repercussion copy() { } } -class RepercussionTriggeredAbility extends TriggeredAbilityImpl { +class RepercussionTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { static final String PLAYER_DAMAGE_AMOUNT_KEY = "playerDamage"; static final String TRIGGERING_CREATURE_KEY = "triggeringCreature"; @@ -57,13 +61,27 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(e -> e.getAmount() > 0); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { Permanent permanent = game.getPermanent(event.getTargetId()); if (permanent == null || !permanent.isCreature(game)) { return false; } - getEffects().setValue(PLAYER_DAMAGE_AMOUNT_KEY, event.getAmount()); + int amount = filterBatchEvent(event, game) + .mapToInt(GameEvent::getAmount) + .sum(); + if (amount <= 0) { + return false; + } + getEffects().setValue(PLAYER_DAMAGE_AMOUNT_KEY, amount); getEffects().setValue(TRIGGERING_CREATURE_KEY, new MageObjectReference(event.getTargetId(), game)); return true; } diff --git a/Mage.Sets/src/mage/cards/r/RisonaAsariCommander.java b/Mage.Sets/src/mage/cards/r/RisonaAsariCommander.java index 910cfe40ec09..efce73ee1b32 100644 --- a/Mage.Sets/src/mage/cards/r/RisonaAsariCommander.java +++ b/Mage.Sets/src/mage/cards/r/RisonaAsariCommander.java @@ -1,30 +1,33 @@ package mage.cards.r; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; import mage.abilities.condition.Condition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.effects.common.counter.RemoveCounterSourceEffect; -import mage.constants.SubType; -import mage.constants.SuperType; import mage.abilities.keyword.HasteAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.DamagedBatchForPlayersEvent; import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author weirddan455 */ public final class RisonaAsariCommander extends CardImpl { @@ -62,7 +65,7 @@ public RisonaAsariCommander copy() { } } -class RisonaAsariCommanderTriggeredAbility extends TriggeredAbilityImpl { +class RisonaAsariCommanderTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public RisonaAsariCommanderTriggeredAbility() { super(Zone.BATTLEFIELD, new RemoveCounterSourceEffect(CounterType.INDESTRUCTIBLE.createInstance())); @@ -84,12 +87,17 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { + public Stream filterBatchEvent(GameEvent event, Game game) { return ((DamagedBatchForPlayersEvent) event) .getEvents() .stream() .filter(DamagedEvent::isCombatDamage) - .anyMatch(e -> e.getTargetId().equals(getControllerId())); + .filter(e -> e.getTargetId().equals(getControllerId())); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return filterBatchEvent(event, game).findAny().isPresent(); } } diff --git a/Mage.Sets/src/mage/cards/r/RiteOfPassage.java b/Mage.Sets/src/mage/cards/r/RiteOfPassage.java index 2242d1c3d19c..a5c1ce0f55c4 100644 --- a/Mage.Sets/src/mage/cards/r/RiteOfPassage.java +++ b/Mage.Sets/src/mage/cards/r/RiteOfPassage.java @@ -1,7 +1,7 @@ package mage.cards.r; -import java.util.UUID; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; @@ -11,20 +11,23 @@ import mage.constants.Zone; import mage.counters.CounterType; import mage.filter.StaticFilters; -import mage.filter.common.FilterControlledCreaturePermanent; import mage.game.Game; +import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import mage.target.targetpointer.FixedTarget; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author Plopman */ public final class RiteOfPassage extends CardImpl { public RiteOfPassage(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{2}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); // Whenever a creature you control is dealt damage, put a +1/+1 counter on it. Effect effect = new AddCountersTargetEffect(CounterType.P1P1.createInstance()); @@ -43,7 +46,7 @@ public RiteOfPassage copy() { } } -class RiteOfPassageTriggeredAbility extends TriggeredAbilityImpl { +class RiteOfPassageTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public RiteOfPassageTriggeredAbility(Effect effect) { super(Zone.BATTLEFIELD, effect); @@ -64,14 +67,27 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(e -> e.getAmount() > 0) + .filter(e -> Optional + .of(e) + .map(DamagedPermanentEvent::getTargetId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> StaticFilters.FILTER_CONTROLLED_CREATURE.match(p, getControllerId(), this, game)) + .isPresent() + ); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - UUID targetId = event.getTargetId(); - Permanent permanent = game.getPermanent(targetId); - if (permanent != null && StaticFilters.FILTER_CONTROLLED_CREATURE.match(permanent, getControllerId(), this, game)) { - getEffects().setTargetPointer(new FixedTarget(targetId, game)); - return true; + if (!filterBatchEvent(event, game).findAny().isPresent()) { + return false; } - return false; + getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game)); + return true; } } diff --git a/Mage.Sets/src/mage/cards/s/SatoruTheInfiltrator.java b/Mage.Sets/src/mage/cards/s/SatoruTheInfiltrator.java index 44456cc59012..c54ad7e95429 100644 --- a/Mage.Sets/src/mage/cards/s/SatoruTheInfiltrator.java +++ b/Mage.Sets/src/mage/cards/s/SatoruTheInfiltrator.java @@ -1,6 +1,7 @@ package mage.cards.s; import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.keyword.MenaceAbility; @@ -21,6 +22,7 @@ import java.util.List; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Susucr @@ -54,7 +56,7 @@ public SatoruTheInfiltrator copy() { } } -class SatoruTheInfiltratorTriggeredAbility extends TriggeredAbilityImpl { +class SatoruTheInfiltratorTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public SatoruTheInfiltratorTriggeredAbility() { super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false); @@ -76,11 +78,9 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; } - // event is GameEvent.EventType.ENTERS_THE_BATTLEFIELD @Override - public boolean checkTrigger(GameEvent event, Game game) { - ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event; - List moved = zEvent.getEvents() + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((ZoneChangeBatchEvent) event).getEvents() .stream() .filter(e -> e.getToZone() == Zone.BATTLEFIELD) // Keep only to the battlefield .filter(e -> { @@ -90,9 +90,13 @@ public boolean checkTrigger(GameEvent event, Game game) { } return permanent.isControlledBy(getControllerId()) // under your control && (permanent.getId().equals(getSourceId()) // {this} - || (permanent.isCreature(game) && !(permanent instanceof PermanentToken)) // other nontoken Creature - ); - }) + || (permanent.isCreature(game) && !(permanent instanceof PermanentToken))); // or other nontoken Creature + }); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + List moved = filterBatchEvent(event, game) .collect(Collectors.toList()); if (moved.isEmpty()) { return false; diff --git a/Mage.Sets/src/mage/cards/s/SoulLink.java b/Mage.Sets/src/mage/cards/s/SoulLink.java index 818a96d3b15d..728657e5be38 100644 --- a/Mage.Sets/src/mage/cards/s/SoulLink.java +++ b/Mage.Sets/src/mage/cards/s/SoulLink.java @@ -1,8 +1,7 @@ package mage.cards.s; -import java.util.UUID; import mage.abilities.common.DealsDamageAttachedTriggeredAbility; -import mage.abilities.common.DealtDamageAttachedTriggeredAbility; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.AttachEffect; import mage.abilities.effects.common.GainLifeEffect; @@ -10,20 +9,21 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * * @author LoneFox */ public final class SoulLink extends CardImpl { public SoulLink(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{1}{W}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{W}{B}"); this.subtype.add(SubType.AURA); // Enchant creature @@ -34,9 +34,11 @@ public SoulLink(UUID ownerId, CardSetInfo setInfo) { // Whenever enchanted creature deals damage, you gain that much life. this.addAbility(new DealsDamageAttachedTriggeredAbility(Zone.BATTLEFIELD, - new GainLifeEffect(SavedDamageValue.MUCH), false)); + new GainLifeEffect(SavedDamageValue.MUCH), false)); // Whenever enchanted creature is dealt damage, you gain that much life. - this.addAbility(new DealtDamageAttachedTriggeredAbility(new GainLifeEffect(SavedDamageValue.MUCH), false)); + this.addAbility(new IsDealtDamageAttachedTriggeredAbility( + new GainLifeEffect(SavedDamageValue.MUCH), false, "enchanted" + )); } private SoulLink(final SoulLink card) { diff --git a/Mage.Sets/src/mage/cards/s/SoulsOfTheFaultless.java b/Mage.Sets/src/mage/cards/s/SoulsOfTheFaultless.java index dd1528d92973..c4924f07fbbb 100644 --- a/Mage.Sets/src/mage/cards/s/SoulsOfTheFaultless.java +++ b/Mage.Sets/src/mage/cards/s/SoulsOfTheFaultless.java @@ -1,32 +1,29 @@ package mage.cards.s; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; +import mage.abilities.common.IsDealtCombatDamageSourceTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.DefenderAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.SubType; import mage.game.Game; -import mage.game.events.DamagedBatchForOnePermanentEvent; -import mage.game.events.GameEvent; import mage.players.Player; +import java.util.UUID; + /** - * * @author North */ public final class SoulsOfTheFaultless extends CardImpl { public SoulsOfTheFaultless(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{W}{B}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}{B}{B}"); this.subtype.add(SubType.SPIRIT); this.power = new MageInt(0); @@ -35,7 +32,9 @@ public SoulsOfTheFaultless(UUID ownerId, CardSetInfo setInfo) { // Defender this.addAbility(DefenderAbility.getInstance()); // Whenever Souls of the Faultless is dealt combat damage, you gain that much life and attacking player loses that much life. - this.addAbility(new SoulsOfTheFaultlessTriggeredAbility()); + this.addAbility(new IsDealtCombatDamageSourceTriggeredAbility( + new SoulsOfTheFaultlessEffect() + )); } private SoulsOfTheFaultless(final SoulsOfTheFaultless card) { @@ -48,45 +47,6 @@ public SoulsOfTheFaultless copy() { } } -class SoulsOfTheFaultlessTriggeredAbility extends TriggeredAbilityImpl { - - public SoulsOfTheFaultlessTriggeredAbility() { - super(Zone.BATTLEFIELD, new SoulsOfTheFaultlessEffect()); - setTriggerPhrase("Whenever {this} is dealt combat damage, "); - } - - private SoulsOfTheFaultlessTriggeredAbility(final SoulsOfTheFaultlessTriggeredAbility effect) { - super(effect); - } - - @Override - public SoulsOfTheFaultlessTriggeredAbility copy() { - return new SoulsOfTheFaultlessTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event; - - int damage = dEvent.getAmount(); - - if (dEvent.getTargetId().equals(this.sourceId) && dEvent.isCombatDamage() && damage > 0) { - UUID attackerId = game.getActivePlayerId(); - for (Effect effect : this.getEffects()) { - effect.setValue("damageAmount", damage); - effect.setValue("attackerId", attackerId); - } - return true; - } - return false; - } -} - class SoulsOfTheFaultlessEffect extends OneShotEffect { SoulsOfTheFaultlessEffect() { @@ -105,14 +65,14 @@ public SoulsOfTheFaultlessEffect copy() { @Override public boolean apply(Game game, Ability source) { - Integer amount = (Integer) this.getValue("damageAmount"); + int amount = SavedDamageValue.MUCH.calculate(game, source, this); Player player = game.getPlayer(source.getControllerId()); if (player != null) { player.gainLife(amount, game, source); } - UUID attackerId = (UUID) this.getValue("attackerId"); + UUID attackerId = game.getActivePlayerId(); Player attacker = game.getPlayer(attackerId); if (attacker != null) { attacker.loseLife(amount, game, source, false); diff --git a/Mage.Sets/src/mage/cards/s/SowerOfDiscord.java b/Mage.Sets/src/mage/cards/s/SowerOfDiscord.java index 308c0337f127..99f516b66b53 100644 --- a/Mage.Sets/src/mage/cards/s/SowerOfDiscord.java +++ b/Mage.Sets/src/mage/cards/s/SowerOfDiscord.java @@ -1,8 +1,8 @@ package mage.cards.s; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.effects.Effect; @@ -16,14 +16,18 @@ import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; +import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.TargetPlayer; import mage.target.targetpointer.FixedTarget; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author TheElk801 */ public final class SowerOfDiscord extends CardImpl { @@ -94,8 +98,8 @@ public boolean apply(Game game, Ability source) { permanent.addInfo( "chosen players", "Chosen players: " - + player1.getName() + ", " - + player2.getName() + "", game + + player1.getName() + ", " + + player2.getName() + "", game ); return true; } @@ -107,7 +111,7 @@ public SowerOfDiscordEntersBattlefieldEffect copy() { } -class SowerOfDiscordTriggeredAbility extends TriggeredAbilityImpl { +class SowerOfDiscordTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public SowerOfDiscordTriggeredAbility() { super(Zone.BATTLEFIELD, null); @@ -127,19 +131,39 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + Player player1 = (Player) game.getState().getValue( + this.getSourceId() + "_player1" + ); + Player player2 = (Player) game.getState().getValue( + this.getSourceId() + "_player2" + ); + if (player1 == null || player2 == null) { + return Stream.empty(); + } + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(e -> e.getTargetId().equals(player1.getId()) || e.getTargetId().equals(player2.getId())) + .filter(e -> e.getAmount() > 0); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - int damage = event.getAmount(); Player player1 = (Player) game.getState().getValue( this.getSourceId() + "_player1" ); Player player2 = (Player) game.getState().getValue( this.getSourceId() + "_player2" ); - if (player1 == null || player2 == null || damage == 0) { + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPlayerEvent::getAmount) + .sum(); + if (player1 == null || player2 == null || amount <= 0) { return false; } - Effect effect = new LoseLifeTargetEffect(damage); + Effect effect = new LoseLifeTargetEffect(amount); if (event.getTargetId().equals(player1.getId())) { this.getEffects().clear(); effect.setTargetPointer(new FixedTarget(player2.getId())); diff --git a/Mage.Sets/src/mage/cards/s/SpitefulShadows.java b/Mage.Sets/src/mage/cards/s/SpitefulShadows.java index 87b518c70b35..432c2e39d883 100644 --- a/Mage.Sets/src/mage/cards/s/SpitefulShadows.java +++ b/Mage.Sets/src/mage/cards/s/SpitefulShadows.java @@ -1,7 +1,7 @@ package mage.cards.s; import mage.abilities.Ability; -import mage.abilities.common.DealtDamageAttachedTriggeredAbility; +import mage.abilities.common.IsDealtDamageAttachedTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.AttachEffect; import mage.abilities.keyword.EnchantAbility; @@ -33,8 +33,9 @@ public SpitefulShadows(UUID ownerId, CardSetInfo setInfo) { this.addAbility(new EnchantAbility(auraTarget)); // Whenever enchanted creature is dealt damage, it deals that much damage to its controller. - this.addAbility(new DealtDamageAttachedTriggeredAbility(Zone.BATTLEFIELD, new SpitefulShadowsEffect(), - false, SetTargetPointer.PERMANENT)); + this.addAbility(new IsDealtDamageAttachedTriggeredAbility( + Zone.BATTLEFIELD, new SpitefulShadowsEffect(), false, "enchanted", SetTargetPointer.PERMANENT + )); } private SpitefulShadows(final SpitefulShadows card) { diff --git a/Mage.Sets/src/mage/cards/s/SunCrownedHunters.java b/Mage.Sets/src/mage/cards/s/SunCrownedHunters.java index 7d487a7128d5..8ecde421e00a 100644 --- a/Mage.Sets/src/mage/cards/s/SunCrownedHunters.java +++ b/Mage.Sets/src/mage/cards/s/SunCrownedHunters.java @@ -2,6 +2,7 @@ package mage.cards.s; import java.util.UUID; + import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.DealtDamageToSourceTriggeredAbility; @@ -13,7 +14,6 @@ import mage.target.common.TargetOpponentOrPlaneswalker; /** - * * @author TheElk801 */ public final class SunCrownedHunters extends CardImpl { diff --git a/Mage.Sets/src/mage/cards/s/SunDroplet.java b/Mage.Sets/src/mage/cards/s/SunDroplet.java index 3830f941642b..e72c5f029a48 100644 --- a/Mage.Sets/src/mage/cards/s/SunDroplet.java +++ b/Mage.Sets/src/mage/cards/s/SunDroplet.java @@ -1,9 +1,10 @@ package mage.cards.s; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.common.IsDealtDamageYouTriggeredAbility; import mage.abilities.costs.common.RemoveCountersSourceCost; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.GainLifeEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; @@ -11,11 +12,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.TargetController; -import mage.constants.Zone; import mage.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import java.util.UUID; @@ -28,7 +25,9 @@ public SunDroplet(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}"); // Whenever you're dealt damage, put that many charge counters on Sun Droplet. - this.addAbility(new SunDropletTriggeredAbility()); + this.addAbility(new IsDealtDamageYouTriggeredAbility( + new AddCountersSourceEffect(CounterType.CHARGE.createInstance(), SavedDamageValue.MANY, true), false + )); // At the beginning of each upkeep, you may remove a charge counter from Sun Droplet. If you do, you gain 1 life. this.addAbility(new BeginningOfUpkeepTriggeredAbility( @@ -46,42 +45,4 @@ private SunDroplet(final SunDroplet card) { public SunDroplet copy() { return new SunDroplet(this); } -} - -class SunDropletTriggeredAbility extends TriggeredAbilityImpl { - - SunDropletTriggeredAbility() { - super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.CHARGE.createInstance()), false); - } - - private SunDropletTriggeredAbility(final SunDropletTriggeredAbility ability) { - super(ability); - } - - @Override - public SunDropletTriggeredAbility copy() { - return new SunDropletTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getTargetId().equals(this.getControllerId())) { - this.getEffects().clear(); - if (event.getAmount() > 0) { - this.addEffect(new AddCountersSourceEffect(CounterType.CHARGE.createInstance(event.getAmount()))); - } - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you're dealt damage, put that many charge counters on {this}."; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SwarmbornGiant.java b/Mage.Sets/src/mage/cards/s/SwarmbornGiant.java index 3cd0f13715c1..38bbbe4ed1f6 100644 --- a/Mage.Sets/src/mage/cards/s/SwarmbornGiant.java +++ b/Mage.Sets/src/mage/cards/s/SwarmbornGiant.java @@ -3,6 +3,7 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.MonstrousCondition; @@ -19,12 +20,13 @@ import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import java.util.UUID; +import java.util.stream.Stream; /** - * * @author LevelX2 */ public final class SwarmbornGiant extends CardImpl { @@ -61,7 +63,7 @@ public SwarmbornGiant copy() { } } -class SwarmbornGiantTriggeredAbility extends TriggeredAbilityImpl { +class SwarmbornGiantTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public SwarmbornGiantTriggeredAbility() { super(Zone.BATTLEFIELD, new SacrificeSourceEffect(), false); @@ -82,12 +84,18 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(DamagedPlayerEvent::isCombatDamage) + .filter(e -> getControllerId().equals(e.getTargetId())) + .filter(e -> e.getAmount() > 0); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event; - if (dEvent.getTargetId().equals(this.getControllerId())) { - return dEvent.isCombatDamage() && dEvent.getAmount() > 0; - } - return false; + return filterBatchEvent(event, game).findAny().isPresent(); } } diff --git a/Mage.Sets/src/mage/cards/t/TheMillenniumCalendar.java b/Mage.Sets/src/mage/cards/t/TheMillenniumCalendar.java index b5b66b14eab7..1e015414c1f7 100644 --- a/Mage.Sets/src/mage/cards/t/TheMillenniumCalendar.java +++ b/Mage.Sets/src/mage/cards/t/TheMillenniumCalendar.java @@ -1,6 +1,7 @@ package mage.cards.t; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.StateTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleActivatedAbility; @@ -23,8 +24,9 @@ import mage.game.events.UntappedEvent; import mage.game.permanent.Permanent; -import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; /** * @author Susucr @@ -61,7 +63,7 @@ public TheMillenniumCalendar copy() { } } -class TheMillenniumCalendarTriggeredAbility extends TriggeredAbilityImpl { +class TheMillenniumCalendarTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public TheMillenniumCalendarTriggeredAbility() { super(Zone.BATTLEFIELD, null, false); @@ -82,30 +84,35 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { + public Stream filterBatchEvent(GameEvent event, Game game) { if (!game.isActivePlayer(getControllerId())) { - return false; + return Stream.empty(); } - UntappedBatchEvent batchEvent = (UntappedBatchEvent) event; - int count = batchEvent + return ((UntappedBatchEvent) event) .getEvents() .stream() .filter(UntappedEvent::isAnUntapStepEvent) - .map(UntappedEvent::getTargetId) - .map(game::getPermanent) - .filter(Objects::nonNull) - .filter(p -> p.getControllerId().equals(getControllerId())) + .filter(e -> Optional + .of(e) + .map(UntappedEvent::getTargetId) + .map(game::getPermanent) + .filter(p -> p.getControllerId().equals(getControllerId())) + .isPresent() + ); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) .mapToInt(p -> 1) .sum(); - - if (count <= 0) { + if (amount <= 0) { return false; } - this.getEffects().clear(); - this.addEffect(new AddCountersSourceEffect(CounterType.TIME.createInstance(count))); + this.addEffect(new AddCountersSourceEffect(CounterType.TIME.createInstance(amount))); this.getHints().clear(); - this.addHint(new StaticHint("Number of untapped permanents: " + count)); + this.addHint(new StaticHint("Number of untapped permanents: " + amount)); return true; } diff --git a/Mage.Sets/src/mage/cards/t/TheRavensWarning.java b/Mage.Sets/src/mage/cards/t/TheRavensWarning.java index 658baa15a572..248e7d88197a 100644 --- a/Mage.Sets/src/mage/cards/t/TheRavensWarning.java +++ b/Mage.Sets/src/mage/cards/t/TheRavensWarning.java @@ -1,26 +1,28 @@ package mage.cards.t; -import java.util.UUID; - +import mage.abilities.BatchTriggeredAbility; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.common.SagaAbility; import mage.abilities.effects.common.*; import mage.abilities.hint.common.OpenSideboardHint; import mage.abilities.keyword.FlyingAbility; -import mage.constants.Duration; -import mage.constants.SagaChapter; -import mage.constants.SubType; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SagaChapter; +import mage.constants.SubType; import mage.game.Game; import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.permanent.token.BlueBirdToken; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author weirddan455 */ public final class TheRavensWarning extends CardImpl { @@ -62,7 +64,7 @@ public TheRavensWarning copy() { } } -class TheRavensWarningTriggeredAbility extends DelayedTriggeredAbility { +class TheRavensWarningTriggeredAbility extends DelayedTriggeredAbility implements BatchTriggeredAbility { public TheRavensWarningTriggeredAbility() { super(new LookAtTargetPlayerHandEffect(), Duration.EndOfTurn, false); @@ -78,30 +80,30 @@ public TheRavensWarningTriggeredAbility copy() { return new TheRavensWarningTriggeredAbility(this); } - // Code based on ControlledCreaturesDealCombatDamagePlayerTriggeredAbility @Override public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; } @Override - public boolean checkTrigger(GameEvent event, Game game) { - - DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event; - - int flyingDamage = dEvent.getEvents() + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() .stream() - .filter(ev -> { - if (!ev.getSourceId().equals(controllerId)) { + .filter(DamagedPlayerEvent::isCombatDamage) + .filter(e -> e.getAmount() > 0) + .filter(e -> { + if (!e.getSourceId().equals(controllerId)) { return false; } - Permanent permanent = game.getPermanentOrLKIBattlefield(ev.getSourceId()); + Permanent permanent = game.getPermanentOrLKIBattlefield(e.getSourceId()); return permanent != null && permanent.isCreature() && permanent.hasAbility(FlyingAbility.getInstance(), game); - }) - .mapToInt(GameEvent::getAmount) - .sum(); + }); + } - return flyingDamage > 0 && dEvent.isCombatDamage(); + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/t/ToralfGodOfFury.java b/Mage.Sets/src/mage/cards/t/ToralfGodOfFury.java index 2af8737fb863..79db89e53dc9 100644 --- a/Mage.Sets/src/mage/cards/t/ToralfGodOfFury.java +++ b/Mage.Sets/src/mage/cards/t/ToralfGodOfFury.java @@ -2,6 +2,7 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.Mode; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleStaticAbility; @@ -28,6 +29,7 @@ import mage.game.Game; import mage.game.events.DamagedBatchForOnePermanentEvent; import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; @@ -35,6 +37,7 @@ import mage.target.common.TargetPermanentOrPlayer; import java.util.UUID; +import java.util.stream.Stream; /** * @author TheElk801 @@ -92,7 +95,7 @@ public ToralfGodOfFury copy() { } } -class ToralfGodOfFuryTriggeredAbility extends TriggeredAbilityImpl { +class ToralfGodOfFuryTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { ToralfGodOfFuryTriggeredAbility() { super(Zone.BATTLEFIELD, null); @@ -108,16 +111,21 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event; - int excessDamage = dEvent.getEvents() + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() .stream() + .filter(e -> !e.isCombatDamage()) + .filter(e -> e.getExcess() > 0) + .filter(e -> game.getOpponents(getControllerId()).contains(game.getControllerId(e.getTargetId()))); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int excessDamage = filterBatchEvent(event, game) .mapToInt(DamagedEvent::getExcess) .sum(); - - if (excessDamage < 1 - || dEvent.isCombatDamage() - || !game.getOpponents(getControllerId()).contains(game.getControllerId(event.getTargetId()))) { + if (excessDamage <= 0) { return false; } this.getEffects().clear(); diff --git a/Mage.Sets/src/mage/cards/v/VengefulPharaoh.java b/Mage.Sets/src/mage/cards/v/VengefulPharaoh.java index 98f75e574e16..d1c3d5d3889d 100644 --- a/Mage.Sets/src/mage/cards/v/VengefulPharaoh.java +++ b/Mage.Sets/src/mage/cards/v/VengefulPharaoh.java @@ -2,27 +2,31 @@ package mage.cards.v; import mage.MageInt; -import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.condition.common.SourceInGraveyardCondition; +import mage.abilities.decorator.ConditionalInterveningIfBatchTriggeredAbility; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.effects.common.PutOnLibrarySourceEffect; import mage.abilities.keyword.DeathtouchAbility; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; -import mage.game.events.*; +import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedEvent; +import mage.game.events.GameEvent; import mage.game.permanent.Permanent; -import mage.players.Player; import mage.target.common.TargetAttackingCreature; import java.util.UUID; +import java.util.stream.Stream; /** - * @author North + * @author Susucr */ public final class VengefulPharaoh extends CardImpl { @@ -37,7 +41,12 @@ public VengefulPharaoh(UUID ownerId, CardSetInfo setInfo) { this.addAbility(DeathtouchAbility.getInstance()); // Whenever combat damage is dealt to you or a planeswalker you control, if Vengeful Pharaoh is in your graveyard, destroy target attacking creature, then put Vengeful Pharaoh on top of your library. - this.addAbility(new VengefulPharaohTriggeredAbility()); + this.addAbility(new ConditionalInterveningIfBatchTriggeredAbility<>( + new VengefulPharaohTriggeredAbility(), SourceInGraveyardCondition.instance, + "Whenever combat damage is dealt to you or a planeswalker you control, " + + "if {this} is in your graveyard, destroy target attacking creature, " + + "then put {this} on top of your library" + )); } private VengefulPharaoh(final VengefulPharaoh card) { @@ -50,11 +59,12 @@ public VengefulPharaoh copy() { } } -class VengefulPharaohTriggeredAbility extends TriggeredAbilityImpl { +class VengefulPharaohTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public VengefulPharaohTriggeredAbility() { - super(Zone.GRAVEYARD, new VengefulPharaohEffect(), false); + super(Zone.GRAVEYARD, new DestroyTargetEffect(), false); this.addTarget(new TargetAttackingCreature()); + this.addEffect(new PutOnLibrarySourceEffect(true)); } private VengefulPharaohTriggeredAbility(final VengefulPharaohTriggeredAbility ability) { @@ -66,17 +76,6 @@ public VengefulPharaohTriggeredAbility copy() { return new VengefulPharaohTriggeredAbility(this); } - @Override - public boolean checkInterveningIfClause(Game game) { - // Vengeful Pharaoh must be in your graveyard when combat damage is dealt to you or a planeswalker you control - // in order for its ability to trigger. That is, it can’t die and trigger from your graveyard during the same - // combat damage step. (2011-09-22) - - // If Vengeful Pharaoh is no longer in your graveyard when the triggered ability would resolve, the triggered - // ability won’t do anything. (2011-09-22) - return game.getState().getZone(getSourceId()) == Zone.GRAVEYARD; - } - @Override public boolean checkEventType(GameEvent event, Game game) { // If multiple creatures deal combat damage to you simultaneously, Vengeful Pharaoh will only trigger once. @@ -86,63 +85,37 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - - if ((event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER - && event.getTargetId().equals(this.getControllerId()))) { - DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event; - return dEvent.isCombatDamage() && dEvent.getAmount() > 0; - } - if (event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT) { - Permanent permanent = game.getPermanent(event.getTargetId()); - DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event; - return permanent != null - && permanent.isPlaneswalker(game) - && permanent.isControlledBy(this.getControllerId()) - && dEvent.isCombatDamage() && dEvent.getAmount() > 0; + public Stream filterBatchEvent(GameEvent event, Game game) { + switch (event.getType()) { + case DAMAGED_BATCH_FOR_ONE_PLAYER: + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(DamagedEvent::isCombatDamage) + .filter(e -> e.getAmount() > 0) + .filter(e -> getControllerId().equals(e.getTargetId())) + .map(DamagedEvent.class::cast); + case DAMAGED_BATCH_FOR_ONE_PERMANENT: + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(DamagedEvent::isCombatDamage) + .filter(e -> e.getAmount() > 0) + .filter(e -> { + // A planeswalker you control + Permanent permanent = game.getPermanentOrLKIBattlefield(e.getTargetId()); + return permanent != null + && permanent.isPlaneswalker(game) + && permanent.isControlledBy(getControllerId()); + }) + .map(DamagedEvent.class::cast); + default: + return Stream.empty(); } - return false; } @Override - public String getRule() { - return "Whenever combat damage is dealt to you or a planeswalker you control, if {this} is in your " + - "graveyard, destroy target attacking creature, then put {this} on top of your library."; - } -} - -class VengefulPharaohEffect extends OneShotEffect { - - VengefulPharaohEffect() { - super(Outcome.DestroyPermanent); - this.staticText = "destroy target attacking creature, then put {this} on top of your library"; - } - - private VengefulPharaohEffect(final VengefulPharaohEffect effect) { - super(effect); - } - - @Override - public VengefulPharaohEffect copy() { - return new VengefulPharaohEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - Card card = game.getCard(source.getSourceId()); - if (card != null && controller != null) { - Permanent permanent = game.getPermanent(source.getFirstTarget()); - if (permanent == null) { - // If the attacking creature is an illegal target when the triggered ability tries to resolve, - // it won’t resolve and none of its effects will happen. Vengeful Pharaoh will remain in your graveyard. - // (2011-09-22) - return false; - } - permanent.destroy(source, game, false); - controller.moveCardToLibraryWithInfo(card, source, game, Zone.GRAVEYARD, true, true); - return true; - } - return false; + public boolean checkTrigger(GameEvent event, Game game) { + return filterBatchEvent(event, game).findAny().isPresent(); } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/w/WallOfEssence.java b/Mage.Sets/src/mage/cards/w/WallOfEssence.java index e1d99e68312b..1643332e0385 100644 --- a/Mage.Sets/src/mage/cards/w/WallOfEssence.java +++ b/Mage.Sets/src/mage/cards/w/WallOfEssence.java @@ -2,20 +2,14 @@ package mage.cards.w; import mage.MageInt; -import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.common.IsDealtCombatDamageSourceTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; +import mage.abilities.effects.common.GainLifeEffect; import mage.abilities.keyword.DefenderAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.SubType; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.DamagedBatchForOnePermanentEvent; -import mage.game.events.GameEvent; -import mage.players.Player; import java.util.UUID; @@ -33,8 +27,11 @@ public WallOfEssence(UUID ownerId, CardSetInfo setInfo) { // Defender this.addAbility(DefenderAbility.getInstance()); + // Whenever Wall of Essence is dealt combat damage, you gain that much life. - this.addAbility(new WallOfEssenceTriggeredAbility()); + this.addAbility(new IsDealtCombatDamageSourceTriggeredAbility( + new GainLifeEffect(SavedDamageValue.MUCH) + )); } private WallOfEssence(final WallOfEssence card) { @@ -45,67 +42,4 @@ private WallOfEssence(final WallOfEssence card) { public WallOfEssence copy() { return new WallOfEssence(this); } -} - -class WallOfEssenceTriggeredAbility extends TriggeredAbilityImpl { - - public WallOfEssenceTriggeredAbility() { - super(Zone.BATTLEFIELD, new PiousWarriorGainLifeEffect()); - setTriggerPhrase("Whenever {this} is dealt combat damage, "); - } - - private WallOfEssenceTriggeredAbility(final WallOfEssenceTriggeredAbility effect) { - super(effect); - } - - @Override - public WallOfEssenceTriggeredAbility copy() { - return new WallOfEssenceTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - - DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event; - int damage = dEvent.getAmount(); - - if (event.getTargetId().equals(this.sourceId) && dEvent.isCombatDamage() && damage > 0) { - this.getEffects().setValue("damageAmount", damage); - return true; - } - return false; - } -} - - -class PiousWarriorGainLifeEffect extends OneShotEffect { - - PiousWarriorGainLifeEffect() { - super(Outcome.GainLife); - staticText = "you gain that much life"; - } - - private PiousWarriorGainLifeEffect(final PiousWarriorGainLifeEffect effect) { - super(effect); - } - - @Override - public PiousWarriorGainLifeEffect copy() { - return new PiousWarriorGainLifeEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - if (player != null) { - player.gainLife((Integer) this.getValue("damageAmount"), game, source); - } - return true; - } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/w/WallOfSouls.java b/Mage.Sets/src/mage/cards/w/WallOfSouls.java index 1168be41b3f4..d5f3d8dc0a1a 100644 --- a/Mage.Sets/src/mage/cards/w/WallOfSouls.java +++ b/Mage.Sets/src/mage/cards/w/WallOfSouls.java @@ -2,7 +2,7 @@ import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.IsDealtCombatDamageSourceTriggeredAbility; import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.keyword.DefenderAbility; @@ -10,16 +10,11 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.DamagedBatchForOnePermanentEvent; -import mage.game.events.GameEvent; import mage.target.common.TargetOpponentOrPlaneswalker; import java.util.UUID; /** - * * @author fireshoes */ public final class WallOfSouls extends CardImpl { @@ -34,7 +29,9 @@ public WallOfSouls(UUID ownerId, CardSetInfo setInfo) { this.addAbility(DefenderAbility.getInstance()); // Whenever Wall of Souls is dealt combat damage, it deals that much damage to target opponent or planeswalker. - Ability ability = new WallOfSoulsTriggeredAbility(); + Ability ability = new IsDealtCombatDamageSourceTriggeredAbility( + new DamageTargetEffect(SavedDamageValue.MUCH, "it") + ); ability.addTarget(new TargetOpponentOrPlaneswalker()); this.addAbility(ability); } @@ -47,39 +44,4 @@ private WallOfSouls(final WallOfSouls card) { public WallOfSouls copy() { return new WallOfSouls(this); } -} - -class WallOfSoulsTriggeredAbility extends TriggeredAbilityImpl { - - public WallOfSoulsTriggeredAbility() { - super(Zone.BATTLEFIELD, new DamageTargetEffect(SavedDamageValue.MUCH, "it")); - setTriggerPhrase("Whenever {this} is dealt combat damage, "); - } - - private WallOfSoulsTriggeredAbility(final WallOfSoulsTriggeredAbility effect) { - super(effect); - } - - @Override - public WallOfSoulsTriggeredAbility copy() { - return new WallOfSoulsTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - - DamagedBatchForOnePermanentEvent dEvent = (DamagedBatchForOnePermanentEvent) event; - int damage = dEvent.getAmount(); - - if (event.getTargetId().equals(this.sourceId) && dEvent.isCombatDamage() && damage > 0) { - this.getEffects().setValue("damage", damage); - return true; - } - return false; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/w/WarElemental.java b/Mage.Sets/src/mage/cards/w/WarElemental.java index 29b0f742e79b..ae09f5fe5f16 100644 --- a/Mage.Sets/src/mage/cards/w/WarElemental.java +++ b/Mage.Sets/src/mage/cards/w/WarElemental.java @@ -1,29 +1,31 @@ package mage.cards.w; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.Condition; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.common.SacrificeSourceUnlessConditionEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Outcome; import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; +import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.watchers.common.BloodthirstWatcher; +import java.util.UUID; +import java.util.stream.Stream; + /** - * * @author spjspj */ public final class WarElemental extends CardImpl { @@ -53,10 +55,11 @@ public WarElemental copy() { } } -class WarElementalTriggeredAbility extends TriggeredAbilityImpl { +class WarElementalTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { public WarElementalTriggeredAbility() { - super(Zone.BATTLEFIELD, new WarElementalEffect(), false); + super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance(), SavedDamageValue.MUCH, true), false); + setTriggerPhrase("Whenever an opponent is dealt damage, "); } private WarElementalTriggeredAbility(final WarElementalTriggeredAbility ability) { @@ -74,38 +77,24 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (game.getOpponents(this.controllerId).contains(event.getTargetId())) { - this.getEffects().get(0).setValue("damageAmount", event.getAmount()); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever an opponent is dealt damage, put that many +1/+1 counters on {this}."; - } -} - -class WarElementalEffect extends OneShotEffect { - - WarElementalEffect() { - super(Outcome.Benefit); - } - - private WarElementalEffect(final WarElementalEffect effect) { - super(effect); - } - - @Override - public WarElementalEffect copy() { - return new WarElementalEffect(this); + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(e -> game.getOpponents(getControllerId()).contains(e.getTargetId())) + .filter(e -> e.getAmount() > 0); } @Override - public boolean apply(Game game, Ability source) { - return new AddCountersSourceEffect(CounterType.P1P1.createInstance((Integer) this.getValue("damageAmount"))).apply(game, source); + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPlayerEvent::getAmount) + .sum(); + if (amount <= 0) { + return false; + } + getEffects().setValue("damage", event.getAmount()); + return true; } } @@ -117,7 +106,7 @@ public OpponentWasDealtDamageCondition() { @Override public boolean apply(Game game, Ability source) { BloodthirstWatcher watcher = game.getState().getWatcher(BloodthirstWatcher.class, source.getControllerId()); - return watcher != null && watcher.conditionMet(); + return watcher != null && watcher.conditionMet(); } @Override diff --git a/Mage.Sets/src/mage/cards/w/WildfireElemental.java b/Mage.Sets/src/mage/cards/w/WildfireElemental.java index 26d8c315f6d1..f0de534edeab 100644 --- a/Mage.Sets/src/mage/cards/w/WildfireElemental.java +++ b/Mage.Sets/src/mage/cards/w/WildfireElemental.java @@ -1,6 +1,7 @@ package mage.cards.w; import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.common.continuous.BoostControlledEffect; import mage.cards.CardImpl; @@ -11,9 +12,11 @@ import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import java.util.UUID; +import java.util.stream.Stream; /** * @author TheElk801 @@ -41,7 +44,7 @@ public WildfireElemental copy() { } } -class WildfireElementalTriggeredAbility extends TriggeredAbilityImpl { +class WildfireElementalTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { WildfireElementalTriggeredAbility() { super(Zone.BATTLEFIELD, new BoostControlledEffect(1, 0, Duration.EndOfTurn), false); @@ -61,10 +64,19 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(e -> !e.isCombatDamage()) + .filter(e -> e.getAmount() > 0) + .filter(e -> game.getOpponents(controllerId).contains(e.getTargetId())); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event; - return !dEvent.isCombatDamage() && dEvent.getAmount() > 0 && game.getOpponents(controllerId).contains(dEvent.getTargetId()); + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/w/WrathfulRaptors.java b/Mage.Sets/src/mage/cards/w/WrathfulRaptors.java index f994d91f5372..6a9ad3763ebb 100644 --- a/Mage.Sets/src/mage/cards/w/WrathfulRaptors.java +++ b/Mage.Sets/src/mage/cards/w/WrathfulRaptors.java @@ -1,9 +1,10 @@ package mage.cards.w; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; @@ -16,11 +17,17 @@ import mage.filter.common.FilterPermanentOrPlayer; import mage.filter.predicate.Predicates; import mage.game.Game; +import mage.game.events.DamagedBatchForPermanentsEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetPermanentOrPlayer; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + /** * @author arcox */ @@ -50,7 +57,7 @@ public WrathfulRaptors copy() { } } -class WrathfulRaptorsTriggeredAbility extends TriggeredAbilityImpl { +class WrathfulRaptorsTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { private static final FilterPermanentOrPlayer filter = new FilterAnyTarget("any target that isn't a Dinosaur"); @@ -78,17 +85,32 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPermanentsEvent) event) + .getEvents() + .stream() + .filter(e -> isControlledBy(game.getControllerId(e.getTargetId()))) + .filter(e -> Optional + .of(e) + .map(DamagedPermanentEvent::getTargetId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> p.hasSubtype(SubType.DINOSAUR, game)) + .isPresent()) + .filter(e -> e.getAmount() > 0); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - Permanent dinosaur = game.getPermanent(event.getTargetId()); - int damage = event.getAmount(); - if (dinosaur == null || damage < 1 - || !dinosaur.isControlledBy(getControllerId()) - || !dinosaur.hasSubtype(SubType.DINOSAUR, game)) { + Permanent dinosaur = game.getPermanentOrLKIBattlefield(event.getTargetId()); + int amount = filterBatchEvent(event, game) + .mapToInt(GameEvent::getAmount) + .sum(); + if (dinosaur == null || amount <= 0) { return false; } this.getEffects().setValue("damagedPermanent", dinosaur); - this.getEffects().setValue("damage", damage); + this.getEffects().setValue("damage", amount); return true; } @@ -117,7 +139,7 @@ public WrathfulRaptorsEffect copy() { @Override public boolean apply(Game game, Ability source) { Permanent dinosaur = (Permanent) getValue("damagedPermanent"); - Integer damage = (Integer) getValue("damage"); + Integer damage = SavedDamageValue.MUCH.calculate(game, source, this); if (dinosaur == null || damage == null) { return false; } diff --git a/Mage.Sets/src/mage/cards/w/WrathfulRedDragon.java b/Mage.Sets/src/mage/cards/w/WrathfulRedDragon.java index ac1d42c58681..819fe53ed3eb 100644 --- a/Mage.Sets/src/mage/cards/w/WrathfulRedDragon.java +++ b/Mage.Sets/src/mage/cards/w/WrathfulRedDragon.java @@ -2,7 +2,9 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; @@ -15,12 +17,15 @@ import mage.filter.common.FilterPermanentOrPlayer; import mage.filter.predicate.Predicates; import mage.game.Game; +import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetPermanentOrPlayer; import java.util.UUID; +import java.util.stream.Stream; /** * @author TheElk801 @@ -51,7 +56,7 @@ public WrathfulRedDragon copy() { } } -class WrathfulRedDragonTriggeredAbility extends TriggeredAbilityImpl { +class WrathfulRedDragonTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { private static final FilterPermanentOrPlayer filter = new FilterAnyTarget("any target that isn't a Dragon"); @@ -80,16 +85,30 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { + public Stream filterBatchEvent(GameEvent event, Game game) { Permanent dragon = game.getPermanent(event.getTargetId()); - int damage = event.getAmount(); - if (dragon == null || damage < 1 + if (dragon == null || !dragon.isControlledBy(getControllerId()) || !dragon.hasSubtype(SubType.DRAGON, game)) { + return Stream.empty(); + } + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(e -> e.getAmount() > 0); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Permanent dragon = game.getPermanent(event.getTargetId()); + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPermanentEvent::getAmount) + .sum(); + if (dragon == null || amount <= 0) { return false; } this.getEffects().setValue("damagedPermanent", dragon); - this.getEffects().setValue("damage", damage); + this.getEffects().setValue("damage", amount); return true; } @@ -118,7 +137,7 @@ public WrathfulRedDragonEffect copy() { @Override public boolean apply(Game game, Ability source) { Permanent dragon = (Permanent) getValue("damagedPermanent"); - Integer damage = (Integer) getValue("damage"); + Integer damage = SavedDamageValue.MUCH.calculate(game, source, this); if (dragon == null || damage == null) { return false; } diff --git a/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java b/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java index bb4d443f73f9..ccf815f11808 100644 --- a/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java +++ b/Mage.Sets/src/mage/cards/z/ZurgoAndOjutai.java @@ -99,9 +99,6 @@ public boolean checkEventType(GameEvent event, Game game) { @Override public Stream filterBatchEvent(GameEvent event, Game game) { - if (!(event instanceof DamagedBatchAllEvent)) { - return Stream.empty(); - } return ((DamagedBatchAllEvent) event) .getEvents() .stream() @@ -109,14 +106,10 @@ public Stream filterBatchEvent(GameEvent event, Game game) { .filter(e -> { Permanent permanent = game.getPermanent(e.getSourceId()); Permanent defender = game.getPermanent(e.getTargetId()); - if (permanent != null + return permanent != null && permanent.hasSubtype(SubType.DRAGON, game) && permanent.isControlledBy(this.getControllerId()) - && ((defender != null && defender.isBattle(game)) - || game.getPlayer(e.getTargetId()) != null)) { - return true; - } - return false; + && ((defender != null && defender.isBattle(game)) || game.getPlayer(e.getTargetId()) != null); }); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/BlazingSunsteelTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/BlazingSunsteelTest.java new file mode 100644 index 000000000000..d8863ac9b3cd --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/cmr/BlazingSunsteelTest.java @@ -0,0 +1,42 @@ +package org.mage.test.cards.single.cmr; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class BlazingSunsteelTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.b.BlazingSunsteel Blazing Sunsteel} {1}{R} + * Artifact — Equipment + * Equipped creature gets +1/+0 for each opponent you have. + * Whenever equipped creature is dealt damage, it deals that much damage to any target. + * Equip {4} + */ + private static final String sunsteel = "Blazing Sunsteel"; + + @Test + public void test_Trigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, sunsteel); + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", "Memnite"); + castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Lightning Bolt", "Memnite"); + addTarget(playerA, playerB); // Sunsteel trigger + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Memnite", 1); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertLife(playerB, 20 - 3); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/ObNixilisCaptiveKingpinTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/ObNixilisCaptiveKingpinTest.java index dbe7c8a74b39..0a003f7e9159 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/ObNixilisCaptiveKingpinTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/damage/ObNixilisCaptiveKingpinTest.java @@ -18,7 +18,7 @@ public class ObNixilisCaptiveKingpinTest extends CardTestCommander4Players { // - 1 opponent loses 2 life -> No trigger // - 2 opponents lose 1 life each -> Ob Nixilis triggers // - 2 opponents lose 2 life each -> No trigger - // - 2 opponents lose 1 and 2 life respectively -> No trigger + // - 2 opponents lose 1 and 2 life respectively -> Ob Nixilis triggers // - 1 opponent loses 1 and controller loses 2 life -> Ob Nixilis triggers // - controller loses 1 life -> No trigger @@ -94,6 +94,7 @@ public void damage2Opp2Points() { assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 0); } + // No clear rulling for that one :( @Test public void damage2Opp1Point1Opp2Points() { addCard(Zone.BATTLEFIELD, playerA, "Ob Nixilis, Captive Kingpin", 1); @@ -107,7 +108,7 @@ public void damage2Opp1Point1Opp2Points() { setStrictChooseMode(true); execute(); - assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 0); + assertCounterCount("Ob Nixilis, Captive Kingpin", CounterType.P1P1, 1); } @Test diff --git a/Mage/src/main/java/mage/abilities/BatchTriggeredAbility.java b/Mage/src/main/java/mage/abilities/BatchTriggeredAbility.java index bec0f0d6fcab..6b1283773e72 100644 --- a/Mage/src/main/java/mage/abilities/BatchTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/BatchTriggeredAbility.java @@ -20,6 +20,10 @@ public interface BatchTriggeredAbility extends TriggeredAbi * Properly filtering is required for further analysis of trigger + event, * for instance for complex NUMBER_OF_TRIGGERS triggers. * e.g. Umezawa's Jitte + Felix Five-Boots. + *

+ * Note: if calling this, make sure the event does pass TriggeredAbility::checkEventType */ - public Stream filterBatchEvent(GameEvent event, Game game); + Stream filterBatchEvent(GameEvent event, Game game); + + BatchTriggeredAbility copy(); } diff --git a/Mage/src/main/java/mage/abilities/common/BecomesTappedOneOrMoreTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BecomesTappedOneOrMoreTriggeredAbility.java index 7b96abf685c1..ad5e73b3ef2b 100644 --- a/Mage/src/main/java/mage/abilities/common/BecomesTappedOneOrMoreTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/BecomesTappedOneOrMoreTriggeredAbility.java @@ -1,5 +1,6 @@ package mage.abilities.common; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.Zone; @@ -7,11 +8,15 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.TappedBatchEvent; +import mage.game.events.TappedEvent; + +import java.util.Optional; +import java.util.stream.Stream; /** * @author Susucr */ -public class BecomesTappedOneOrMoreTriggeredAbility extends TriggeredAbilityImpl { +public class BecomesTappedOneOrMoreTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { protected FilterPermanent filter; @@ -37,12 +42,21 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - TappedBatchEvent batchEvent = (TappedBatchEvent) event; - return batchEvent - .getTargetIds() + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((TappedBatchEvent) event) + .getEvents() .stream() - .map(game::getPermanent) - .anyMatch(p -> filter.match(p, getControllerId(), this, game)); + .filter(e -> Optional + .of(e) + .map(TappedEvent::getTargetId) + .map(game::getPermanent) + .filter(p -> filter.match(p, getControllerId(), this, game)) + .isPresent() + ); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return filterBatchEvent(event, game).findAny().isPresent(); } } diff --git a/Mage/src/main/java/mage/abilities/common/CombatDamageDealtToYouTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/CombatDamageDealtToYouTriggeredAbility.java index c2e3b4128b92..f4c127306780 100644 --- a/Mage/src/main/java/mage/abilities/common/CombatDamageDealtToYouTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/CombatDamageDealtToYouTriggeredAbility.java @@ -1,16 +1,20 @@ package mage.abilities.common; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchForPlayersEvent; import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.target.targetpointer.FixedTarget; import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A triggered ability for whenever one or more creatures deal combat damage to @@ -19,7 +23,8 @@ * * @author alexander-novo */ -public class CombatDamageDealtToYouTriggeredAbility extends TriggeredAbilityImpl { +// TODO Susucr: rename to DealsCombatDamageOneOrMoreToYouTriggeredAbility after merge for some consistency +public class CombatDamageDealtToYouTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { // Whether the ability should set a target targetting the opponent who // controls the creatures who dealt damage to you @@ -71,20 +76,25 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForPlayersEvent dEvent = (DamagedBatchForPlayersEvent) event; + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPlayersEvent) event) + .getEvents() + .stream() + .filter(DamagedPlayerEvent::isCombatDamage) + .filter(e -> e.getPlayerId() == getControllerId()); + } + @Override + public boolean checkTrigger(GameEvent event, Game game) { boolean isDamaged = false; UUID damageSourceControllerID = null; - for (DamagedEvent damagedEvent : dEvent.getEvents()) { - if (damagedEvent.isCombatDamage() && damagedEvent.getPlayerId() == this.controllerId) { - isDamaged = true; - // TODO: current code support only one controller - // (it's can be potentially bugged in team mode with multiple attack players) - Permanent damageSource = game.getPermanent(damagedEvent.getSourceId()); - if (damageSource != null) { - damageSourceControllerID = damageSource.getControllerId(); - } + for (DamagedEvent damagedEvent : filterBatchEvent(event, game).collect(Collectors.toList())) { + isDamaged = true; + // TODO: current code support only one controller + // (it's can be potentially bugged in team mode with multiple attack players) + Permanent damageSource = game.getPermanent(damagedEvent.getSourceId()); + if (damageSource != null) { + damageSourceControllerID = damageSource.getControllerId(); } } diff --git a/Mage/src/main/java/mage/abilities/common/DealsCombatDamageEquippedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealsCombatDamageEquippedTriggeredAbility.java index a81ceb14da2d..d3da0e43654a 100644 --- a/Mage/src/main/java/mage/abilities/common/DealsCombatDamageEquippedTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealsCombatDamageEquippedTriggeredAbility.java @@ -42,9 +42,6 @@ public boolean checkEventType(GameEvent event, Game game) { @Override public Stream filterBatchEvent(GameEvent event, Game game) { - if (!checkEventType(event, game)) { - return Stream.empty(); - } Permanent sourcePermanent = getSourcePermanentOrLKI(game); if (sourcePermanent == null || sourcePermanent.getAttachedTo() == null) { return Stream.empty(); diff --git a/Mage/src/main/java/mage/abilities/common/DealsCombatDamageTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealsCombatDamageTriggeredAbility.java index 690fdbd08eef..2d413c0d9833 100644 --- a/Mage/src/main/java/mage/abilities/common/DealsCombatDamageTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealsCombatDamageTriggeredAbility.java @@ -42,9 +42,6 @@ public boolean checkEventType(GameEvent event, Game game) { @Override public Stream filterBatchEvent(GameEvent event, Game game) { - if (!(event instanceof DamagedBatchAllEvent)) { - return Stream.empty(); - } return ((DamagedBatchAllEvent) event) .getEvents() .stream() diff --git a/Mage/src/main/java/mage/abilities/common/DealtDamageAttachedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealtDamageAttachedTriggeredAbility.java deleted file mode 100644 index 48227613ddd1..000000000000 --- a/Mage/src/main/java/mage/abilities/common/DealtDamageAttachedTriggeredAbility.java +++ /dev/null @@ -1,68 +0,0 @@ - -package mage.abilities.common; - -import java.util.UUID; - -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; -import mage.constants.SetTargetPointer; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent.EventType; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.target.targetpointer.FixedTarget; - -/** - * @author LoneFox - */ -public class DealtDamageAttachedTriggeredAbility extends TriggeredAbilityImpl { - - protected SetTargetPointer setTargetPointer; - - public DealtDamageAttachedTriggeredAbility(Effect effect, boolean optional) { - this(Zone.BATTLEFIELD, effect, optional, SetTargetPointer.NONE); - } - - public DealtDamageAttachedTriggeredAbility(Zone zone, Effect effect, boolean optional, SetTargetPointer setTargetPointer) { - super(zone, effect, optional); - this.setTargetPointer = setTargetPointer; - setTriggerPhrase("Whenever enchanted creature is dealt damage, "); - } - - protected DealtDamageAttachedTriggeredAbility(final DealtDamageAttachedTriggeredAbility ability) { - super(ability); - this.setTargetPointer = ability.setTargetPointer; - } - - @Override - public DealtDamageAttachedTriggeredAbility copy() { - return new DealtDamageAttachedTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Permanent enchantment = game.getPermanent(sourceId); - UUID targetId = event.getTargetId(); - if (enchantment != null && enchantment.getAttachedTo() != null && targetId.equals(enchantment.getAttachedTo())) { - for (Effect effect : this.getEffects()) { - effect.setValue("damage", event.getAmount()); - switch (setTargetPointer) { - case PERMANENT: - effect.setTargetPointer(new FixedTarget(targetId, game)); - break; - case PLAYER: - effect.setTargetPointer(new FixedTarget(game.getPermanentOrLKIBattlefield(targetId).getControllerId())); - break; - } - } - return true; - } - return false; - } -} diff --git a/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java index 28def61a7387..55a2957aa9da 100644 --- a/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java @@ -1,17 +1,26 @@ package mage.abilities.common; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.AbilityWord; import mage.constants.Zone; import mage.game.Game; import mage.game.events.DamagedBatchForPermanentsEvent; +import mage.game.events.DamagedPermanentEvent; import mage.game.events.GameEvent; +import java.util.stream.Stream; + /** * @author LevelX2 */ -public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl { +// TODO Susucr: rename to IsDealtDamageSourceTriggeredAbility after merge for some consistency. +public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { + + public DealtDamageToSourceTriggeredAbility(Effect effect) { + this(effect, false); + } public DealtDamageToSourceTriggeredAbility(Effect effect, boolean optional) { this(effect, optional, false); @@ -41,12 +50,17 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForPermanentsEvent dEvent = (DamagedBatchForPermanentsEvent) event; - int damage = dEvent + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPermanentsEvent) event) .getEvents() .stream() - .filter(damagedEvent -> getSourceId().equals(damagedEvent.getTargetId())) + .filter(damagedEvent -> getSourceId().equals(damagedEvent.getTargetId()) + && damagedEvent.getAmount() > 0); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int damage = filterBatchEvent(event, game) .mapToInt(GameEvent::getAmount) .sum(); if (damage < 1) { diff --git a/Mage/src/main/java/mage/abilities/common/DiesOneOrMoreCreatureTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DiesOneOrMoreCreatureTriggeredAbility.java index 7377db0cd3b3..fe769ceb8af4 100644 --- a/Mage/src/main/java/mage/abilities/common/DiesOneOrMoreCreatureTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DiesOneOrMoreCreatureTriggeredAbility.java @@ -1,6 +1,7 @@ package mage.abilities.common; import mage.MageObject; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.Zone; @@ -10,12 +11,13 @@ import mage.game.events.ZoneChangeBatchEvent; import mage.game.events.ZoneChangeEvent; -import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; /** * @author Susucr */ -public class DiesOneOrMoreCreatureTriggeredAbility extends TriggeredAbilityImpl { +public class DiesOneOrMoreCreatureTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { private final FilterCreaturePermanent filter; @@ -41,15 +43,22 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { + public Stream filterBatchEvent(GameEvent event, Game game) { return ((ZoneChangeBatchEvent) event) .getEvents() .stream() .filter(ZoneChangeEvent::isDiesEvent) - .map(ZoneChangeEvent::getTargetId) - .map(game::getPermanentOrLKIBattlefield) - .filter(Objects::nonNull) - .anyMatch(p -> filter.match(p, getControllerId(), this, game)); + .filter(e -> Optional + .of(e) + .map(ZoneChangeEvent::getTargetId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> filter.match(p, getControllerId(), this, game)) + .isPresent()); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage/src/main/java/mage/abilities/common/IsDealtCombatDamageSourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/IsDealtCombatDamageSourceTriggeredAbility.java new file mode 100644 index 000000000000..56c510bde6ac --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/IsDealtCombatDamageSourceTriggeredAbility.java @@ -0,0 +1,38 @@ +package mage.abilities.common; + +import mage.abilities.effects.Effect; +import mage.game.Game; +import mage.game.events.DamagedPermanentEvent; +import mage.game.events.GameEvent; + +import java.util.stream.Stream; + +/** + * @author Susucr + */ +public class IsDealtCombatDamageSourceTriggeredAbility extends DealtDamageToSourceTriggeredAbility { + + public IsDealtCombatDamageSourceTriggeredAbility(Effect effect) { + this(effect, false); + } + + public IsDealtCombatDamageSourceTriggeredAbility(Effect effect, boolean optional) { + super(effect, optional); + setTriggerPhrase("Whenever {this} is dealt combat damage, "); + } + + protected IsDealtCombatDamageSourceTriggeredAbility(final IsDealtCombatDamageSourceTriggeredAbility ability) { + super(ability); + } + + @Override + public IsDealtCombatDamageSourceTriggeredAbility copy() { + return new IsDealtCombatDamageSourceTriggeredAbility(this); + } + + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return super.filterBatchEvent(event, game) + .filter(e -> e.isCombatDamage()); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/IsDealtDamageAttachedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/IsDealtDamageAttachedTriggeredAbility.java new file mode 100644 index 000000000000..3eb0f8b2acb3 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/IsDealtDamageAttachedTriggeredAbility.java @@ -0,0 +1,95 @@ + +package mage.abilities.common; + +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.SetTargetPointer; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.DamagedBatchForOnePermanentEvent; +import mage.game.events.DamagedPermanentEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; +import java.util.stream.Stream; + +/** + * @author LoneFox + */ +public class IsDealtDamageAttachedTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { + + protected SetTargetPointer setTargetPointer; + + public IsDealtDamageAttachedTriggeredAbility(Effect effect, boolean optional, String attachedDescription) { + this(Zone.BATTLEFIELD, effect, optional, attachedDescription, SetTargetPointer.NONE); + } + + public IsDealtDamageAttachedTriggeredAbility(Zone zone, Effect effect, boolean optional, String attachedDescription, SetTargetPointer setTargetPointer) { + super(zone, effect, optional); + this.setTargetPointer = setTargetPointer; + setTriggerPhrase("Whenever " + attachedDescription + " creature is dealt damage, "); + } + + protected IsDealtDamageAttachedTriggeredAbility(final IsDealtDamageAttachedTriggeredAbility ability) { + super(ability); + this.setTargetPointer = ability.setTargetPointer; + } + + @Override + public IsDealtDamageAttachedTriggeredAbility copy() { + return new IsDealtDamageAttachedTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT; + } + + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + Permanent attachment = game.getPermanent(sourceId); + if (attachment == null) { + return Stream.empty(); + } + UUID targetId = event.getTargetId(); + UUID enchantedToId = attachment.getAttachedTo(); + if (enchantedToId == null || !targetId.equals(enchantedToId)) { + return Stream.empty(); + } + return ((DamagedBatchForOnePermanentEvent) event) + .getEvents() + .stream() + .filter(e -> e.getAmount() > 0); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPermanentEvent::getAmount) + .sum(); + if (amount <= 0) { + return false; + } + switch (setTargetPointer) { + case PERMANENT: + getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game)); + break; + case PLAYER: + Permanent enchanted = game.getPermanentOrLKIBattlefield(event.getTargetId()); + if (enchanted == null) { + return false; + } + getEffects().setTargetPointer(new FixedTarget(enchanted.getControllerId())); + break; + case NONE: + break; + default: + throw new IllegalArgumentException("Wrong code usage: setTargetPointer not expected: " + setTargetPointer); + } + getEffects().setValue("damage", amount); + return true; + } +} diff --git a/Mage/src/main/java/mage/abilities/common/IsDealtDamageYouTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/IsDealtDamageYouTriggeredAbility.java new file mode 100644 index 000000000000..05c1eb8f7f8a --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/IsDealtDamageYouTriggeredAbility.java @@ -0,0 +1,58 @@ +package mage.abilities.common; + +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedPlayerEvent; +import mage.game.events.GameEvent; + +import java.util.stream.Stream; + +/** + * @author Susucr + */ +public class IsDealtDamageYouTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { + + public IsDealtDamageYouTriggeredAbility(Effect effect, boolean optional) { + super(Zone.BATTLEFIELD, effect, optional); + setTriggerPhrase("Whenever you're dealt damage, "); + } + + private IsDealtDamageYouTriggeredAbility(final IsDealtDamageYouTriggeredAbility ability) { + super(ability); + } + + @Override + public IsDealtDamageYouTriggeredAbility copy() { + return new IsDealtDamageYouTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; + } + + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(e -> getControllerId().equals(e.getTargetId())) + .filter(e -> e.getAmount() > 0); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedPlayerEvent::getAmount) + .sum(); + if (amount <= 0) { + return false; + } + getEffects().setValue("damage", amount); + return true; + } +} diff --git a/Mage/src/main/java/mage/abilities/common/OneOrMoreDealDamageTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/OneOrMoreDealDamageTriggeredAbility.java index c7cf17da6ed2..74569175cfb5 100644 --- a/Mage/src/main/java/mage/abilities/common/OneOrMoreDealDamageTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/OneOrMoreDealDamageTriggeredAbility.java @@ -1,5 +1,6 @@ package mage.abilities.common; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.SetTargetPointer; @@ -8,17 +9,18 @@ import mage.game.Game; import mage.game.events.DamagedBatchForOnePlayerEvent; import mage.game.events.DamagedEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.target.targetpointer.FixedTarget; -import java.util.List; -import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Xanderhall, xenohedron */ -public class OneOrMoreDealDamageTriggeredAbility extends TriggeredAbilityImpl { +// TODO Susucr: rename to DealsDamageOneOrMoreToAPlayerTriggeredAbility after merge for some consistency +public class OneOrMoreDealDamageTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { private final SetTargetPointer setTargetPointer; private final FilterPermanent filter; @@ -59,12 +61,12 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { + public Stream filterBatchEvent(GameEvent event, Game game) { DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event; if (onlyCombat && !dEvent.isCombatDamage()) { - return false; + return Stream.empty(); } - List events = dEvent + return dEvent .getEvents() .stream() .filter(e -> { @@ -76,14 +78,18 @@ public boolean checkTrigger(GameEvent event, Game game) { return false; } return filter.match(permanent, this.getControllerId(), this, game); - }) - .collect(Collectors.toList()); + }); + } - if (events.isEmpty()) { + @Override + public boolean checkTrigger(GameEvent event, Game game) { + int amount = filterBatchEvent(event, game) + .mapToInt(DamagedEvent::getAmount) + .sum(); + if (amount <= 0) { return false; } - - this.getAllEffects().setValue("damage", events.stream().mapToInt(DamagedEvent::getAmount).sum()); + this.getAllEffects().setValue("damage", amount); switch (setTargetPointer) { case PLAYER: this.getAllEffects().setTargetPointer(new FixedTarget(event.getTargetId())); diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalInterveningIfBatchTriggeredAbility.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalInterveningIfBatchTriggeredAbility.java new file mode 100644 index 000000000000..2760c74aed0c --- /dev/null +++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalInterveningIfBatchTriggeredAbility.java @@ -0,0 +1,36 @@ +package mage.abilities.decorator; + +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.stream.Stream; + +/** + * @author Susucr + */ +public class ConditionalInterveningIfBatchTriggeredAbility extends ConditionalInterveningIfTriggeredAbility implements BatchTriggeredAbility { + + private final BatchTriggeredAbility ability; + + public ConditionalInterveningIfBatchTriggeredAbility(BatchTriggeredAbility ability, Condition condition, String text) { + super(ability, condition, text); + this.ability = ability; + } + + protected ConditionalInterveningIfBatchTriggeredAbility(final ConditionalInterveningIfBatchTriggeredAbility triggered) { + super(triggered); + this.ability = triggered.ability.copy(); + } + + @Override + public ConditionalInterveningIfBatchTriggeredAbility copy() { + return new ConditionalInterveningIfBatchTriggeredAbility(this); + } + + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ability.filterBatchEvent(event, game); + } +} diff --git a/Mage/src/main/java/mage/game/events/BatchEvent.java b/Mage/src/main/java/mage/game/events/BatchEvent.java index f2dcdf762599..4de7cc205fbd 100644 --- a/Mage/src/main/java/mage/game/events/BatchEvent.java +++ b/Mage/src/main/java/mage/game/events/BatchEvent.java @@ -39,6 +39,9 @@ protected BatchEvent(EventType eventType, boolean singleTargetId, boolean single if (firstEvent instanceof BatchEvent) { // sanity check, if you need it then think twice and research carefully throw new UnsupportedOperationException("Wrong code usage: nesting batch events not supported"); } + if (!eventType.isBatch()) { // sanity check, a batch event should use a batch EventType + throw new IllegalArgumentException("Wrong code usage: batch event should use batch EventType: " + eventType); + } this.addEvent(firstEvent); } @@ -47,6 +50,9 @@ protected BatchEvent(EventType eventType, boolean singleTargetId, boolean single */ protected BatchEvent(EventType eventType) { super(eventType, null, null, null); + if (!eventType.isBatch()) { // sanity check, a batch event should use a batch EventType + throw new IllegalArgumentException("Wrong code usage: batch event should use batch EventType: " + eventType); + } this.singleTargetId = false; this.singleSourceId = false; this.singlePlayerId = false; diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 36b6ccfdeab5..b5a0dd2be5e2 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -74,7 +74,7 @@ public enum EventType { */ ZONE_CHANGE, ZONE_CHANGE_GROUP, // between two specific zones only; TODO: rework all usages to ZONE_CHANGE_BATCH instead, see #11895 - ZONE_CHANGE_BATCH, // all zone changes that occurred from a single effect + ZONE_CHANGE_BATCH(true), // all zone changes that occurred from a single effect DRAW_CARDS, // event calls for multi draws only (if player draws 2+ cards at once) DRAW_CARD, DREW_CARD, EXPLORE, EXPLORED, // targetId is exploring permanent, playerId is its controller @@ -135,13 +135,13 @@ playerId the id of the player milling the card (not the source's controller) /* DAMAGED_BATCH_FOR_PLAYERS, combines all player damage events to a single batch (event) */ - DAMAGED_BATCH_FOR_PLAYERS, + DAMAGED_BATCH_FOR_PLAYERS(true), /* DAMAGED_BATCH_FOR_ONE_PLAYER combines all player damage events to a single batch (event) and split it per damaged player targetId the id of the damaged player (playerId won't work for batch) */ - DAMAGED_BATCH_FOR_ONE_PLAYER, + DAMAGED_BATCH_FOR_ONE_PLAYER(true), /* DAMAGED_BATCH_FOR_ALL includes all damage events, both permanent damage and player damage, in single batch event @@ -172,7 +172,7 @@ targetId the id of the damaged player (playerId won't work for batch) amount amount of life loss flag true = from combat damage - other from non combat damage */ - LOST_LIFE_BATCH, + LOST_LIFE_BATCH(true), /* LOST_LIFE_BATCH combines all player life lost events to a single batch (event) */ @@ -433,7 +433,7 @@ raise one time per attacker (e.g. only one event per attacker allows) /* TAPPED_BATCH combine all TAPPED events occuring at the same time in a single event */ - TAPPED_BATCH, + TAPPED_BATCH(true), UNTAP, /* UNTAPPED, targetId untapped permanent @@ -446,7 +446,7 @@ flag true if untapped during untap step (event is checked at upkeep so ca /* UNTAPPED_BATCH combine all UNTAPPED events occuring at the same time in a single event */ - UNTAPPED_BATCH, + UNTAPPED_BATCH(true), FLIP, FLIPPED, TRANSFORMING, TRANSFORMED, ADAPT, @@ -500,12 +500,12 @@ playerId player who makes the exert (can be different from permanent's contro /* DAMAGED_BATCH_FOR_PERMANENTS combine all permanent damage events to a single batch (event) */ - DAMAGED_BATCH_FOR_PERMANENTS, + DAMAGED_BATCH_FOR_PERMANENTS(true), /* DAMAGED_BATCH_FOR_ONE_PERMANENT combines all permanent damage events to a single batch (event) and split it per damaged permanent */ - DAMAGED_BATCH_FOR_ONE_PERMANENT, + DAMAGED_BATCH_FOR_ONE_PERMANENT(true), DESTROY_PERMANENT, /* DESTROY_PERMANENT_BY_LEGENDARY_RULE From e330b190b5ef8963973eef9747b5be2405c6f2e0 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sat, 27 Apr 2024 17:38:30 +0200 Subject: [PATCH 06/16] add simple tests --- .../cards/single/mir/BindingAgonyTest.java | 62 ++++++++++++++ .../test/cards/single/mir/SunDropletTest.java | 74 +++++++++++++++++ .../cards/single/mir/WarElementalTest.java | 81 +++++++++++++++++++ .../cards/single/sth/WallOfEssenceTest.java | 77 ++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mir/BindingAgonyTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mir/SunDropletTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mir/WarElementalTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/sth/WallOfEssenceTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/BindingAgonyTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/BindingAgonyTest.java new file mode 100644 index 000000000000..9360089029e2 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/BindingAgonyTest.java @@ -0,0 +1,62 @@ +package org.mage.test.cards.single.mir; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class BindingAgonyTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.b.BindingAgony Binding Agony} {1}{B} + * Enchantment — Aura + * Enchant creature + * Whenever enchanted creature is dealt damage, Binding Agony deals that much damage to that creature’s controller. + */ + private static final String agony = "Binding Agony"; + + @Test + public void test_Trigger_Once_DoubleBlocked() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); + addCard(Zone.HAND, playerA, agony); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerB, "Memnite"); + addCard(Zone.BATTLEFIELD, playerB, "Centaur Courser"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, agony, "Grizzly Bears"); + + attack(1, playerA, "Grizzly Bears"); + block(1, playerB, "Centaur Courser", "Grizzly Bears"); + block(1, playerB, "Memnite", "Grizzly Bears"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Grizzly Bears", 1); + assertLife(playerA, 20 - 1 - 3); + } + + @Test + public void test_NonCombat_NoTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); + addCard(Zone.HAND, playerA, agony); + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 3); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, agony, "Grizzly Bears", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Grizzly Bears"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerB, "Grizzly Bears", 1); + assertLife(playerB, 20 - 3); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/SunDropletTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/SunDropletTest.java new file mode 100644 index 000000000000..8b7bc77476e4 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/SunDropletTest.java @@ -0,0 +1,74 @@ +package org.mage.test.cards.single.mir; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class SunDropletTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.s.SunDroplet Sun Droplet} {2} + * Artifact + * Whenever you’re dealt damage, put that many charge counters on Sun Droplet. + * At the beginning of each upkeep, you may remove a charge counter from Sun Droplet. If you do, you gain 1 life. + */ + private static final String droplet = "Sun Droplet"; + + @Test + public void test_Trigger_Combat() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, droplet); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 2); // 2/2 + + attack(1, playerA, "Grizzly Bears"); + attack(1, playerA, "Grizzly Bears"); + + // Only one trigger. + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertCounterCount(playerB, droplet, CounterType.CHARGE, 2 * 2); + assertLife(playerB, 20 - 2 * 2); + } + + @Test + public void test_Trigger_NonCombat() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, droplet); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerB, droplet, CounterType.CHARGE, 3); + assertLife(playerB, 20 - 3); + } + + @Test + public void test_NoTrigger_OtherPlayer() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, droplet); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerB, droplet, CounterType.CHARGE, 0); + assertLife(playerA, 20 - 3); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/WarElementalTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/WarElementalTest.java new file mode 100644 index 000000000000..8c87d37dd1d7 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/WarElementalTest.java @@ -0,0 +1,81 @@ +package org.mage.test.cards.single.mir; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class WarElementalTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.w.WarElemental War Elemental} {R}{R}{R} + * Creature — Elemental + * When War Elemental enters the battlefield, sacrifice it unless an opponent was dealt damage this turn. + * Whenever an opponent is dealt damage, put that many +1/+1 counters on War Elemental. + * 1/1 + */ + private static final String elemental = "War Elemental"; + + @Test + public void test_Trigger_Combat() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, elemental); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerA, "Elite Vanguard"); + addCard(Zone.BATTLEFIELD, playerB, "Memnite"); + + attack(1, playerA, "War Elemental"); + attack(1, playerA, "Grizzly Bears"); + attack(1, playerA, "Elite Vanguard"); + block(1, playerB, "Memnite", "Elite Vanguard"); + + // Only one trigger. + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, 1); + assertGraveyardCount(playerB, 1); + assertCounterCount(playerA, elemental, CounterType.P1P1, 1 + 2); + assertLife(playerB, 20 - 1 - 2); + } + + @Test + public void test_Trigger_NonCombat() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, elemental); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, elemental, CounterType.P1P1, 3); + assertLife(playerB, 20 - 3); + } + + @Test + public void test_NoTrigger_You() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, elemental); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertCounterCount(playerA, elemental, CounterType.P1P1, 0); + assertLife(playerA, 20 - 3); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/sth/WallOfEssenceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/sth/WallOfEssenceTest.java new file mode 100644 index 000000000000..2048a9ea9f7a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/sth/WallOfEssenceTest.java @@ -0,0 +1,77 @@ +package org.mage.test.cards.single.sth; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class WallOfEssenceTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.w.WallOfEssence Wall of Essence} {1}{W} + * Creature — Wall + * Defender (This creature can’t attack.) + * Whenever Wall of Essence is dealt combat damage, you gain that much life. + * 0/4 + */ + private static final String wall = "Wall of Essence"; + + @Test + public void test_Trigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, wall); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); // 2/2 + + attack(1, playerA, "Grizzly Bears"); + block(1, playerB, wall, "Grizzly Bears"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertDamageReceived(playerB, wall, 2); + assertLife(playerB, 20 + 2); + } + + @Test + public void test_NotTrigger_OtherCombatDamage() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, wall); + addCard(Zone.BATTLEFIELD, playerB, "Memnite"); // 1/1 + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); // 2/2 + + attack(1, playerA, "Grizzly Bears"); + block(1, playerB, "Memnite", "Grizzly Bears"); + block(1, playerB, wall, "Grizzly Bears"); + + setChoice(playerA, "X=2"); // 2 damage on Memnite, no damage to Wall + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertDamageReceived(playerB, wall, 0); + assertGraveyardCount(playerB, "Memnite", 1); + assertLife(playerB, 20); + } + + @Test + public void test_NonCombat_NoTrigger() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, wall); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", wall); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertDamageReceived(playerB, wall, 3); + assertLife(playerB, 20); + } +} From 1f8e41c25189d57d32ea6a308b22988260ecd2eb Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:04:31 +0200 Subject: [PATCH 07/16] add Sower of Discord tests --- .../cards/single/c18/SowerOfDiscordTest.java | 141 ++++++++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 32 +++- .../base/impl/CardTestPlayerAPIImpl.java | 15 ++ 3 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/c18/SowerOfDiscordTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/SowerOfDiscordTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/SowerOfDiscordTest.java new file mode 100644 index 000000000000..42fd860c5750 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c18/SowerOfDiscordTest.java @@ -0,0 +1,141 @@ +package org.mage.test.cards.single.c18; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.multiplayer.MultiplayerTriggerTest; + +/** + * @author Susucr + */ +public class SowerOfDiscordTest extends MultiplayerTriggerTest { + + /** + * {@link mage.cards.s.SowerOfDiscord Sower of Discord} {4}{B}{B} + * Creature — Demon + * Flying + * As Sower of Discord enters the battlefield, choose two players. + * Whenever damage is dealt to one of the chosen players, the other chosen player also loses that much life. + * 6/6 + */ + private static final String sower = "Sower of Discord"; + + @Test + public void test_Trigger_Bolt_First() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, sower); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 7); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sower, true); + addTarget(playerA, playerB, playerD); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 40); + assertLife(playerB, 40 - 3); + assertLife(playerC, 40); + assertLife(playerD, 40 - 3); + } + + @Test + public void test_Trigger_Bolt_Second() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, sower); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 7); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sower, true); + addTarget(playerA, playerB, playerD); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerD); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 40); + assertLife(playerB, 40 - 3); + assertLife(playerC, 40); + assertLife(playerD, 40 - 3); + } + + @Test + public void test_NoTrigger_Bolt_Other() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, sower); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 7); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sower, true); + addTarget(playerA, playerB, playerD); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 40 - 3); + assertLife(playerB, 40); + assertLife(playerC, 40); + assertLife(playerD, 40); + } + + @Test + public void test_Trigger_Attack_Both() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, sower); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 6); + addCard(Zone.BATTLEFIELD, playerA, "Elite Vanguard"); + addCard(Zone.BATTLEFIELD, playerA, "Eager Cadet"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sower); + addTarget(playerA, playerB, playerD); + + attack(1, playerA, "Elite Vanguard", playerB); + attack(1, playerA, "Eager Cadet", playerD); + + // No ruling on that, but current implementation has 1 trigger per damaged player. + setChoice(playerA, "Whenever damage is dealt"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 40); + assertLife(playerB, 40 - 2 - 1); + assertLife(playerC, 40); + assertLife(playerD, 40 - 1 - 2); + } + + @Test + public void test_Trigger_NonCombat_Both() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, sower); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 6); + addCard(Zone.HAND, playerC, "Flame Rift"); // 4 damage to each player + addCard(Zone.BATTLEFIELD, playerC, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sower); + addTarget(playerA, playerB, playerD); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerC, "Flame Rift"); + + // No ruling on that, but current implementation has 1 trigger per damaged player. + setChoice(playerA, "Whenever damage is dealt"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 40 - 4); + assertLife(playerB, 40 - 4 - 4); + assertLife(playerC, 40 - 4); + assertLife(playerD, 40 - 4 - 4); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 6c6543cfe1ec..35ba3c1e7117 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -2427,15 +2427,31 @@ public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game if (!targetDefinition.startsWith("targetPlayer=")) { continue; } - checkTargetDefinitionMarksSupport(target, targetDefinition, "="); - String playerName = targetDefinition.substring(targetDefinition.indexOf("targetPlayer=") + 13); - for (Player player : game.getPlayers().values()) { - if (player.getName().equals(playerName) - && target.canTarget(abilityControllerId, player.getId(), source, game)) { - target.addTarget(player.getId(), source, game); - targets.remove(targetDefinition); - return true; + checkTargetDefinitionMarksSupport(target, targetDefinition, "^="); + String[] targetList = targetDefinition.substring(targetDefinition.indexOf("targetPlayer=") + 13).split("\\^"); + boolean allTargetFound = true; + boolean someTargetFound = false; + for (String playerName : targetList) { + boolean targetFound = false; + for (Player player : game.getPlayers().values()) { + if (player.getName().equals(playerName) + && target.canTarget(abilityControllerId, player.getId(), source, game)) { + target.addTarget(player.getId(), source, game); + targetFound = true; + break; + } } + someTargetFound |= targetFound; + allTargetFound &= targetFound; + } + if (!allTargetFound && someTargetFound) { + Assert.fail("target only partially matching for: " + + targetDefinition + " — " + + target.getOriginalTarget().getClass().getCanonicalName()); + } + if (allTargetFound && someTargetFound) { + targets.remove(targetDefinition); + return true; } } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 4917f1f9a2dd..d62ca4e877d2 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -2149,6 +2149,21 @@ public void addTarget(TestPlayer player, TestPlayer targetPlayer, int timesToCho } } + /** + * Sets players as target + */ + public void addTarget(TestPlayer player, TestPlayer... targetPlayers) { + String target = "targetPlayer="; + for (int i = 0; i < targetPlayers.length; ++i) { + if (i > 0) { + target += "^"; + } + target += targetPlayers[i].getName(); + } + player.addTarget(target); + } + + /** * @param player * @param target use TestPlayer.TARGET_SKIP to 0 targets selects or to stop From 64dbc3ee35642fbf33f6cb659ac855b81201b144 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:26:56 +0200 Subject: [PATCH 08/16] fix & test Phyrexian Negator --- .../src/mage/cards/p/PhyrexianNegator.java | 4 +- .../src/mage/cards/p/PhyrexianTotem.java | 4 +- .../single/uds/PhyrexianNegatorTest.java | 65 +++++++++++++++++++ .../DealtDamageToSourceTriggeredAbility.java | 10 +-- 4 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/uds/PhyrexianNegatorTest.java diff --git a/Mage.Sets/src/mage/cards/p/PhyrexianNegator.java b/Mage.Sets/src/mage/cards/p/PhyrexianNegator.java index 9838a7455a7c..ca39e465369e 100644 --- a/Mage.Sets/src/mage/cards/p/PhyrexianNegator.java +++ b/Mage.Sets/src/mage/cards/p/PhyrexianNegator.java @@ -4,7 +4,7 @@ import mage.MageInt; import mage.abilities.common.DealtDamageToSourceTriggeredAbility; import mage.abilities.dynamicvalue.common.SavedDamageValue; -import mage.abilities.effects.common.SacrificeEffect; +import mage.abilities.effects.common.SacrificeControllerEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -32,7 +32,7 @@ public PhyrexianNegator(UUID ownerId, CardSetInfo setInfo) { // Whenever Phyrexian Negator is dealt damage, sacrifice that many permanents. this.addAbility(new DealtDamageToSourceTriggeredAbility( - new SacrificeEffect(new FilterPermanent(), SavedDamageValue.MANY, "") + new SacrificeControllerEffect(new FilterPermanent(), SavedDamageValue.MANY, "") )); } diff --git a/Mage.Sets/src/mage/cards/p/PhyrexianTotem.java b/Mage.Sets/src/mage/cards/p/PhyrexianTotem.java index 43a777c8f91b..e066fa5d136a 100644 --- a/Mage.Sets/src/mage/cards/p/PhyrexianTotem.java +++ b/Mage.Sets/src/mage/cards/p/PhyrexianTotem.java @@ -9,7 +9,7 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.decorator.ConditionalInterveningIfBatchTriggeredAbility; import mage.abilities.dynamicvalue.common.SavedDamageValue; -import mage.abilities.effects.common.SacrificeEffect; +import mage.abilities.effects.common.SacrificeControllerEffect; import mage.abilities.effects.common.continuous.BecomesCreatureSourceEffect; import mage.abilities.keyword.TrampleAbility; import mage.abilities.mana.BlackManaAbility; @@ -44,7 +44,7 @@ public PhyrexianTotem(UUID ownerId, CardSetInfo setInfo) { // Whenever Phyrexian Totem is dealt damage, if it's a creature, sacrifice that many permanents. this.addAbility(new ConditionalInterveningIfBatchTriggeredAbility( - new DealtDamageToSourceTriggeredAbility(new SacrificeEffect(new FilterPermanent(), SavedDamageValue.MANY, "")), + new DealtDamageToSourceTriggeredAbility(new SacrificeControllerEffect(new FilterPermanent(), SavedDamageValue.MANY, "")), condition, "Whenever {this} is dealt damage, if it's a creature, sacrifice that many permanents." )); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/uds/PhyrexianNegatorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/uds/PhyrexianNegatorTest.java new file mode 100644 index 000000000000..0a7e17089f81 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/uds/PhyrexianNegatorTest.java @@ -0,0 +1,65 @@ +package org.mage.test.cards.single.uds; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class PhyrexianNegatorTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.p.PhyrexianNegator Phyrexian Negator} {2}{B} + * Creature — Phyrexian Horror + * Trample + * Whenever Phyrexian Negator is dealt damage, sacrifice that many permanents. + * 5/5 + */ + private static final String negator = "Phyrexian Negator"; + + @Test + public void test_Trigger_Combat() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, negator); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerB, "Memnite"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + + attack(1, playerA, negator); + block(1, playerB, "Grizzly Bears", negator); + block(1, playerB, "Memnite", negator); + + setChoice(playerA, "X=2"); // damage to Bears + setChoice(playerA, "X=3"); // damage to Memnite + + setChoice(playerA, "Mountain", 3); // sac those. + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertDamageReceived(playerA, negator, 3); + assertPermanentCount(playerA, "Mountain", 10 - 3); + } + + @Test + public void test_Trigger_NonCombat() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, negator); + addCard(Zone.HAND, playerA, "Shock"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shock", negator); + + setChoice(playerA, "Mountain", 2); // sac those. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertDamageReceived(playerA, negator, 2); + assertPermanentCount(playerA, "Mountain", 10 - 2); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java index 55a2957aa9da..da3d85d46e05 100644 --- a/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DealtDamageToSourceTriggeredAbility.java @@ -54,19 +54,19 @@ public Stream filterBatchEvent(GameEvent event, Game game return ((DamagedBatchForPermanentsEvent) event) .getEvents() .stream() - .filter(damagedEvent -> getSourceId().equals(damagedEvent.getTargetId()) - && damagedEvent.getAmount() > 0); + .filter(e -> getSourceId().equals(e.getTargetId())) + .filter(e -> e.getAmount() > 0); } @Override public boolean checkTrigger(GameEvent event, Game game) { - int damage = filterBatchEvent(event, game) + int amount = filterBatchEvent(event, game) .mapToInt(GameEvent::getAmount) .sum(); - if (damage < 1) { + if (amount <= 0) { return false; } - this.getEffects().setValue("damage", damage); + this.getEffects().setValue("damage", amount); return true; } } From f49ceb3583afcd906c94cb36e8e0f052bfe7bb6b Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:34:43 +0200 Subject: [PATCH 09/16] fix Kaya Spirit's Justice --- Mage.Sets/src/mage/cards/k/KayaSpiritsJustice.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Mage.Sets/src/mage/cards/k/KayaSpiritsJustice.java b/Mage.Sets/src/mage/cards/k/KayaSpiritsJustice.java index 0121ebc495ee..d074199376e9 100644 --- a/Mage.Sets/src/mage/cards/k/KayaSpiritsJustice.java +++ b/Mage.Sets/src/mage/cards/k/KayaSpiritsJustice.java @@ -118,7 +118,7 @@ public Stream filterBatchEvent(GameEvent event, Game game) { .map(game::getCard) .filter(card -> { Permanent permanent = game.getPermanentOrLKIBattlefield(card.getId()); - return StaticFilters.FILTER_PERMANENT_CREATURE + return StaticFilters.FILTER_CONTROLLED_CREATURE .match(permanent, getControllerId(), this, game); }) .isPresent() @@ -134,22 +134,24 @@ public Stream filterBatchEvent(GameEvent event, Game game) { .of(e) .map(ZoneChangeEvent::getTargetId) .map(game::getCard) + .filter(card -> card.getOwnerId().equals(getControllerId())) // indirect test for "from your graveyard" .filter(card -> StaticFilters.FILTER_CARD_CREATURE.match(card, getControllerId(), this, game)) .isPresent() ); - + return Stream.concat(filteredBattlefield, filteredGraveyard); } @Override public boolean checkTrigger(GameEvent event, Game game) { - Stream filteredEvents = filterBatchEvent(event, game); - if (!filteredEvents.findAny().isPresent()) { + List filteredEvents = filterBatchEvent(event, game).collect(Collectors.toList()); + if (filteredEvents.isEmpty()) { return false; } // From Battlefield Set battlefieldCards = filteredEvents + .stream() .filter(e -> e.getFromZone() == Zone.BATTLEFIELD) .filter(e -> e.getToZone() == Zone.EXILED) .map(ZoneChangeEvent::getTargetId) @@ -165,6 +167,7 @@ public boolean checkTrigger(GameEvent event, Game game) { // From Graveyard Set graveyardCards = filteredEvents + .stream() .filter(e -> e.getFromZone() == Zone.GRAVEYARD) .filter(e -> e.getToZone() == Zone.EXILED) .map(ZoneChangeEvent::getTargetId) From ea474e9b96092f475d30424311156646a5440a65 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sun, 28 Apr 2024 13:45:17 +0200 Subject: [PATCH 10/16] fix Jace & Tamiyo. add new batch event for Tamiyo. --- .../src/mage/cards/j/JaceCunningCastaway.java | 62 +++++++------------ .../mage/cards/t/TamiyoFieldResearcher.java | 35 ++++++++--- .../mage/cards/v/VesselOfTheAllConsuming.java | 6 +- .../single/xln/JaceCunningCastawayTest.java | 44 +++++++++++++ Mage/src/main/java/mage/game/GameState.java | 18 ++++++ .../events/DamagedBatchBySourceEvent.java | 19 ++++++ .../main/java/mage/game/events/GameEvent.java | 5 +- 7 files changed, 136 insertions(+), 53 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/xln/JaceCunningCastawayTest.java create mode 100644 Mage/src/main/java/mage/game/events/DamagedBatchBySourceEvent.java diff --git a/Mage.Sets/src/mage/cards/j/JaceCunningCastaway.java b/Mage.Sets/src/mage/cards/j/JaceCunningCastaway.java index fbcadcb0dab0..4ec15e73e0f2 100644 --- a/Mage.Sets/src/mage/cards/j/JaceCunningCastaway.java +++ b/Mage.Sets/src/mage/cards/j/JaceCunningCastaway.java @@ -2,9 +2,11 @@ package mage.cards.j; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.LoyaltyAbility; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.DrawDiscardControllerEffect; @@ -13,14 +15,15 @@ import mage.constants.*; import mage.game.Game; import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.permanent.token.JaceCunningCastawayIllusionToken; import mage.target.targetpointer.FixedTarget; -import java.util.ArrayList; -import java.util.List; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; /** * @author TheElk801 @@ -36,7 +39,7 @@ public JaceCunningCastaway(UUID ownerId, CardSetInfo setInfo) { this.setStartingLoyalty(3); // +1: Whenever one or more creatures you control deal combat damage to a player this turn, draw a card, then discard a card. - this.addAbility(new LoyaltyAbility(new JaceCunningCastawayEffect1(), 1)); + this.addAbility(new LoyaltyAbility(new CreateDelayedTriggeredAbilityEffect(new JaceCunningCastawayDamageTriggeredAbility()), 1)); // -2: Create a 2/2 blue Illusion creature token with "When this creature becomes the target of a spell, sacrifice it." this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new JaceCunningCastawayIllusionToken()), -2)); @@ -55,33 +58,7 @@ public JaceCunningCastaway copy() { } } -class JaceCunningCastawayEffect1 extends OneShotEffect { - - JaceCunningCastawayEffect1() { - super(Outcome.DrawCard); - this.staticText = "Whenever one or more creatures you control deal combat damage to a player this turn, draw a card, then discard a card"; - } - - private JaceCunningCastawayEffect1(final JaceCunningCastawayEffect1 effect) { - super(effect); - } - - @Override - public JaceCunningCastawayEffect1 copy() { - return new JaceCunningCastawayEffect1(this); - } - - @Override - public boolean apply(Game game, Ability source) { - DelayedTriggeredAbility delayedAbility = new JaceCunningCastawayDamageTriggeredAbility(); - game.addDelayedTriggeredAbility(delayedAbility, source); - return true; - } -} - -class JaceCunningCastawayDamageTriggeredAbility extends DelayedTriggeredAbility { - - private final List damagedPlayerIds = new ArrayList<>(); +class JaceCunningCastawayDamageTriggeredAbility extends DelayedTriggeredAbility implements BatchTriggeredAbility { JaceCunningCastawayDamageTriggeredAbility() { super(new DrawDiscardControllerEffect(1, 1), Duration.EndOfTurn, false); @@ -102,17 +79,24 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - - DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event; - - int damageFromYours = dEvent.getEvents() + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForOnePlayerEvent) event) + .getEvents() .stream() - .filter(ev -> ev.getSourceId().equals(controllerId)) - .mapToInt(GameEvent::getAmount) - .sum(); + .filter(DamagedPlayerEvent::isCombatDamage) + .filter(e -> Optional + .of(e) + .map(DamagedPlayerEvent::getSourceId) + .map(game::getPermanentOrLKIBattlefield) + .filter(p -> p.isCreature(game)) + .filter(p -> p.isControlledBy(getControllerId())) + .isPresent()) + .filter(e -> e.getAmount() > 0); + } - return dEvent.isCombatDamage() && damageFromYours > 0; + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/t/TamiyoFieldResearcher.java b/Mage.Sets/src/mage/cards/t/TamiyoFieldResearcher.java index 440193445219..4a1fdc9cce4a 100644 --- a/Mage.Sets/src/mage/cards/t/TamiyoFieldResearcher.java +++ b/Mage.Sets/src/mage/cards/t/TamiyoFieldResearcher.java @@ -3,6 +3,7 @@ import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.LoyaltyAbility; import mage.abilities.effects.OneShotEffect; @@ -18,16 +19,18 @@ import mage.filter.predicate.Predicates; import mage.game.Game; import mage.game.command.emblems.TamiyoFieldResearcherEmblem; +import mage.game.events.DamagedBatchBySourceEvent; import mage.game.events.DamagedEvent; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; /** * @author LevelX2 @@ -108,7 +111,10 @@ public boolean apply(Game game, Ability source) { } } -class TamiyoFieldResearcherDelayedTriggeredAbility extends DelayedTriggeredAbility { +// batch per source: +// > If Tamiyo’s first ability targets two creatures, and both deal combat damage at the same time, the delayed triggered ability triggers twice. +// > (2016-08-23) +class TamiyoFieldResearcherDelayedTriggeredAbility extends DelayedTriggeredAbility implements BatchTriggeredAbility { private List creatures; @@ -124,18 +130,27 @@ private TamiyoFieldResearcherDelayedTriggeredAbility(final TamiyoFieldResearcher @Override public boolean checkEventType(GameEvent event, Game game) { - return event instanceof DamagedEvent; + return event.getType() == GameEvent.EventType.DAMAGED_BATCH_BY_SOURCE; + } + + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchBySourceEvent) event) + .getEvents() + .stream() + .filter(DamagedEvent::isCombatDamage) + .filter(e -> Optional + .of(e) + .map(DamagedEvent::getSourceId) + .map(id -> new MageObjectReference(id, game)) + .filter(mor -> creatures.contains(mor)) + .isPresent()) + .filter(e -> e.getAmount() > 0); } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (((DamagedEvent) event).isCombatDamage()) { - Permanent damageSource = game.getPermanent(event.getSourceId()); - if (damageSource != null) { - return creatures.contains(new MageObjectReference(damageSource, game)); - } - } - return false; + return filterBatchEvent(event, game).findAny().isPresent(); } @Override diff --git a/Mage.Sets/src/mage/cards/v/VesselOfTheAllConsuming.java b/Mage.Sets/src/mage/cards/v/VesselOfTheAllConsuming.java index 31987548af94..0916c4a753fb 100644 --- a/Mage.Sets/src/mage/cards/v/VesselOfTheAllConsuming.java +++ b/Mage.Sets/src/mage/cards/v/VesselOfTheAllConsuming.java @@ -17,7 +17,6 @@ import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; -import mage.game.events.DamagedEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.watchers.Watcher; @@ -25,8 +24,8 @@ import java.util.AbstractMap; import java.util.HashMap; import java.util.Map; -import java.util.UUID; import java.util.Map.Entry; +import java.util.UUID; /** * @author TheElk801 @@ -90,7 +89,8 @@ public VesselOfTheAllConsumingTriggeredAbility copy() { @Override public boolean checkEventType(GameEvent event, Game game) { - return event instanceof DamagedEvent; + return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT + || event.getType() == GameEvent.EventType.DAMAGED_PLAYER; } @Override diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/xln/JaceCunningCastawayTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/xln/JaceCunningCastawayTest.java new file mode 100644 index 000000000000..439eb0e674e4 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/xln/JaceCunningCastawayTest.java @@ -0,0 +1,44 @@ +package org.mage.test.cards.single.xln; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class JaceCunningCastawayTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.j.JaceCunningCastaway Jace, Cunning Castaway} {1}{U}{U} + * Legendary Planeswalker — Jace + * +1: Whenever one or more creatures you control deal combat damage to a player this turn, draw a card, then discard a card. + * −2: Create a 2/2 blue Illusion creature token with “When this creature becomes the target of a spell, sacrifice it.” + * −5: Create two tokens that are copies of Jace, Cunning Castaway, except they’re not legendary. + * Loyalty: 3 + */ + private static final String jace = "Jace, Cunning Castaway"; + + @Test + public void test_PlusOne_Trigger() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, jace); + addCard(Zone.BATTLEFIELD, playerA, "Savannah Lions"); + addCard(Zone.BATTLEFIELD, playerA, "Alaborn Trooper"); + addCard(Zone.LIBRARY, playerA, "Taiga"); // for looting. + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1:"); + + attack(1, playerA, "Savannah Lions", playerB); + attack(1, playerA, "Alaborn Trooper", playerB); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 2 - 2); + assertGraveyardCount(playerA, "Taiga", 1); + } +} diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 53a9a29392d7..8e8cdc132e88 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -853,6 +853,8 @@ public void addSimultaneousDamage(DamagedEvent damagedEvent, Game game) { // DAMAGED_BATCH_FOR_PERMANENTS + DAMAGED_BATCH_FOR_ONE_PERMANENT addSimultaneousDamageToPermanentBatches((DamagedPermanentEvent) damagedEvent, game); } + // DAMAGED_BATCH_BY_SOURCE + addSimultaneousDamageBySourceBatched(damagedEvent, game); // DAMAGED_BATCH_FOR_ALL addSimultaneousDamageToBatchForAll(damagedEvent, game); } @@ -903,6 +905,22 @@ public void addSimultaneousDamageToPermanentBatches(DamagedPermanentEvent damage } } + public void addSimultaneousDamageBySourceBatched(DamagedEvent damageEvent, Game game) { + // find existing batch first + boolean isBatchUsed = false; + for (GameEvent event : simultaneousEvents) { + if (event instanceof DamagedBatchBySourceEvent + && damageEvent.getSourceId().equals(event.getSourceId())) { + ((DamagedBatchBySourceEvent) event).addEvent(damageEvent); + isBatchUsed = true; + } + } + // new batch if necessary + if (!isBatchUsed) { + addSimultaneousEvent(new DamagedBatchBySourceEvent(damageEvent), game); + } + } + public void addSimultaneousDamageToBatchForAll(DamagedEvent damagedEvent, Game game) { boolean isBatchUsed = false; for (GameEvent event : simultaneousEvents) { diff --git a/Mage/src/main/java/mage/game/events/DamagedBatchBySourceEvent.java b/Mage/src/main/java/mage/game/events/DamagedBatchBySourceEvent.java new file mode 100644 index 000000000000..31918ff62868 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/DamagedBatchBySourceEvent.java @@ -0,0 +1,19 @@ +package mage.game.events; + +/** + * Batch all simultaneous damage events dealt by a single source. + * + * @author Susucr + */ +public class DamagedBatchBySourceEvent extends BatchEvent { + + public DamagedBatchBySourceEvent(DamagedEvent firstEvent) { + super(EventType.DAMAGED_BATCH_BY_SOURCE, false, true, firstEvent); + } + + public boolean isCombatDamage() { + return getEvents() + .stream() + .anyMatch(DamagedEvent::isCombatDamage); + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index b5a0dd2be5e2..450ce7304617 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -142,7 +142,10 @@ combines all player damage events to a single batch (event) and split it per dam targetId the id of the damaged player (playerId won't work for batch) */ DAMAGED_BATCH_FOR_ONE_PLAYER(true), - + /* DAMAGED_BATCH_BY_SOURCE + combine all damage events from a single source to a single batch (event) + */ + DAMAGED_BATCH_BY_SOURCE(true), /* DAMAGED_BATCH_FOR_ALL includes all damage events, both permanent damage and player damage, in single batch event */ From 36bd16d79ed210f888c3c3b658b1a5b6dfb88763 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sun, 28 Apr 2024 15:11:20 +0200 Subject: [PATCH 11/16] fix & test Initiative --- .../cards/designations/InitiativeTest.java | 105 ++++++++++++++++++ .../test/cards/designations/MonarchTest.java | 1 - .../java/org/mage/test/player/TestPlayer.java | 23 +++- .../base/impl/CardTestPlayerAPIImpl.java | 5 + .../java/mage/designations/Initiative.java | 20 +++- 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/designations/InitiativeTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/designations/InitiativeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/designations/InitiativeTest.java new file mode 100644 index 000000000000..246963dad850 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/designations/InitiativeTest.java @@ -0,0 +1,105 @@ +package org.mage.test.cards.designations; + +import mage.constants.MultiplayerAttackOption; +import mage.constants.PhaseStep; +import mage.constants.RangeOfInfluence; +import mage.constants.Zone; +import mage.game.FreeForAll; +import mage.game.Game; +import mage.game.GameException; +import mage.game.mulligan.MulliganType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBase; + +import java.io.FileNotFoundException; + +/** + * @author Susucr + */ +public class InitiativeTest extends CardTestMultiPlayerBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + // reason: must use MultiplayerAttackOption.MULTIPLE + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 20, 7); + // Player order: A -> D -> C -> B + playerA = createPlayer(game, "PlayerA"); + playerB = createPlayer(game, "PlayerB"); + playerC = createPlayer(game, "PlayerC"); + playerD = createPlayer(game, "PlayerD"); + return game; + } + + @Test + public void test_InitiativeByCards() { + // Player order: A -> D -> C -> B + + // When Aarakocra Sneak enters the battlefield, you take the initiative. + addCard(Zone.HAND, playerA, "Aarakocra Sneak", 1); // {3}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + // + addCard(Zone.HAND, playerD, "Aarakocra Sneak", 1); // {3}{U} + addCard(Zone.BATTLEFIELD, playerD, "Island", 4); + + // no initiative before + checkInitative("no initiative before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, null); + + // A as monarch + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aarakocra Sneak"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + addTarget(playerA, "Mountain"); // search for "Secret Entrance" room + checkInitative("initiative 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, playerA); + + // D as monarch + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Aarakocra Sneak"); + waitStackResolved(2, PhaseStep.PRECOMBAT_MAIN); + addTarget(playerD, "Mountain"); // search for "Secret Entrance" room + checkInitative("initiative 2", 2, PhaseStep.PRECOMBAT_MAIN, playerD, playerD); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_InitiativeByDamage() { + // Player order: A -> D -> C -> B + // game must use MultiplayerAttackOption.MULTIPLE + + // When Aarakocra Sneak enters the battlefield, you take the initiative. + addCard(Zone.HAND, playerA, "Aarakocra Sneak", 1); // {3}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + // + addCard(Zone.BATTLEFIELD, playerD, "Grizzly Bears", 1); + // + addCard(Zone.BATTLEFIELD, playerC, "Grizzly Bears", 1); + + // A as monarch + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aarakocra Sneak"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + addTarget(playerA, "Mountain"); // search for "Secret Entrance" room + checkInitative("initiative to A", 1, PhaseStep.PRECOMBAT_MAIN, playerA, playerA); + + // D steal monarch from A + attack(2, playerD, "Grizzly Bears", playerA); + addTarget(playerD, "Mountain"); // search for "Secret Entrance" room + checkInitative("initiative to D", 2, PhaseStep.POSTCOMBAT_MAIN, playerD, playerD); + + // C can't steal from A (nothing to steal) + attack(3, playerC, "Grizzly Bears", playerA); + checkInitative("nothing to steal (keep on D)", 3, PhaseStep.POSTCOMBAT_MAIN, playerC, playerD); + + // D 2nd turn, move to another room. + setChoice(playerD, false); // Go to "Lost Well" in dungeon + addTarget(playerD, "Mountain"); // for the Scry 2 of the "Lost Well" room. + + // C steal from D + attack(3 + 4, playerC, "Grizzly Bears", playerD); + addTarget(playerC, "Mountain"); // search for "Secret Entrance" room + checkInitative("initiative to C", 3 + 4, PhaseStep.POSTCOMBAT_MAIN, playerC, playerC); + + setStrictChooseMode(true); + setStopAt(3 + 4, PhaseStep.END_TURN); + execute(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/designations/MonarchTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/designations/MonarchTest.java index 8ebf61abfeaf..a4407881e0d3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/designations/MonarchTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/designations/MonarchTest.java @@ -70,7 +70,6 @@ public void test_MonarchByDamage() { // addCard(Zone.BATTLEFIELD, playerD, "Grizzly Bears", 1); // - // {T} : Prodigal Pyromancer deals 1 damage to any target. addCard(Zone.BATTLEFIELD, playerC, "Grizzly Bears", 1); // A as monarch diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 35ba3c1e7117..c64728c89882 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -941,12 +941,19 @@ public boolean priority(Game game) { wasProccessed = true; } - // check monarch: plyer id with monarch + // check monarch: player id with monarch if (params[0].equals(CHECK_COMMAND_MONARCH) && params.length == 2) { assertMonarch(action, game, params[1].equals("null") ? null : game.getPlayer(UUID.fromString(params[1]))); actions.remove(action); wasProccessed = true; } + + // check initiative: player id with monarch + if (params[0].equals(CHECK_COMMAND_INITIATIVE) && params.length == 2) { + assertInitiative(action, game, params[1].equals("null") ? null : game.getPlayer(UUID.fromString(params[1]))); + actions.remove(action); + wasProccessed = true; + } } if (wasProccessed) { return true; @@ -1684,6 +1691,20 @@ private void assertMonarch(PlayerAction action, Game game, Player player) { } } + private void assertInitiative(PlayerAction action, Game game, Player player) { + if (player != null) { + // must be + if (game.getInitiativeId() != player.getId()) { + Assert.fail(action.getActionName() + " - game must have " + player.getName() + " as player with the initiative, but found " + game.getPlayer(game.getInitiativeId())); + } + } else { + // must not be + if (game.getInitiativeId() != null) { + Assert.fail(action.getActionName() + " - game must be without initiative, but found " + game.getPlayer(game.getInitiativeId())); + } + } + } + private void assertManaPoolInner(PlayerAction action, Player player, ManaType manaType, Integer amount) { Integer normal = player.getManaPool().getMana().get(manaType); Integer conditional = player.getManaPool().getConditionalMana().stream().mapToInt(a -> a.get(manaType)).sum(); // calcs FULL conditional mana, not real conditions diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index d62ca4e877d2..8ef8192c7486 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -106,6 +106,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public static final String CHECK_COMMAND_STACK_SIZE = "STACK_SIZE"; public static final String CHECK_COMMAND_STACK_OBJECT = "STACK_OBJECT"; public static final String CHECK_COMMAND_MONARCH = "MONARCH"; + public static final String CHECK_COMMAND_INITIATIVE = "INITIATIVE"; // TODO: add target player param to commands public static final String SHOW_COMMAND_LIBRARY = "LIBRARY"; @@ -510,6 +511,10 @@ public void checkMonarch(String checkName, int turnNum, PhaseStep step, TestPlay check(checkName, turnNum, step, player, CHECK_COMMAND_MONARCH, (monarch == null ? "null" : monarch.getId().toString())); } + public void checkInitative(String checkName, int turnNum, PhaseStep step, TestPlayer player, TestPlayer withInitiative) { + check(checkName, turnNum, step, player, CHECK_COMMAND_INITIATIVE, (withInitiative == null ? "null" : withInitiative.getId().toString())); + } + // show commands private void show(String showName, int turnNum, PhaseStep step, TestPlayer player, String command, String... params) { String res = "show:" + command; diff --git a/Mage/src/main/java/mage/designations/Initiative.java b/Mage/src/main/java/mage/designations/Initiative.java index da72667065a2..a86fa75ed88e 100644 --- a/Mage/src/main/java/mage/designations/Initiative.java +++ b/Mage/src/main/java/mage/designations/Initiative.java @@ -1,19 +1,21 @@ package mage.designations; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.OneShotEffect; import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Controllable; import mage.game.Game; -import mage.game.events.DamagedEvent; import mage.game.events.DamagedBatchForPlayersEvent; +import mage.game.events.DamagedPlayerEvent; import mage.game.events.GameEvent; import mage.target.targetpointer.FixedTarget; import java.util.Objects; import java.util.UUID; +import java.util.stream.Stream; /** * @author TheElk801 @@ -40,7 +42,8 @@ public Initiative copy() { } } -class InitiativeDamageTriggeredAbility extends TriggeredAbilityImpl { +// TODO: this will be wrong if 2HG is ever implemented. Would need new batching per (damaging player, damaged player) +class InitiativeDamageTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { InitiativeDamageTriggeredAbility() { super(Zone.ALL, new InitiativeTakeEffect()); @@ -61,13 +64,18 @@ public boolean checkEventType(GameEvent event, Game game) { } @Override - public boolean checkTrigger(GameEvent event, Game game) { - DamagedBatchForPlayersEvent dEvent = (DamagedBatchForPlayersEvent) event; - UUID playerId = dEvent + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((DamagedBatchForPlayersEvent) event) .getEvents() .stream() - .filter(DamagedEvent::isCombatDamage) + .filter(DamagedPlayerEvent::isCombatDamage) .filter(e -> e.getTargetId().equals(game.getInitiativeId())) + .filter(e -> e.getAmount() > 0); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + UUID playerId = filterBatchEvent(event, game) .map(GameEvent::getSourceId) .map(game::getPermanent) .filter(Objects::nonNull) From a994a4857389fbca6b17d94cdd147b1b3e255f4a Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sun, 5 May 2024 20:56:34 +0200 Subject: [PATCH 12/16] handle Milled batch events --- .../src/mage/cards/z/ZellixSanityFlayer.java | 20 ++++++++++++++-- .../OneOrMoreMilledTriggeredAbility.java | 23 +++++++++++++++++-- .../events/DamagedBatchBySourceEvent.java | 2 +- .../main/java/mage/game/events/GameEvent.java | 4 ++-- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Mage.Sets/src/mage/cards/z/ZellixSanityFlayer.java b/Mage.Sets/src/mage/cards/z/ZellixSanityFlayer.java index 7c7e9670fc10..5b53d3af4cd6 100644 --- a/Mage.Sets/src/mage/cards/z/ZellixSanityFlayer.java +++ b/Mage.Sets/src/mage/cards/z/ZellixSanityFlayer.java @@ -2,6 +2,7 @@ import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.ChooseABackgroundAbility; import mage.abilities.common.SimpleActivatedAbility; @@ -19,10 +20,13 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.MilledBatchForOnePlayerEvent; +import mage.game.events.MilledCardEvent; import mage.game.permanent.token.Horror2Token; import mage.target.TargetPlayer; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Stream; /** * @author TheElk801 @@ -60,7 +64,7 @@ public ZellixSanityFlayer copy() { } } -class ZellixSanityFlayerTriggeredAbility extends TriggeredAbilityImpl { +class ZellixSanityFlayerTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { ZellixSanityFlayerTriggeredAbility() { super(Zone.BATTLEFIELD, new CreateTokenEffect(new Horror2Token())); @@ -82,8 +86,20 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.MILLED_CARDS_BATCH_FOR_ONE_PLAYER; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((MilledBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(e -> Optional + .of(e) + .map(mce -> mce.getCard(game)) + .filter(card -> StaticFilters.FILTER_CARD_CREATURE.match(card, getControllerId(), this, game)) + .isPresent()); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - return ((MilledBatchForOnePlayerEvent) event).getCards(game).count(StaticFilters.FILTER_CARD_CREATURE, game) > 0; + return filterBatchEvent(event, game).findAny().isPresent(); } } diff --git a/Mage/src/main/java/mage/abilities/common/OneOrMoreMilledTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/OneOrMoreMilledTriggeredAbility.java index 3f418ddb6d8f..46627234b697 100644 --- a/Mage/src/main/java/mage/abilities/common/OneOrMoreMilledTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/OneOrMoreMilledTriggeredAbility.java @@ -1,6 +1,7 @@ package mage.abilities.common; +import mage.abilities.BatchTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.dynamicvalue.common.SavedMilledValue; import mage.abilities.effects.Effect; @@ -9,11 +10,15 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.MilledBatchAllEvent; +import mage.game.events.MilledCardEvent; + +import java.util.Optional; +import java.util.stream.Stream; /** * @author Susucr */ -public class OneOrMoreMilledTriggeredAbility extends TriggeredAbilityImpl { +public class OneOrMoreMilledTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { private final FilterCard filter; @@ -42,9 +47,23 @@ public boolean checkEventType(GameEvent event, Game game) { return event.getType() == GameEvent.EventType.MILLED_CARDS_BATCH_FOR_ALL; } + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((MilledBatchAllEvent) event) + .getEvents() + .stream() + .filter(e -> Optional + .of(e) + .map(mce -> mce.getCard(game)) + .filter(card -> filter.match(card, getControllerId(), this, game)) + .isPresent()); + } + @Override public boolean checkTrigger(GameEvent event, Game game) { - int count = ((MilledBatchAllEvent) event).getCards(game).count(filter, getControllerId(), this, game); + int count = filterBatchEvent(event, game) + .mapToInt(k -> 1) + .sum(); if (count <= 0) { return false; } diff --git a/Mage/src/main/java/mage/game/events/DamagedBatchBySourceEvent.java b/Mage/src/main/java/mage/game/events/DamagedBatchBySourceEvent.java index 31918ff62868..90c8bbd20405 100644 --- a/Mage/src/main/java/mage/game/events/DamagedBatchBySourceEvent.java +++ b/Mage/src/main/java/mage/game/events/DamagedBatchBySourceEvent.java @@ -8,7 +8,7 @@ public class DamagedBatchBySourceEvent extends BatchEvent { public DamagedBatchBySourceEvent(DamagedEvent firstEvent) { - super(EventType.DAMAGED_BATCH_BY_SOURCE, false, true, firstEvent); + super(EventType.DAMAGED_BATCH_BY_SOURCE, false, true, false, firstEvent); } public boolean isCombatDamage() { diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 450ce7304617..fcf309f4952f 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -117,11 +117,11 @@ playerId the id of the player milling the card (not the source's controller) combines all MILLED_CARD events for a player milling card at the same time in a single batch playerId the id of the player whose batch it is */ - MILLED_CARDS_BATCH_FOR_ONE_PLAYER, + MILLED_CARDS_BATCH_FOR_ONE_PLAYER(true), /* MILLED_CARDS_BATCH_FOR_ALL, combines all MILLED_CARD events for any player in a single batch */ - MILLED_CARDS_BATCH_FOR_ALL, + MILLED_CARDS_BATCH_FOR_ALL(true), /* DAMAGED_PLAYER targetId the id of the damaged player From 6c5931a25840a9973b58854fd9bd3b1cd834b356 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:04:18 +0200 Subject: [PATCH 13/16] introduce, use and test LOST_LIFE_BATCH_FOR_ONE_PLAYER new event --- .../src/mage/cards/e/ExquisiteBlood.java | 58 ++-------- .../src/mage/cards/g/GontisMachinations.java | 66 +++--------- Mage.Sets/src/mage/cards/l/LichsMastery.java | 64 ++--------- Mage.Sets/src/mage/cards/l/LichsTomb.java | 57 ++-------- Mage.Sets/src/mage/cards/m/Mindcrank.java | 93 ++-------------- Mage.Sets/src/mage/cards/o/OathOfLimDul.java | 90 +++++----------- Mage.Sets/src/mage/cards/t/Transcendence.java | 100 +++--------------- .../src/mage/cards/v/VampireScrivener.java | 45 ++------ .../src/mage/cards/v/VengefulWarchief.java | 74 ++----------- .../src/mage/cards/v/VilisBrokerOfBlood.java | 50 ++------- .../cards/single/avr/ExquisiteBloodTest.java | 50 ++++++--- ...LifeFirstTimeEachTurnTriggeredAbility.java | 51 +++++++++ .../common/LoseLifeTriggeredAbility.java | 100 ++++++++++++++++++ .../ConditionalBatchTriggeredAbility.java | 47 ++++++++ .../common/SavedLifeLossValue.java | 49 +++++++++ Mage/src/main/java/mage/game/GameState.java | 10 +- .../main/java/mage/game/events/GameEvent.java | 10 +- .../LifeLostBatchForOnePlayerEvent.java | 11 ++ .../common/LifeLostThisTurnWatcher.java | 47 ++++++++ 19 files changed, 475 insertions(+), 597 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/common/LoseLifeFirstTimeEachTurnTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/abilities/common/LoseLifeTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/abilities/decorator/ConditionalBatchTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedLifeLossValue.java create mode 100644 Mage/src/main/java/mage/game/events/LifeLostBatchForOnePlayerEvent.java create mode 100644 Mage/src/main/java/mage/watchers/common/LifeLostThisTurnWatcher.java diff --git a/Mage.Sets/src/mage/cards/e/ExquisiteBlood.java b/Mage.Sets/src/mage/cards/e/ExquisiteBlood.java index c84713ae9c03..b6c20f8532f2 100644 --- a/Mage.Sets/src/mage/cards/e/ExquisiteBlood.java +++ b/Mage.Sets/src/mage/cards/e/ExquisiteBlood.java @@ -1,16 +1,15 @@ package mage.cards.e; -import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.LoseLifeTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedLifeLossValue; import mage.abilities.effects.common.GainLifeEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; +import mage.constants.TargetController; + +import java.util.UUID; /** * @author noxx @@ -18,12 +17,13 @@ public final class ExquisiteBlood extends CardImpl { public ExquisiteBlood(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{4}{B}"); - + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{B}"); // Whenever an opponent loses life, you gain that much life. - ExquisiteBloodTriggeredAbility ability = new ExquisiteBloodTriggeredAbility(); - this.addAbility(ability); + this.addAbility(new LoseLifeTriggeredAbility( + new GainLifeEffect(SavedLifeLossValue.MUCH), + TargetController.OPPONENT + )); } private ExquisiteBlood(final ExquisiteBlood card) { @@ -34,40 +34,4 @@ private ExquisiteBlood(final ExquisiteBlood card) { public ExquisiteBlood copy() { return new ExquisiteBlood(this); } -} - -class ExquisiteBloodTriggeredAbility extends TriggeredAbilityImpl { - - public ExquisiteBloodTriggeredAbility() { - super(Zone.BATTLEFIELD, null); - } - - private ExquisiteBloodTriggeredAbility(final ExquisiteBloodTriggeredAbility ability) { - super(ability); - } - - @Override - public ExquisiteBloodTriggeredAbility copy() { - return new ExquisiteBloodTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (game.getOpponents(this.controllerId).contains(event.getPlayerId())) { - this.getEffects().clear(); - this.addEffect(new GainLifeEffect(event.getAmount())); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever an opponent loses life, you gain that much life."; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/g/GontisMachinations.java b/Mage.Sets/src/mage/cards/g/GontisMachinations.java index c561745b9e1d..ff4e7a27ed98 100644 --- a/Mage.Sets/src/mage/cards/g/GontisMachinations.java +++ b/Mage.Sets/src/mage/cards/g/GontisMachinations.java @@ -1,7 +1,7 @@ package mage.cards.g; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.LoseLifeTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.PayEnergyCost; import mage.abilities.costs.common.SacrificeSourceCost; @@ -10,14 +10,12 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.WatcherScope; +import mage.constants.TargetController; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; -import mage.watchers.Watcher; +import mage.watchers.common.LifeLostThisTurnWatcher; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -29,8 +27,7 @@ public GontisMachinations(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}"); // Whenever you lose life for the first time each turn, you get {E}. - this.addAbility(new GontisMachinationsTriggeredAbility(), - new GontisMachinationsFirstLostLifeThisTurnWatcher()); + this.addAbility(new GontisMachinationsTriggeredAbility()); // Pay {E}{E}, Sacrifice Gonti's Machinations: Each opponent loses 3 life. You gain life equal to the life lost this way. Ability ability = new SimpleActivatedAbility( @@ -52,67 +49,28 @@ public GontisMachinations copy() { } } -class GontisMachinationsTriggeredAbility extends TriggeredAbilityImpl { +class GontisMachinationsTriggeredAbility extends LoseLifeTriggeredAbility { public GontisMachinationsTriggeredAbility() { - super(Zone.BATTLEFIELD, new GetEnergyCountersControllerEffect(1), false); + super(new GetEnergyCountersControllerEffect(1), TargetController.YOU); setTriggerPhrase("Whenever you lose life for the first time each turn, "); + addWatcher(new LifeLostThisTurnWatcher()); } private GontisMachinationsTriggeredAbility(final GontisMachinationsTriggeredAbility ability) { super(ability); } - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getPlayerId().equals(getControllerId())) { - GontisMachinationsFirstLostLifeThisTurnWatcher watcher - = game.getState().getWatcher(GontisMachinationsFirstLostLifeThisTurnWatcher.class); - if (watcher != null - && watcher.timesLostLifeThisTurn(event.getPlayerId()) < 2) { - return true; - } - } - return false; - } - @Override public GontisMachinationsTriggeredAbility copy() { return new GontisMachinationsTriggeredAbility(this); } -} - -class GontisMachinationsFirstLostLifeThisTurnWatcher extends Watcher { - - private final Map playersLostLife = new HashMap<>(); - - public GontisMachinationsFirstLostLifeThisTurnWatcher() { - super(WatcherScope.GAME); - } @Override - public void watch(GameEvent event, Game game) { - switch (event.getType()) { - case LOST_LIFE: - int timesLifeLost = playersLostLife.getOrDefault(event.getPlayerId(), 0); - timesLifeLost++; - playersLostLife.put(event.getPlayerId(), timesLifeLost); - } - } - - - @Override - public void reset() { - super.reset(); - playersLostLife.clear(); - } - - public int timesLostLifeThisTurn(UUID playerId) { - return playersLostLife.getOrDefault(playerId, 0); + public boolean checkTrigger(GameEvent event, Game game) { + LifeLostThisTurnWatcher watcher = game.getState().getWatcher(LifeLostThisTurnWatcher.class); + return watcher != null + && watcher.timesLostLifeThisTurn(event.getPlayerId()) <= 1 + && super.checkTrigger(event, game); } } diff --git a/Mage.Sets/src/mage/cards/l/LichsMastery.java b/Mage.Sets/src/mage/cards/l/LichsMastery.java index 5a5bca91da7c..91564cccabe0 100644 --- a/Mage.Sets/src/mage/cards/l/LichsMastery.java +++ b/Mage.Sets/src/mage/cards/l/LichsMastery.java @@ -1,27 +1,23 @@ package mage.cards.l; -import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.GainLifeControllerTriggeredAbility; import mage.abilities.common.LeavesBattlefieldTriggeredAbility; +import mage.abilities.common.LoseLifeTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.SavedLifeLossValue; import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.ExileTargetEffect; import mage.abilities.effects.common.LoseGameSourceControllerEffect; -import mage.constants.SuperType; import mage.abilities.keyword.HexproofAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.FilterCard; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledPermanent; @@ -35,8 +31,9 @@ import mage.target.common.TargetControlledPermanent; import mage.target.targetpointer.FixedTarget; +import java.util.UUID; + /** - * * @author TheElk801 */ public final class LichsMastery extends CardImpl { @@ -56,7 +53,7 @@ public LichsMastery(UUID ownerId, CardSetInfo setInfo) { this.addAbility(new GainLifeControllerTriggeredAbility(new LichsMasteryDrawCardsEffect(), false, true)); // Whenever you lose life, for each 1 life you lost, exile a permanent you control or a card from your hand or graveyard. - this.addAbility(new LichsMasteryLoseLifeTriggeredAbility()); + this.addAbility(new LoseLifeTriggeredAbility(new LichsMasteryLoseLifeEffect(), TargetController.YOU)); // When Lich's Mastery leaves the battlefield, you lose the game. this.addAbility(new LeavesBattlefieldTriggeredAbility(new LoseGameSourceControllerEffect(), false)); @@ -125,49 +122,8 @@ public boolean apply(Game game, Ability source) { } } -class LichsMasteryLoseLifeTriggeredAbility extends TriggeredAbilityImpl { - - public LichsMasteryLoseLifeTriggeredAbility() { - super(Zone.BATTLEFIELD, new LichsMasteryLoseLifeEffect(), false); - } - - private LichsMasteryLoseLifeTriggeredAbility(final LichsMasteryLoseLifeTriggeredAbility ability) { - super(ability); - } - - @Override - public LichsMasteryLoseLifeTriggeredAbility copy() { - return new LichsMasteryLoseLifeTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getPlayerId().equals(this.getControllerId())) { - for (Effect effect : this.getEffects()) { - if (effect instanceof LichsMasteryLoseLifeEffect) { - ((LichsMasteryLoseLifeEffect) effect).setAmount(event.getAmount()); - } - } - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you lose life, for each 1 life you lost, exile a permanent you control or a card from your hand or graveyard."; - } -} - class LichsMasteryLoseLifeEffect extends OneShotEffect { - private int amount = 0; - public LichsMasteryLoseLifeEffect() { super(Outcome.Exile); this.staticText = "for each 1 life you lost, exile a permanent you control or a card from your hand or graveyard."; @@ -175,7 +131,6 @@ public LichsMasteryLoseLifeEffect() { private LichsMasteryLoseLifeEffect(final LichsMasteryLoseLifeEffect effect) { super(effect); - this.amount = effect.amount; } @Override @@ -186,7 +141,8 @@ public LichsMasteryLoseLifeEffect copy() { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { + int amount = SavedLifeLossValue.MANY.calculate(game, source, this); + if (controller == null || amount <= 0) { return false; } FilterPermanent filter = new FilterPermanent(); @@ -219,8 +175,4 @@ public boolean apply(Game game, Ability source) { } return true; } - - public void setAmount(int amount) { - this.amount = amount; - } } diff --git a/Mage.Sets/src/mage/cards/l/LichsTomb.java b/Mage.Sets/src/mage/cards/l/LichsTomb.java index 44727140f34d..629a48437011 100644 --- a/Mage.Sets/src/mage/cards/l/LichsTomb.java +++ b/Mage.Sets/src/mage/cards/l/LichsTomb.java @@ -1,37 +1,37 @@ package mage.cards.l; -import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.LoseLifeTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.dynamicvalue.common.SavedLifeLossValue; import mage.abilities.effects.common.SacrificeControllerEffect; -import mage.abilities.effects.common.SacrificeEffect; import mage.abilities.effects.common.continuous.DontLoseByZeroOrLessLifeEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.TargetController; import mage.constants.Zone; import mage.filter.FilterPermanent; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; + +import java.util.UUID; /** - * * @author emerald000 */ public final class LichsTomb extends CardImpl { public LichsTomb(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT},"{4}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}"); // You don't lose the game for having 0 or less life. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new DontLoseByZeroOrLessLifeEffect(Duration.WhileOnBattlefield))); // Whenever you lose life, sacrifice a permanent for each 1 life you lost. - this.addAbility(new LichsTombTriggeredAbility()); + this.addAbility(new LoseLifeTriggeredAbility( + new SacrificeControllerEffect(new FilterPermanent(), SavedLifeLossValue.MUCH, ""), + TargetController.YOU + )); } private LichsTomb(final LichsTomb card) { @@ -42,39 +42,4 @@ private LichsTomb(final LichsTomb card) { public LichsTomb copy() { return new LichsTomb(this); } -} - -class LichsTombTriggeredAbility extends TriggeredAbilityImpl { - - LichsTombTriggeredAbility() { - super(Zone.BATTLEFIELD, new SacrificeControllerEffect(new FilterPermanent(), 0, ""), false); - } - - private LichsTombTriggeredAbility(final LichsTombTriggeredAbility ability) { - super(ability); - } - - @Override - public LichsTombTriggeredAbility copy() { - return new LichsTombTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getPlayerId().equals(this.getControllerId())) { - ((SacrificeEffect) this.getEffects().get(0)).setAmount(StaticValue.get(event.getAmount())); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you lose life, sacrifice a permanent for each 1 life you lost."; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/Mindcrank.java b/Mage.Sets/src/mage/cards/m/Mindcrank.java index b2c5aebe1094..95b70b9733dc 100644 --- a/Mage.Sets/src/mage/cards/m/Mindcrank.java +++ b/Mage.Sets/src/mage/cards/m/Mindcrank.java @@ -1,21 +1,14 @@ package mage.cards.m; -import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.common.LoseLifeTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedLifeLossValue; +import mage.abilities.effects.common.MillCardsTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.players.Player; -import mage.target.targetpointer.FixedTarget; +import mage.constants.TargetController; -import java.util.Set; import java.util.UUID; /** @@ -26,9 +19,11 @@ public final class Mindcrank extends CardImpl { public Mindcrank(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}"); - // Whenever an opponent loses life, that player puts that many cards from the top of their library into their graveyard. - // (Damage dealt by sources without infect causes loss of life.) - this.addAbility(new MindcrankTriggeredAbility()); + // Whenever an opponent loses life, that player mills that many cards. + this.addAbility(new LoseLifeTriggeredAbility( + new MillCardsTargetEffect(SavedLifeLossValue.MANY), + TargetController.OPPONENT, false, true + )); } private Mindcrank(final Mindcrank card) { @@ -39,72 +34,4 @@ private Mindcrank(final Mindcrank card) { public Mindcrank copy() { return new Mindcrank(this); } -} - -class MindcrankTriggeredAbility extends TriggeredAbilityImpl { - - public MindcrankTriggeredAbility() { - super(Zone.BATTLEFIELD, new MindcrankEffect(), false); - } - - private MindcrankTriggeredAbility(final MindcrankTriggeredAbility ability) { - super(ability); - } - - @Override - public MindcrankTriggeredAbility copy() { - return new MindcrankTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Set opponents = game.getOpponents(this.getControllerId()); - if (opponents.contains(event.getPlayerId())) { - for (Effect effect : this.getEffects()) { - effect.setValue("lostLife", event.getAmount()); - effect.setTargetPointer(new FixedTarget(event.getPlayerId())); - } - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever an opponent loses life, that player mills that many cards."; - } -} - -class MindcrankEffect extends OneShotEffect { - - MindcrankEffect() { - super(Outcome.Detriment); - } - - private MindcrankEffect(final MindcrankEffect effect) { - super(effect); - } - - @Override - public MindcrankEffect copy() { - return new MindcrankEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player targetPlayer = game.getPlayer(getTargetPointer().getFirst(game, source)); - if (targetPlayer != null) { - Integer amount = (Integer) getValue("lostLife"); - if (amount == null) { - amount = 0; - } - targetPlayer.millCards(amount, source, game); - } - return true; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/o/OathOfLimDul.java b/Mage.Sets/src/mage/cards/o/OathOfLimDul.java index 15d9883acb0c..08bac36480d1 100644 --- a/Mage.Sets/src/mage/cards/o/OathOfLimDul.java +++ b/Mage.Sets/src/mage/cards/o/OathOfLimDul.java @@ -1,31 +1,32 @@ package mage.cards.o; -import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.LoseLifeTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.common.DiscardTargetCost; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.SavedLifeLossValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; +import mage.constants.TargetController; import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.common.FilterControlledPermanent; import mage.filter.predicate.mageobject.AnotherPredicate; import mage.game.Game; -import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCardInHand; import mage.target.common.TargetControlledPermanent; +import java.util.UUID; + /** - * * @author jeffwadsworth */ public final class OathOfLimDul extends CardImpl { @@ -34,11 +35,10 @@ public OathOfLimDul(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{B}"); // Whenever you lose life, for each 1 life you lost, sacrifice a permanent other than Oath of Lim-Dul unless you discard a card. - this.addAbility(new OathOfLimDulTriggeredAbility()); + this.addAbility(new LoseLifeTriggeredAbility(new OathOfLimDulEffect(), TargetController.YOU)); // {B}{B}: Draw a card. this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), new ManaCostsImpl<>("{B}{B}"))); - } private OathOfLimDul(final OathOfLimDul card) { @@ -51,41 +51,6 @@ public OathOfLimDul copy() { } } -class OathOfLimDulTriggeredAbility extends TriggeredAbilityImpl { - - public OathOfLimDulTriggeredAbility() { - super(Zone.BATTLEFIELD, new OathOfLimDulEffect()); - } - - private OathOfLimDulTriggeredAbility(final OathOfLimDulTriggeredAbility ability) { - super(ability); - } - - @Override - public OathOfLimDulTriggeredAbility copy() { - return new OathOfLimDulTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getPlayerId().equals(controllerId)) { - game.getState().setValue(sourceId.toString() + "oathOfLimDul", event.getAmount()); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you lose life, for each 1 life you lost, sacrifice a permanent other than {this} unless you discard a card."; - } -} - class OathOfLimDulEffect extends OneShotEffect { private static final FilterControlledPermanent filter = new FilterControlledPermanent("controlled permanent other than Oath of Lim-Dul to sacrifice"); @@ -95,7 +60,8 @@ class OathOfLimDulEffect extends OneShotEffect { } public OathOfLimDulEffect() { - super(Outcome.Neutral); + super(Outcome.Detriment); + staticText = "for each 1 life you lost, sacrifice a permanent other than {this} unless you discard a card"; } private OathOfLimDulEffect(final OathOfLimDulEffect effect) { @@ -104,32 +70,32 @@ private OathOfLimDulEffect(final OathOfLimDulEffect effect) { @Override public boolean apply(Game game, Ability source) { + int amountDamage = SavedLifeLossValue.MANY.calculate(game, source, this); + Player controller = game.getPlayer(source.getControllerId()); + if (amountDamage <= 0 || controller == null) { + return false; + } boolean sacrificeDone = false; int numberSacrificed = 0; int numberToDiscard = 0; int numberOfControlledPermanents = 0; - Player controller = game.getPlayer(source.getControllerId()); - int amountDamage = (int) game.getState().getValue(source.getSourceId().toString() + "oathOfLimDul"); - if (amountDamage > 0 - && controller != null) { - TargetControlledPermanent target = new TargetControlledPermanent(0, numberOfControlledPermanents, filter, true); - target.withNotTarget(true); - if (controller.choose(Outcome.Detriment, target, source, game)) { - for (UUID targetPermanentId : target.getTargets()) { - Permanent permanent = game.getPermanent(targetPermanentId); - if (permanent != null - && permanent.sacrifice(source, game)) { - numberSacrificed += 1; - sacrificeDone = true; - } + TargetControlledPermanent target = new TargetControlledPermanent(0, numberOfControlledPermanents, filter, true); + target.withNotTarget(true); + if (controller.choose(Outcome.Detriment, target, source, game)) { + for (UUID targetPermanentId : target.getTargets()) { + Permanent permanent = game.getPermanent(targetPermanentId); + if (permanent != null + && permanent.sacrifice(source, game)) { + numberSacrificed += 1; + sacrificeDone = true; } } - numberToDiscard = amountDamage - numberSacrificed; - Cost cost = new DiscardTargetCost(new TargetCardInHand(numberToDiscard, new FilterCard("card(s) in your hand to discard"))); - if (numberToDiscard > 0 - && cost.canPay(source, source, controller.getId(), game)) { - return cost.pay(source, game, source, controller.getId(), true); // discard cost paid simultaneously - } + } + numberToDiscard = amountDamage - numberSacrificed; + Cost cost = new DiscardTargetCost(new TargetCardInHand(numberToDiscard, new FilterCard("card(s) in your hand to discard"))); + if (numberToDiscard > 0 + && cost.canPay(source, source, controller.getId(), game)) { + return cost.pay(source, game, source, controller.getId(), true); // discard cost paid simultaneously } return sacrificeDone; } diff --git a/Mage.Sets/src/mage/cards/t/Transcendence.java b/Mage.Sets/src/mage/cards/t/Transcendence.java index cdc2849b9f4a..a16d055315e8 100644 --- a/Mage.Sets/src/mage/cards/t/Transcendence.java +++ b/Mage.Sets/src/mage/cards/t/Transcendence.java @@ -1,34 +1,36 @@ package mage.cards.t; -import java.util.UUID; -import mage.abilities.Ability; import mage.abilities.StateTriggeredAbility; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.LoseLifeTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.Effect; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.MultipliedValue; +import mage.abilities.dynamicvalue.common.SavedLifeLossValue; +import mage.abilities.effects.common.GainLifeEffect; import mage.abilities.effects.common.LoseGameSourceControllerEffect; import mage.abilities.effects.common.continuous.DontLoseByZeroOrLessLifeEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.constants.Outcome; +import mage.constants.TargetController; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.players.Player; +import java.util.UUID; + /** - * * @author emerald000 */ public final class Transcendence extends CardImpl { + private static final DynamicValue value = new MultipliedValue(SavedLifeLossValue.MUCH, 2); + public Transcendence(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{3}{W}{W}{W}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{W}{W}{W}"); // You don't lose the game for having 0 or less life. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new DontLoseByZeroOrLessLifeEffect(Duration.WhileOnBattlefield))); @@ -37,7 +39,10 @@ public Transcendence(UUID ownerId, CardSetInfo setInfo) { this.addAbility(new TranscendenceStateTriggeredAbility()); // Whenever you lose life, you gain 2 life for each 1 life you lost. - this.addAbility(new TranscendenceLoseLifeTriggeredAbility()); + this.addAbility(new LoseLifeTriggeredAbility( + new GainLifeEffect(value).setText("you gain 2 life for each 1 life you lost"), + TargetController.YOU + )); } private Transcendence(final Transcendence card) { @@ -78,77 +83,4 @@ public boolean checkTrigger(GameEvent event, Game game) { public String getRule() { return "When you have 20 or more life, you lose the game."; } -} - -class TranscendenceLoseLifeTriggeredAbility extends TriggeredAbilityImpl { - - TranscendenceLoseLifeTriggeredAbility() { - super(Zone.BATTLEFIELD, new TranscendenceLoseLifeEffect(), false); - } - - private TranscendenceLoseLifeTriggeredAbility(final TranscendenceLoseLifeTriggeredAbility ability) { - super(ability); - } - - @Override - public TranscendenceLoseLifeTriggeredAbility copy() { - return new TranscendenceLoseLifeTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getPlayerId().equals(this.getControllerId())) { - for (Effect effect : this.getEffects()) { - if (effect instanceof TranscendenceLoseLifeEffect) { - ((TranscendenceLoseLifeEffect) effect).setAmount(event.getAmount()); - } - } - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you lose life, you gain 2 life for each 1 life you lost."; - } -} - -class TranscendenceLoseLifeEffect extends OneShotEffect { - - private int amount = 0; - - TranscendenceLoseLifeEffect() { - super(Outcome.GainLife); - this.staticText = "you gain 2 life for each 1 life you lost"; - } - - private TranscendenceLoseLifeEffect(final TranscendenceLoseLifeEffect effect) { - super(effect); - this.amount = effect.amount; - } - - @Override - public TranscendenceLoseLifeEffect copy() { - return new TranscendenceLoseLifeEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - controller.gainLife(2 * amount, game, source); - return true; - } - return false; - } - - public void setAmount(int amount) { - this.amount = amount; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/v/VampireScrivener.java b/Mage.Sets/src/mage/cards/v/VampireScrivener.java index 8da71789ddbc..278e1a7260d7 100644 --- a/Mage.Sets/src/mage/cards/v/VampireScrivener.java +++ b/Mage.Sets/src/mage/cards/v/VampireScrivener.java @@ -1,9 +1,10 @@ package mage.cards.v; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.GainLifeControllerTriggeredAbility; +import mage.abilities.common.LoseLifeTriggeredAbility; import mage.abilities.condition.common.MyTurnCondition; +import mage.abilities.decorator.ConditionalBatchTriggeredAbility; import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.keyword.FlyingAbility; @@ -11,10 +12,8 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.TargetController; import mage.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; import java.util.UUID; @@ -41,7 +40,10 @@ public VampireScrivener(UUID ownerId, CardSetInfo setInfo) { )); // Whenever you lose life during your turn, put a +1/+1 counter on Vampire Scrivener. - this.addAbility(new VampireScrivenerTriggeredAbility()); + this.addAbility(new ConditionalBatchTriggeredAbility(new LoseLifeTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), TargetController.YOU), + MyTurnCondition.instance, "Whenever you lose life during your turn, put a +1/+1 counter on {this}." + )); } private VampireScrivener(final VampireScrivener card) { @@ -52,35 +54,4 @@ private VampireScrivener(final VampireScrivener card) { public VampireScrivener copy() { return new VampireScrivener(this); } -} - -class VampireScrivenerTriggeredAbility extends TriggeredAbilityImpl { - - VampireScrivenerTriggeredAbility() { - super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance())); - } - - private VampireScrivenerTriggeredAbility(final VampireScrivenerTriggeredAbility ability) { - super(ability); - } - - @Override - public VampireScrivenerTriggeredAbility copy() { - return new VampireScrivenerTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - return game.isActivePlayer(event.getPlayerId()) && game.isActivePlayer(getControllerId()); - } - - @Override - public String getRule() { - return "Whenever you lose life during your turn, put a +1/+1 counter on {this}."; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/v/VengefulWarchief.java b/Mage.Sets/src/mage/cards/v/VengefulWarchief.java index 8555a795f0fe..574d5b9341b9 100644 --- a/Mage.Sets/src/mage/cards/v/VengefulWarchief.java +++ b/Mage.Sets/src/mage/cards/v/VengefulWarchief.java @@ -1,21 +1,15 @@ package mage.cards.v; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.LoseLifeFirstTimeEachTurnTriggeredAbility; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.WatcherScope; -import mage.constants.Zone; +import mage.constants.TargetController; import mage.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.watchers.Watcher; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -32,7 +26,10 @@ public VengefulWarchief(UUID ownerId, CardSetInfo setInfo) { this.toughness = new MageInt(4); // Whenever you lose life for the first time each turn, put a +1/+1 counter on Vengeful Warchief. - this.addAbility(new VengefulWarchiefTriggeredAbility(), new VengefulWarchiefWatcher()); + this.addAbility(new LoseLifeFirstTimeEachTurnTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), + TargetController.YOU + )); } private VengefulWarchief(final VengefulWarchief card) { @@ -44,62 +41,3 @@ public VengefulWarchief copy() { return new VengefulWarchief(this); } } - -class VengefulWarchiefTriggeredAbility extends TriggeredAbilityImpl { - - VengefulWarchiefTriggeredAbility() { - super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance()), false); - setTriggerPhrase("Whenever you lose life for the first time each turn, "); - } - - private VengefulWarchiefTriggeredAbility(final VengefulWarchiefTriggeredAbility ability) { - super(ability); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (!event.getPlayerId().equals(getControllerId())) { - return false; - } - VengefulWarchiefWatcher watcher = game.getState().getWatcher(VengefulWarchiefWatcher.class); - return watcher != null && watcher.timesLostLifeThisTurn(event.getPlayerId()) < 2; - } - - @Override - public VengefulWarchiefTriggeredAbility copy() { - return new VengefulWarchiefTriggeredAbility(this); - } -} - -class VengefulWarchiefWatcher extends Watcher { - - private final Map playersLostLife = new HashMap<>(); - - VengefulWarchiefWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.LOST_LIFE) { - int timesLifeLost = playersLostLife.getOrDefault(event.getPlayerId(), 0); - timesLifeLost++; - playersLostLife.put(event.getPlayerId(), timesLifeLost); - } - } - - @Override - public void reset() { - super.reset(); - playersLostLife.clear(); - } - - int timesLostLifeThisTurn(UUID playerId) { - return playersLostLife.getOrDefault(playerId, 0); - } -} diff --git a/Mage.Sets/src/mage/cards/v/VilisBrokerOfBlood.java b/Mage.Sets/src/mage/cards/v/VilisBrokerOfBlood.java index abf53785dbd0..fa3948d7e422 100644 --- a/Mage.Sets/src/mage/cards/v/VilisBrokerOfBlood.java +++ b/Mage.Sets/src/mage/cards/v/VilisBrokerOfBlood.java @@ -2,10 +2,11 @@ import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.LoseLifeTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.PayLifeCost; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.SavedLifeLossValue; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.continuous.BoostTargetEffect; import mage.abilities.keyword.FlyingAbility; @@ -14,9 +15,7 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; +import mage.constants.TargetController; import mage.target.common.TargetCreaturePermanent; import java.util.UUID; @@ -46,7 +45,10 @@ public VilisBrokerOfBlood(UUID ownerId, CardSetInfo setInfo) { this.addAbility(ability); // Whenever you lose life, draw that many cards. - this.addAbility(new VilisBrokerOfBloodTriggeredAbility()); + this.addAbility(new LoseLifeTriggeredAbility( + new DrawCardSourceControllerEffect(SavedLifeLossValue.MANY), + TargetController.YOU + )); } private VilisBrokerOfBlood(final VilisBrokerOfBlood card) { @@ -57,40 +59,4 @@ private VilisBrokerOfBlood(final VilisBrokerOfBlood card) { public VilisBrokerOfBlood copy() { return new VilisBrokerOfBlood(this); } -} - -class VilisBrokerOfBloodTriggeredAbility extends TriggeredAbilityImpl { - - VilisBrokerOfBloodTriggeredAbility() { - super(Zone.BATTLEFIELD, null, false); - } - - private VilisBrokerOfBloodTriggeredAbility(final VilisBrokerOfBloodTriggeredAbility ability) { - super(ability); - } - - @Override - public VilisBrokerOfBloodTriggeredAbility copy() { - return new VilisBrokerOfBloodTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.LOST_LIFE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getPlayerId().equals(this.getControllerId())) { - this.getEffects().clear(); - this.addEffect(new DrawCardSourceControllerEffect(event.getAmount())); - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you lose life, draw that many cards."; - } -} +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/ExquisiteBloodTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/ExquisiteBloodTest.java index db6b4e2b1b2f..9f653ae1d6dd 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/ExquisiteBloodTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/ExquisiteBloodTest.java @@ -7,7 +7,6 @@ import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author noxx */ public class ExquisiteBloodTest extends CardTestPlayerBase { @@ -45,7 +44,7 @@ public void BasicCardTest() { } /** - * Ajani, Inspiring leader does not trigger Exquisite Blood + Defiant Bloodlord #6464 + * Ajani, Inspiring leader does not trigger Exquisite Blood + Defiant Bloodlord #6464 */ @Test public void triggerCascadeTest() { @@ -57,14 +56,12 @@ public void triggerCascadeTest() { // Flying // Whenever you gain life, target opponent loses that much life. addCard(Zone.BATTLEFIELD, playerA, "Defiant Bloodlord", 1); // Creature 4/5 {5}{B}{B} - + // Whenever an opponent loses life, you gain that much life. addCard(Zone.BATTLEFIELD, playerA, "Exquisite Blood", 1); // Enchantment {4}{B} activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2:", "Defiant Bloodlord"); addTarget(playerA, playerB); // Target opponent of Defiant Bloodlord triggered ability (looping until opponent is dead) - addTarget(playerA, playerB); - addTarget(playerA, playerB); addTarget(playerA, playerB); addTarget(playerA, playerB); addTarget(playerA, playerB); @@ -72,7 +69,9 @@ public void triggerCascadeTest() { addTarget(playerA, playerB); addTarget(playerA, playerB); addTarget(playerA, playerB); - + addTarget(playerA, playerB); + addTarget(playerA, playerB); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -82,12 +81,12 @@ public void triggerCascadeTest() { assertLife(playerB, 0); // Player B is dead, game ends assertLife(playerA, 40); - - + + } /** - * Ajani, Inspiring leader does not trigger Exquisite Blood + Defiant Bloodlord #6464 + * Ajani, Inspiring leader does not trigger Exquisite Blood + Defiant Bloodlord #6464 */ @Test public void triggerCascadeAjaniSecondAbilityTest() { @@ -100,14 +99,12 @@ public void triggerCascadeAjaniSecondAbilityTest() { // Flying // Whenever you gain life, target opponent loses that much life. addCard(Zone.BATTLEFIELD, playerA, "Defiant Bloodlord", 1); // Creature 4/5 {5}{B}{B} - + // Whenever an opponent loses life, you gain that much life. addCard(Zone.BATTLEFIELD, playerA, "Exquisite Blood", 1); // Enchantment {4}{B} activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-3:", "Silvercoat Lion"); addTarget(playerA, playerB); // Target opponent of Defiant Bloodlord triggered ability (looping until opponent is dead) - addTarget(playerA, playerB); - addTarget(playerA, playerB); addTarget(playerA, playerB); addTarget(playerA, playerB); addTarget(playerA, playerB); @@ -115,7 +112,9 @@ public void triggerCascadeAjaniSecondAbilityTest() { addTarget(playerA, playerB); addTarget(playerA, playerB); addTarget(playerA, playerB); - + addTarget(playerA, playerB); + addTarget(playerA, playerB); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -126,7 +125,28 @@ public void triggerCascadeAjaniSecondAbilityTest() { assertLife(playerB, 0); // Player B is dead, game ends assertLife(playerA, 40); - - + + + } + + @Test + public void attackWithTwoCreatures() { + setStrictChooseMode(true); + + // Whenever an opponent loses life, you gain that much life. + addCard(Zone.BATTLEFIELD, playerA, "Exquisite Blood", 1); + addCard(Zone.BATTLEFIELD, playerA, "Elite Vanguard"); + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + + attack(1, playerA, "Elite Vanguard", playerB); + attack(1, playerA, "Memnite", playerB); + + // no trigger stacking, only 1 trigger + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 2 - 1); + assertLife(playerA, 20 + 3); } } diff --git a/Mage/src/main/java/mage/abilities/common/LoseLifeFirstTimeEachTurnTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/LoseLifeFirstTimeEachTurnTriggeredAbility.java new file mode 100644 index 000000000000..c259f30f63e6 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/LoseLifeFirstTimeEachTurnTriggeredAbility.java @@ -0,0 +1,51 @@ +package mage.abilities.common; + +import mage.abilities.effects.Effect; +import mage.constants.TargetController; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.watchers.common.LifeLostThisTurnWatcher; + +/** + * @author Susucr + */ +public class LoseLifeFirstTimeEachTurnTriggeredAbility extends LoseLifeTriggeredAbility { + + public LoseLifeFirstTimeEachTurnTriggeredAbility(Effect effect, TargetController targetController) { + this(effect, targetController, false, false); + } + + public LoseLifeFirstTimeEachTurnTriggeredAbility(Effect effect, TargetController targetController, boolean optional, boolean setTargetPointer) { + super(effect, targetController, optional, setTargetPointer); + addWatcher(new LifeLostThisTurnWatcher()); + } + + private LoseLifeFirstTimeEachTurnTriggeredAbility(final LoseLifeFirstTimeEachTurnTriggeredAbility ability) { + super(ability); + } + + @Override + public LoseLifeFirstTimeEachTurnTriggeredAbility copy() { + return new LoseLifeFirstTimeEachTurnTriggeredAbility(this); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + LifeLostThisTurnWatcher watcher = game.getState().getWatcher(LifeLostThisTurnWatcher.class); + return watcher != null + && watcher.timesLostLifeThisTurn(event.getPlayerId()) <= 1 + && super.checkTrigger(event, game); + } + + @Override + protected String generateTriggerPhrase() { + switch (targetController) { + case YOU: + return "Whenever you lose life for the first time each turn, "; + case OPPONENT: + return "Whenever an opponent loses life for the first time each turn, "; + default: + throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController); + } + } +} diff --git a/Mage/src/main/java/mage/abilities/common/LoseLifeTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/LoseLifeTriggeredAbility.java new file mode 100644 index 000000000000..a2703d371ff3 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/LoseLifeTriggeredAbility.java @@ -0,0 +1,100 @@ +package mage.abilities.common; + +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.dynamicvalue.common.SavedLifeLossValue; +import mage.abilities.effects.Effect; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.LifeLostBatchForOnePlayerEvent; +import mage.game.events.LifeLostEvent; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; +import java.util.stream.Stream; + +/** + * @author Susucr + */ +public class LoseLifeTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { + + protected final TargetController targetController; + private final boolean setTargetPointer; + + public LoseLifeTriggeredAbility(Effect effect, TargetController targetController) { + this(effect, targetController, false, false); + } + + public LoseLifeTriggeredAbility(Effect effect, TargetController targetController, boolean optional, boolean setTargetPointer) { + super(Zone.BATTLEFIELD, effect, optional); + this.targetController = targetController; + this.setTargetPointer = setTargetPointer; + setTriggerPhrase(generateTriggerPhrase()); + } + + protected LoseLifeTriggeredAbility(final LoseLifeTriggeredAbility ability) { + super(ability); + this.targetController = ability.targetController; + this.setTargetPointer = ability.setTargetPointer; + } + + @Override + public LoseLifeTriggeredAbility copy() { + return new LoseLifeTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER; + } + + private boolean filterPlayer(UUID playerId, Game game) { + switch (targetController) { + case YOU: + return isControlledBy(playerId); + case OPPONENT: + return game.getOpponents(getControllerId()).contains(playerId); + default: + throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController); + } + } + + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ((LifeLostBatchForOnePlayerEvent) event) + .getEvents() + .stream() + .filter(e -> e.getAmount() > 0); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!filterPlayer(event.getTargetId(), game)) { + return false; + } + int amount = filterBatchEvent(event, game) + .mapToInt(e -> e.getAmount()) + .sum(); + if (amount <= 0) { + return false; + } + this.getEffects().setValue(SavedLifeLossValue.getValueKey(), event.getAmount()); + if (setTargetPointer) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + } + return true; + } + + protected String generateTriggerPhrase() { + switch (targetController) { + case YOU: + return "Whenever you lose life, "; + case OPPONENT: + return "Whenever an opponent loses life, "; + default: + throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController); + } + } +} diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalBatchTriggeredAbility.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalBatchTriggeredAbility.java new file mode 100644 index 000000000000..891db82b0260 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalBatchTriggeredAbility.java @@ -0,0 +1,47 @@ +package mage.abilities.decorator; + +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.stream.Stream; + +/** + * Same as ConditionalTriggeredAbility, but for batch triggers. + * + * @author Susucr + */ +public class ConditionalBatchTriggeredAbility extends ConditionalTriggeredAbility implements BatchTriggeredAbility { + + private final BatchTriggeredAbility ability; + + /** + * Triggered ability with a condition. Set the optionality for the trigger + * ability itself. + * + * @param ability + * @param condition + * @param text explicit rule text for the ability, if null or empty, the + * rule text generated by the triggered ability itself is used. + */ + public ConditionalBatchTriggeredAbility(BatchTriggeredAbility ability, Condition condition, String text) { + super(ability, condition, text); + this.ability = ability; + } + + protected ConditionalBatchTriggeredAbility(final ConditionalBatchTriggeredAbility triggered) { + super(triggered); + this.ability = triggered.ability.copy(); + } + + @Override + public ConditionalBatchTriggeredAbility copy() { + return new ConditionalBatchTriggeredAbility(this); + } + + @Override + public Stream filterBatchEvent(GameEvent event, Game game) { + return ability.filterBatchEvent(event, game); + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedLifeLossValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedLifeLossValue.java new file mode 100644 index 000000000000..8df824f6664b --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedLifeLossValue.java @@ -0,0 +1,49 @@ +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.game.Game; + +/** + * @author Susucr + */ +public enum SavedLifeLossValue implements DynamicValue { + MANY("many"), + MUCH("much"); + + private final String message; + + private static final String key = "SavedLifeLoss"; + + /** + * value key used to store the amount of life lost + */ + public static String getValueKey() { + return key; + } + + SavedLifeLossValue(String message) { + this.message = "that " + message; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return (Integer) effect.getValue(getValueKey()); + } + + @Override + public SavedLifeLossValue copy() { + return this; + } + + @Override + public String toString() { + return message; + } + + @Override + public String getMessage() { + return ""; + } +} diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 8e8cdc132e88..2ea15a00a04e 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -965,12 +965,17 @@ public void addSimultaneousLifeLossToBatch(LifeLostEvent lifeLossEvent, Game gam // Combine multiple life loss events in the single event (batch) // see GameEvent.LOST_LIFE_BATCH - // existing batch + // existing batchs boolean isLifeLostBatchUsed = false; + boolean isSingleBatchUsed = false; for (GameEvent event : simultaneousEvents) { if (event instanceof LifeLostBatchEvent) { ((LifeLostBatchEvent) event).addEvent(lifeLossEvent); isLifeLostBatchUsed = true; + } else if (event instanceof LifeLostBatchForOnePlayerEvent + && event.getTargetId().equals(lifeLossEvent.getTargetId())) { + ((LifeLostBatchForOnePlayerEvent) event).addEvent(lifeLossEvent); + isSingleBatchUsed = true; } } @@ -978,6 +983,9 @@ public void addSimultaneousLifeLossToBatch(LifeLostEvent lifeLossEvent, Game gam if (!isLifeLostBatchUsed) { addSimultaneousEvent(new LifeLostBatchEvent(lifeLossEvent), game); } + if (!isSingleBatchUsed) { + addSimultaneousEvent(new LifeLostBatchForOnePlayerEvent(lifeLossEvent), game); + } } public void addSimultaneousTappedToBatch(TappedEvent tappedEvent, Game game) { diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index fcf309f4952f..bd08b8f76d20 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -167,7 +167,6 @@ combine all damage events from a single source to a single batch (event) DAMAGE_CAUSES_LIFE_LOSS, PLAYER_LIFE_CHANGE, GAIN_LIFE, GAINED_LIFE, - LOSE_LIFE, LOST_LIFE, /* LOSE_LIFE + LOST_LIFE targetId the id of the player loosing life sourceId sourceId of the ability which caused the lose @@ -175,10 +174,17 @@ combine all damage events from a single source to a single batch (event) amount amount of life loss flag true = from combat damage - other from non combat damage */ - LOST_LIFE_BATCH(true), + + LOSE_LIFE, LOST_LIFE, + /* LOST_LIFE_BATCH_FOR_ONE_PLAYER + combines all life lost events for a player to a single batch (event) + */ + LOST_LIFE_BATCH_FOR_ONE_PLAYER(true), /* LOST_LIFE_BATCH combines all player life lost events to a single batch (event) */ + LOST_LIFE_BATCH(true), + PLAY_LAND, LAND_PLAYED, CREATURE_CHAMPIONED, /* CREATURE_CHAMPIONED diff --git a/Mage/src/main/java/mage/game/events/LifeLostBatchForOnePlayerEvent.java b/Mage/src/main/java/mage/game/events/LifeLostBatchForOnePlayerEvent.java new file mode 100644 index 000000000000..e464bdfec8ec --- /dev/null +++ b/Mage/src/main/java/mage/game/events/LifeLostBatchForOnePlayerEvent.java @@ -0,0 +1,11 @@ +package mage.game.events; + +/** + * @author Susucr + */ +public class LifeLostBatchForOnePlayerEvent extends BatchEvent { + + public LifeLostBatchForOnePlayerEvent(LifeLostEvent firstEvent) { + super(EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER, true, false, firstEvent); + } +} diff --git a/Mage/src/main/java/mage/watchers/common/LifeLostThisTurnWatcher.java b/Mage/src/main/java/mage/watchers/common/LifeLostThisTurnWatcher.java new file mode 100644 index 000000000000..671971c68311 --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/LifeLostThisTurnWatcher.java @@ -0,0 +1,47 @@ + +package mage.watchers.common; + +import mage.constants.WatcherScope; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Susucr + */ +public class LifeLostThisTurnWatcher extends Watcher { + + // player -> number of times (not amount!) that player lost life this turn. + private final Map playersLostLife = new HashMap<>(); + + public LifeLostThisTurnWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + switch (event.getType()) { + case LOST_LIFE_BATCH_FOR_ONE_PLAYER: + if (event.getAmount() <= 0) { + return; + } + playersLostLife.compute(event.getTargetId(), CardUtil::setOrIncrementValue); + } + } + + + @Override + public void reset() { + super.reset(); + playersLostLife.clear(); + } + + public int timesLostLifeThisTurn(UUID playerId) { + return playersLostLife.getOrDefault(playerId, 0); + } +} From f6bcb039ceba3292eefa3d463250bc6399946298 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:30:55 +0200 Subject: [PATCH 14/16] fix Gonti's Machinations --- .../src/mage/cards/g/GontisMachinations.java | 38 +++---------------- ...LifeFirstTimeEachTurnTriggeredAbility.java | 4 +- 2 files changed, 7 insertions(+), 35 deletions(-) diff --git a/Mage.Sets/src/mage/cards/g/GontisMachinations.java b/Mage.Sets/src/mage/cards/g/GontisMachinations.java index ff4e7a27ed98..6822a3b5d926 100644 --- a/Mage.Sets/src/mage/cards/g/GontisMachinations.java +++ b/Mage.Sets/src/mage/cards/g/GontisMachinations.java @@ -1,7 +1,7 @@ package mage.cards.g; import mage.abilities.Ability; -import mage.abilities.common.LoseLifeTriggeredAbility; +import mage.abilities.common.LoseLifeFirstTimeEachTurnTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.PayEnergyCost; import mage.abilities.costs.common.SacrificeSourceCost; @@ -12,9 +12,6 @@ import mage.constants.CardType; import mage.constants.TargetController; import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.watchers.common.LifeLostThisTurnWatcher; import java.util.UUID; @@ -27,7 +24,10 @@ public GontisMachinations(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}"); // Whenever you lose life for the first time each turn, you get {E}. - this.addAbility(new GontisMachinationsTriggeredAbility()); + this.addAbility(new LoseLifeFirstTimeEachTurnTriggeredAbility( + new GetEnergyCountersControllerEffect(1), + TargetController.YOU + )); // Pay {E}{E}, Sacrifice Gonti's Machinations: Each opponent loses 3 life. You gain life equal to the life lost this way. Ability ability = new SimpleActivatedAbility( @@ -47,30 +47,4 @@ private GontisMachinations(final GontisMachinations card) { public GontisMachinations copy() { return new GontisMachinations(this); } -} - -class GontisMachinationsTriggeredAbility extends LoseLifeTriggeredAbility { - - public GontisMachinationsTriggeredAbility() { - super(new GetEnergyCountersControllerEffect(1), TargetController.YOU); - setTriggerPhrase("Whenever you lose life for the first time each turn, "); - addWatcher(new LifeLostThisTurnWatcher()); - } - - private GontisMachinationsTriggeredAbility(final GontisMachinationsTriggeredAbility ability) { - super(ability); - } - - @Override - public GontisMachinationsTriggeredAbility copy() { - return new GontisMachinationsTriggeredAbility(this); - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - LifeLostThisTurnWatcher watcher = game.getState().getWatcher(LifeLostThisTurnWatcher.class); - return watcher != null - && watcher.timesLostLifeThisTurn(event.getPlayerId()) <= 1 - && super.checkTrigger(event, game); - } -} +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/common/LoseLifeFirstTimeEachTurnTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/LoseLifeFirstTimeEachTurnTriggeredAbility.java index c259f30f63e6..98cde48b33e0 100644 --- a/Mage/src/main/java/mage/abilities/common/LoseLifeFirstTimeEachTurnTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/LoseLifeFirstTimeEachTurnTriggeredAbility.java @@ -33,7 +33,7 @@ public LoseLifeFirstTimeEachTurnTriggeredAbility copy() { public boolean checkTrigger(GameEvent event, Game game) { LifeLostThisTurnWatcher watcher = game.getState().getWatcher(LifeLostThisTurnWatcher.class); return watcher != null - && watcher.timesLostLifeThisTurn(event.getPlayerId()) <= 1 + && watcher.timesLostLifeThisTurn(event.getTargetId()) <= 1 && super.checkTrigger(event, game); } @@ -42,8 +42,6 @@ protected String generateTriggerPhrase() { switch (targetController) { case YOU: return "Whenever you lose life for the first time each turn, "; - case OPPONENT: - return "Whenever an opponent loses life for the first time each turn, "; default: throw new IllegalArgumentException("Wrong code usage: not supported targetController: " + targetController); } From afe1b56d4f20ee8a45ab768e13b23b3b91e11ea7 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:28:01 +0200 Subject: [PATCH 15/16] add tests --- .../aer}/GontisMachinationsTest.java | 37 ++++----- .../cards/single/avr/ExquisiteBloodTest.java | 8 +- .../single/snc/VampireScrivenerTest.java | 79 +++++++++++++++++++ 3 files changed, 105 insertions(+), 19 deletions(-) rename Mage.Tests/src/test/java/org/mage/test/cards/{watchers => single/aer}/GontisMachinationsTest.java (89%) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/snc/VampireScrivenerTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/watchers/GontisMachinationsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/aer/GontisMachinationsTest.java similarity index 89% rename from Mage.Tests/src/test/java/org/mage/test/cards/watchers/GontisMachinationsTest.java rename to Mage.Tests/src/test/java/org/mage/test/cards/single/aer/GontisMachinationsTest.java index 26cc85ba36d1..e74abeb44e48 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/watchers/GontisMachinationsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/aer/GontisMachinationsTest.java @@ -1,4 +1,4 @@ -package org.mage.test.cards.watchers; +package org.mage.test.cards.single.aer; import mage.constants.PhaseStep; import mage.constants.Zone; @@ -7,7 +7,6 @@ import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author escplan9 */ public class GontisMachinationsTest extends CardTestPlayerBase { @@ -19,57 +18,59 @@ public class GontisMachinationsTest extends CardTestPlayerBase { Pay {E}{E}, Sacrifice Gonti's Machinations: Each opponent loses 3 life. You gain life equal to the life lost this way. */ private final String gMachinations = "Gonti's Machinations"; - + /* * Reported bug: [[Gonti's Machinations]] currently triggers and gain 1 energy whenever you lose life instead of only the first life loss of each turn. - * See issue #3499 (test is currently failing due to bug in code) - */ + * See issue #3499 for context + */ @Test public void machinations_ThreeCreaturesCombatDamage_OneTrigger() { - + setStrictChooseMode(true); + String memnite = "Memnite"; // {0} 1/1 String gBears = "Grizzly Bears"; // {1}{G} 2/2 String hGiant = "Hill Giant"; // {2}{R} 3/3 - + addCard(Zone.BATTLEFIELD, playerB, gMachinations); addCard(Zone.BATTLEFIELD, playerA, memnite); addCard(Zone.BATTLEFIELD, playerA, gBears); addCard(Zone.BATTLEFIELD, playerA, hGiant); - + attack(3, playerA, memnite); attack(3, playerA, gBears); attack(3, playerA, hGiant); - + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); execute(); - + assertTapped(memnite, true); assertTapped(gBears, true); assertTapped(hGiant, true); assertLife(playerB, 14); // 1 + 2 + 3 damage assertCounterCount(playerB, CounterType.ENERGY, 1); } - + /* * Reported bug: [[Gonti's Machinations]] currently triggers and gain 1 energy whenever you lose life instead of only the first life loss of each turn. - * See issue #3499 (test is currently failing due to bug in code) - */ + * See issue #3499 for context + */ @Test public void machinations_NonCombatDamageThreeTimes_OneTrigger() { - + setStrictChooseMode(true); + String bolt = "Lightning Bolt"; // {R} deal 3 - + addCard(Zone.BATTLEFIELD, playerB, gMachinations); addCard(Zone.HAND, playerA, bolt, 3); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); - + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); - + setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - + assertTappedCount("Mountain", true, 3); assertGraveyardCount(playerA, bolt, 3); assertLife(playerB, 11); // 3 x 3 damage diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/ExquisiteBloodTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/ExquisiteBloodTest.java index 9f653ae1d6dd..036ee0f44418 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/ExquisiteBloodTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/ExquisiteBloodTest.java @@ -12,7 +12,9 @@ public class ExquisiteBloodTest extends CardTestPlayerBase { @Test - public void BasicCardTest() { + public void basicCardTest() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); @@ -48,6 +50,8 @@ public void BasicCardTest() { */ @Test public void triggerCascadeTest() { + setStrictChooseMode(true); + // +2: You gain 2 life. Put two +1/+1 counters on up to one target creature. // −3: Exile target creature. Its controller gains 2 life. // −10: Creatures you control gain flying and double strike until end of turn. @@ -90,6 +94,8 @@ public void triggerCascadeTest() { */ @Test public void triggerCascadeAjaniSecondAbilityTest() { + setStrictChooseMode(true); + // +2: You gain 2 life. Put two +1/+1 counters on up to one target creature. // −3: Exile target creature. Its controller gains 2 life. // −10: Creatures you control gain flying and double strike until end of turn. diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/snc/VampireScrivenerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/snc/VampireScrivenerTest.java new file mode 100644 index 000000000000..ec5b050ce86a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/snc/VampireScrivenerTest.java @@ -0,0 +1,79 @@ +package org.mage.test.cards.single.snc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class VampireScrivenerTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.v.VampireScrivener Vampire Scrivener} {4}{B} + * Creature — Vampire Warlock + * Flying + * Whenever you gain life during your turn, put a +1/+1 counter on Vampire Scrivener. + * Whenever you lose life during your turn, put a +1/+1 counter on Vampire Scrivener. + * 2/2 + */ + private static final String scrivener = "Vampire Scrivener"; + + @Test + public void test_LoseLife_Twice() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, scrivener, 1); + addCard(Zone.BATTLEFIELD, playerA, "Battlefield Forge"); // painland + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}"); // cause 1 trigger + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); // cause 1 trigger + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 3 - 1); + assertCounterCount(playerA, scrivener, CounterType.P1P1, 2); + } + + @Test + public void test_RakdosCharm() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, scrivener, 1); + addCard(Zone.BATTLEFIELD, playerA, "Kobolds of Kher Keep", 3); + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 2); + addCard(Zone.HAND, playerA, "Rakdos Charm"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rakdos Charm"); + setModeChoice(playerA, "3"); // Choose third mode + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 4); + assertCounterCount(playerA, scrivener, CounterType.P1P1, 1); + } + + @Test + public void test_RakdosCharm_NotYourTurn() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, scrivener, 1); + addCard(Zone.BATTLEFIELD, playerA, "Kobolds of Kher Keep", 3); + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 2); + addCard(Zone.HAND, playerA, "Rakdos Charm"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Rakdos Charm"); + setModeChoice(playerA, "3"); // Choose third mode + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 4); + assertCounterCount(playerA, scrivener, CounterType.P1P1, 0); // No trigger, as not your turn. + } +} From 7d12394d83b9d6046456c936afbf7ee1912cc8a0 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:28:14 +0200 Subject: [PATCH 16/16] fix & test Oath of Lim-Dul --- Mage.Sets/src/mage/cards/o/OathOfLimDul.java | 41 ++----- .../cards/single/ice/OathOfLimDulTest.java | 116 ++++++++++++++++++ 2 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/ice/OathOfLimDulTest.java diff --git a/Mage.Sets/src/mage/cards/o/OathOfLimDul.java b/Mage.Sets/src/mage/cards/o/OathOfLimDul.java index 08bac36480d1..743f30f42b6b 100644 --- a/Mage.Sets/src/mage/cards/o/OathOfLimDul.java +++ b/Mage.Sets/src/mage/cards/o/OathOfLimDul.java @@ -3,26 +3,24 @@ import mage.abilities.Ability; import mage.abilities.common.LoseLifeTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.Cost; -import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.common.DiscardCardCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.SavedLifeLossValue; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DoUnlessControllerPaysEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.SacrificeControllerEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.TargetController; import mage.constants.Zone; -import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.filter.common.FilterControlledPermanent; import mage.filter.predicate.mageobject.AnotherPredicate; import mage.game.Game; -import mage.game.permanent.Permanent; import mage.players.Player; -import mage.target.common.TargetCardInHand; -import mage.target.common.TargetControlledPermanent; import java.util.UUID; @@ -53,7 +51,7 @@ public OathOfLimDul copy() { class OathOfLimDulEffect extends OneShotEffect { - private static final FilterControlledPermanent filter = new FilterControlledPermanent("controlled permanent other than Oath of Lim-Dul to sacrifice"); + private static final FilterControlledPermanent filter = new FilterControlledPermanent("controlled permanent other than {this} to sacrifice"); static { filter.add(AnotherPredicate.instance); @@ -75,29 +73,14 @@ public boolean apply(Game game, Ability source) { if (amountDamage <= 0 || controller == null) { return false; } - boolean sacrificeDone = false; - int numberSacrificed = 0; - int numberToDiscard = 0; - int numberOfControlledPermanents = 0; - TargetControlledPermanent target = new TargetControlledPermanent(0, numberOfControlledPermanents, filter, true); - target.withNotTarget(true); - if (controller.choose(Outcome.Detriment, target, source, game)) { - for (UUID targetPermanentId : target.getTargets()) { - Permanent permanent = game.getPermanent(targetPermanentId); - if (permanent != null - && permanent.sacrifice(source, game)) { - numberSacrificed += 1; - sacrificeDone = true; - } - } + boolean didSomething = false; + for (int i = 0; i < amountDamage; ++i) { + didSomething |= new DoUnlessControllerPaysEffect( + new SacrificeControllerEffect(StaticFilters.FILTER_CONTROLLED_ANOTHER_PERMANENT, 1, ""), + new DiscardCardCost() + ).apply(game, source); } - numberToDiscard = amountDamage - numberSacrificed; - Cost cost = new DiscardTargetCost(new TargetCardInHand(numberToDiscard, new FilterCard("card(s) in your hand to discard"))); - if (numberToDiscard > 0 - && cost.canPay(source, source, controller.getId(), game)) { - return cost.pay(source, game, source, controller.getId(), true); // discard cost paid simultaneously - } - return sacrificeDone; + return didSomething; } @Override diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ice/OathOfLimDulTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ice/OathOfLimDulTest.java new file mode 100644 index 000000000000..f71233c32be1 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ice/OathOfLimDulTest.java @@ -0,0 +1,116 @@ +package org.mage.test.cards.single.ice; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class OathOfLimDulTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.o.OathOfLimDul Oath of Lim-Dûl} {3}{B} + * Enchantment + * Whenever you lose life, for each 1 life you lost, sacrifice a permanent other than Oath of Lim-Dûl unless you discard a card. (Damage dealt to you causes you to lose life.) + * {B}{B}: Draw a card. + */ + private static final String oath = "Oath of Lim-Dul"; + + @Test + public void test_3Sacrifice() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, oath, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.HAND, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); + setChoice(playerA, false); // No to discard on first instance. + setChoice(playerA, "Mountain"); // sacrifice Mountain + setChoice(playerA, false); // No to discard on second instance. + setChoice(playerA, "Mountain"); // sacrifice Mountain + setChoice(playerA, false); // No to discard on third instance. + setChoice(playerA, "Mountain"); // sacrifice Mountain + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 3); + assertGraveyardCount(playerA, "Mountain", 3); + } + + @Test + public void test_3Discard() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, oath, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.HAND, playerA, "Swamp", 5); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); + setChoice(playerA, true); // Yes to discard on first instance. + setChoice(playerA, "Swamp"); // sacrifice Swamp + setChoice(playerA, true); // Yes to discard on second instance. + setChoice(playerA, "Swamp"); // sacrifice Swamp + setChoice(playerA, true); // Yes to discard on third instance. + setChoice(playerA, "Swamp"); // sacrifice Swamp + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 3); + assertGraveyardCount(playerA, "Swamp", 3); + } + + @Test + public void test_1Sacrifice1Discard_NoOther() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, oath, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Swamp", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); + setChoice(playerA, true); // Yes to discard on first instance. + setChoice(playerA, "Swamp"); // discard Swamp + // No more possibility to Discard + setChoice(playerA, "Mountain"); // sacrifice Mountain + // No more things to Sacrifice + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 3); + assertGraveyardCount(playerA, "Mountain", 1); + assertGraveyardCount(playerA, "Swamp", 1); + } + + @Test + public void test_AllSacrificeNoDiscard_KeepCardInHand() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, oath, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.HAND, playerA, "Swamp", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerA); + setChoice(playerA, false); // No to discard on first instance. + setChoice(playerA, "Mountain"); // sacrifice Mountain + setChoice(playerA, false); // No to discard on second instance. + setChoice(playerA, false); // No to discard on third instance. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, oath, 1); + assertGraveyardCount(playerA, "Mountain", 1); + assertHandCount(playerA, "Swamp", 1); + } +}