mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
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:
parent
6164953637
commit
e851e04906
8 changed files with 403 additions and 0 deletions
54
Mage.Sets/src/mage/cards/a/AchillesDavenport.java
Normal file
54
Mage.Sets/src/mage/cards/a/AchillesDavenport.java
Normal 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);
|
||||
}
|
||||
}
|
||||
77
Mage.Sets/src/mage/cards/c/ChainAssassination.java
Normal file
77
Mage.Sets/src/mage/cards/c/ChainAssassination.java
Normal 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;
|
||||
}
|
||||
}
|
||||
34
Mage.Sets/src/mage/cards/e/EagleVision.java
Normal file
34
Mage.Sets/src/mage/cards/e/EagleVision.java
Normal 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);
|
||||
}
|
||||
}
|
||||
37
Mage.Sets/src/mage/cards/r/RestartSequence.java
Normal file
37
Mage.Sets/src/mage/cards/r/RestartSequence.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +59,7 @@ Forestcycling|cost|
|
|||
Forestwalk|new|
|
||||
Foretell|card, manaString|
|
||||
For Mirrodin!|new|
|
||||
Freerunning|manaString|
|
||||
Friends forever|instance|
|
||||
Haste|instance|
|
||||
Hexproof|instance|
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue