[LCI] Implement Deeproot Pilgrimage (#11350)

This commit is contained in:
Susucre 2023-10-26 18:06:10 +02:00 committed by GitHub
parent 77b9faad84
commit 27b8d3e198
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 298 additions and 4 deletions

View file

@ -0,0 +1,46 @@
package mage.cards.d;
import mage.abilities.common.BecomesTappedOneOrMoreTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.permanent.TokenPredicate;
import mage.game.permanent.token.MerfolkHexproofToken;
import java.util.UUID;
/**
* @author Susucr
*/
public final class DeeprootPilgrimage extends CardImpl {
private static final FilterPermanent filter = new FilterControlledPermanent(SubType.MERFOLK, "nontoken Merfolk you control");
static {
filter.add(TokenPredicate.FALSE);
}
public DeeprootPilgrimage(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}");
// Whenever one or more nontoken Merfolk you control become tapped, create a 1/1 blue Merfolk creature token with hexproof.
this.addAbility(new BecomesTappedOneOrMoreTriggeredAbility(
Zone.BATTLEFIELD, new CreateTokenEffect(new MerfolkHexproofToken()), false, filter
));
}
private DeeprootPilgrimage(final DeeprootPilgrimage card) {
super(card);
}
@Override
public DeeprootPilgrimage copy() {
return new DeeprootPilgrimage(this);
}
}

View file

@ -27,6 +27,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet {
cards.add(new SetCardInfo("Cavern of Souls", 269, Rarity.MYTHIC, mage.cards.c.CavernOfSouls.class));
cards.add(new SetCardInfo("Cenote Scout", 178, Rarity.UNCOMMON, mage.cards.c.CenoteScout.class));
cards.add(new SetCardInfo("Chart a Course", 48, Rarity.UNCOMMON, mage.cards.c.ChartACourse.class));
cards.add(new SetCardInfo("Deeproot Pilgrimage", 52, Rarity.RARE, mage.cards.d.DeeprootPilgrimage.class));
cards.add(new SetCardInfo("Didact Echo", 53, Rarity.COMMON, mage.cards.d.DidactEcho.class));
cards.add(new SetCardInfo("Dinotomaton", 144, Rarity.COMMON, mage.cards.d.Dinotomaton.class));
cards.add(new SetCardInfo("Forest", 401, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS));

View file

@ -0,0 +1,102 @@
package org.mage.test.cards.single.lci;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class DeeprootPilgrimageTest extends CardTestPlayerBase {
/**
* {@link mage.cards.d.DeeprootPilgrimage} <br>
* Deeproot Pilgrimage {1}{U} <br>
* Enchantment <br>
* Whenever one or more nontoken Merfolk you control become tapped, create a 1/1 blue Merfolk creature token with hexproof.
*/
private static final String pilgrimage = "Deeproot Pilgrimage";
// {2}{U}{U} Sorcery
// Tap all creatures target player controls. Those creatures dont untap during that players next untap step.
private static final String sleep = "Sleep";
// 3/2 Vanilla merfolk
private static final String commando = "Coral Commando";
@Test
public void test_batch_tapped() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, pilgrimage);
addCard(Zone.BATTLEFIELD, playerA, commando, 4);
addCard(Zone.HAND, playerA, sleep);
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sleep", playerA);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertTappedCount(commando, true, 4);
assertPermanentCount(playerA, "Merfolk Token", 1);
}
@Test
public void test_triggering_on_attack() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, pilgrimage);
addCard(Zone.BATTLEFIELD, playerA, commando);
attack(1, playerA, commando, playerB);
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
execute();
assertTappedCount(commando, true, 1);
assertPermanentCount(playerA, "Merfolk Token", 1);
}
@Test
public void test_not_triggering_on_non_merfolk() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, pilgrimage);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); // 2/2, not a merfolk.
addCard(Zone.HAND, playerA, sleep);
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sleep", playerA);
// no trigger, bears is no Merfolk
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertTappedCount("Grizzly Bears", true, 1);
assertPermanentCount(playerA, "Merfolk Token", 0);
}
@Test
public void test_not_triggering_on_opp_merfolks() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, pilgrimage);
addCard(Zone.BATTLEFIELD, playerB, commando);
addCard(Zone.HAND, playerA, sleep);
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sleep", playerB);
// no trigger, as Pilgrimage only triggers on Merfolk you control
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertTappedCount(commando, true, 1);
assertPermanentCount(playerA, "Merfolk Token", 0);
}
}

View file

@ -0,0 +1,48 @@
package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.TappedBatchEvent;
/**
* @author Susucr
*/
public class BecomesTappedOneOrMoreTriggeredAbility extends TriggeredAbilityImpl {
protected FilterPermanent filter;
public BecomesTappedOneOrMoreTriggeredAbility(Zone zone, Effect effect, boolean optional, FilterPermanent filter) {
super(zone, effect, optional);
this.filter = filter;
setTriggerPhrase("Whenever one or more " + filter.getMessage() + " become tapped, ");
}
protected BecomesTappedOneOrMoreTriggeredAbility(final BecomesTappedOneOrMoreTriggeredAbility ability) {
super(ability);
this.filter = ability.filter.copy();
}
@Override
public BecomesTappedOneOrMoreTriggeredAbility copy() {
return new BecomesTappedOneOrMoreTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.TAPPED_BATCH;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
TappedBatchEvent batchEvent = (TappedBatchEvent) event;
return batchEvent
.getTargets()
.stream()
.map(game::getPermanent)
.anyMatch(p -> filter.match(p, getControllerId(), this, game));
}
}

View file

@ -1,5 +1,6 @@
package mage.game;
import static java.util.Collections.emptyList;
import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.*;
@ -44,8 +45,6 @@ import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
/**
* @author BetaSteward_at_googlemail.com
* <p>
@ -860,6 +859,27 @@ public class GameState implements Serializable, Copyable<GameState> {
}
}
public void addSimultaneousTapped(TappedEvent tappedEvent, Game game) {
// Combine multiple tapped events in the single event (batch)
boolean isTappedBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (event instanceof TappedBatchEvent) {
// Adding to the existing batch
((TappedBatchEvent) event).addEvent(tappedEvent);
isTappedBatchUsed = true;
break;
}
}
// new batch
if (!isTappedBatchUsed) {
TappedBatchEvent batch = new TappedBatchEvent();
batch.addEvent(tappedEvent);
addSimultaneousEvent(batch, game);
}
}
public void handleEvent(GameEvent event, Game game) {
watchers.watch(event, game);
delayed.checkTriggers(event, game);

View file

@ -350,12 +350,17 @@ public class GameEvent implements Serializable {
flag is it tapped for combat
*/
TAPPED,
TAPPED_FOR_MANA,
/* TAPPED_FOR_MANA
During calculation of the available mana for a player the "TappedForMana" event is fired to simulate triggered mana production.
By checking the inCheckPlayableState these events are handled to give back only the available mana of instead really producing mana.
IMPORTANT: Triggered non mana abilities have to ignore the event if game.inCheckPlayableState is true.
*/
TAPPED_FOR_MANA,
/* TAPPED_BATCH
combine all TAPPED events occuring at the same time in a single event
*/
TAPPED_BATCH,
UNTAP, UNTAPPED,
FLIP, FLIPPED,
TRANSFORMING, TRANSFORMED,

View file

@ -0,0 +1,56 @@
package mage.game.events;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author Susucr
*/
public class TappedBatchEvent extends GameEvent implements BatchGameEvent<TappedEvent> {
private final Set<TappedEvent> events = new HashSet<>();
public TappedBatchEvent() {
super(EventType.TAPPED_BATCH, null, null, null);
}
@Override
public Set<TappedEvent> getEvents() {
return events;
}
@Override
public Set<UUID> getTargets() {
return events.stream()
.map(GameEvent::getTargetId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
@Override
public int getAmount() {
return events
.stream()
.mapToInt(GameEvent::getAmount)
.sum();
}
@Override
@Deprecated // events can store a diff value, so search it from events list instead
public UUID getTargetId() {
throw new IllegalStateException("Wrong code usage. Must search value from a getEvents list or use CardUtil.getEventTargets(event)");
}
@Override
@Deprecated // events can store a diff value, so search it from events list instead
public UUID getSourceId() {
throw new IllegalStateException("Wrong code usage. Must search value from a getEvents list.");
}
public void addEvent(TappedEvent event) {
this.events.add(event);
}
}

View file

@ -0,0 +1,14 @@
package mage.game.events;
import mage.abilities.Ability;
import java.util.UUID;
/**
* @author Susucr
*/
public class TappedEvent extends GameEvent {
public TappedEvent(UUID targetId, Ability source, UUID playerId, boolean forCombat) {
super(EventType.TAPPED, targetId, source, playerId, 0, forCombat);
}
}

View file

@ -561,7 +561,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
//20091005 - 701.15a
if (!tapped && !replaceEvent(EventType.TAP, game)) {
this.tapped = true;
game.fireEvent(new GameEvent(GameEvent.EventType.TAPPED, objectId, source, source == null ? null : source.getControllerId(), 0, forCombat));
TappedEvent event = new TappedEvent(objectId, source, source == null ? null : source.getControllerId(), forCombat);
game.fireEvent(event);
game.getState().addSimultaneousTapped(event, game);
return true;
}
return false;