Implement Freerunning mechanic (ready for review) (#12485)

* [ACR] Implement Eagle Vision

* add test

* [ACR] Implement Achilles Davenport

* [ACR] Implement Chain Assassination

* [ACR] Implement Restart Sequence

* update test

* add test for non-assassin, non-commander
This commit is contained in:
Evan Kranzler 2024-06-19 18:05:20 -04:00 committed by GitHub
parent 6164953637
commit e851e04906
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 403 additions and 0 deletions

View file

@ -0,0 +1,54 @@
package mage.cards.a;
import mage.MageInt;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.continuous.BoostControlledEffect;
import mage.abilities.keyword.FreerunningAbility;
import mage.abilities.keyword.MenaceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.common.FilterCreaturePermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class AchillesDavenport extends CardImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(SubType.ASSASSIN, "Assassins");
public AchillesDavenport(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.ASSASSIN);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
// Freerunning {U}{B}
this.addAbility(new FreerunningAbility("{U}{B}"));
// Menace
this.addAbility(new MenaceAbility());
// Other Assassins you control get +1/+1.
this.addAbility(new SimpleStaticAbility(new BoostControlledEffect(
1, 1, Duration.WhileOnBattlefield, filter, true
)));
}
private AchillesDavenport(final AchillesDavenport card) {
super(card);
}
@Override
public AchillesDavenport copy() {
return new AchillesDavenport(this);
}
}

View file

@ -0,0 +1,77 @@
package mage.cards.c;
import mage.abilities.Ability;
import mage.abilities.condition.common.MorbidCondition;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.FreerunningAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class ChainAssassination extends CardImpl {
public ChainAssassination(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{B}{B}");
// Freerunning {1}{B}
this.addAbility(new FreerunningAbility("{1}{B}"));
// Destroy target creature. If another creature died this turn, draw a card.
this.getSpellAbility().addEffect(new ChainAssassinationEffect());
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
}
private ChainAssassination(final ChainAssassination card) {
super(card);
}
@Override
public ChainAssassination copy() {
return new ChainAssassination(this);
}
}
class ChainAssassinationEffect extends OneShotEffect {
ChainAssassinationEffect() {
super(Outcome.Benefit);
staticText = "destroy target creature. If another creature died this turn, draw a card";
}
private ChainAssassinationEffect(final ChainAssassinationEffect effect) {
super(effect);
}
@Override
public ChainAssassinationEffect copy() {
return new ChainAssassinationEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
boolean flag = MorbidCondition.instance.apply(game, source);
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (permanent == null) {
return false;
}
permanent.destroy(source, game);
if (!flag) {
return true;
}
Player player = game.getPlayer(source.getControllerId());
if (player != null) {
player.drawCards(1, source, game);
}
return true;
}
}

View file

@ -0,0 +1,34 @@
package mage.cards.e;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.keyword.FreerunningAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class EagleVision extends CardImpl {
public EagleVision(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{U}");
// Freerunning {1}{U}
this.addAbility(new FreerunningAbility("{1}{U}"));
// Draw three cards.
this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(3));
}
private EagleVision(final EagleVision card) {
super(card);
}
@Override
public EagleVision copy() {
return new EagleVision(this);
}
}

View file

@ -0,0 +1,37 @@
package mage.cards.r;
import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect;
import mage.abilities.keyword.FreerunningAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.StaticFilters;
import mage.target.common.TargetCardInYourGraveyard;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class RestartSequence extends CardImpl {
public RestartSequence(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}");
// Freerunning {1}{B}
this.addAbility(new FreerunningAbility("{1}{B}"));
// Return target creature card from your graveyard to the battlefield.
this.getSpellAbility().addEffect(new ReturnFromGraveyardToBattlefieldTargetEffect());
this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD));
}
private RestartSequence(final RestartSequence card) {
super(card);
}
@Override
public RestartSequence copy() {
return new RestartSequence(this);
}
}

View file

