-
Notifications
You must be signed in to change notification settings - Fork 810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MKM] Implement Kaya, Spirits' Justice and new zone change batch event #11753
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6019163
[MKM] Implement Kaya, Spirits' Justice
MatthewVaadin 57f87c7
[MKM] Implement Kaya, Spirits' Justice
MatthewVaadin d3fe2eb
[MKM] Implement Kaya, Spirits' Justice
MatthewVaadin 3d6d342
Cleanup
MatthewVaadin c21fb63
Address PR comments
MatthewVaadin 48ba592
Merge branch 'master' into kaya-spirits-justice
xenohedron File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,282 @@ | ||
package mage.cards.k; | ||
|
||
import java.util.HashSet; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
import java.util.UUID; | ||
import java.util.stream.Collectors; | ||
|
||
import mage.MageObject; | ||
import mage.abilities.Ability; | ||
import mage.abilities.LoyaltyAbility; | ||
import mage.abilities.TriggeredAbilityImpl; | ||
import mage.abilities.effects.OneShotEffect; | ||
import mage.abilities.effects.common.CopyEffect; | ||
import mage.abilities.effects.common.CreateTokenEffect; | ||
import mage.abilities.effects.common.ExileTargetEffect; | ||
import mage.abilities.effects.keyword.SurveilEffect; | ||
import mage.abilities.keyword.FlyingAbility; | ||
import mage.cards.Card; | ||
import mage.cards.Cards; | ||
import mage.cards.CardsImpl; | ||
import mage.constants.Duration; | ||
import mage.constants.Outcome; | ||
import mage.constants.SubType; | ||
import mage.constants.SuperType; | ||
import mage.cards.CardImpl; | ||
import mage.cards.CardSetInfo; | ||
import mage.constants.CardType; | ||
import mage.constants.Zone; | ||
import mage.filter.FilterPermanent; | ||
import mage.filter.StaticFilters; | ||
import mage.filter.common.FilterCreaturePermanent; | ||
import mage.filter.predicate.permanent.ControllerIdPredicate; | ||
import mage.game.Game; | ||
import mage.game.events.GameEvent; | ||
import mage.game.events.ZoneChangeBatchEvent; | ||
import mage.game.events.ZoneChangeEvent; | ||
import mage.game.permanent.Permanent; | ||
import mage.game.permanent.PermanentCard; | ||
import mage.game.permanent.token.WhiteBlackSpiritToken; | ||
import mage.players.Player; | ||
import mage.target.TargetCard; | ||
import mage.target.TargetPermanent; | ||
import mage.target.common.TargetCardInExile; | ||
import mage.target.common.TargetCardInGraveyard; | ||
import mage.target.targetadjustment.TargetAdjuster; | ||
import mage.target.targetpointer.EachTargetPointer; | ||
import mage.target.targetpointer.FixedTargets; | ||
import mage.util.CardUtil; | ||
import mage.util.functions.CopyApplier; | ||
|
||
/** | ||
* | ||
* @author DominionSpy | ||
*/ | ||
public final class KayaSpiritsJustice extends CardImpl { | ||
|
||
public KayaSpiritsJustice(UUID ownerId, CardSetInfo setInfo) { | ||
super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{W}{B}"); | ||
|
||
this.supertype.add(SuperType.LEGENDARY); | ||
this.subtype.add(SubType.KAYA); | ||
this.setStartingLoyalty(3); | ||
|
||
// Whenever one or more creatures you control and/or creature cards in your graveyard are put into exile, you may choose a creature card from among them. | ||
// Until end of turn, target token you control becomes a copy of it, except it has flying. | ||
Ability ability = new KayaSpiritsJusticeTriggeredAbility(); | ||
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_TOKEN)); | ||
this.addAbility(ability); | ||
|
||
// +2: Surveil 2, then exile a card from a graveyard. | ||
ability = new LoyaltyAbility(new SurveilEffect(2, false), 2); | ||
ability.addEffect(new KayaSpiritsJusticeExileEffect().concatBy(", then")); | ||
this.addAbility(ability); | ||
// +1: Create a 1/1 white and black Spirit creature token with flying. | ||
ability = new LoyaltyAbility(new CreateTokenEffect(new WhiteBlackSpiritToken()), 1); | ||
this.addAbility(ability); | ||
// -2: Exile target creature you control. For each other player, exile up to one target creature that player controls. | ||
ability = new LoyaltyAbility(new ExileTargetEffect() | ||
.setText("exile target creature you control. For each other player, " + | ||
"exile up to one target creature that player controls") | ||
.setTargetPointer(new EachTargetPointer()), -2); | ||
ability.setTargetAdjuster(KayaSpiritsJusticeAdjuster.instance); | ||
this.addAbility(ability); | ||
} | ||
|
||
private KayaSpiritsJustice(final KayaSpiritsJustice card) { | ||
super(card); | ||
} | ||
|
||
@Override | ||
public KayaSpiritsJustice copy() { | ||
return new KayaSpiritsJustice(this); | ||
} | ||
} | ||
|
||
class KayaSpiritsJusticeTriggeredAbility extends TriggeredAbilityImpl { | ||
|
||
KayaSpiritsJusticeTriggeredAbility() { | ||
super(Zone.BATTLEFIELD, new KayaSpiritsJusticeCopyEffect(), false); | ||
setTriggerPhrase("Whenever one or more creatures you control and/or creature cards in your graveyard are put into exile, " + | ||
"you may choose a creature card from among them. Until end of turn, target token you control becomes a copy of it, " + | ||
"except it has flying."); | ||
} | ||
|
||
private KayaSpiritsJusticeTriggeredAbility(final KayaSpiritsJusticeTriggeredAbility ability) { | ||
super(ability); | ||
} | ||
|
||
@Override | ||
public KayaSpiritsJusticeTriggeredAbility copy() { | ||
return new KayaSpiritsJusticeTriggeredAbility(this); | ||
} | ||
|
||
@Override | ||
public boolean checkEventType(GameEvent event, Game game) { | ||
return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; | ||
} | ||
|
||
@Override | ||
public boolean checkTrigger(GameEvent event, Game game) { | ||
ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event; | ||
if (zEvent == null) { | ||
return false; | ||
} | ||
|
||
Set<Card> battlefieldCards = zEvent.getEvents() | ||
.stream() | ||
.filter(e -> e.getFromZone() == Zone.BATTLEFIELD) | ||
.filter(e -> e.getToZone() == Zone.EXILED) | ||
.map(ZoneChangeEvent::getTargetId) | ||
.filter(Objects::nonNull) | ||
.map(game::getCard) | ||
.filter(Objects::nonNull) | ||
.filter(card -> { | ||
Permanent permanent = game.getPermanentOrLKIBattlefield(card.getId()); | ||
return StaticFilters.FILTER_PERMANENT_CREATURE | ||
.match(permanent, getControllerId(), this, game); | ||
}) | ||
.collect(Collectors.toSet()); | ||
|
||
Set<Card> graveyardCards = zEvent.getEvents() | ||
.stream() | ||
.filter(e -> e.getFromZone() == Zone.GRAVEYARD) | ||
.filter(e -> e.getToZone() == Zone.EXILED) | ||
.map(ZoneChangeEvent::getTargetId) | ||
.filter(Objects::nonNull) | ||
.map(game::getCard) | ||
.filter(Objects::nonNull) | ||
.filter(card -> StaticFilters.FILTER_CARD_CREATURE | ||
.match(card, getControllerId(), this, game)) | ||
.collect(Collectors.toSet()); | ||
|
||
Set<Card> cards = new HashSet<>(battlefieldCards); | ||
cards.addAll(graveyardCards); | ||
if (cards.isEmpty()) { | ||
return false; | ||
} | ||
|
||
getEffects().setTargetPointer(new FixedTargets(new CardsImpl(cards), game)); | ||
return true; | ||
} | ||
} | ||
|
||
class KayaSpiritsJusticeCopyEffect extends OneShotEffect { | ||
|
||
KayaSpiritsJusticeCopyEffect() { | ||
super(Outcome.Copy); | ||
} | ||
|
||
private KayaSpiritsJusticeCopyEffect(final KayaSpiritsJusticeCopyEffect effect) { | ||
super(effect); | ||
} | ||
|
||
@Override | ||
public KayaSpiritsJusticeCopyEffect copy() { | ||
return new KayaSpiritsJusticeCopyEffect(this); | ||
} | ||
|
||
@Override | ||
public boolean apply(Game game, Ability source) { | ||
Player controller = game.getPlayer(source.getControllerId()); | ||
Permanent copyToPermanent = game.getPermanent(source.getFirstTarget()); | ||
Cards exiledCards = new CardsImpl(getTargetPointer().getTargets(game, source)); | ||
if (controller == null || copyToPermanent == null || exiledCards.isEmpty()) { | ||
return false; | ||
} | ||
|
||
TargetCard target = new TargetCardInExile(0, 1, StaticFilters.FILTER_CARD_CREATURE, null); | ||
if (!controller.chooseTarget(outcome, exiledCards, target, source, game)) { | ||
return false; | ||
} | ||
|
||
Card copyFromCard = game.getCard(target.getFirstTarget()); | ||
if (copyFromCard == null) { | ||
return false; | ||
} | ||
|
||
Permanent newBlueprint = new PermanentCard(copyFromCard, source.getControllerId(), game); | ||
newBlueprint.assignNewId(); | ||
CopyApplier applier = new KayaSpiritsJusticeCopyApplier(); | ||
applier.apply(game, newBlueprint, source, copyToPermanent.getId()); | ||
CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBlueprint, copyToPermanent.getId()); | ||
copyEffect.newId(); | ||
copyEffect.setApplier(applier); | ||
Ability newAbility = source.copy(); | ||
copyEffect.init(newAbility, game); | ||
game.addEffect(copyEffect, source); | ||
|
||
return true; | ||
} | ||
} | ||
|
||
class KayaSpiritsJusticeCopyApplier extends CopyApplier { | ||
|
||
@Override | ||
public boolean apply(Game game, MageObject blueprint, Ability source, UUID copyToObjectId) { | ||
blueprint.getAbilities().add(FlyingAbility.getInstance()); | ||
return true; | ||
} | ||
} | ||
|
||
class KayaSpiritsJusticeExileEffect extends OneShotEffect { | ||
|
||
KayaSpiritsJusticeExileEffect() { | ||
super(Outcome.Exile); | ||
staticText = "exile a card from a graveyard"; | ||
} | ||
|
||
private KayaSpiritsJusticeExileEffect(final KayaSpiritsJusticeExileEffect effect) { | ||
super(effect); | ||
} | ||
|
||
@Override | ||
public KayaSpiritsJusticeExileEffect copy() { | ||
return new KayaSpiritsJusticeExileEffect(this); | ||
} | ||
|
||
@Override | ||
public boolean apply(Game game, Ability source) { | ||
Player controller = game.getPlayer(source.getControllerId()); | ||
if (controller != null) { | ||
TargetCardInGraveyard target = new TargetCardInGraveyard(); | ||
target.withNotTarget(true); | ||
controller.choose(outcome, target, source, game); | ||
Card card = game.getCard(target.getFirstTarget()); | ||
if (card != null) { | ||
UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter()); | ||
MageObject sourceObject = source.getSourceObject(game); | ||
String exileName = sourceObject == null ? null : sourceObject.getIdName(); | ||
return controller.moveCardsToExile(card, source, game, true, exileId, exileName); | ||
} | ||
} | ||
return false; | ||
} | ||
} | ||
|
||
enum KayaSpiritsJusticeAdjuster implements TargetAdjuster { | ||
instance; | ||
|
||
@Override | ||
public void adjustTargets(Ability ability, Game game) { | ||
ability.getTargets().clear(); | ||
|
||
Player controller = game.getPlayer(ability.getControllerId()); | ||
if (controller == null) { | ||
return; | ||
} | ||
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_CREATURE)); | ||
|
||
for (UUID playerId : game.getState().getPlayersInRange(ability.getControllerId(), game)) { | ||
Player player = game.getPlayer(playerId); | ||
if (player == null || player == controller) { | ||
continue; | ||
} | ||
FilterPermanent filter = new FilterCreaturePermanent("creature that player controls"); | ||
filter.add(new ControllerIdPredicate(playerId)); | ||
ability.addTarget(new TargetPermanent(0, 1, filter) | ||
.withChooseHint("from " + player.getLogName())); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/KayaSpiritsJusticeTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package org.mage.test.cards.single.mkm; | ||
|
||
import mage.abilities.keyword.FlyingAbility; | ||
import mage.constants.PhaseStep; | ||
import mage.constants.Zone; | ||
import org.junit.Test; | ||
import org.mage.test.serverside.base.CardTestPlayerBase; | ||
|
||
/** | ||
* {@link mage.cards.k.KayaSpiritsJustice} | ||
* @author DominionSpy | ||
*/ | ||
public class KayaSpiritsJusticeTest extends CardTestPlayerBase { | ||
|
||
// Test first ability of Kaya | ||
@Test | ||
public void test_TriggeredAbility() { | ||
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1 + 2 + 6); | ||
addCard(Zone.BATTLEFIELD, playerA, "Kaya, Spirits' Justice"); | ||
xenohedron marked this conversation as resolved.
Show resolved
Hide resolved
|
||
addCard(Zone.BATTLEFIELD, playerA, "Llanowar Elves"); | ||
addCard(Zone.GRAVEYARD, playerA, "Fyndhorn Elves"); | ||
addCard(Zone.HAND, playerA, "Thraben Inspector"); | ||
addCard(Zone.HAND, playerA, "Astrid Peth"); | ||
addCard(Zone.HAND, playerA, "Farewell"); | ||
|
||
// Creates a Clue token | ||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thraben Inspector"); | ||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); | ||
|
||
// Creates a Food token | ||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Astrid Peth"); | ||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); | ||
|
||
// Exile all creatures and graveyards | ||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Farewell"); | ||
setModeChoice(playerA, "2"); | ||
setModeChoice(playerA, "4"); | ||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); | ||
|
||
// Kaya's first ability triggers twice, so choose which is put on the stack: | ||
// Whenever one or more creatures you control and/or creature cards in your graveyard are put into exile, | ||
// you may choose a creature card from among them. Until end of turn, target token you control becomes a copy of it, | ||
// except it has flying. | ||
setChoice(playerA, "Whenever", 1); | ||
// Trigger targets | ||
addTarget(playerA, "Clue Token"); | ||
addTarget(playerA, "Food Token"); | ||
// Copy choices | ||
addTarget(playerA, "Fyndhorn Elves"); | ||
addTarget(playerA, "Llanowar Elves"); | ||
|
||
setStrictChooseMode(true); | ||
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); | ||
execute(); | ||
|
||
assertPermanentCount(playerA, "Clue Token", 0); | ||
assertPermanentCount(playerA, "Fyndhorn Elves", 1); | ||
assertAbility(playerA, "Fyndhorn Elves", FlyingAbility.getInstance(), true); | ||
assertPermanentCount(playerA, "Food Token", 0); | ||
assertPermanentCount(playerA, "Llanowar Elves", 1); | ||
assertAbility(playerA, "Llanowar Elves", FlyingAbility.getInstance(), true); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compare UUID with equals rather than player objects