[LCI] Implement The Millennium Calendar (#11359)

new UNTAPPED_BATCH event.
This commit is contained in:
Susucre 2023-10-29 12:43:24 +01:00 committed by GitHub
parent 9ff307cefa
commit 0c485ec593
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 319 additions and 6 deletions

View file

@ -0,0 +1,147 @@
package mage.cards.t;
import mage.abilities.Ability;
import mage.abilities.StateTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.DoubleCountersSourceEffect;
import mage.abilities.effects.common.LoseLifeOpponentsEffect;
import mage.abilities.effects.common.SacrificeSourceEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.hint.StaticHint;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.UntappedBatchEvent;
import mage.game.events.UntappedEvent;
import mage.game.permanent.Permanent;
import java.util.Objects;
import java.util.UUID;
/**
* @author Susucr
*/
public final class TheMillenniumCalendar extends CardImpl {
public TheMillenniumCalendar(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}");
this.supertype.add(SuperType.LEGENDARY);
// Whenever you untap one or more permanents during your untap step, put that many time counters on The Millennium Calendar.
this.addAbility(new TheMillenniumCalendarTriggeredAbility());
// {2}, {T}: Double the number of time counters on The Millennium Calendar.
Ability ability = new SimpleActivatedAbility(
new DoubleCountersSourceEffect(CounterType.TIME),
new ManaCostsImpl<>("{2}")
);
ability.addCost(new TapSourceCost());
this.addAbility(ability);
// When there are 1,000 or more time counters on The Millennium Calendar, sacrifice it and each opponent loses 1,000 life.
this.addAbility(new TheMillenniumCalendarStateTriggeredAbility());
}
private TheMillenniumCalendar(final TheMillenniumCalendar card) {
super(card);
}
@Override
public TheMillenniumCalendar copy() {
return new TheMillenniumCalendar(this);
}
}
class TheMillenniumCalendarTriggeredAbility extends TriggeredAbilityImpl {
public TheMillenniumCalendarTriggeredAbility() {
super(Zone.BATTLEFIELD, null, false);
}
protected TheMillenniumCalendarTriggeredAbility(final TheMillenniumCalendarTriggeredAbility ability) {
super(ability);
}
@Override
public TheMillenniumCalendarTriggeredAbility copy() {
return new TheMillenniumCalendarTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.UNTAPPED_BATCH;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!game.isActivePlayer(getControllerId())) {
return false;
}
UntappedBatchEvent batchEvent = (UntappedBatchEvent) event;
int count = batchEvent
.getEvents()
.stream()
.filter(UntappedEvent::isAnUntapStepEvent)
.map(UntappedEvent::getTargetId)
.map(game::getPermanent)
.filter(Objects::nonNull)
.filter(p -> p.getControllerId().equals(getControllerId()))
.mapToInt(p -> 1)
.sum();
if (count <= 0) {
return false;
}
this.getEffects().clear();
this.addEffect(new AddCountersSourceEffect(CounterType.TIME.createInstance(count)));
this.getHints().clear();
this.addHint(new StaticHint("Number of untapped permanents: " + count));
return true;
}
private static final String infoKey = "number_untapped";
@Override
public String getRule() {
return "Whenever you untap one or more permanents during your untap step, "
+ "put that many time counters on {this}.";
}
}
/**
* Inspired by {@link mage.cards.n.NineLives}
*/
class TheMillenniumCalendarStateTriggeredAbility extends StateTriggeredAbility {
public TheMillenniumCalendarStateTriggeredAbility() {
super(Zone.BATTLEFIELD, new SacrificeSourceEffect());
withRuleTextReplacement(true);
addEffect(new LoseLifeOpponentsEffect(1000).concatBy("and"));
setTriggerPhrase("When there are 1,000 or more time counters on {this}, ");
}
private TheMillenniumCalendarStateTriggeredAbility(final TheMillenniumCalendarStateTriggeredAbility ability) {
super(ability);
}
@Override
public TheMillenniumCalendarStateTriggeredAbility copy() {
return new TheMillenniumCalendarStateTriggeredAbility(this);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Permanent permanent = game.getPermanent(getSourceId());
return permanent != null && permanent.getCounters(game).getCount(CounterType.TIME) >= 1000;
}
}

View file

