[C21] Implemented Combat Calligrapher

This commit is contained in:
Evan Kranzler 2021-04-27 09:29:27 -04:00
parent 1fad23b9fb
commit 0c3b294527
7 changed files with 193 additions and 41 deletions

View file

@ -16,16 +16,16 @@ import mage.filter.FilterPermanent;
import mage.filter.common.FilterEquipmentPermanent; import mage.filter.common.FilterEquipmentPermanent;
import mage.filter.predicate.ObjectPlayer; import mage.filter.predicate.ObjectPlayer;
import mage.filter.predicate.ObjectPlayerPredicate; import mage.filter.predicate.ObjectPlayerPredicate;
import mage.filter.predicate.permanent.EquippedPredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.DefenderAttackedEvent;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import java.util.HashSet; import java.util.Collection;
import java.util.Set; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
/** /**
@ -63,14 +63,6 @@ public final class AkiriFearlessVoyager extends CardImpl {
class AkiriFearlessVoyagerTriggeredAbility extends TriggeredAbilityImpl { class AkiriFearlessVoyagerTriggeredAbility extends TriggeredAbilityImpl {
private static final FilterPermanent filter = new FilterPermanent();
static {
filter.add(EquippedPredicate.instance);
}
private final Set<UUID> attackedPlayerIds = new HashSet<>();
AkiriFearlessVoyagerTriggeredAbility() { AkiriFearlessVoyagerTriggeredAbility() {
super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false); super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false);
} }
@ -86,28 +78,21 @@ class AkiriFearlessVoyagerTriggeredAbility extends TriggeredAbilityImpl {
@Override @Override
public boolean checkEventType(GameEvent event, Game game) { public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ATTACKER_DECLARED return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED;
|| event.getType() == GameEvent.EventType.DECLARE_ATTACKERS_STEP_POST;
} }
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.DECLARE_ATTACKERS_STEP_POST) { return isControlledBy(event.getPlayerId())
attackedPlayerIds.clear(); && game.getPlayer(event.getTargetId()) != null
return false; && ((DefenderAttackedEvent) event)
} .getAttackers(game)
if (event.getType() == GameEvent.EventType.ATTACKER_DECLARED) { .stream()
Permanent creature = game.getPermanent(event.getSourceId()); .map(Permanent::getAttachments)
if (creature != null .flatMap(Collection::stream)
&& creature.isControlledBy(controllerId) .map(game::getPermanent)
&& filter.match(creature, game) .filter(Objects::nonNull)
&& game.getPlayer(event.getTargetId()) != null .anyMatch(permanent -> permanent.hasSubtype(SubType.EQUIPMENT, game));
&& !attackedPlayerIds.contains(event.getTargetId())) {
attackedPlayerIds.add(event.getTargetId());
return true;
}
}
return false;
} }
@Override @Override

View file

@ -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);
}
}

View file

@ -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("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("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("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("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("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)); cards.add(new SetCardInfo("Commander's Insight", 23, Rarity.RARE, mage.cards.c.CommandersInsight.class));

View file

@ -10,6 +10,8 @@ import mage.game.Game;
import mage.game.permanent.token.Token; import mage.game.permanent.token.Token;
import mage.util.CardUtil; import mage.util.CardUtil;
import java.util.UUID;
/** /**
* @author Loki * @author Loki
*/ */
@ -57,7 +59,7 @@ public class CreateTokenTargetEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
int value = amount.calculate(game, source, this); int value = amount.calculate(game, source, this);
if (value > 0) { 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; return true;
} }

View file

@ -1,8 +1,7 @@
package mage.game.combat; package mage.game.combat;
import java.io.Serializable;
import java.util.*;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.RequirementEffect; import mage.abilities.effects.RequirementEffect;
import mage.abilities.effects.RestrictionEffect; 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.AttackingSameNotBandedPredicate;
import mage.filter.predicate.permanent.PermanentIdPredicate; import mage.filter.predicate.permanent.PermanentIdPredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.AttackerDeclaredEvent; import mage.game.events.*;
import mage.game.events.DeclareAttackerEvent;
import mage.game.events.DeclareBlockerEvent;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.players.PlayerList; import mage.players.PlayerList;
@ -38,6 +33,9 @@ import mage.util.Copyable;
import mage.util.trace.TraceUtil; import mage.util.trace.TraceUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -269,6 +267,7 @@ public class Combat implements Serializable, Copyable<Combat> {
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public void resumeSelectAttackers(Game game) { public void resumeSelectAttackers(Game game) {
Map<UUID, Set<MageObjectReference>> morSetMap = new HashMap<>();
for (CombatGroup group : groups) { for (CombatGroup group : groups) {
for (UUID attacker : group.getAttackers()) { for (UUID attacker : group.getAttackers()) {
if (attackersTappedByAttack.contains(attacker)) { if (attackersTappedByAttack.contains(attacker)) {
@ -282,10 +281,12 @@ public class Combat implements Serializable, Copyable<Combat> {
// This can only be used to modify the event, the attack can't be replaced here // 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.replaceEvent(new AttackerDeclaredEvent(group.defenderId, attacker, attackingPlayerId));
game.addSimultaneousEvent(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(); attackersTappedByAttack.clear();
DefenderAttackedEvent.makeAddEvents(morSetMap, attackingPlayerId, game);
game.addSimultaneousEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_ATTACKERS, attackingPlayerId, attackingPlayerId)); game.addSimultaneousEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_ATTACKERS, attackingPlayerId, attackingPlayerId));
if (!game.isSimulation()) { if (!game.isSimulation()) {
Player player = game.getPlayer(attackingPlayerId); Player player = game.getPlayer(attackingPlayerId);
@ -352,8 +353,8 @@ public class Combat implements Serializable, Copyable<Combat> {
if (game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, attackingPlayerId, attackingPlayerId)) if (game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, attackingPlayerId, attackingPlayerId))
|| (!canBand && !canBandWithOther) || (!canBand && !canBandWithOther)
|| !player.chooseUse(Outcome.Benefit, || !player.chooseUse(Outcome.Benefit,
(isBanded ? "Band " + attacker.getLogName() (isBanded ? "Band " + attacker.getLogName()
+ " with another " : "Form a band with " + attacker.getLogName() + " and an ") + " with another " : "Form a band with " + attacker.getLogName() + " and an ")
+ "attacking creature?", null, game)) { + "attacking creature?", null, game)) {
break; break;
} }
@ -571,7 +572,7 @@ public class Combat implements Serializable, Copyable<Combat> {
* Handle the blocker selection process * Handle the blocker selection process
* *
* @param blockController player that controls how to block, if null the * @param blockController player that controls how to block, if null the
* defender is the controller * defender is the controller
* @param game * @param game
*/ */
public void selectBlockers(Player blockController, Ability source, Game game) { public void selectBlockers(Player blockController, Ability source, Game game) {
@ -1388,7 +1389,7 @@ public class Combat implements Serializable, Copyable<Combat> {
* @param playerId * @param playerId
* @param game * @param game
* @param solveBanding check whether also add creatures banded with * @param solveBanding check whether also add creatures banded with
* attackerId * attackerId
*/ */
public void addBlockingGroup(UUID blockerId, UUID attackerId, UUID playerId, Game game, boolean solveBanding) { public void addBlockingGroup(UUID blockerId, UUID attackerId, UUID playerId, Game game, boolean solveBanding) {
Permanent blocker = game.getPermanent(blockerId); Permanent blocker = game.getPermanent(blockerId);

View file

@ -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<MageObjectReference> morSet = new HashSet<>();
public DefenderAttackedEvent(UUID targetId, UUID playerId) {
super(EventType.DEFENDER_ATTACKED, targetId, null, playerId);
}
public static void makeAddEvents(Map<UUID, Set<MageObjectReference>> morMapSet, UUID attackingPlayerId, Game game) {
for (Map.Entry<UUID, Set<MageObjectReference>> entry : morMapSet.entrySet()) {
DefenderAttackedEvent event = new DefenderAttackedEvent(entry.getKey(), attackingPlayerId);
event.morSet.addAll(entry.getValue());
game.addSimultaneousEvent(event);
}
}
public List<Permanent> getAttackers(Game game) {
return morSet
.stream()
.map(mor -> mor.getPermanent(game))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}

View file

@ -275,6 +275,7 @@ public class GameEvent implements Serializable {
amount not used for this event amount not used for this event
flag not used for this event flag not used for this event
*/ */
DEFENDER_ATTACKED,
DECLARING_BLOCKERS, DECLARING_BLOCKERS,
DECLARED_BLOCKERS, DECLARED_BLOCKERS,
DECLARE_BLOCKER, DECLARE_BLOCKER,