mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
[LCI] Implement The Millennium Calendar (#11359)
new UNTAPPED_BATCH event.
This commit is contained in:
parent
9ff307cefa
commit
0c485ec593
9 changed files with 319 additions and 6 deletions
147
Mage.Sets/src/mage/cards/t/TheMillenniumCalendar.java
Normal file
147
Mage.Sets/src/mage/cards/t/TheMillenniumCalendar.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 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 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 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("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("Thrashing Brontodon", 216, Rarity.UNCOMMON, mage.cards.t.ThrashingBrontodon.class));
|
||||||
cards.add(new SetCardInfo("Threefold Thunderhulk", 265, Rarity.RARE, mage.cards.t.ThreefoldThunderhulk.class));
|
cards.add(new SetCardInfo("Threefold Thunderhulk", 265, Rarity.RARE, mage.cards.t.ThreefoldThunderhulk.class));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,9 +36,7 @@ import mage.server.util.SystemUtil;
|
||||||
import mage.sets.TherosBeyondDeath;
|
import mage.sets.TherosBeyondDeath;
|
||||||
import mage.target.targetpointer.TargetPointer;
|
import mage.target.targetpointer.TargetPointer;
|
||||||
import mage.util.CardUtil;
|
import mage.util.CardUtil;
|
||||||
import mage.verify.mtgjson.MtgJsonCard;
|
import mage.verify.mtgjson.*;
|
||||||
import mage.verify.mtgjson.MtgJsonService;
|
|
||||||
import mage.verify.mtgjson.MtgJsonSet;
|
|
||||||
import mage.watchers.Watcher;
|
import mage.watchers.Watcher;
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
public void handleEvent(GameEvent event, Game game) {
|
||||||
watchers.watch(event, game);
|
watchers.watch(event, game);
|
||||||
delayed.checkTriggers(event, game);
|
delayed.checkTriggers(event, game);
|
||||||
|
|
|
||||||
|
|
@ -360,8 +360,19 @@ public class GameEvent implements Serializable {
|
||||||
combine all TAPPED events occuring at the same time in a single event
|
combine all TAPPED events occuring at the same time in a single event
|
||||||
*/
|
*/
|
||||||
TAPPED_BATCH,
|
TAPPED_BATCH,
|
||||||
|
UNTAP,
|
||||||
UNTAP, UNTAPPED,
|
/* 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,
|
FLIP, FLIPPED,
|
||||||
TRANSFORMING, TRANSFORMED,
|
TRANSFORMING, TRANSFORMED,
|
||||||
ADAPT,
|
ADAPT,
|
||||||
|
|
|
||||||
56
Mage/src/main/java/mage/game/events/UntappedBatchEvent.java
Normal file
56
Mage/src/main/java/mage/game/events/UntappedBatchEvent.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Mage/src/main/java/mage/game/events/UntappedEvent.java
Normal file
16
Mage/src/main/java/mage/game/events/UntappedEvent.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -545,7 +545,14 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
||||||
//20091005 - 701.15b
|
//20091005 - 701.15b
|
||||||
if (tapped && !replaceEvent(EventType.UNTAP, game)) {
|
if (tapped && !replaceEvent(EventType.UNTAP, game)) {
|
||||||
this.tapped = false;
|
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 true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue