[LTR] Implement Witch-king of Angmar (#10563)

* Add card

* Add TapSourceEffect

* De-duplicate watcher logic

* Abstract and fix(?) logic

* Fix sacrifice targets

* Controller instead of Owner

* Add tests, fix, and refactor

* Throw if controller not supported

* Fix test (not supposed to start tapped)
This commit is contained in:
Bobby McCann 2023-07-06 00:22:21 +01:00 committed by GitHub
parent 008662be5e
commit c8564efbb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 202 additions and 28 deletions

View file

@ -0,0 +1,73 @@
package mage.cards.w;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.CombatDamageDealtToYouTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.DiscardCardCost;
import mage.abilities.effects.common.SacrificeOpponentsEffect;
import mage.abilities.effects.common.TapSourceEffect;
import mage.abilities.effects.common.continuous.GainAbilitySourceEffect;
import mage.abilities.effects.keyword.TheRingTemptsYouEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.IndestructibleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.other.DamagedPlayerThisTurnPredicate;
import java.util.UUID;
/**
*
* @author bobby-mccann
*/
public final class WitchKingOfAngmar extends CardImpl {
private static final FilterCreaturePermanent filter
= new FilterCreaturePermanent("creature that dealt combat damage to this ability's controller this turn");
static {
filter.add(new DamagedPlayerThisTurnPredicate(TargetController.SOURCE_CONTROLLER, true));
}
public WitchKingOfAngmar(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.WRAITH);
this.subtype.add(SubType.NOBLE);
this.power = new MageInt(5);
this.toughness = new MageInt(3);
// Flying
this.addAbility(FlyingAbility.getInstance());
// Whenever one or more creatures deal combat damage to you, each opponent sacrifices a creature that dealt combat damage to you this turn. The Ring tempts you.
{
Ability ability = new CombatDamageDealtToYouTriggeredAbility(new SacrificeOpponentsEffect(filter));
ability.addEffect(new TheRingTemptsYouEffect());
this.addAbility(ability);
}
// Discard a card: Witch-king of Angmar gains indestructible until end of turn. Tap it.
{
Ability ability = new SimpleActivatedAbility(
new GainAbilitySourceEffect(IndestructibleAbility.getInstance(), Duration.EndOfTurn),
new DiscardCardCost()
);
ability.addEffect(new TapSourceEffect().setText("tap it"));
this.addAbility(ability);
}
}
private WitchKingOfAngmar(final WitchKingOfAngmar card) {
super(card);
}
@Override
public WitchKingOfAngmar copy() {
return new WitchKingOfAngmar(this);
}
}

View file

@ -280,6 +280,7 @@ public final class TheLordOfTheRingsTalesOfMiddleEarth extends ExpansionSet {
cards.add(new SetCardInfo("Warbeast of Gorgoroth", 152, Rarity.COMMON, mage.cards.w.WarbeastOfGorgoroth.class)); cards.add(new SetCardInfo("Warbeast of Gorgoroth", 152, Rarity.COMMON, mage.cards.w.WarbeastOfGorgoroth.class));
cards.add(new SetCardInfo("Westfold Rider", 37, Rarity.COMMON, mage.cards.w.WestfoldRider.class)); cards.add(new SetCardInfo("Westfold Rider", 37, Rarity.COMMON, mage.cards.w.WestfoldRider.class));
cards.add(new SetCardInfo("Willow-Wind", 76, Rarity.COMMON, mage.cards.w.WillowWind.class)); cards.add(new SetCardInfo("Willow-Wind", 76, Rarity.COMMON, mage.cards.w.WillowWind.class));
cards.add(new SetCardInfo("Witch-king of Angmar", 114, Rarity.MYTHIC, mage.cards.w.WitchKingOfAngmar.class));
cards.add(new SetCardInfo("Witch-king, Bringer of Ruin", 293, Rarity.RARE, mage.cards.w.WitchKingBringerOfRuin.class)); cards.add(new SetCardInfo("Witch-king, Bringer of Ruin", 293, Rarity.RARE, mage.cards.w.WitchKingBringerOfRuin.class));
cards.add(new SetCardInfo("Wizard's Rockets", 252, Rarity.COMMON, mage.cards.w.WizardsRockets.class)); cards.add(new SetCardInfo("Wizard's Rockets", 252, Rarity.COMMON, mage.cards.w.WizardsRockets.class));
cards.add(new SetCardInfo("Wose Pathfinder", 190, Rarity.COMMON, mage.cards.w.WosePathfinder.class)); cards.add(new SetCardInfo("Wose Pathfinder", 190, Rarity.COMMON, mage.cards.w.WosePathfinder.class));

View file

@ -0,0 +1,62 @@
package org.mage.test.cards.single.ltr;
import mage.abilities.keyword.IndestructibleAbility;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class WitchKingOfAngmarTest extends CardTestPlayerBase {
static final String witchKing = "Witch-king of Angmar";
@Test
public void testSacrifice() {
setStrictChooseMode(true);
String watchwolf = "Watchwolf";
String swallower = "Simic Sky Swallower"; // Has shroud - should still be a choice
addCard(Zone.BATTLEFIELD, playerA, witchKing, 1, true);
addCard(Zone.HAND, playerA, "Swamp", 5);
addCard(Zone.BATTLEFIELD, playerB, watchwolf, 1);
addCard(Zone.BATTLEFIELD, playerB, swallower, 1);
addCard(Zone.BATTLEFIELD, playerB, "Nivix Cyclops", 1);
attack(2, playerB, watchwolf);
attack(2, playerB, swallower);
checkStackObject("Sacrifice trigger check", 2, PhaseStep.COMBAT_DAMAGE, playerB, "Whenever one or more creatures deal combat damage to you", 1);
// Choose which creature to sacrifice
addTarget(playerB, watchwolf);
// The ring tempts you choice:
setChoice(playerA, witchKing);
setStopAt(2, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertLife(playerA, 20 - 3 - 6);
// Player B had to sacrifice one permanent:
assertPermanentCount(playerB, 2);
}
@Test
public void testIndestructible() {
addCard(Zone.BATTLEFIELD, playerA, witchKing, 1);
addCard(Zone.HAND, playerA, "Swamp", 5);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Discard a card:");
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertAbility(playerA, witchKing, IndestructibleAbility.getInstance(), true);
assertTapped(witchKing, true);
assertHandCount(playerA, 4);
setStopAt(2, PhaseStep.UNTAP);
execute();
assertAbility(playerA, witchKing, IndestructibleAbility.getInstance(), false);
}
}

View file

@ -27,7 +27,8 @@ public enum TargetController {
EACH_PLAYER, EACH_PLAYER,
ENCHANTED, ENCHANTED,
SOURCE_TARGETS, SOURCE_TARGETS,
MONARCH; MONARCH,
SOURCE_CONTROLLER;
private final OwnerPredicate ownerPredicate; private final OwnerPredicate ownerPredicate;
private final PlayerPredicate playerPredicate; private final PlayerPredicate playerPredicate;
@ -78,6 +79,8 @@ public enum TargetController {
case ENCHANTED: case ENCHANTED:
Permanent permanent = input.getSource().getSourcePermanentIfItStillExists(game); Permanent permanent = input.getSource().getSourcePermanentIfItStillExists(game);
return permanent != null && input.getObject().isOwnedBy(permanent.getAttachedTo()); return permanent != null && input.getObject().isOwnedBy(permanent.getAttachedTo());
case SOURCE_CONTROLLER:
return card.isOwnedBy(input.getSource().getControllerId());
case SOURCE_TARGETS: case SOURCE_TARGETS:
return card.isOwnedBy(input.getSource().getFirstTarget()); return card.isOwnedBy(input.getSource().getFirstTarget());
case MONARCH: case MONARCH:
@ -119,8 +122,10 @@ public enum TargetController {
game.getPlayer(playerId).hasOpponent(player.getId(), game); game.getPlayer(playerId).hasOpponent(player.getId(), game);
case NOT_YOU: case NOT_YOU:
return !player.getId().equals(playerId); return !player.getId().equals(playerId);
case SOURCE_CONTROLLER:
return player.getId().equals(input.getSource().getControllerId());
case SOURCE_TARGETS: case SOURCE_TARGETS:
return player.equals(input.getSource().getFirstTarget()); return player.getId().equals(input.getSource().getFirstTarget());
case MONARCH: case MONARCH:
return player.getId().equals(game.getMonarchId()); return player.getId().equals(game.getMonarchId());
default: default:
@ -162,6 +167,8 @@ public enum TargetController {
case ENCHANTED: case ENCHANTED:
Permanent permanent = input.getSource().getSourcePermanentIfItStillExists(game); Permanent permanent = input.getSource().getSourcePermanentIfItStillExists(game);
return permanent != null && input.getObject().isControlledBy(permanent.getAttachedTo()); return permanent != null && input.getObject().isControlledBy(permanent.getAttachedTo());
case SOURCE_CONTROLLER:
return object.isControlledBy(input.getSource().getControllerId());
case SOURCE_TARGETS: case SOURCE_TARGETS:
return object.isControlledBy(input.getSource().getFirstTarget()); return object.isControlledBy(input.getSource().getFirstTarget());
case MONARCH: case MONARCH:

View file

@ -10,61 +10,83 @@ import mage.watchers.common.PlayerDamagedBySourceWatcher;
import java.util.UUID; import java.util.UUID;
/** /**
* For use in abilities with this predicate:
* "_ that dealt (combat) damage to _ this turn"
*
* @author LevelX2 * @author LevelX2
*/ */
public class DamagedPlayerThisTurnPredicate implements ObjectSourcePlayerPredicate<Controllable> { public class DamagedPlayerThisTurnPredicate implements ObjectSourcePlayerPredicate<Controllable> {
private final TargetController controller; private final TargetController playerDamaged;
public DamagedPlayerThisTurnPredicate(TargetController controller) { private final boolean combatDamageOnly;
this.controller = controller;
public DamagedPlayerThisTurnPredicate(TargetController playerDamaged) {
this(playerDamaged, false);
}
public DamagedPlayerThisTurnPredicate(TargetController playerDamaged, boolean combatDamageOnly) {
this.playerDamaged = playerDamaged;
this.combatDamageOnly = combatDamageOnly;
} }
@Override @Override
public boolean apply(ObjectSourcePlayer<Controllable> input, Game game) { public boolean apply(ObjectSourcePlayer<Controllable> input, Game game) {
Controllable object = input.getObject(); UUID objectId = input.getObject().getId();
UUID playerId = input.getPlayerId(); UUID playerId = input.getPlayerId();
switch (controller) { switch (playerDamaged) {
case YOU: case YOU:
PlayerDamagedBySourceWatcher watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, playerId); // that dealt damage to you this turn
if (watcher != null) { return playerDealtDamageBy(playerId, objectId, game);
return watcher.hasSourceDoneDamage(object.getId(), game); case SOURCE_CONTROLLER:
} // that dealt damage to this spell/ability's controller this turn
break; UUID controllerId = input.getSource().getControllerId();
return playerDealtDamageBy(controllerId, objectId, game);
case OPPONENT: case OPPONENT:
// that dealt damage to an opponent this turn
for (UUID opponentId : game.getOpponents(playerId)) { for (UUID opponentId : game.getOpponents(playerId)) {
watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, opponentId); if (playerDealtDamageBy(opponentId, objectId, game)) {
if (watcher != null) { return true;
return watcher.hasSourceDoneDamage(object.getId(), game);
} }
} }
break; return false;
case NOT_YOU: case NOT_YOU:
// that dealt damage to another player this turn
for (UUID notYouId : game.getState().getPlayersInRange(playerId, game)) { for (UUID notYouId : game.getState().getPlayersInRange(playerId, game)) {
if (!notYouId.equals(playerId)) { if (!notYouId.equals(playerId)) {
watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, notYouId); if (playerDealtDamageBy(notYouId, objectId, game)) {
if (watcher != null) { return true;
return watcher.hasSourceDoneDamage(object.getId(), game);
} }
} }
} }
break; return false;
case ANY: case ANY:
// that dealt damage to a player this turn
for (UUID anyId : game.getState().getPlayersInRange(playerId, game)) { for (UUID anyId : game.getState().getPlayersInRange(playerId, game)) {
watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, anyId); if (playerDealtDamageBy(anyId, objectId, game)) {
if (watcher != null) { return true;
return watcher.hasSourceDoneDamage(object.getId(), game);
} }
} }
return true; return false;
default:
throw new UnsupportedOperationException("TargetController not supported");
} }
}
return false; private boolean playerDealtDamageBy(UUID playerId, UUID objectId, Game game) {
PlayerDamagedBySourceWatcher watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, playerId);
if (watcher == null) {
return false;
}
if (combatDamageOnly) {
return watcher.hasSourceDoneCombatDamage(objectId, game);
}
return watcher.hasSourceDoneDamage(objectId, game);
} }
@Override @Override
public String toString() { public String toString() {
return "Damaged player (" + controller.toString() + ')'; return "Damaged player (" + playerDamaged.toString() + ')';
} }
} }

View file

@ -6,8 +6,8 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import mage.constants.WatcherScope; import mage.constants.WatcherScope;
import mage.game.Game; import mage.game.Game;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.watchers.Watcher; import mage.watchers.Watcher;
@ -19,6 +19,7 @@ import mage.watchers.Watcher;
public class PlayerDamagedBySourceWatcher extends Watcher { public class PlayerDamagedBySourceWatcher extends Watcher {
private final Set<String> damageSourceIds = new HashSet<>(); private final Set<String> damageSourceIds = new HashSet<>();
private final Set<String> combatDamageSourceIds = new HashSet<>();
public PlayerDamagedBySourceWatcher() { public PlayerDamagedBySourceWatcher() {
super(WatcherScope.PLAYER); super(WatcherScope.PLAYER);
@ -28,7 +29,11 @@ public class PlayerDamagedBySourceWatcher extends Watcher {
public void watch(GameEvent event, Game game) { public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.DAMAGED_PLAYER) { if (event.getType() == GameEvent.EventType.DAMAGED_PLAYER) {
if (event.getTargetId().equals(controllerId)) { if (event.getTargetId().equals(controllerId)) {
damageSourceIds.add(CardUtil.getCardZoneString(null, event.getSourceId(), game)); String sourceId = CardUtil.getCardZoneString(null, event.getSourceId(), game);
damageSourceIds.add(sourceId);
if (((DamagedEvent) event).isCombatDamage()) {
combatDamageSourceIds.add(sourceId);
}
} }
} }
} }
@ -45,6 +50,10 @@ public class PlayerDamagedBySourceWatcher extends Watcher {
return damageSourceIds.contains(CardUtil.getCardZoneString(null, sourceId, game)); return damageSourceIds.contains(CardUtil.getCardZoneString(null, sourceId, game));
} }
public boolean hasSourceDoneCombatDamage(UUID sourceId, Game game) {
return combatDamageSourceIds.contains(CardUtil.getCardZoneString(null, sourceId, game));
}
@Override @Override
public void reset() { public void reset() {
super.reset(); super.reset();