Mutate first step

This commit is contained in:
Lyu Zong-Hong
2021-02-06 13:40:54 +09:00
parent 7d3703bc1e
commit c845d0566f
15 changed files with 233 additions and 6 deletions

View File

@@ -105,6 +105,7 @@ public enum ApiType {
MultiplyCounter (CountersMultiplyEffect.class), MultiplyCounter (CountersMultiplyEffect.class),
MustAttack (MustAttackEffect.class), MustAttack (MustAttackEffect.class),
MustBlock (MustBlockEffect.class), MustBlock (MustBlockEffect.class),
Mutate (MutateEffect.class),
NameCard (ChooseCardNameEffect.class), NameCard (ChooseCardNameEffect.class),
NoteCounters (CountersNoteEffect.class), NoteCounters (CountersNoteEffect.class),
PeekAndReveal (PeekAndRevealEffect.class), PeekAndReveal (PeekAndRevealEffect.class),

View File

@@ -0,0 +1,76 @@
package forge.game.ability.effects;
import java.util.*;
import com.google.common.collect.Lists;
import forge.game.GameObject;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.util.Lang;
public class MutateEffect extends SpellAbilityEffect {
@Override
public String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
final List<GameObject> targets = getTargets(sa);
sb.append(" Mutates with ");
sb.append(Lang.joinHomogenous(targets));
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
final Player p = sa.getActivatingPlayer();
final Card host = sa.getHostCard();
// There shouldn't be any mutate abilities, but for now.
if (sa.isSpell()) {
host.setController(p, 0);
}
// 111.11. A copy of a permanent spell becomes a token as it resolves.
// The token has the characteristics of the spell that became that token.
// The token is not “created” for the purposes of any replacement effects or triggered abilities that refer to creating a token.
if (host.isCopiedSpell()) {
host.setCopiedSpell(false);
host.setToken(true);
}
final List<GameObject> targets = getDefinedOrTargeted(sa, "Defined");
Card target = (Card)targets.get(0);
CardCollectionView view = CardCollection.getView(Lists.newArrayList(host, target));
Card topCard = host.getController().getController().chooseSingleEntityForEffect(
view,
sa,
"Choose which creature to be the top",
false,
new HashMap<>()
);
final boolean putOnTop = (topCard == host);
if (putOnTop) {
host.addMergedCard(target);
host.addMergedCards(target.getMergedCards());
target.clearMergedCards();
target.setMergedToCard(host);
} else {
target.addMergedCard(host);
host.setMergedToCard(target);
}
final Card c = p.getGame().getAction().moveToPlay(host, p, sa);
sa.setHostCard(c);
p.getGame().getTriggerHandler().runTrigger(TriggerType.Mutates, AbilityKey.mapFromCard(c), false);
}
}

View File

@@ -98,9 +98,11 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// cards attached or otherwise linked to this card // cards attached or otherwise linked to this card
private CardCollection hauntedBy, devouredCards, exploitedCards, delvedCards, convokedCards, imprintedCards, encodedCards; private CardCollection hauntedBy, devouredCards, exploitedCards, delvedCards, convokedCards, imprintedCards, encodedCards;
private CardCollection mustBlockCards, gainControlTargets, chosenCards, blockedThisTurn, blockedByThisTurn; private CardCollection mustBlockCards, gainControlTargets, chosenCards, blockedThisTurn, blockedByThisTurn;
private CardCollection mergedCards;
// if this card is attached or linked to something, what card is it currently attached to // if this card is attached or linked to something, what card is it currently attached to
private Card encoding, cloneOrigin, haunting, effectSource, pairedWith, meldedWith; private Card encoding, cloneOrigin, haunting, effectSource, pairedWith, meldedWith;
private Card mergedTo;
private SpellAbility effectSourceAbility = null; private SpellAbility effectSourceAbility = null;
@@ -994,6 +996,35 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
encoding = e; encoding = e;
} }
public final CardCollectionView getMergedCards() {
return CardCollection.getView(mergedCards);
}
public final boolean hasMergedCard() {
return FCollection.hasElements(mergedCards);
}
public final boolean hasMergedCard(Card c) {
return FCollection.hasElement(mergedCards, c);
}
public final void addMergedCard(final Card c) {
mergedCards = view.addCard(mergedCards, c, TrackableProperty.MergedCards);
}
public final void addMergedCards(final Iterable<Card> cards) {
mergedCards = view.addCards(mergedCards, cards, TrackableProperty.MergedCards);
}
public final void removeMergedCard(final Card c) {
mergedCards = view.removeCard(mergedCards, c, TrackableProperty.MergedCards);
}
public final void clearMergedCards() {
mergedCards = view.clearCards(mergedCards, TrackableProperty.MergedCards);
}
public final Card getMergedToCard() {
return mergedTo;
}
public final void setMergedToCard(final Card c) {
mergedTo = view.setCard(mergedTo, c, TrackableProperty.MergedTo);
}
public final String getFlipResult(final Player flipper) { public final String getFlipResult(final Player flipper) {
if (flipResult == null) { if (flipResult == null) {
return null; return null;
@@ -1842,7 +1873,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt") || keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt")
|| keyword.startsWith("Transfigure") || keyword.startsWith("Aura swap") || keyword.startsWith("Transfigure") || keyword.startsWith("Aura swap")
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling") || keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")
|| keyword.startsWith("Encore")) { || keyword.startsWith("Encore") || keyword.startsWith("Mutate")) {
// keyword parsing takes care of adding a proper description // keyword parsing takes care of adding a proper description
} else if (keyword.startsWith("CantBeBlockedByAmount")) { } else if (keyword.startsWith("CantBeBlockedByAmount")) {
sbLong.append(getName()).append(" can't be blocked "); sbLong.append(getName()).append(" can't be blocked ");

View File

@@ -4273,6 +4273,22 @@ public class CardFactoryUtil {
} else { } else {
sa.addAnnounceVar("Multikicker"); sa.addAnnounceVar("Multikicker");
} }
} else if (keyword.startsWith("Mutate")) {
final String[] params = keyword.split(":");
final String cost = params[1];
final StringBuilder sbMutate = new StringBuilder();
sbMutate.append("SP$ Mutate | Cost$ ");
sbMutate.append(cost);
sbMutate.append(" | Mutate True | ValidTgts$ Creature.YouOwn+nonHuman");
final SpellAbility sa = AbilityFactory.getAbility(sbMutate.toString(), card);
sa.setDescription("Mutate " + ManaCostParser.parse(cost) +
" (" + inst.getReminderText() + ")");
sa.setStackDescription("Mutate - " + card.getName());
sa.setAlternativeCost(AlternativeCost.Mutate);
sa.setIntrinsic(intrinsic);
inst.addSpellAbility(sa);
} else if (keyword.startsWith("Ninjutsu")) { } else if (keyword.startsWith("Ninjutsu")) {
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final String manacost = k[1]; final String manacost = k[1];

View File

@@ -507,6 +507,17 @@ public class CardView extends GameEntityView {
return get(TrackableProperty.EncodedCards); return get(TrackableProperty.EncodedCards);
} }
public FCollectionView<CardView> getMergedCards() {
return get(TrackableProperty.MergedCards);
}
public boolean hasMergedCards() {
return getMergedCards() != null;
}
public CardView getMergedTo() {
return get(TrackableProperty.MergedTo);
}
public GameEntityView getEntityAttachedTo() { public GameEntityView getEntityAttachedTo() {
return get(TrackableProperty.EntityAttachedTo); return get(TrackableProperty.EntityAttachedTo);
} }

View File

@@ -102,6 +102,7 @@ public enum Keyword {
MODULAR("Modular", Modular.class, false, "This creature enters the battlefield with {%d:+1/+1 counter} on it. When it dies, you may put its +1/+1 counters on target artifact creature."), MODULAR("Modular", Modular.class, false, "This creature enters the battlefield with {%d:+1/+1 counter} on it. When it dies, you may put its +1/+1 counters on target artifact creature."),
MORPH("Morph", KeywordWithCost.class, false, "You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its morph cost."), MORPH("Morph", KeywordWithCost.class, false, "You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its morph cost."),
MULTIKICKER("Multikicker", KeywordWithCost.class, false, "You may pay an additional %s any number of times as you cast this spell."), MULTIKICKER("Multikicker", KeywordWithCost.class, false, "You may pay an additional %s any number of times as you cast this spell."),
MUTATE("Mutate", KeywordWithCost.class, true, "If you cast this spell for its mutate cost, put it over or under target non-Human creature you own. They mutate into the creature on top plus all abilities under it."),
MYRIAD("Myriad", SimpleKeyword.class, false, "Whenever this creature attacks, for each opponent other than defending player, you may create a token that's a copy of this creature that's tapped and attacking that player or a planeswalker they control. Exile the tokens at end of combat."), MYRIAD("Myriad", SimpleKeyword.class, false, "Whenever this creature attacks, for each opponent other than defending player, you may create a token that's a copy of this creature that's tapped and attacking that player or a planeswalker they control. Exile the tokens at end of combat."),
NINJUTSU("Ninjutsu", Ninjutsu.class, false, "%s, Return an unblocked attacker you control to hand: Put this card onto the battlefield from your %s tapped and attacking."), NINJUTSU("Ninjutsu", Ninjutsu.class, false, "%s, Return an unblocked attacker you control to hand: Put this card onto the battlefield from your %s tapped and attacking."),
OUTLAST("Outlast", KeywordWithCost.class, false, "%s, {T}: Put a +1/+1 counter on this creature. Outlast only as a sorcery."), OUTLAST("Outlast", KeywordWithCost.class, false, "%s, {T}: Put a +1/+1 counter on this creature. Outlast only as a sorcery."),

View File

@@ -11,6 +11,7 @@ public enum AlternativeCost {
Flashback, Flashback,
Foretold, Foretold,
Madness, Madness,
Mutate,
Offering, Offering,
Outlast, // ActivatedAbility Outlast, // ActivatedAbility
Prowl, Prowl,

View File

@@ -1219,6 +1219,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return isAlternativeCost(AlternativeCost.Madness); return isAlternativeCost(AlternativeCost.Madness);
} }
public final boolean isMutate() {
return isAlternativeCost(AlternativeCost.Mutate);
}
public final boolean isProwl() { public final boolean isProwl() {
return isAlternativeCost(AlternativeCost.Prowl); return isAlternativeCost(AlternativeCost.Prowl);
} }

View File

@@ -0,0 +1,35 @@
package forge.game.trigger;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import java.util.*;
public class TriggerMutates extends Trigger {
public TriggerMutates(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
@Override
public boolean performTest(Map<AbilityKey, Object> runParams) {
if (hasParam("ValidCard")) {
return matchesValid(runParams.get(AbilityKey.Card), getParam("ValidCard").split(","),
this.getHostCard());
}
return true;
}
@Override
public void setTriggeringObjects(SpellAbility sa, Map<AbilityKey, Object> runParams) {
sa.setTriggeringObject(AbilityKey.Card, runParams.get(AbilityKey.Card));
}
@Override
public String getImportantStackObjects(SpellAbility sa) {
StringBuilder sb = new StringBuilder();
sb.append("Mutates").append(": ").append(sa.getTriggeringObject(AbilityKey.Card));
return sb.toString();
}
}

View File

@@ -71,6 +71,7 @@ public enum TriggerType {
LifeGained(TriggerLifeGained.class), LifeGained(TriggerLifeGained.class),
LifeLost(TriggerLifeLost.class), LifeLost(TriggerLifeLost.class),
LosesGame(TriggerLosesGame.class), LosesGame(TriggerLosesGame.class),
Mutates(TriggerMutates.class),
NewGame(TriggerNewGame.class), NewGame(TriggerNewGame.class),
PayCumulativeUpkeep(TriggerPayCumulativeUpkeep.class), PayCumulativeUpkeep(TriggerPayCumulativeUpkeep.class),
PayEcho(TriggerPayEcho.class), PayEcho(TriggerPayEcho.class),

View File

@@ -470,6 +470,9 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
first.setActivatingPlayer(sa.getActivatingPlayer()); first.setActivatingPlayer(sa.getActivatingPlayer());
game.fireEvent(new GameEventCardStatsChanged(source)); game.fireEvent(new GameEventCardStatsChanged(source));
AbilityUtils.resolve(first); AbilityUtils.resolve(first);
} else if (sa.isMutate()) {
game.fireEvent(new GameEventCardStatsChanged(source));
AbilityUtils.resolve(sa.getHostCard().getFirstSpellAbility());
} else { } else {
// TODO: Spell fizzles, what's the best way to alert player? // TODO: Spell fizzles, what's the best way to alert player?
Log.debug(source.getName() + " ability fizzles."); Log.debug(source.getName() + " ability fizzles.");

View File

@@ -62,12 +62,14 @@ public enum TrackableProperty {
PlayerMayLook(TrackableTypes.PlayerViewCollectionType, FreezeMode.IgnoresFreeze), PlayerMayLook(TrackableTypes.PlayerViewCollectionType, FreezeMode.IgnoresFreeze),
EntityAttachedTo(TrackableTypes.GameEntityViewType), EntityAttachedTo(TrackableTypes.GameEntityViewType),
EncodedCards(TrackableTypes.CardViewCollectionType), EncodedCards(TrackableTypes.CardViewCollectionType),
MergedCards(TrackableTypes.CardViewCollectionType),
GainControlTargets(TrackableTypes.CardViewCollectionType), GainControlTargets(TrackableTypes.CardViewCollectionType),
CloneOrigin(TrackableTypes.CardViewType), CloneOrigin(TrackableTypes.CardViewType),
ImprintedCards(TrackableTypes.CardViewCollectionType), ImprintedCards(TrackableTypes.CardViewCollectionType),
HauntedBy(TrackableTypes.CardViewCollectionType), HauntedBy(TrackableTypes.CardViewCollectionType),
Haunting(TrackableTypes.CardViewType), Haunting(TrackableTypes.CardViewType),
MergedTo(TrackableTypes.CardViewType),
MustBlockCards(TrackableTypes.CardViewCollectionType), MustBlockCards(TrackableTypes.CardViewCollectionType),
PairedWith(TrackableTypes.CardViewType), PairedWith(TrackableTypes.CardViewType),
CurrentState(TrackableTypes.CardStateViewType, FreezeMode.IgnoresFreeze), CurrentState(TrackableTypes.CardStateViewType, FreezeMode.IgnoresFreeze),

View File

@@ -685,6 +685,21 @@ public class PlayArea extends CardPanelContainer implements CardPanelMouseListen
} }
toPanel.getAttachedPanels().clear(); toPanel.getAttachedPanels().clear();
// Treat merged cards like attached cards
if (card.hasMergedCards()) {
final Iterable<CardView> merged = card.getMergedCards();
for (final CardView c : merged) {
final CardPanel cardC = getCardPanel(c.getId());
if (cardC != null) {
if (cardC.getAttachedToPanel() != toPanel) {
cardC.setAttachedToPanel(toPanel);
needLayoutRefresh = true; //ensure layout refreshed if any attachments change
}
toPanel.getAttachedPanels().add(cardC);
}
}
}
if (card.hasCardAttachments()) { if (card.hasCardAttachments()) {
final Iterable<CardView> enchants = card.getAttachedCards(); final Iterable<CardView> enchants = card.getAttachedCards();
for (final CardView e : enchants) { for (final CardView e : enchants) {
@@ -699,11 +714,13 @@ public class PlayArea extends CardPanelContainer implements CardPanelMouseListen
} }
} }
// Treat merged cards like attached cards
CardPanel attachedToPanel; CardPanel attachedToPanel;
if (card.getAttachedTo() != null) { if (card.getMergedTo() != null) {
attachedToPanel = getCardPanel(card.getMergedTo().getId());
} else if (card.getAttachedTo() != null) {
attachedToPanel = getCardPanel(card.getAttachedTo().getId()); attachedToPanel = getCardPanel(card.getAttachedTo().getId());
} } else {
else {
attachedToPanel = null; attachedToPanel = null;
} }
if (toPanel.getAttachedToPanel() != attachedToPanel) { if (toPanel.getAttachedToPanel() != attachedToPanel) {

View File

@@ -286,6 +286,17 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
attachedPanels.clear(); attachedPanels.clear();
// Treat merged cards like attached cards
if (card.hasMergedCards()) {
final Iterable<CardView> merged = card.getMergedCards();
for (final CardView c : merged) {
final CardAreaPanel cardC = CardAreaPanel.get(c);
if (cardC != null) {
attachedPanels.add(cardC);
}
}
}
if (card.hasCardAttachments()) { if (card.hasCardAttachments()) {
final Iterable<CardView> enchants = card.getAttachedCards(); final Iterable<CardView> enchants = card.getAttachedCards();
for (final CardView e : enchants) { for (final CardView e : enchants) {
@@ -295,8 +306,11 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
} }
} }
} }
if (card.getAttachedTo() != null) { if (card.getMergedTo() != null ) {
setAttachedToPanel(CardAreaPanel.get(card.getMergedTo()));
}
else if (card.getAttachedTo() != null) {
setAttachedToPanel(CardAreaPanel.get(card.getAttachedTo())); setAttachedToPanel(CardAreaPanel.get(card.getAttachedTo()));
} }
else { else {

View File

@@ -536,6 +536,20 @@ public class CardDetailUtil {
area.append("Encoded: ").append(card.getEncodedCards()); area.append("Encoded: ").append(card.getEncodedCards());
} }
// Merge
if (card.getMergedCards() != null) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Merged: ").append(card.getMergedCards());
}
if (card.getMergedTo() != null) {
if (area.length() != 0) {
area.append("\n");
}
area.append("Merged to: ").append(card.getMergedTo());
}
// must block // must block
if (card.getMustBlockCards() != null) { if (card.getMustBlockCards() != null) {
if (area.length() != 0) { if (area.length() != 0) {