Add Saga mechanic (#1564)

* Add AtBeginOfFirstMainPhaseTrigger and adding lore counter to saga

* Make add counter trigger more generic

* Change begin main phase trigger to Saga-specific

* Add magic.data.RomanToInt

* Add chapter parsing to ARG

* Add SagaChapterTrigger

* Add chapter ability to MagicAbility

* Add lore counter mechanic to Saga card definiton

* Add Saga sacrifice state-based action

* Fix import

* Fix getPendingTriggers

* Add History of Benalia

* Fix adding ETB trigger

* Remove AtBeginningOfFirstMainPhaseTrigger.Saga, Add counters directly in MagicMainPhase.executeBeginStep()

* Remove null case from RomanToInt

* Revert "Remove AtBeginningOfFirstMainPhaseTrigger.Saga, Add counters directly in MagicMainPhase.executeBeginStep()"

This reverts commit d8e5d1f253ab7b54f401f4151b44d39ee927f44c.

* Move isSaga method to IRenderableCard
master
Ada Joule 2018-05-04 13:17:55 +07:00 committed by Melvin Zhang
parent fb306e34a8
commit d213884dc2
11 changed files with 141 additions and 2 deletions

View File

@ -5,7 +5,7 @@ rarity=M
type=Enchantment
subtype=Saga
cost={1}{W}{W}
ability=Create a 2/2 white Knight creature token with vigilance.;\
Knights you control get +2/+1 until end of turn.
ability=I, II — Create a 2/2 white Knight creature token with vigilance.;\
III — Knights you control get +2/+1 until end of turn.
timing=enchantment
oracle=I, II — Create a 2/2 white Knight creature token with vigilance.\nIII — Knights you control get +2/+1 until end of turn.

View File

@ -0,0 +1,14 @@
package magic.data;
public class RomanToInt {
public static int convert(String num) {
// Only support 1-3 for now since Sagas only have these numbers
switch (num) {
case "I": return 1;
case "II": return 2;
case "III": return 3;
default: throw new RuntimeException("unknown roman numeral \"" + num + "\"");
}
}
}

View File

@ -1,9 +1,11 @@
package magic.model;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import magic.data.EnglishToInt;
import magic.data.RomanToInt;
import magic.model.action.MagicPlayMod;
import magic.model.event.MagicEvent;
import magic.model.stack.MagicItemOnStack;
@ -320,4 +322,11 @@ public class ARG {
return MagicTargetFilterFactory.SPELL_OR_ABILITY;
}
}
public static final String CHAPTERS = "(?<chapters>I+(, I+)*)";
public static int[] chapters(final Matcher m) {
final String[] chapters = m.group("chapters").split(", ");
return Arrays.stream(chapters).mapToInt(RomanToInt::convert).toArray();
}
}

View File

@ -78,6 +78,10 @@ public interface IRenderableCard {
return hasType(MagicType.Sorcery);
}
default boolean isSaga() {
return isEnchantment() && hasSubType(MagicSubType.Saga);
}
default boolean isHidden() {
return getCardDefinition().isHidden();
}

View File

@ -639,6 +639,16 @@ public enum MagicAbility {
card.add(MagicStatic.Ascend);
}
},
Chapter(ARG.CHAPTERS + " — " + ARG.EFFECT, 0) {
@Override
protected void addAbilityImpl(final MagicAbilityStore card, final Matcher arg) {
final int[] chapters = ARG.chapters(arg);
final MagicSourceEvent sourceEvent = MagicRuleEventAction.create(ARG.effect(arg));
for (int chapter : chapters) {
card.add(SagaChapterTrigger.create(chapter, sourceEvent));
}
}
},
// abilities that involve SN
ShockLand("As SN enters the battlefield, you may " + ARG.COST + "\\. If you don't, SN enters the battlefield tapped\\.", -10) {

View File

@ -9,6 +9,7 @@ import magic.model.event.*;
import magic.model.mstatic.MagicCDA;
import magic.model.mstatic.MagicStatic;
import magic.model.condition.MagicCondition;
import magic.model.trigger.AtBeginOfFirstMainPhaseTrigger;
import magic.model.trigger.EntersBattlefieldTrigger;
import magic.model.trigger.EntersWithCounterTrigger;
import magic.model.trigger.MagicTrigger;
@ -239,6 +240,10 @@ public class MagicCardDefinition implements MagicAbilityStore, IRenderableCard {
startingLoyalty
));
}
if (isSaga() && etbTriggers.isEmpty()) {
add(new EntersWithCounterTrigger(MagicCounterType.Lore, 1));
add(AtBeginOfFirstMainPhaseTrigger.Saga);
}
if (requiresGroovy != null) {
CardProperty.LOAD_GROOVY_CODE.setProperty(this, requiresGroovy);
requiresGroovy = null;

View File

@ -20,6 +20,7 @@ import magic.model.action.ChangeCountersAction;
import magic.model.action.ChangeStateAction;
import magic.model.action.DestroyAction;
import magic.model.action.RemoveFromPlayAction;
import magic.model.action.SacrificeAction;
import magic.model.action.SoulbondAction;
import magic.model.choice.MagicTargetChoice;
import magic.model.event.MagicActivation;
@ -883,6 +884,19 @@ public class MagicPermanent extends MagicObjectImpl implements MagicSource, Magi
game.addDelayedAction(new RemoveFromPlayAction(this, MagicLocationType.Graveyard));
}
// Only support Sagas with 3 chapters for now.
// The comprehensive rules doesn't say that all Sagas must have exactly 3 chapters though.
if (isSaga() && getCounters(MagicCounterType.Lore) >= 3 &&
game.getStack().stream().noneMatch(item -> item.getSource() == this) &&
game.getPendingTriggers().stream().noneMatch(item -> item.getSource() == this)) {
game.logAppendMessage(
getController(),
MagicMessage.format("Sacrifice %s.", this)
);
game.addDelayedAction(new SacrificeAction(this));
}
// +1/+1 and -1/-1 counters cancel each other out.
final int plusCounters = getCounters(MagicCounterType.PlusOne);
if (plusCounters > 0) {

View File

@ -1,6 +1,7 @@
package magic.model.phase;
import magic.model.MagicGame;
import magic.model.trigger.MagicTriggerType;
public class MagicMainPhase extends MagicPhase {
@ -21,6 +22,10 @@ public class MagicMainPhase extends MagicPhase {
@Override
public void executeBeginStep(final MagicGame game) {
if (this == FIRST_INSTANCE) {
game.executeTrigger(MagicTriggerType.AtBeginOfFirstMainPhase,game.getTurnPlayer());
}
game.setStep(MagicStep.ActivePlayer);
}

View File

@ -0,0 +1,34 @@
package magic.model.trigger;
import magic.model.MagicCounterType;
import magic.model.MagicGame;
import magic.model.MagicPermanent;
import magic.model.MagicPlayer;
import magic.model.action.ChangeCountersAction;
import magic.model.event.MagicEvent;
public abstract class AtBeginOfFirstMainPhaseTrigger extends MagicTrigger<MagicPlayer> {
public AtBeginOfFirstMainPhaseTrigger(final int priority) {
super(priority);
}
public AtBeginOfFirstMainPhaseTrigger() {}
@Override
public MagicTriggerType getType() {
return MagicTriggerType.AtBeginOfFirstMainPhase;
}
public static final AtBeginOfFirstMainPhaseTrigger Saga = new AtBeginOfFirstMainPhaseTrigger() {
@Override
public boolean accept(final MagicPermanent permanent, final MagicPlayer player) {
return permanent.isController(player);
}
@Override
public MagicEvent executeTrigger(final MagicGame game,final MagicPermanent permanent, final MagicPlayer player) {
game.doAction(new ChangeCountersAction(player, permanent, MagicCounterType.Lore, 1));
return MagicEvent.NONE;
}
};
}

View File

@ -5,6 +5,7 @@ public enum MagicTriggerType {
AtUpkeep, // player
AtDraw, // player
AtEndOfTurn, // player
AtBeginOfFirstMainPhase, // player
AtBeginOfCombat, // player
AtEndOfCombat, // player
WhenDamageIsDealt, // damage

View File

@ -0,0 +1,43 @@
package magic.model.trigger;
import magic.model.MagicCounterType;
import magic.model.MagicGame;
import magic.model.MagicPermanent;
import magic.model.event.MagicEvent;
import magic.model.event.MagicSourceEvent;
import magic.model.target.MagicTargetFilter;
public abstract class SagaChapterTrigger extends OneOrMoreCountersArePutTrigger {
protected int chapter;
public SagaChapterTrigger(final int priority, final int chapter) {
super(priority);
this.chapter = chapter;
}
public SagaChapterTrigger(final int chapter) {
this.chapter = chapter;
}
@Override
public boolean accept(final MagicPermanent permanent, final MagicCounterChangeTriggerData data) {
final int current = permanent.getCounters(MagicCounterType.Lore);
final int before = current - data.amount;
return super.accept(permanent, data) &&
permanent == data.obj &&
data.counterType == MagicCounterType.Lore &&
before < chapter &&
current >= chapter;
}
public static SagaChapterTrigger create(final int chapter, final MagicSourceEvent sourceEvent) {
return new SagaChapterTrigger(chapter) {
@Override
public MagicEvent executeTrigger(final MagicGame game,final MagicPermanent permanent, final MagicCounterChangeTriggerData data) {
return sourceEvent.getTriggerEvent(permanent);
}
};
}
}