mirror of
https://github.com/magefree/mage.git
synced 2025-12-21 19:11:59 -08:00
[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:
parent
008662be5e
commit
c8564efbb7
6 changed files with 202 additions and 28 deletions
73
Mage.Sets/src/mage/cards/w/WitchKingOfAngmar.java
Normal file
73
Mage.Sets/src/mage/cards/w/WitchKingOfAngmar.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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() + ')';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue