Skip to content
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

[CLU] Implement Amzu, Swarm's Hunger and batches for zone change events #12686

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
120 changes: 120 additions & 0 deletions Mage.Sets/src/mage/cards/a/AmzuSwarmsHunger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package mage.cards.a;

import java.util.Set;
import java.util.UUID;
import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.CardsLeaveGraveyardTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.continuous.GainAbilityControlledEffect;
import mage.cards.Card;
import mage.constants.*;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.MenaceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.IzoniInsectToken;

/**
*
* @author jimga150
*/
public final class AmzuSwarmsHunger extends CardImpl {

private static final FilterPermanent filter = new FilterPermanent(SubType.INSECT, "Insects");

public AmzuSwarmsHunger(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}{G}");

this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.INSECT);
this.subtype.add(SubType.SHAMAN);
this.power = new MageInt(3);
this.toughness = new MageInt(3);

// Flying
this.addAbility(FlyingAbility.getInstance());

// Menace
this.addAbility(new MenaceAbility());

// Other Insects you control have menace.
this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect(
new MenaceAbility(false), Duration.WhileOnBattlefield,
filter, true
)));

// Whenever one or more cards leave your graveyard, you may create a 1/1 black and green Insect creature token,
// then put a number of +1/+1 counters on it equal to the greatest mana value among those cards.
// Do this only once each turn.
this.addAbility(new CardsLeaveGraveyardTriggeredAbility(new AmzuSwarmsHungerEffect()).setDoOnlyOnceEachTurn(true));
}

private AmzuSwarmsHunger(final AmzuSwarmsHunger card) {
super(card);
}

@Override
public AmzuSwarmsHunger copy() {
return new AmzuSwarmsHunger(this);
}
}

// Based on OutlawStitcherEffect
class AmzuSwarmsHungerEffect extends OneShotEffect {

AmzuSwarmsHungerEffect() {
super(Outcome.PutCreatureInPlay);
staticText = "create a 1/1 black and green Insect creature token, " +
"then put a number of +1/+1 counters on it equal to the greatest mana value among those cards.";
}

private AmzuSwarmsHungerEffect(final AmzuSwarmsHungerEffect effect) {
super(effect);
}

@Override
public AmzuSwarmsHungerEffect copy() {
return new AmzuSwarmsHungerEffect(this);
}

@Override
public boolean apply(Game game, Ability source) {
CreateTokenEffect effect = new CreateTokenEffect(new IzoniInsectToken());
boolean result = effect.apply(game, source);
xenohedron marked this conversation as resolved.
Show resolved Hide resolved
if (!result){
return false;
}

Object cardsLeavingGraveyardObj = this.getValue("cardsLeavingGraveyard");
if (!(cardsLeavingGraveyardObj instanceof Set)) {
return false;
}
Set<Card> cardsLeavingGraveyard = (Set<Card>) cardsLeavingGraveyardObj;
int xvalue = cardsLeavingGraveyard
.stream()
.mapToInt(MageObject::getManaValue)
.max()
.orElse(0);

if (xvalue <= 0) {
return true;
}
for (UUID id : effect.getLastAddedTokenIds()) {
Permanent token = game.getPermanent(id);
if (token == null) {
continue;
}
token.addCounters(CounterType.P1P1.createInstance(xvalue), source.getControllerId(), source, game);
}
return true;
}

}
1 change: 1 addition & 0 deletions Mage.Sets/src/mage/sets/RavnicaClueEdition.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ private RavnicaClueEdition() {
cards.add(new SetCardInfo("Affectionate Indrik", 155, Rarity.UNCOMMON, mage.cards.a.AffectionateIndrik.class));
cards.add(new SetCardInfo("Afterlife Insurance", 23, Rarity.UNCOMMON, mage.cards.a.AfterlifeInsurance.class));
cards.add(new SetCardInfo("Ajani's Pridemate", 52, Rarity.UNCOMMON, mage.cards.a.AjanisPridemate.class));
cards.add(new SetCardInfo("Amzu, Swarm's Hunger", 24, Rarity.RARE, mage.cards.a.AmzuSwarmsHunger.class));
cards.add(new SetCardInfo("Angel of Vitality", 53, Rarity.UNCOMMON, mage.cards.a.AngelOfVitality.class));
cards.add(new SetCardInfo("Apothecary White", 1, Rarity.RARE, mage.cards.a.ApothecaryWhite.class));
cards.add(new SetCardInfo("Azorius Arrester", 54, Rarity.COMMON, mage.cards.a.AzoriusArrester.class));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@

package org.mage.test.cards.single.clu;

import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;

/**
* @author Susucr
*/
public class AmzuSwarmsHungerTest extends CardTestPlayerBase {

/**
* {@link mage.cards.a.AmzuSwarmsHunger Amzu, Swarm's Hunger} {3}{B}{G}
* Legendary Creature — Insect Shaman
* Flying, menace
* Other Insects you control have menace.
* Whenever one or more cards leave your graveyard, you may create a 1/1 black and green Insect creature token,
* then put a number of +1/+1 counters on it equal to the greatest mana value among those cards.
* Do this only once each turn.
* 3/3
*/
private static final String amzu = "Amzu, Swarm's Hunger";

@Test
public void testTwoCreatures() {

addCard(Zone.BATTLEFIELD, playerA, amzu);
addCard(Zone.BATTLEFIELD, playerA, "Zask, Skittering Swarmlord");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4);
addCard(Zone.GRAVEYARD, playerA, "Battlefly Swarm");
addCard(Zone.GRAVEYARD, playerA, "Saber Ants");

castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Saber Ants", true);
setChoice(playerA, "Yes");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Battlefly Swarm", true);

setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

assertPermanentCount(playerA, "Insect Token", 1);
assertCounterCount(playerA, "Insect Token", CounterType.P1P1, 4);
}

@Test
public void testGraveyardExile() {

addCard(Zone.BATTLEFIELD, playerA, amzu);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.GRAVEYARD, playerA, "Living Hive");
addCard(Zone.GRAVEYARD, playerA, "Saber Ants");
addCard(Zone.GRAVEYARD, playerA, "Blasphemous Act"); // test non-permanent card type
addCard(Zone.HAND, playerA, "Rakdos Charm");

castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rakdos Charm", true);
setModeChoice(playerA, "1"); // Exile target player’s graveyard.
addTarget(playerA, playerA);
setChoice(playerA, "Yes");

setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();

assertPermanentCount(playerA, "Insect Token", 1);
assertCounterCount(playerA, "Insect Token", CounterType.P1P1, 9);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeGroupEvent;
import mage.game.events.ZoneChangeBatchEvent;

import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
* @author TheElk801
Expand All @@ -36,22 +38,32 @@ private CardsLeaveGraveyardTriggeredAbility(final CardsLeaveGraveyardTriggeredAb

@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE_GROUP;
return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH;
}

@Override
public boolean checkTrigger(GameEvent event, Game game) {
ZoneChangeGroupEvent zEvent = (ZoneChangeGroupEvent) event;
return zEvent != null
&& Zone.GRAVEYARD == zEvent.getFromZone()
&& Zone.GRAVEYARD != zEvent.getToZone()
&& zEvent.getCards() != null
&& zEvent.getCards()
ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event;
if (zEvent == null){
return false;
}
Set<Card> cards = zEvent.getEvents()
.stream()
.filter(Objects::nonNull)
.filter(ev -> ev.getFromZone() == Zone.GRAVEYARD)
.filter(ev -> ev.getToZone() != Zone.GRAVEYARD)
.map(GameEvent::getTargetId)
.map(game::getCard)
.filter(Objects::nonNull)
.filter(card -> filter.match(card, getControllerId(), this, game))
.map(Card::getOwnerId)
.anyMatch(this::isControlledBy);
.filter(card -> this.isControlledBy(card.getOwnerId()))
.collect(Collectors.toSet());

if (cards.isEmpty()){
return false;
}
this.getAllEffects().setValue("cardsLeavingGraveyard", cards);
xenohedron marked this conversation as resolved.
Show resolved Hide resolved
return true;
}

@Override
Expand Down
8 changes: 8 additions & 0 deletions Mage/src/main/java/mage/game/ZonesHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.command.Commander;
import mage.game.events.ZoneChangeBatchEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.events.ZoneChangeGroupEvent;
import mage.game.permanent.Permanent;
Expand All @@ -27,6 +28,7 @@ public final class ZonesHandler {
public static boolean cast(ZoneChangeInfo info, Ability source, Game game) {
if (maybeRemoveFromSourceZone(info, game, source)) {
placeInDestinationZone(info, 0, source, game);

// create a group zone change event if a card is moved to stack for casting (it's always only one card, but some effects check for group events (one or more xxx))
Set<Card> cards = new HashSet<>();
Set<PermanentToken> tokens = new HashSet<>();
Expand All @@ -44,6 +46,12 @@ public static boolean cast(ZoneChangeInfo info, Ability source, Game game) {
info.event.getPlayerId(),
info.event.getFromZone(),
info.event.getToZone()));

// Fire batch event for cards moving to stack as well
ZoneChangeBatchEvent batchEvent = new ZoneChangeBatchEvent();
batchEvent.addEvent(info.event);
game.fireEvent(batchEvent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm.... why is this needed? That is, why does the ZoneChangeBatchEvent code from #11753 not cover it?

Any thoughts @JayDi85 or @DominionSpy ? It was previously stated that batch logic was correct and ZONE_CHANGE_GROUP should be deprecated in favor of ZONE_CHANGE_BATCH (see #11895 and #8009), but seems like it may have been incomplete. Is this change sufficient to cover all necessary usages?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this specific case, i found that the batch event wasn't being fired. I know there's code to collect simultaneous events and generate batch events for them, but they wouldn't apply here since things can't be cast simultaneously as far as i know.

Maybe some test cases should be written to check for batch zone change events firing 1:1 with single zone change events covering each instance of making a new zone change event? I could take a shot, but it sounds out of scope for this PR.

Copy link
Member

@JayDi85 JayDi85 Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// create a group zone change event if a card is moved to stack for casting (it's always only one card, but some effects check for group events (one or more xxx))

Need more research, I remember there were some use case with not fired batch or like that. Current code looks like a workaround, but it can be a more deep problem with firing and batching events logic (if same event can be called directly and simultaneously in diff use cases). I’ll try to research it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, can split this PR and implement the card without reworking the trigger to the batch event. Sorry that ended up being nontrivial.


// normal movement
game.fireEvent(info.event);
return true;
Expand Down
Loading