@ -22,15 +22,18 @@ public final class AssassinsCreed extends ExpansionSet {
this.hasBoosters = false;
cards.add(new SetCardInfo("Abstergo Entertainment", 79, Rarity.RARE, mage.cards.a.AbstergoEntertainment.class));
cards.add(new SetCardInfo("Achilles Davenport", 294, Rarity.RARE, mage.cards.a.AchillesDavenport.class));
cards.add(new SetCardInfo("Assassin's Trophy", 166, Rarity.RARE, mage.cards.a.AssassinsTrophy.class));
cards.add(new SetCardInfo("Aya of Alexandria", 48, Rarity.RARE, mage.cards.a.AyaOfAlexandria.class));
cards.add(new SetCardInfo("Basim Ibn Ishaq", 49, Rarity.RARE, mage.cards.b.BasimIbnIshaq.class));
cards.add(new SetCardInfo("Bayek of Siwa", 50, Rarity.RARE, mage.cards.b.BayekOfSiwa.class));
cards.add(new SetCardInfo("Cathartic Reunion", 94, Rarity.UNCOMMON, mage.cards.c.CatharticReunion.class));
cards.add(new SetCardInfo("Chain Assassination", 23, Rarity.UNCOMMON, mage.cards.c.ChainAssassination.class));
cards.add(new SetCardInfo("Cleopatra, Exiled Pharaoh", 52, Rarity.MYTHIC, mage.cards.c.CleopatraExiledPharaoh.class));
cards.add(new SetCardInfo("Coastal Piracy", 84, Rarity.UNCOMMON, mage.cards.c.CoastalPiracy.class));
cards.add(new SetCardInfo("Conspiracy", 88, Rarity.RARE, mage.cards.c.Conspiracy.class));
cards.add(new SetCardInfo("Cover of Darkness", 89, Rarity.RARE, mage.cards.c.CoverOfDarkness.class));
cards.add(new SetCardInfo("Eagle Vision", 17, Rarity.UNCOMMON, mage.cards.e.EagleVision.class));
cards.add(new SetCardInfo("Eivor, Battle-Ready", 274, Rarity.MYTHIC, mage.cards.e.EivorBattleReady.class));
cards.add(new SetCardInfo("Escarpment Fortress", 278, Rarity.RARE, mage.cards.e.EscarpmentFortress.class));
cards.add(new SetCardInfo("Ezio, Blade of Vengeance", 275, Rarity.MYTHIC, mage.cards.e.EzioBladeOfVengeance.class));
@ -46,6 +49,7 @@ public final class AssassinsCreed extends ExpansionSet {
cards.add(new SetCardInfo("Raven Clan War-Axe", 297, Rarity.RARE, mage.cards.r.RavenClanWarAxe.class));
cards.add(new SetCardInfo("Reconstruct History", 97, Rarity.UNCOMMON, mage.cards.r.ReconstructHistory.class));
cards.add(new SetCardInfo("Rest in Peace", 83, Rarity.RARE, mage.cards.r.RestInPeace.class));
cards.add(new SetCardInfo("Restart Sequence", 30, Rarity.UNCOMMON, mage.cards.r.RestartSequence.class));
cards.add(new SetCardInfo("Royal Assassin", 93, Rarity.RARE, mage.cards.r.RoyalAssassin.class));
cards.add(new SetCardInfo("Silent Clearing", 115, Rarity.RARE, mage.cards.s.SilentClearing.class));
cards.add(new SetCardInfo("Sunbaked Canyon", 111, Rarity.RARE, mage.cards.s.SunbakedCanyon.class));

View file

@ -0,0 +1,91 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestCommanderDuelBase;
/**
* @author TheElk801
*/
public class FreerunningTest extends CardTestCommanderDuelBase {
private static final String vision = "Eagle Vision";
@Test
public void testRegular() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.HAND, playerA, vision);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 3);
assertTappedCount("Island", true, 5);
}
private static final String poisoner = "Hired Poisoner";
@Test
public void testAssassin() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.BATTLEFIELD, playerA, poisoner);
addCard(Zone.HAND, playerA, vision);
attack(1, playerA, poisoner, playerB);
setChoice(playerA, true);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 3);
}
private static final String goblin = "Raging Goblin";
@Test
public void testCommander() {
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 2 + 1);
addCard(Zone.COMMAND, playerA, goblin);
addCard(Zone.HAND, playerA, vision);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblin);
attack(1, playerA, goblin, playerB);
setChoice(playerA, true);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 3);
}
@Test
public void testNeither() {
addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 5 + 1);
addCard(Zone.HAND, playerA, goblin);
addCard(Zone.HAND, playerA, vision);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblin);
attack(1, playerA, goblin, playerB);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 3);
assertTappedCount("Volcanic Island", true, 5 + 1);
}
}

View file

@ -0,0 +1,105 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.costs.AlternativeSourceCostsImpl;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.constants.SubType;
import mage.constants.WatcherScope;
import mage.filter.predicate.mageobject.CommanderPredicate;
import mage.game.Game;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.watchers.Watcher;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
* @author TheElk801
*/
public class FreerunningAbility extends AlternativeSourceCostsImpl {
private static final String FREERUNNING_KEYWORD = "Freerunning";
private static final String FREERUNNING_REMINDER = "You may cast this spell for its freerunning cost " +
"if you dealt combat damage to a player this turn with an Assassin or commander";
public FreerunningAbility(String manaString) {
super(FREERUNNING_KEYWORD, FREERUNNING_REMINDER, manaString);
this.setRuleAtTheTop(true);
this.addWatcher(new FreerunningWatcher());
this.addHint(FreerunningCondition.getHint());
}
private FreerunningAbility(final FreerunningAbility ability) {
super(ability);
}
@Override
public FreerunningAbility copy() {
return new FreerunningAbility(this);
}
@Override
public boolean isAvailable(Ability source, Game game) {
return FreerunningCondition.instance.apply(game, source);
}
public static String getActivationKey() {
return getActivationKey(FREERUNNING_KEYWORD);
}
}
enum FreerunningCondition implements Condition {
instance;
private static final Hint hint = new ConditionHint(instance, "Freerunning can be used");
public static Hint getHint() {
return hint;
}
@Override
public boolean apply(Game game, Ability source) {
return FreerunningWatcher.checkPlayer(source.getControllerId(), game);
}
}
class FreerunningWatcher extends Watcher {
private final Set<UUID> players = new HashSet<>();
FreerunningWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() != GameEvent.EventType.DAMAGED_PLAYER
|| !((DamagedEvent) event).isCombatDamage()) {
return;
}
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId());
if (permanent != null
&& (permanent.hasSubtype(SubType.ASSASSIN, game)
|| CommanderPredicate.instance.apply(permanent, game))) {
players.add(permanent.getControllerId());
}
}
@Override
public void reset() {
players.clear();
super.reset();
}
static boolean checkPlayer(UUID playerId, Game game) {
return game
.getState()
.getWatcher(FreerunningWatcher.class)
.players
.contains(playerId);
}
}

View file

@ -59,6 +59,7 @@ Forestcycling|cost|
Forestwalk|new|
Foretell|card, manaString|
For Mirrodin!|new|
Freerunning|manaString|
Friends forever|instance|
Haste|instance|
Hexproof|instance|