From ec0166bf7f3d909e9a74e4ec1ff64ab0018c2a1c Mon Sep 17 00:00:00 2001 From: ssk97 Date: Sun, 10 Dec 2023 18:30:20 -0800 Subject: [PATCH] [LCI] Implement Fabrication Foundry; Unstable Glyphbridge (#11521) * First pass at Fabrication Foundry * misc cleanup * First pass at Unstable Glyphbridge/Sandswirl Wanderglyph * Set up backside correctly * Fix Unstable Bridge cast condition * Improve Fabrication Foundry to use Crew-like hint * Fix equality check Tested, though only manually. Alongside the direct card implementations, I also: - Fix Oaken Siren's mana to not be able to pay for soft counterspells from artifact sources - Change PlayersAttackedThisTurnWatcher to follow the rules better and to have a separate section for planeswalkers' controllers who were attacked (needed for Sandswirl Wanderglyph) - Removed an unused constructor in ExileTargetCost (though I didn't end up using that cost in the end) - minor cleanup in CrewAbility --- .../src/mage/cards/f/FabricationFoundry.java | 179 ++++++++++++++++++ Mage.Sets/src/mage/cards/o/OakenSiren.java | 2 +- .../mage/cards/s/SandswirlWanderglyph.java | 123 ++++++++++++ .../src/mage/cards/u/UnstableGlyphbridge.java | 105 ++++++++++ .../src/mage/sets/TheLostCavernsOfIxalan.java | 3 + .../costs/common/ExileTargetCost.java | 4 - .../mage/abilities/keyword/CrewAbility.java | 4 +- .../PlayersAttackedThisTurnWatcher.java | 43 ++++- 8 files changed, 449 insertions(+), 14 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/f/FabricationFoundry.java create mode 100644 Mage.Sets/src/mage/cards/s/SandswirlWanderglyph.java create mode 100644 Mage.Sets/src/mage/cards/u/UnstableGlyphbridge.java diff --git a/Mage.Sets/src/mage/cards/f/FabricationFoundry.java b/Mage.Sets/src/mage/cards/f/FabricationFoundry.java new file mode 100644 index 00000000000..51048566128 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FabricationFoundry.java @@ -0,0 +1,179 @@ +package mage.cards.f; + +import mage.ConditionalMana; +import mage.MageObject; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.Cost; +import mage.abilities.costs.CostImpl; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.hint.HintUtils; +import mage.abilities.mana.ConditionalColoredManaAbility; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledArtifactPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.Target; +import mage.target.TargetPermanent; +import mage.target.common.TargetCardInYourGraveyard; + +import java.awt.*; +import java.util.Objects; +import java.util.UUID; + +/** + * + * @author notgreat + */ +public final class FabricationFoundry extends CardImpl { + + public FabricationFoundry(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{W}"); + + + // {T}: Add {W}. Spend this mana only to cast an artifact spell or activate an ability of an artifact source. + this.addAbility(new ConditionalColoredManaAbility(Mana.WhiteMana(1), new ArtifactManaBuilder())); + + // {2}{W}, {T}, Exile one or more other artifacts you control with total mana value X: Return target artifact card with mana value X or less from your graveyard to the battlefield. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility(new ReturnFromGraveyardToBattlefieldTargetEffect(), new ManaCostsImpl<>("{2}{W}")); + ability.addCost(new TapSourceCost()); + ability.addCost(new ExileTargetsTotalManaValueCost()); + Target target = new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_ARTIFACT_FROM_YOUR_GRAVEYARD); + target.setTargetName("artifact card with mana value X or less from your graveyard"); + ability.addTarget(target); + this.addAbility(ability); + } + + private FabricationFoundry(final FabricationFoundry card) { + super(card); + } + + @Override + public FabricationFoundry copy() { + return new FabricationFoundry(this); + } +} + +//Mana based on Oaken Siren +class ArtifactManaBuilder extends ConditionalManaBuilder { + + @Override + public ConditionalMana build(Object... options) { + return new ArtifactConditionalMana(this.mana); + } + + @Override + public String getRule() { + return "Spend this mana only to cast an artifact spell or activate an ability of an artifact source"; + } +} + +class ArtifactConditionalMana extends ConditionalMana { + + ArtifactConditionalMana(Mana mana) { + super(mana); + addCondition(ArtifactSpellOrActivatedAbilityCondition.instance); + } +} + +enum ArtifactSpellOrActivatedAbilityCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + MageObject object = game.getObject(source); + return object != null && object.isArtifact(game) && !source.isActivated(); + } +} +//Cost based on Kozilek, The Great Distortion and CrewAbility +class ExileTargetsTotalManaValueCost extends CostImpl { + private static final FilterPermanent filter = new FilterControlledArtifactPermanent("one or more other artifacts you control with total mana value X"); + + static { + filter.add(AnotherPredicate.instance); + } + public ExileTargetsTotalManaValueCost() { + this.text = "Exile one or more other artifacts you control with total mana value X"; + } + + public ExileTargetsTotalManaValueCost(ExileTargetsTotalManaValueCost cost) { + super(cost); + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + Card abilityTarget = game.getCard(ability.getFirstTarget()); + if (abilityTarget == null) { + return paid; + } + Player player = game.getPlayer(ability.getControllerId()); + if (player == null) { + return paid; + } + int minX = abilityTarget.getManaValue(); + int sum = 0; + Target target = new TargetPermanent(1, Integer.MAX_VALUE, filter, true){ + @Override + public String getMessage() { + // shows selected mana value + int selectedPower = this.targets.keySet().stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .mapToInt(Permanent::getManaValue) + .sum(); + String extraInfo = "(selected mana value " + selectedPower + " of " + minX + ")"; + if (selectedPower >= minX) { + extraInfo = HintUtils.prepareText(extraInfo, Color.GREEN); + } + return super.getMessage() + " " + extraInfo; + } + }; + if (!target.choose(Outcome.Exile, controllerId, source.getSourceId(), source, game)){ + return paid; + } + Cards cards = new CardsImpl(); + cards.addAll(target.getTargets()); + for (UUID targetId : target.getTargets()) { + Permanent permanent = game.getPermanent(targetId); + if (permanent != null) { + sum += permanent.getManaValue(); + } + } + paid = (sum >= minX); + if (paid) { + player.moveCardsToExile(cards.getCards(game), source, game, false,null,null); + } + return paid; + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + int totalExileMV = 0; + boolean anyExileFound = false; + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, controllerId, source, game)){ + totalExileMV += permanent.getManaValue(); + anyExileFound = true; + } + int minTargetMV = Integer.MAX_VALUE; + for (Card card : game.getPlayer(controllerId).getGraveyard().getCards(StaticFilters.FILTER_CARD_ARTIFACT_FROM_YOUR_GRAVEYARD, game)){ + minTargetMV = Integer.min(minTargetMV, card.getManaValue()); + } + return anyExileFound && totalExileMV >= minTargetMV; + } + + @Override + public ExileTargetsTotalManaValueCost copy() { + return new ExileTargetsTotalManaValueCost(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/o/OakenSiren.java b/Mage.Sets/src/mage/cards/o/OakenSiren.java index df7ffbc6cfb..201cd9d2a8f 100644 --- a/Mage.Sets/src/mage/cards/o/OakenSiren.java +++ b/Mage.Sets/src/mage/cards/o/OakenSiren.java @@ -78,6 +78,6 @@ enum OakenSirenCondition implements Condition { @Override public boolean apply(Game game, Ability source) { MageObject object = game.getObject(source); - return object != null && object.isArtifact(game); + return object != null && object.isArtifact(game) && !source.isActivated(); } } diff --git a/Mage.Sets/src/mage/cards/s/SandswirlWanderglyph.java b/Mage.Sets/src/mage/cards/s/SandswirlWanderglyph.java new file mode 100644 index 00000000000..2f6e8951762 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SandswirlWanderglyph.java @@ -0,0 +1,123 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.SpellCastOpponentTriggeredAbility; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterSpell; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.watchers.common.PlayersAttackedThisTurnWatcher; + +import java.util.UUID; + +/** + * + * @author notgreat + */ +public final class SandswirlWanderglyph extends CardImpl { + private static final FilterSpell filter = new FilterSpell("a spell during their turn"); + + static { + filter.add(TargetController.ACTIVE.getControllerPredicate()); + } + public SandswirlWanderglyph(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, ""); + + this.subtype.add(SubType.GOLEM); + this.power = new MageInt(5); + this.toughness = new MageInt(3); + this.nightCard = true; + this.color.setWhite(true); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever an opponent casts a spell during their turn, they can't attack you or planeswalkers you control this turn. + this.addAbility(new SpellCastOpponentTriggeredAbility(Zone.BATTLEFIELD, + new CantAttackSourcePlayerOrPlaneswalkerThisTurnEffect(), filter, false, SetTargetPointer.PLAYER)); + + // Each opponent who attacked you or a planeswalker you control this turn can't cast spells. + this.addAbility(new SimpleStaticAbility(new SandswirlWanderglyphCantCastEffect()), new PlayersAttackedThisTurnWatcher()); + } + + private SandswirlWanderglyph(final SandswirlWanderglyph card) { + super(card); + } + + @Override + public SandswirlWanderglyph copy() { + return new SandswirlWanderglyph(this); + } +} + +class SandswirlWanderglyphCantCastEffect extends ContinuousRuleModifyingEffectImpl { + + public SandswirlWanderglyphCantCastEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "Each opponent who attacked you or a planeswalker you control this turn can't cast spells"; + } + + private SandswirlWanderglyphCantCastEffect(final SandswirlWanderglyphCantCastEffect effect) { + super(effect); + } + + @Override + public SandswirlWanderglyphCantCastEffect copy() { + return new SandswirlWanderglyphCantCastEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CAST_SPELL; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (game.isActivePlayer(event.getPlayerId()) && game.getOpponents(source.getControllerId()).contains(event.getPlayerId())) { + PlayersAttackedThisTurnWatcher watcher = game.getState().getWatcher(PlayersAttackedThisTurnWatcher.class); + return watcher != null && watcher.hasPlayerAttackedPlayerOrControlledPlaneswalker(event.getPlayerId(), source.getControllerId()); + } + return false; + } + +} +class CantAttackSourcePlayerOrPlaneswalkerThisTurnEffect extends RestrictionEffect { + CantAttackSourcePlayerOrPlaneswalkerThisTurnEffect() { + super(Duration.EndOfTurn); + staticText = "they can't attack you or planeswalkers you control this turn"; + } + + private CantAttackSourcePlayerOrPlaneswalkerThisTurnEffect(final CantAttackSourcePlayerOrPlaneswalkerThisTurnEffect effect) { + super(effect); + } + + @Override + public CantAttackSourcePlayerOrPlaneswalkerThisTurnEffect copy() { + return new CantAttackSourcePlayerOrPlaneswalkerThisTurnEffect(this); + } + + @Override + public boolean canAttack(Permanent attacker, UUID defenderId, Ability source, Game game, boolean canUseChooseDialogs) { + if (game.getPlayer(defenderId) != null){ + return !(source.getControllerId().equals(defenderId)); + } + Permanent defender = game.getPermanent(defenderId); + if (defender != null && defender.isPlaneswalker()){ + return !(source.getControllerId().equals(defender.getControllerId())); + } + return true; + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + return permanent.getControllerId().equals(getTargetPointer().getFirst(game, source)); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/u/UnstableGlyphbridge.java b/Mage.Sets/src/mage/cards/u/UnstableGlyphbridge.java new file mode 100644 index 00000000000..0af083ac8d6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/u/UnstableGlyphbridge.java @@ -0,0 +1,105 @@ +package mage.cards.u; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.CastFromEverywhereSourceCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.CraftAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.filter.predicate.permanent.ControllerIdPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * + * @author notgreat + */ +public final class UnstableGlyphbridge extends CardImpl { + + public UnstableGlyphbridge(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}{W}{W}"); + this.secondSideCardClazz = mage.cards.s.SandswirlWanderglyph.class; + + // When Unstable Glyphbridge enters the battlefield, if you cast it, for each player, choose a creature with power 2 or less that player controls. Then destroy all creatures except creatures chosen this way. + this.addAbility(new ConditionalInterveningIfTriggeredAbility(new EntersBattlefieldTriggeredAbility( + new UnstableGlyphbridgeEffect()), CastFromEverywhereSourceCondition.instance, + "When {this} enters the battlefield, if you cast it, " + + "for each player, choose a creature with power 2 or less that player controls. " + + "Then destroy all creatures except creatures chosen this way." + )); + + // Craft with artifact {3}{W}{W} + this.addAbility(new CraftAbility("{3}{W}{W}")); + } + + private UnstableGlyphbridge(final UnstableGlyphbridge card) { + super(card); + } + + @Override + public UnstableGlyphbridge copy() { + return new UnstableGlyphbridge(this); + } +} + + +class UnstableGlyphbridgeEffect extends OneShotEffect { + UnstableGlyphbridgeEffect() { + super(Outcome.Benefit); + staticText = "for each player, choose a creature with power 2 or less that player controls. " + + "Then destroy all creatures except creatures chosen this way."; + } + + private UnstableGlyphbridgeEffect(final UnstableGlyphbridgeEffect effect) { + super(effect); + } + + @Override + public UnstableGlyphbridgeEffect copy() { + return new UnstableGlyphbridgeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Cards cards = new CardsImpl(); + for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with power 2 or less"); + filter.add(new PowerPredicate(ComparisonType.OR_LESS,2)); + filter.add(new ControllerIdPredicate(playerId)); + TargetCreaturePermanent target = new TargetCreaturePermanent(filter); + target.withNotTarget(true); + target.withChooseHint(player.getName() + " controls"); + + controller.choose(Outcome.PutCreatureInPlay, target, source, game); + cards.add(target.getFirstTarget()); + } + for (Permanent permanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES, source.getControllerId(), source, game)) { + if (!cards.contains(permanent.getId())) { + permanent.destroy(source, game); + } + } + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java index e6f7a0bd76f..f60326df175 100644 --- a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java +++ b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java @@ -134,6 +134,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet { cards.add(new SetCardInfo("Envoy of Okinec Ahau", 11, Rarity.COMMON, mage.cards.e.EnvoyOfOkinecAhau.class)); cards.add(new SetCardInfo("Etali's Favor", 149, Rarity.COMMON, mage.cards.e.EtalisFavor.class)); cards.add(new SetCardInfo("Explorer's Cache", 184, Rarity.UNCOMMON, mage.cards.e.ExplorersCache.class)); + cards.add(new SetCardInfo("Fabrication Foundry", 12, Rarity.RARE, mage.cards.f.FabricationFoundry.class)); cards.add(new SetCardInfo("Family Reunion", 13, Rarity.COMMON, mage.cards.f.FamilyReunion.class)); cards.add(new SetCardInfo("Fanatical Offering", 105, Rarity.COMMON, mage.cards.f.FanaticalOffering.class)); cards.add(new SetCardInfo("Forest", 291, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_UST_VARIOUS)); @@ -273,6 +274,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet { cards.add(new SetCardInfo("Sage of Days", 73, Rarity.COMMON, mage.cards.s.SageOfDays.class)); cards.add(new SetCardInfo("Saheeli's Lattice", 164, Rarity.UNCOMMON, mage.cards.s.SaheelisLattice.class)); cards.add(new SetCardInfo("Saheeli, the Sun's Brilliance", 239, Rarity.MYTHIC, mage.cards.s.SaheeliTheSunsBrilliance.class)); + cards.add(new SetCardInfo("Sandswirl Wanderglyph", 41, Rarity.RARE, mage.cards.s.SandswirlWanderglyph.class)); cards.add(new SetCardInfo("Sanguine Evangelist", 34, Rarity.RARE, mage.cards.s.SanguineEvangelist.class)); cards.add(new SetCardInfo("Scampering Surveyor", 260, Rarity.UNCOMMON, mage.cards.s.ScamperingSurveyor.class)); cards.add(new SetCardInfo("Screaming Phantom", 118, Rarity.COMMON, mage.cards.s.ScreamingPhantom.class)); @@ -347,6 +349,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet { cards.add(new SetCardInfo("Twists and Turns", 217, Rarity.UNCOMMON, mage.cards.t.TwistsAndTurns.class)); cards.add(new SetCardInfo("Uchbenbak, the Great Mistake", 242, Rarity.UNCOMMON, mage.cards.u.UchbenbakTheGreatMistake.class)); cards.add(new SetCardInfo("Unlucky Drop", 82, Rarity.COMMON, mage.cards.u.UnluckyDrop.class)); + cards.add(new SetCardInfo("Unstable Glyphbridge", 41, Rarity.RARE, mage.cards.u.UnstableGlyphbridge.class)); cards.add(new SetCardInfo("Vanguard of the Rose", 42, Rarity.UNCOMMON, mage.cards.v.VanguardOfTheRose.class)); cards.add(new SetCardInfo("Visage of Dread", 129, Rarity.UNCOMMON, mage.cards.v.VisageOfDread.class)); cards.add(new SetCardInfo("Vito's Inquisitor", 130, Rarity.COMMON, mage.cards.v.VitosInquisitor.class)); diff --git a/Mage/src/main/java/mage/abilities/costs/common/ExileTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/ExileTargetCost.java index eb87a780d5c..0c194f32ed1 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/ExileTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/ExileTargetCost.java @@ -31,10 +31,6 @@ public class ExileTargetCost extends CostImpl { this.text = "exile " + target.getDescription(); } - public ExileTargetCost(TargetControlledPermanent target, boolean noText) { - this.addTarget(target); - } - public ExileTargetCost(ExileTargetCost cost) { super(cost); for (Permanent permanent : cost.permanents) { diff --git a/Mage/src/main/java/mage/abilities/keyword/CrewAbility.java b/Mage/src/main/java/mage/abilities/keyword/CrewAbility.java index 205ceaeed30..db154867f9a 100644 --- a/Mage/src/main/java/mage/abilities/keyword/CrewAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/CrewAbility.java @@ -152,8 +152,8 @@ class CrewCost extends CostImpl { @Override public String getMessage() { // shows selected power - int selectedPower = this.targets.entrySet().stream() - .map(entry -> (game.getPermanent(entry.getKey()))) + int selectedPower = this.targets.keySet().stream() + .map(game::getPermanent) .filter(Objects::nonNull) .mapToInt(p -> (getCrewPower(p, game))) .sum(); diff --git a/Mage/src/main/java/mage/watchers/common/PlayersAttackedThisTurnWatcher.java b/Mage/src/main/java/mage/watchers/common/PlayersAttackedThisTurnWatcher.java index 5314e684c74..8e8a8fc316b 100644 --- a/Mage/src/main/java/mage/watchers/common/PlayersAttackedThisTurnWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/PlayersAttackedThisTurnWatcher.java @@ -3,6 +3,7 @@ package mage.watchers.common; import mage.constants.WatcherScope; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; import mage.players.PlayerList; import mage.watchers.Watcher; @@ -18,6 +19,7 @@ public class PlayersAttackedThisTurnWatcher extends Watcher { // how many players or opponents each player attacked this turn private final Map playersAttackedThisTurn = new HashMap<>(); private final Map opponentsAttackedThisTurn = new HashMap<>(); + private final Map planeswalkerControllerAttackedThisTurn = new HashMap<>(); public PlayersAttackedThisTurnWatcher() { super(WatcherScope.GAME); @@ -25,10 +27,10 @@ public class PlayersAttackedThisTurnWatcher extends Watcher { @Override public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.BEGINNING_PHASE_PRE) { - playersAttackedThisTurn.clear(); - opponentsAttackedThisTurn.clear(); - } + /* + Faramir, Prince of Ithilien: + Attacking a planeswalker you control or battle you're protecting doesn't count as attacking you. (2023-06-16) + */ if (event.getType() == GameEvent.EventType.ATTACKER_DECLARED) { @@ -37,8 +39,8 @@ public class PlayersAttackedThisTurnWatcher extends Watcher { if (playersAttacked == null) { playersAttacked = new PlayerList(); } - UUID playerDefender = game.getCombat().getDefendingPlayerId(event.getSourceId(), game); - if (playerDefender != null + UUID playerDefender = event.getTargetId(); + if (playerDefender != null && game.getPlayer(playerDefender) != null && !playersAttacked.contains(playerDefender)) { playersAttacked.add(playerDefender); } @@ -49,13 +51,26 @@ public class PlayersAttackedThisTurnWatcher extends Watcher { if (opponentsAttacked == null) { opponentsAttacked = new PlayerList(); } - UUID opponentDefender = game.getCombat().getDefendingPlayerId(event.getSourceId(), game); + UUID opponentDefender = event.getTargetId(); if (opponentDefender != null && game.getOpponents(event.getPlayerId()).contains(opponentDefender) && !opponentsAttacked.contains(opponentDefender)) { opponentsAttacked.add(opponentDefender); } opponentsAttackedThisTurn.putIfAbsent(event.getPlayerId(), opponentsAttacked); + + //Planeswalker controllers + PlayerList controllersAttacked = planeswalkerControllerAttackedThisTurn.get(event.getPlayerId()); + if (controllersAttacked == null) { + controllersAttacked = new PlayerList(); + } + Permanent permanent = game.getPermanent(event.getTargetId()); + UUID controllingDefender = game.getCombat().getDefendingPlayerId(event.getSourceId(), game); + if (controllingDefender != null && permanent != null && permanent.isPlaneswalker(game) + && !controllersAttacked.contains(controllingDefender)) { + controllersAttacked.add(controllingDefender); + } + planeswalkerControllerAttackedThisTurn.putIfAbsent(event.getPlayerId(), controllersAttacked); } } @@ -63,6 +78,12 @@ public class PlayersAttackedThisTurnWatcher extends Watcher { PlayerList defendersList = playersAttackedThisTurn.getOrDefault(attacker, null); return defendersList != null && defendersList.contains(defender); } + public boolean hasPlayerAttackedPlayerOrControlledPlaneswalker(UUID attacker, UUID defender){ + PlayerList defendersList = playersAttackedThisTurn.getOrDefault(attacker, null); + PlayerList planeswalkerControllersList = planeswalkerControllerAttackedThisTurn.getOrDefault(attacker, null); + return (defendersList != null && defendersList.contains(defender)) || + (planeswalkerControllersList != null && planeswalkerControllersList.contains(defender)); + } public int getAttackedPlayersCount(UUID playerID) { PlayerList defendersList = playersAttackedThisTurn.getOrDefault(playerID, null); @@ -79,4 +100,12 @@ public class PlayersAttackedThisTurnWatcher extends Watcher { } return 0; } + + @Override + public void reset() { + super.reset(); + playersAttackedThisTurn.clear(); + opponentsAttackedThisTurn.clear(); + planeswalkerControllerAttackedThisTurn.clear(); + } }