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("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("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("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));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
ENCHANTED,
|
||||
SOURCE_TARGETS,
|
||||
MONARCH;
|
||||
MONARCH,
|
||||
SOURCE_CONTROLLER;
|
||||
|
||||
private final OwnerPredicate ownerPredicate;
|
||||
private final PlayerPredicate playerPredicate;
|
||||
|
|
@ -78,6 +79,8 @@ public enum TargetController {
|
|||
case ENCHANTED:
|
||||
Permanent permanent = input.getSource().getSourcePermanentIfItStillExists(game);
|
||||
return permanent != null && input.getObject().isOwnedBy(permanent.getAttachedTo());
|
||||
case SOURCE_CONTROLLER:
|
||||
return card.isOwnedBy(input.getSource().getControllerId());
|
||||
case SOURCE_TARGETS:
|
||||
return card.isOwnedBy(input.getSource().getFirstTarget());
|
||||
case MONARCH:
|
||||
|
|
@ -119,8 +122,10 @@ public enum TargetController {
|
|||
game.getPlayer(playerId).hasOpponent(player.getId(), game);
|
||||
case NOT_YOU:
|
||||
return !player.getId().equals(playerId);
|
||||
case SOURCE_CONTROLLER:
|
||||
return player.getId().equals(input.getSource().getControllerId());
|
||||
case SOURCE_TARGETS:
|
||||
return player.equals(input.getSource().getFirstTarget());
|
||||
return player.getId().equals(input.getSource().getFirstTarget());
|
||||
case MONARCH:
|
||||
return player.getId().equals(game.getMonarchId());
|
||||
default:
|
||||
|
|
@ -162,6 +167,8 @@ public enum TargetController {
|
|||
case ENCHANTED:
|
||||
Permanent permanent = input.getSource().getSourcePermanentIfItStillExists(game);
|
||||
return permanent != null && input.getObject().isControlledBy(permanent.getAttachedTo());
|
||||
case SOURCE_CONTROLLER:
|
||||
return object.isControlledBy(input.getSource().getControllerId());
|
||||
case SOURCE_TARGETS:
|
||||
return object.isControlledBy(input.getSource().getFirstTarget());
|
||||
case MONARCH:
|
||||
|
|
|
|||
|
|
@ -10,61 +10,83 @@ import mage.watchers.common.PlayerDamagedBySourceWatcher;
|
|||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* For use in abilities with this predicate:
|
||||
* "_ that dealt (combat) damage to _ this turn"
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
public class DamagedPlayerThisTurnPredicate implements ObjectSourcePlayerPredicate<Controllable> {
|
||||
|
||||
private final TargetController controller;
|
||||
private final TargetController playerDamaged;
|
||||
|
||||
public DamagedPlayerThisTurnPredicate(TargetController controller) {
|
||||
this.controller = controller;
|
||||
private final boolean combatDamageOnly;
|
||||
|
||||
public DamagedPlayerThisTurnPredicate(TargetController playerDamaged) {
|
||||
this(playerDamaged, false);
|
||||
}
|
||||
|
||||
public DamagedPlayerThisTurnPredicate(TargetController playerDamaged, boolean combatDamageOnly) {
|
||||
this.playerDamaged = playerDamaged;
|
||||
this.combatDamageOnly = combatDamageOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(ObjectSourcePlayer<Controllable> input, Game game) {
|
||||
Controllable object = input.getObject();
|
||||
UUID objectId = input.getObject().getId();
|
||||
UUID playerId = input.getPlayerId();
|
||||
|
||||
switch (controller) {
|
||||
switch (playerDamaged) {
|
||||
case YOU:
|
||||
PlayerDamagedBySourceWatcher watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, playerId);
|
||||
if (watcher != null) {
|
||||
return watcher.hasSourceDoneDamage(object.getId(), game);
|
||||
}
|
||||
break;
|
||||
// that dealt damage to you this turn
|
||||
return playerDealtDamageBy(playerId, objectId, game);
|
||||
case SOURCE_CONTROLLER:
|
||||
// that dealt damage to this spell/ability's controller this turn
|
||||
UUID controllerId = input.getSource().getControllerId();
|
||||
return playerDealtDamageBy(controllerId, objectId, game);
|
||||
case OPPONENT:
|
||||
// that dealt damage to an opponent this turn
|
||||
for (UUID opponentId : game.getOpponents(playerId)) {
|
||||
watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, opponentId);
|
||||
if (watcher != null) {
|
||||
return watcher.hasSourceDoneDamage(object.getId(), game);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NOT_YOU:
|
||||
for (UUID notYouId : game.getState().getPlayersInRange(playerId, game)) {
|
||||
if (!notYouId.equals(playerId)) {
|
||||
watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, notYouId);
|
||||
if (watcher != null) {
|
||||
return watcher.hasSourceDoneDamage(object.getId(), game);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ANY:
|
||||
for (UUID anyId : game.getState().getPlayersInRange(playerId, game)) {
|
||||
watcher = game.getState().getWatcher(PlayerDamagedBySourceWatcher.class, anyId);
|
||||
if (watcher != null) {
|
||||
return watcher.hasSourceDoneDamage(object.getId(), game);
|
||||
}
|
||||
}
|
||||
if (playerDealtDamageBy(opponentId, objectId, game)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
case NOT_YOU:
|
||||
// that dealt damage to another player this turn
|
||||
for (UUID notYouId : game.getState().getPlayersInRange(playerId, game)) {
|
||||
if (!notYouId.equals(playerId)) {
|
||||
if (playerDealtDamageBy(notYouId, objectId, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case ANY:
|
||||
// that dealt damage to a player this turn
|
||||
for (UUID anyId : game.getState().getPlayersInRange(playerId, game)) {
|
||||
if (playerDealtDamageBy(anyId, objectId, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
throw new UnsupportedOperationException("TargetController not supported");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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 mage.constants.WatcherScope;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.DamagedEvent;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.events.GameEvent.EventType;
|
||||
import mage.util.CardUtil;
|
||||
import mage.watchers.Watcher;
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ import mage.watchers.Watcher;
|
|||
public class PlayerDamagedBySourceWatcher extends Watcher {
|
||||
|
||||
private final Set<String> damageSourceIds = new HashSet<>();
|
||||
private final Set<String> combatDamageSourceIds = new HashSet<>();
|
||||
|
||||
public PlayerDamagedBySourceWatcher() {
|
||||
super(WatcherScope.PLAYER);
|
||||
|
|
@ -28,7 +29,11 @@ public class PlayerDamagedBySourceWatcher extends Watcher {
|
|||
public void watch(GameEvent event, Game game) {
|
||||
if (event.getType() == GameEvent.EventType.DAMAGED_PLAYER) {
|
||||
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));
|
||||
}
|
||||
|
||||
public boolean hasSourceDoneCombatDamage(UUID sourceId, Game game) {
|
||||
return combatDamageSourceIds.contains(CardUtil.getCardZoneString(null, sourceId, game));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue