diff --git a/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java b/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java index 84db60a03cd..cf08f48cc2c 100644 --- a/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java +++ b/Mage.Sets/src/mage/cards/a/AkiriFearlessVoyager.java @@ -16,16 +16,16 @@ import mage.filter.FilterPermanent; import mage.filter.common.FilterEquipmentPermanent; import mage.filter.predicate.ObjectPlayer; import mage.filter.predicate.ObjectPlayerPredicate; -import mage.filter.predicate.permanent.EquippedPredicate; import mage.game.Game; +import mage.game.events.DefenderAttackedEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.TargetPermanent; import mage.target.targetpointer.FixedTarget; -import java.util.HashSet; -import java.util.Set; +import java.util.Collection; +import java.util.Objects; import java.util.UUID; /** @@ -63,14 +63,6 @@ public final class AkiriFearlessVoyager extends CardImpl { class AkiriFearlessVoyagerTriggeredAbility extends TriggeredAbilityImpl { - private static final FilterPermanent filter = new FilterPermanent(); - - static { - filter.add(EquippedPredicate.instance); - } - - private final Set attackedPlayerIds = new HashSet<>(); - AkiriFearlessVoyagerTriggeredAbility() { super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false); } @@ -86,28 +78,21 @@ class AkiriFearlessVoyagerTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ATTACKER_DECLARED - || event.getType() == GameEvent.EventType.DECLARE_ATTACKERS_STEP_POST; + return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED; } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.DECLARE_ATTACKERS_STEP_POST) { - attackedPlayerIds.clear(); - return false; - } - if (event.getType() == GameEvent.EventType.ATTACKER_DECLARED) { - Permanent creature = game.getPermanent(event.getSourceId()); - if (creature != null - && creature.isControlledBy(controllerId) - && filter.match(creature, game) - && game.getPlayer(event.getTargetId()) != null - && !attackedPlayerIds.contains(event.getTargetId())) { - attackedPlayerIds.add(event.getTargetId()); - return true; - } - } - return false; + return isControlledBy(event.getPlayerId()) + && game.getPlayer(event.getTargetId()) != null + && ((DefenderAttackedEvent) event) + .getAttackers(game) + .stream() + .map(Permanent::getAttachments) + .flatMap(Collection::stream) + .map(game::getPermanent) + .filter(Objects::nonNull) + .anyMatch(permanent -> permanent.hasSubtype(SubType.EQUIPMENT, game)); } @Override diff --git a/Mage.Sets/src/mage/cards/c/CombatCalligrapher.java b/Mage.Sets/src/mage/cards/c/CombatCalligrapher.java new file mode 100644 index 00000000000..98bbde793e0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CombatCalligrapher.java @@ -0,0 +1,125 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.common.CreateTokenTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.SilverquillToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CombatCalligrapher extends CardImpl { + + public CombatCalligrapher(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + + this.subtype.add(SubType.BIRD); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Inklings can't attack you or planeswalkers you control. + this.addAbility(new SimpleStaticAbility(new CombatCalligrapherEffect())); + + // Whenever a player attacks one of your opponents, that attacking player creates a tapped 2/1 white and black Inkling creature token with flying that's attacking that opponent. + this.addAbility(new CombatCalligrapherTriggeredAbility()); + } + + private CombatCalligrapher(final CombatCalligrapher card) { + super(card); + } + + @Override + public CombatCalligrapher copy() { + return new CombatCalligrapher(this); + } +} + +class CombatCalligrapherTriggeredAbility extends TriggeredAbilityImpl { + + CombatCalligrapherTriggeredAbility() { + super(Zone.BATTLEFIELD, new CreateTokenTargetEffect( + new SilverquillToken(), StaticValue.get(1), true, true + ), false); + } + + private CombatCalligrapherTriggeredAbility(final CombatCalligrapherTriggeredAbility ability) { + super(ability); + } + + @Override + public CombatCalligrapherTriggeredAbility copy() { + return new CombatCalligrapherTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!game.getOpponents(getControllerId()).contains(event.getTargetId())) { + return false; + } + getEffects().setValue("playerToAttack", event.getPlayerId()); + return true; + } + + @Override + public String getRule() { + return "Whenever a player attacks one of your opponents, that attacking player creates " + + "a tapped 2/1 white and black Inkling creature token with flying that's attacking that opponent."; + } +} + +class CombatCalligrapherEffect extends RestrictionEffect { + + public CombatCalligrapherEffect() { + super(Duration.WhileOnBattlefield); + this.staticText = "Inklings can't attack you or planeswalkers you control"; + } + + public CombatCalligrapherEffect(final CombatCalligrapherEffect effect) { + super(effect); + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + return permanent.hasSubtype(SubType.INKLING, game); + } + + @Override + public boolean canAttack(Permanent attacker, UUID defenderId, Ability source, Game game, boolean canUseChooseDialogs) { + if (source.isControlledBy(defenderId)) { + return false; + } + Permanent planeswalker = game.getPermanent(defenderId); + return planeswalker == null || !planeswalker.isControlledBy(source.getControllerId()); + } + + + @Override + public CombatCalligrapherEffect copy() { + return new CombatCalligrapherEffect(this); + } +} diff --git a/Mage.Sets/src/mage/sets/Commander2021Edition.java b/Mage.Sets/src/mage/sets/Commander2021Edition.java index bf1867d8ed4..764f06439b8 100644 --- a/Mage.Sets/src/mage/sets/Commander2021Edition.java +++ b/Mage.Sets/src/mage/sets/Commander2021Edition.java @@ -69,6 +69,7 @@ public final class Commander2021Edition extends ExpansionSet { cards.add(new SetCardInfo("Citadel Siege", 85, Rarity.RARE, mage.cards.c.CitadelSiege.class)); cards.add(new SetCardInfo("Cleansing Nova", 86, Rarity.RARE, mage.cards.c.CleansingNova.class)); cards.add(new SetCardInfo("Coiling Oracle", 212, Rarity.COMMON, mage.cards.c.CoilingOracle.class)); + cards.add(new SetCardInfo("Combat Calligrapher", 14, Rarity.RARE, mage.cards.c.CombatCalligrapher.class)); cards.add(new SetCardInfo("Combustible Gearhulk", 163, Rarity.MYTHIC, mage.cards.c.CombustibleGearhulk.class)); cards.add(new SetCardInfo("Command Tower", 284, Rarity.COMMON, mage.cards.c.CommandTower.class)); cards.add(new SetCardInfo("Commander's Insight", 23, Rarity.RARE, mage.cards.c.CommandersInsight.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenTargetEffect.java index ce3992cb417..49384ac7d2c 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenTargetEffect.java @@ -10,6 +10,8 @@ import mage.game.Game; import mage.game.permanent.token.Token; import mage.util.CardUtil; +import java.util.UUID; + /** * @author Loki */ @@ -57,7 +59,7 @@ public class CreateTokenTargetEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { int value = amount.calculate(game, source, this); if (value > 0) { - return token.putOntoBattlefield(value, game, source, targetPointer.getFirst(game, source), tapped, attacking); + return token.putOntoBattlefield(value, game, source, targetPointer.getFirst(game, source), tapped, attacking, (UUID) getValue("playerToAttack")); } return true; } diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index 3abe7b08d9a..dc0abd48e7a 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -1,8 +1,7 @@ package mage.game.combat; -import java.io.Serializable; -import java.util.*; import mage.MageObject; +import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.effects.RequirementEffect; import mage.abilities.effects.RestrictionEffect; @@ -23,11 +22,7 @@ import mage.filter.predicate.mageobject.NamePredicate; import mage.filter.predicate.permanent.AttackingSameNotBandedPredicate; import mage.filter.predicate.permanent.PermanentIdPredicate; import mage.game.Game; -import mage.game.events.AttackerDeclaredEvent; -import mage.game.events.DeclareAttackerEvent; -import mage.game.events.DeclareBlockerEvent; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; +import mage.game.events.*; import mage.game.permanent.Permanent; import mage.players.Player; import mage.players.PlayerList; @@ -38,6 +33,9 @@ import mage.util.Copyable; import mage.util.trace.TraceUtil; import org.apache.log4j.Logger; +import java.io.Serializable; +import java.util.*; + /** * @author BetaSteward_at_googlemail.com */ @@ -269,6 +267,7 @@ public class Combat implements Serializable, Copyable { @SuppressWarnings("deprecation") public void resumeSelectAttackers(Game game) { + Map> morSetMap = new HashMap<>(); for (CombatGroup group : groups) { for (UUID attacker : group.getAttackers()) { if (attackersTappedByAttack.contains(attacker)) { @@ -282,10 +281,12 @@ public class Combat implements Serializable, Copyable { // This can only be used to modify the event, the attack can't be replaced here game.replaceEvent(new AttackerDeclaredEvent(group.defenderId, attacker, attackingPlayerId)); game.addSimultaneousEvent(new AttackerDeclaredEvent(group.defenderId, attacker, attackingPlayerId)); + morSetMap.computeIfAbsent(group.defenderId, x -> new HashSet<>()).add(new MageObjectReference(attacker, game)); } } attackersTappedByAttack.clear(); + DefenderAttackedEvent.makeAddEvents(morSetMap, attackingPlayerId, game); game.addSimultaneousEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_ATTACKERS, attackingPlayerId, attackingPlayerId)); if (!game.isSimulation()) { Player player = game.getPlayer(attackingPlayerId); @@ -352,8 +353,8 @@ public class Combat implements Serializable, Copyable { if (game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, attackingPlayerId, attackingPlayerId)) || (!canBand && !canBandWithOther) || !player.chooseUse(Outcome.Benefit, - (isBanded ? "Band " + attacker.getLogName() - + " with another " : "Form a band with " + attacker.getLogName() + " and an ") + (isBanded ? "Band " + attacker.getLogName() + + " with another " : "Form a band with " + attacker.getLogName() + " and an ") + "attacking creature?", null, game)) { break; } @@ -571,7 +572,7 @@ public class Combat implements Serializable, Copyable { * Handle the blocker selection process * * @param blockController player that controls how to block, if null the - * defender is the controller + * defender is the controller * @param game */ public void selectBlockers(Player blockController, Ability source, Game game) { @@ -1388,7 +1389,7 @@ public class Combat implements Serializable, Copyable { * @param playerId * @param game * @param solveBanding check whether also add creatures banded with - * attackerId + * attackerId */ public void addBlockingGroup(UUID blockerId, UUID attackerId, UUID playerId, Game game, boolean solveBanding) { Permanent blocker = game.getPermanent(blockerId); diff --git a/Mage/src/main/java/mage/game/events/DefenderAttackedEvent.java b/Mage/src/main/java/mage/game/events/DefenderAttackedEvent.java new file mode 100644 index 00000000000..2f215c36977 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/DefenderAttackedEvent.java @@ -0,0 +1,37 @@ +package mage.game.events; + +import mage.MageObjectReference; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author TheElk801 + */ +public class DefenderAttackedEvent extends GameEvent { + + private final Set morSet = new HashSet<>(); + + public DefenderAttackedEvent(UUID targetId, UUID playerId) { + super(EventType.DEFENDER_ATTACKED, targetId, null, playerId); + + } + + public static void makeAddEvents(Map> morMapSet, UUID attackingPlayerId, Game game) { + for (Map.Entry> entry : morMapSet.entrySet()) { + DefenderAttackedEvent event = new DefenderAttackedEvent(entry.getKey(), attackingPlayerId); + event.morSet.addAll(entry.getValue()); + game.addSimultaneousEvent(event); + } + } + + public List getAttackers(Game game) { + return morSet + .stream() + .map(mor -> mor.getPermanent(game)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 02dec5b72e1..ad1f6d481c8 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -275,6 +275,7 @@ public class GameEvent implements Serializable { amount not used for this event flag not used for this event */ + DEFENDER_ATTACKED, DECLARING_BLOCKERS, DECLARED_BLOCKERS, DECLARE_BLOCKER,