Implement [CLB] Aboleth Spawn (#9857)

Update `StackAbility::createSingleCopy` to properly set controller

Includes test coverage

---------

Co-authored-by: Rowan Gudmundsson <rowan.gudmundsson@oddball.io>
Co-authored-by: xenohedron <xenohedron@users.noreply.github.com>
This commit is contained in:
Rowan Gudmundsson 2023-10-14 14:12:15 -07:00 committed by GitHub
parent d705fa0e41
commit 76f38201c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 186 additions and 1 deletions

View file

@ -0,0 +1,105 @@
package mage.cards.a;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.CopyStackObjectEffect;
import mage.abilities.keyword.FlashAbility;
import mage.abilities.keyword.WardAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
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.stack.StackObject;
import java.util.UUID;
/**
* @author Rowan-Gudmundsson, xenohedron
*/
public final class AbolethSpawn extends CardImpl {
public AbolethSpawn(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}");
this.subtype.add(SubType.FISH);
this.subtype.add(SubType.HORROR);
this.power = new MageInt(2);
this.toughness = new MageInt(3);
// Flash
this.addAbility(FlashAbility.getInstance());
// Ward {2}
this.addAbility(new WardAbility(new ManaCostsImpl<>("{2}"), false));
// Whenever a creature entering the battlefield under an opponent's control causes a triggered ability of that creature to trigger,
// you may copy that ability. You may choose new targets for the copy.
this.addAbility(new AbolethSpawnTriggeredAbility());
}
private AbolethSpawn(final AbolethSpawn card) {
super(card);
}
@Override
public AbolethSpawn copy() {
return new AbolethSpawn(this);
}
}
class AbolethSpawnTriggeredAbility extends TriggeredAbilityImpl {
AbolethSpawnTriggeredAbility() {
super(Zone.BATTLEFIELD, new CopyStackObjectEffect(), true);
setTriggerPhrase("Whenever a creature entering the battlefield under an opponent's control causes a triggered ability of that creature to trigger, ");
}
private AbolethSpawnTriggeredAbility(final AbolethSpawnTriggeredAbility ability) {
super(ability);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.TRIGGERED_ABILITY;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
StackObject stackObject = game.getStack().getStackObject(event.getTargetId());
Permanent permanent = game.getPermanent(event.getSourceId());
if (stackObject == null || permanent == null || !permanent.isCreature(game)) {
return false; // only creatures
}
if (!game.getOpponents(this.getControllerId()).contains(permanent.getControllerId())) {
return false; // only creatures entering under opponent's control
}
Ability stackAbility = stackObject.getStackAbility();
if (!(stackAbility instanceof TriggeredAbility)) {
return false;
}
GameEvent triggerEvent = ((TriggeredAbility) stackAbility).getTriggerEvent();
if (triggerEvent == null || triggerEvent.getType() != GameEvent.EventType.ENTERS_THE_BATTLEFIELD) {
return false; // only ETB triggers
}
if (triggerEvent.getSourceId() != permanent.getId()) {
return false; // only triggered abilities of that creature
}
// CopyStackObjectEffect needs value set
getEffects().setValue("stackObject", stackObject);
return true;
}
@Override
public AbolethSpawnTriggeredAbility copy() {
return new AbolethSpawnTriggeredAbility(this);
}
}

View file

@ -23,6 +23,7 @@ public final class CommanderLegendsBattleForBaldursGate extends ExpansionSet {
cards.add(new SetCardInfo("Aarakocra Sneak", 54, Rarity.COMMON, mage.cards.a.AarakocraSneak.class));
cards.add(new SetCardInfo("Abdel Adrian, Gorion's Ward", 2, Rarity.UNCOMMON, mage.cards.a.AbdelAdrianGorionsWard.class));
cards.add(new SetCardInfo("Aboleth Spawn", 662, Rarity.RARE, mage.cards.a.AbolethSpawn.class));
cards.add(new SetCardInfo("Acolyte of Bahamut", 212, Rarity.UNCOMMON, mage.cards.a.AcolyteOfBahamut.class));
cards.add(new SetCardInfo("Aether Gale", 712, Rarity.RARE, mage.cards.a.AetherGale.class));
cards.add(new SetCardInfo("Agent of the Iron Throne", 107, Rarity.UNCOMMON, mage.cards.a.AgentOfTheIronThrone.class));

View file

@ -0,0 +1,77 @@
package org.mage.test.cards.single.clb;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author xenohedron
*/
public class AbolethSpawnTest extends CardTestPlayerBase {
private static final String aboleth = "Aboleth Spawn"; // 2/3 Flash, ward {2}
// Whenever a creature entering the battlefield under an opponent's control
// causes a triggered ability of that creature to trigger,
// you may copy that ability. You may choose new targets for the copy.
private static final String sparkmage = "Sparkmage Apprentice"; // 1/1
// When Sparkmage Apprentice enters the battlefield, it deals 1 damage to any target.
private static final String attendant = "Soul's Attendant"; // 1/1
// Whenever another creature enters the battlefield, you may gain 1 life.
private static final String hatchling = "Kraken Hatchling"; // 0/4
private static final String ridgescale = "Ridgescale Tusker"; // 5/5
// When Ridgescale Tusker enters the battlefield, put a +1/+1 counter on each other creature you control.
@Test
public void testTriggerCopies() {
addCard(Zone.BATTLEFIELD, playerA, aboleth);
addCard(Zone.BATTLEFIELD, playerA, hatchling);
addCard(Zone.BATTLEFIELD, playerB, attendant);
addCard(Zone.HAND, playerB, sparkmage);
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2);
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, sparkmage);
setChoice(playerB, "Whenever another"); // order triggers
addTarget(playerB, hatchling); // to deal 1 damage
setChoice(playerA, true); // yes to copy sparkmage trigger
setChoice(playerA, true); // yes to change targets
addTarget(playerA, aboleth); // to deal 1 damage
setChoice(playerB, true); // yes to gain 1 life
setStopAt(2, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertDamageReceived(playerA, hatchling, 1);
assertDamageReceived(playerA, aboleth, 1);
assertLife(playerA, 20);
assertLife(playerB, 21);
}
@Test
public void testTriggerController() {
addCard(Zone.BATTLEFIELD, playerA, aboleth);
addCard(Zone.BATTLEFIELD, playerB, hatchling);
addCard(Zone.BATTLEFIELD, playerA, attendant);
addCard(Zone.HAND, playerB, ridgescale);
addCard(Zone.BATTLEFIELD, playerB, "Forest", 5);
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, ridgescale);
setChoice(playerA, "Whenever another"); // order triggers
setChoice(playerA, true); // yes to copy ridgescale trigger
setChoice(playerA, true); // yes to gain 1 life
setStopAt(2, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertCounterCount(playerA, aboleth, CounterType.P1P1, 1);
assertCounterCount(playerA, attendant, CounterType.P1P1, 1);
assertCounterCount(playerB, hatchling, CounterType.P1P1, 1);
assertLife(playerA, 21);
assertLife(playerB, 20);
}
}

View file

@ -632,8 +632,10 @@ public class StackAbility extends StackObjectImpl implements Ability {
@Override
public void createSingleCopy(UUID newControllerId, StackObjectCopyApplier applier, MageObjectReferencePredicate newTargetFilterPredicate, Game game, Ability source, boolean chooseNewTargets) {
Ability newAbility = this.copy();
Ability newAbility = this.ability.copy();
newAbility.newId();
newAbility.setControllerId(newControllerId);
StackAbility newStackAbility = new StackAbility(newAbility, newControllerId);
game.getStack().push(newStackAbility);