[MKM] Implement A Killer Among Us (#11704)

* [MKM] Implement A Killer Among Us

* Address PR comments

* Address PR comments

---------

Co-authored-by: Matthew Wilson <matthew_w@vaadin.com>
This commit is contained in:
Matthew Wilson 2024-01-27 02:47:43 +02:00 committed by GitHub
parent e431cd90ab
commit 91312228d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 370 additions and 0 deletions

View file

@ -0,0 +1,250 @@
package mage.cards.a;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.UUID;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.costs.common.SacrificeSourceCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.keyword.DeathtouchAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.choices.Choice;
import mage.choices.ChoiceImpl;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.counters.CounterType;
import mage.filter.common.FilterAttackingCreature;
import mage.filter.predicate.permanent.TokenPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.GoblinToken;
import mage.game.permanent.token.HumanToken;
import mage.game.permanent.token.MerfolkToken;
import mage.players.Player;
import mage.target.TargetPermanent;
/**
* A Killer Among Us {4}{G}
* Enchantment
* When A Killer Among Us enters the battlefield, create a 1/1 white Human creature token, a 1/1 blue Merfolk creature token, and a 1/1 red Goblin creature token. Then secretly choose Human, Merfolk, or Goblin.
* Sacrifice A Killer Among Us, Reveal the chosen creature type: If target attacking creature token is the chosen type, put three +1/+1 counters on it and it gains deathtouch until end of turn.
*
*
*
*
*
*
* @author DominionSpy
*/
public final class AKillerAmongUs extends CardImpl {
private static final FilterAttackingCreature filter = new FilterAttackingCreature("attacking creature token");
static {
filter.add(TokenPredicate.TRUE);
}
public AKillerAmongUs(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{G}");
// When A Killer Among Us enters the battlefield, create a 1/1 white Human creature token, a 1/1 blue Merfolk creature token, and a 1/1 red Goblin creature token. Then secretly choose Human, Merfolk, or Goblin.
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new HumanToken()));
ability.addEffect(new CreateTokenEffect(new MerfolkToken())
.setText(", a 1/1 blue Merfolk creature token"));
ability.addEffect(new CreateTokenEffect(new GoblinToken())
.setText(", and a 1/1 red Goblin creature token."));
ability.addEffect(new ChooseHumanMerfolkOrGoblinEffect());
this.addAbility(ability);
// Sacrifice A Killer Among Us, Reveal the chosen creature type: If target attacking creature token is the chosen type, put three +1/+1 counters on it and it gains deathtouch until end of turn.
ability = new SimpleActivatedAbility(new AKillerAmongUsEffect(), new SacrificeSourceCost());
ability.addCost(new AKillerAmongUsCost());
ability.addTarget(new TargetPermanent(filter));
this.addAbility(ability);
}
private AKillerAmongUs(final AKillerAmongUs card) {
super(card);
}
@Override
public AKillerAmongUs copy() {
return new AKillerAmongUs(this);
}
}
class ChooseHumanMerfolkOrGoblinEffect extends OneShotEffect {
public static final String SECRET_CREATURE_TYPE = "_susCreatureType";
public static final String SECRET_OWNER = "_secOwn";
ChooseHumanMerfolkOrGoblinEffect() {
super(Outcome.Neutral);
staticText = "Then secretly choose Human, Merfolk, or Goblin.";
}
private ChooseHumanMerfolkOrGoblinEffect(final ChooseHumanMerfolkOrGoblinEffect effect) {
super(effect);
}
@Override
public ChooseHumanMerfolkOrGoblinEffect copy() {
return new ChooseHumanMerfolkOrGoblinEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
Permanent permanent = game.getPermanentOrLKIBattlefield(source.getSourceId());
if (permanent == null) {
return false;
}
Choice choice = new ChoiceImpl();
Set<String> choices = new LinkedHashSet<>();
choices.add("Human");
choices.add("Merfolk");
choices.add("Goblin");
choice.setChoices(choices);
choice.setMessage("Choose Human, Merfolk, or Goblin");
controller.choose(outcome, choice, game);
game.informPlayers(permanent.getName() + ": " + controller.getLogName() + " has secretly chosen a creature type.");
SubType chosenType = SubType.fromString(choice.getChoice());
setSecretCreatureType(chosenType, source, game);
setSecretOwner(source.getControllerId(), source, game);
return true;
}
public static void setSecretCreatureType(SubType type, Ability source, Game game) {
String uniqueRef = getUniqueReference(source, game);
if (uniqueRef != null) {
game.getState().setValue(uniqueRef + SECRET_CREATURE_TYPE, type);
}
}
public static SubType getSecretCreatureType(Ability source, Game game) {
String uniqueRef = getUniqueReference(source, game);
if (uniqueRef != null) {
return (SubType) game.getState().getValue(uniqueRef +
ChooseHumanMerfolkOrGoblinEffect.SECRET_CREATURE_TYPE);
}
return null;
}
public static void setSecretOwner(UUID owner, Ability source, Game game) {
String uniqueRef = getUniqueReference(source, game);
if (uniqueRef != null) {
game.getState().setValue(getUniqueReference(source, game) + SECRET_OWNER, owner);
}
}
public static UUID getSecretOwner(Ability source, Game game) {
String uniqueRef = getUniqueReference(source, game);
if (uniqueRef != null) {
return (UUID) game.getState().getValue(getUniqueReference(source, game) +
ChooseHumanMerfolkOrGoblinEffect.SECRET_OWNER);
}
return null;
}
private static String getUniqueReference(Ability source, Game game) {
if (game.getPermanentOrLKIBattlefield(source.getSourceId()) != null) {
return source.getSourceId() + "_" + (game.getPermanentOrLKIBattlefield(source.getSourceId()).getZoneChangeCounter(game));
}
return null;
}
}
class AKillerAmongUsEffect extends OneShotEffect {
AKillerAmongUsEffect() {
super(Outcome.Benefit);
this.staticText = "If target attacking creature token is the chosen type, " +
"put three +1/+1 counters on it and it gains deathtouch until end of turn.";
}
private AKillerAmongUsEffect(final AKillerAmongUsEffect effect) {
super(effect);
}
@Override
public AKillerAmongUsEffect copy() {
return new AKillerAmongUsEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent creature = game.getPermanent(source.getFirstTarget());
if (creature == null) {
return false;
}
SubType creatureType = ChooseHumanMerfolkOrGoblinEffect.getSecretCreatureType(source, game);
if (creatureType != null && creature.getSubtype().contains(creatureType)) {
creature.addCounters(CounterType.P1P1.createInstance(3), source, game);
game.addEffect(new GainAbilityTargetEffect(
DeathtouchAbility.getInstance(), Duration.EndOfTurn
), source);
}
return true;
}
}
class AKillerAmongUsCost extends CostImpl {
AKillerAmongUsCost() {
this.text = "Reveal the chosen creature type";
}
private AKillerAmongUsCost(final AKillerAmongUsCost cost) {
super(cost);
}
@Override
public AKillerAmongUsCost copy() {
return new AKillerAmongUsCost(this);
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
return controllerId != null
&& controllerId.equals(ChooseHumanMerfolkOrGoblinEffect.getSecretOwner(source, game))
&& ChooseHumanMerfolkOrGoblinEffect.getSecretCreatureType(source, game) != null;
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
if (controllerId == null || !controllerId.equals(ChooseHumanMerfolkOrGoblinEffect.getSecretOwner(source, game))) {
return false;
}
SubType creatureType = ChooseHumanMerfolkOrGoblinEffect.getSecretCreatureType(source, game);
if (creatureType == null) {
return paid;
}
Player controller = game.getPlayer(controllerId);
MageObject sourceObject = game.getObject(source);
if (controller != null && sourceObject != null) {
game.informPlayers(sourceObject.getLogName() + ": " + controller.getLogName() +
" reveals the secretly chosen creature type " + creatureType);
}
paid = true;
return paid;
}
}

View file

@ -26,6 +26,7 @@ public final class MurdersAtKarlovManor extends ExpansionSet {
this.hasBasicLands = true;
this.hasBoosters = false; // temporary
cards.add(new SetCardInfo("A Killer Among Us", 167, Rarity.UNCOMMON, mage.cards.a.AKillerAmongUs.class));
cards.add(new SetCardInfo("Absolving Lammasu", 2, Rarity.UNCOMMON, mage.cards.a.AbsolvingLammasu.class));
cards.add(new SetCardInfo("Aftermath Analyst", 148, Rarity.UNCOMMON, mage.cards.a.AftermathAnalyst.class));
cards.add(new SetCardInfo("Agrus Kos, Spirit of Justice", 184, Rarity.MYTHIC, mage.cards.a.AgrusKosSpiritOfJustice.class));

View file

@ -0,0 +1,119 @@
package org.mage.test.cards.single.mkm;
import mage.abilities.keyword.DeathtouchAbility;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class AKillerAmongUsTest extends CardTestPlayerBase {
@Test
public void test_TargetChosenCreatureType() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
addCard(Zone.HAND, playerA, "A Killer Among Us");
// When A Killer Among Us enters the battlefield, create a 1/1 white Human creature token, a 1/1 blue Merfolk creature token, and a 1/1 red Goblin creature token. Then secretly choose Human, Merfolk, or Goblin.
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "A Killer Among Us");
setChoice(playerA, "Merfolk");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("human token", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Human Token", 1);
checkPermanentCount("merfolk token", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Merfolk Token", 1);
checkPermanentCount("goblin token", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin Token", 1);
attack(3, playerA, "Merfolk Token");
// Sacrifice A Killer Among Us, Reveal the chosen creature type: If target attacking creature token is the chosen type, put three +1/+1 counters on it and it gains deathtouch until end of turn.
activateAbility(3, PhaseStep.DECLARE_BLOCKERS, playerA, "Sacrifice", "Merfolk Token");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertPermanentCount(playerA, "A Killer Among Us", 0);
assertCounterCount(playerA, "Merfolk Token", CounterType.P1P1, 3);
assertAbility(playerA, "Merfolk Token", DeathtouchAbility.getInstance(), true);
}
@Test
public void test_TargetNotChosenCreatureType() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
addCard(Zone.HAND, playerA, "A Killer Among Us");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "A Killer Among Us");
setChoice(playerA, "Merfolk");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("human token", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Human Token", 1);
checkPermanentCount("merfolk token", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Merfolk Token", 1);
checkPermanentCount("goblin token", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin Token", 1);
attack(3, playerA, "Human Token");
activateAbility(3, PhaseStep.DECLARE_BLOCKERS, playerA, "Sacrifice", "Human Token");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertCounterCount(playerA, "Human Token", CounterType.P1P1, 0);
assertAbility(playerA, "Human Token", DeathtouchAbility.getInstance(), false);
}
// If the A Killer Among Us ETB trigger is copied, the last chosen creature type
// will be used for the second ability
@Test
public void test_CopyTrigger_TargetLastChosenCreatureType() {
addCard(Zone.BATTLEFIELD, playerA, "Taiga", 5 + 2);
addCard(Zone.BATTLEFIELD, playerA, "Lithoform Engine");
addCard(Zone.HAND, playerA, "A Killer Among Us");
addCard(Zone.HAND, playerA, "Tremor");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "A Killer Among Us");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
// Copy A Killer Among Us ETB trigger
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}", "stack ability (When");
setChoice(playerA, "Human");
setChoice(playerA, "Merfolk");
attack(3, playerA, "Merfolk Token");
activateAbility(3, PhaseStep.DECLARE_BLOCKERS, playerA, "Sacrifice", "Merfolk Token");
// Destroy all the other tokens
castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Tremor");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertCounterCount(playerA, "Merfolk Token", CounterType.P1P1, 3);
assertAbility(playerA, "Merfolk Token", DeathtouchAbility.getInstance(), true);
}
@Test
public void test_CopyTrigger_TargetNotLastChosenCreatureType() {
addCard(Zone.BATTLEFIELD, playerA, "Taiga", 5 + 2);
addCard(Zone.BATTLEFIELD, playerA, "Lithoform Engine");
addCard(Zone.HAND, playerA, "A Killer Among Us");
addCard(Zone.HAND, playerA, "Tremor");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "A Killer Among Us");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1);
// Copy A Killer Among Us ETB trigger
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}", "stack ability (When");
setChoice(playerA, "Human");
setChoice(playerA, "Merfolk");
attack(3, playerA, "Human Token");
activateAbility(3, PhaseStep.DECLARE_BLOCKERS, playerA, "Sacrifice", "Human Token");
// Destroy all the tokens
castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Tremor");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertPermanentCount(playerA, "Human Token", 0);
}
}