@ -123,6 +123,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet {
cards.add(new SetCardInfo("The Belligerent", 225, Rarity.RARE, mage.cards.t.TheBelligerent.class));
cards.add(new SetCardInfo("The Core", 256, Rarity.RARE, mage.cards.t.TheCore.class));
cards.add(new SetCardInfo("The Grim Captain", 266, Rarity.RARE, mage.cards.t.TheGrimCaptain.class));
cards.add(new SetCardInfo("The Millennium Calendar", 257, Rarity.MYTHIC, mage.cards.t.TheMillenniumCalendar.class));
cards.add(new SetCardInfo("The Skullspore Nexus", 212, Rarity.MYTHIC, mage.cards.t.TheSkullsporeNexus.class));
cards.add(new SetCardInfo("Thrashing Brontodon", 216, Rarity.UNCOMMON, mage.cards.t.ThrashingBrontodon.class));
cards.add(new SetCardInfo("Threefold Thunderhulk", 265, Rarity.RARE, mage.cards.t.ThreefoldThunderhulk.class));

View file

@ -0,0 +1,56 @@
package org.mage.test.cards.single.lci;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class TheMillenniumCalendarTest extends CardTestPlayerBase {
/**
* {@link mage.cards.t.TheMillenniumCalendar} <br>
* The Millennium Calendar {1} <br>
* Legendary Artifact <br>
* Whenever you untap one or more permanents during your untap step, put that many time counters on The Millennium Calendar. <br>
* {2}, {T}: Double the number of time counters on The Millennium Calendar. <br>
* When there are 1,000 or more time counters on The Millennium Calendar, sacrifice it and each opponent loses 1,000 life. <br>
*/
private static final String calendar = "The Millennium Calendar";
@Test
public void test_untap_effect_not_triggering() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, calendar);
addCard(Zone.BATTLEFIELD, playerA, "Aphetto Alchemist"); // {T}: Untap target artifact or creature.
activateAbility(1, PhaseStep.UPKEEP, playerA, "{T}: Untap target artifact or creature.", "Aphetto Alchemist");
setStopAt(1, PhaseStep.DRAW);
execute();
assertCounterCount(calendar, CounterType.TIME, 0);
}
@Test
public void test_untap_trigger() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, calendar);
addCard(Zone.BATTLEFIELD, playerA, "Raging Goblin", 10);
for (int i = 0; i < 10; ++i) {
attack(1, playerA, "Raging Goblin", playerB);
}
setStopAt(3, PhaseStep.DRAW);
execute();
assertCounterCount(calendar, CounterType.TIME, 10);
assertLife(playerB, 20 - 10);
}
}

View file

@ -36,9 +36,7 @@ import mage.server.util.SystemUtil;
import mage.sets.TherosBeyondDeath;
import mage.target.targetpointer.TargetPointer;
import mage.util.CardUtil;
import mage.verify.mtgjson.MtgJsonCard;
import mage.verify.mtgjson.MtgJsonService;
import mage.verify.mtgjson.MtgJsonSet;
import mage.verify.mtgjson.*;
import mage.watchers.Watcher;
import org.apache.log4j.Logger;
import org.junit.Assert;

View file

@ -880,6 +880,27 @@ public class GameState implements Serializable, Copyable<GameState> {
}
}
public void addSimultaneousUntapped(UntappedEvent untappedEvent, Game game) {
// Combine multiple untapped events in the single event (batch)
boolean isUntappedBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (event instanceof UntappedBatchEvent) {
// Adding to the existing batch
((UntappedBatchEvent) event).addEvent(untappedEvent);
isUntappedBatchUsed = true;
break;
}
}
// new batch
if (!isUntappedBatchUsed) {
UntappedBatchEvent batch = new UntappedBatchEvent();
batch.addEvent(untappedEvent);
addSimultaneousEvent(batch, game);
}
}
public void handleEvent(GameEvent event, Game game) {
watchers.watch(event, game);
delayed.checkTriggers(event, game);

View file

@ -360,8 +360,19 @@ public class GameEvent implements Serializable {
combine all TAPPED events occuring at the same time in a single event
*/
TAPPED_BATCH,
UNTAP, UNTAPPED,
UNTAP,
/* UNTAPPED,
targetId untapped permanent
sourceId not used for this event // TODO: add source for untap?
playerId controller of permanent // TODO: replace by source controller of untap? need to check every usage if so.
amount not used for this event
flag true if untapped during untap step (event is checked at upkeep so can't trust the current Phase)
*/
UNTAPPED,
/* UNTAPPED_BATCH
combine all UNTAPPED events occuring at the same time in a single event
*/
UNTAPPED_BATCH,
FLIP, FLIPPED,
TRANSFORMING, TRANSFORMED,
ADAPT,

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 UntappedBatchEvent extends GameEvent implements BatchGameEvent<UntappedEvent> {
private final Set<UntappedEvent> events = new HashSet<>();
public UntappedBatchEvent() {
super(EventType.UNTAPPED_BATCH, null, null, null);
}
@Override
public Set<UntappedEvent> 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(UntappedEvent event) {
this.events.add(event);
}
}

View file

@ -0,0 +1,16 @@
package mage.game.events;
import java.util.UUID;
/**
* @author Susucr
*/
public class UntappedEvent extends GameEvent {
public UntappedEvent(UUID targetId, UUID playerId, boolean duringUntapPhase) {
super(EventType.UNTAPPED, targetId, null, playerId, 0, duringUntapPhase);
}
public boolean isAnUntapStepEvent() {
return this.flag;
}
}

View file

@ -545,7 +545,14 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
//20091005 - 701.15b
if (tapped && !replaceEvent(EventType.UNTAP, game)) {
this.tapped = false;
fireEvent(EventType.UNTAPPED, game);
UntappedEvent event = new UntappedEvent(
objectId, this.controllerId,
// Since triggers are not checked until the next step,
// we use the event flag to know if untapping was done during the untap step
game.getTurnStepType() == PhaseStep.UNTAP
);
game.fireEvent(event);
game.getState().addSimultaneousUntapped(event, game);
return true;
}
return false;