diff --git a/forge-game/src/main/java/forge/game/ability/ApiType.java b/forge-game/src/main/java/forge/game/ability/ApiType.java index f21cb010fc6..8df8f0f2a0c 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -105,6 +105,7 @@ public enum ApiType { MultiplyCounter (CountersMultiplyEffect.class), MustAttack (MustAttackEffect.class), MustBlock (MustBlockEffect.class), + Mutate (MutateEffect.class), NameCard (ChooseCardNameEffect.class), NoteCounters (CountersNoteEffect.class), PeekAndReveal (PeekAndRevealEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/effects/MutateEffect.java b/forge-game/src/main/java/forge/game/ability/effects/MutateEffect.java new file mode 100644 index 00000000000..4c4c1d8e0e8 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/MutateEffect.java @@ -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 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 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); + } + +} diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index d4badf7d1a4..ccd964bdb35 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -98,9 +98,11 @@ public class Card extends GameEntity implements Comparable, IHasSVars { // cards attached or otherwise linked to this card private CardCollection hauntedBy, devouredCards, exploitedCards, delvedCards, convokedCards, imprintedCards, encodedCards; 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 private Card encoding, cloneOrigin, haunting, effectSource, pairedWith, meldedWith; + private Card mergedTo; private SpellAbility effectSourceAbility = null; @@ -994,6 +996,35 @@ public class Card extends GameEntity implements Comparable, IHasSVars { 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 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) { if (flipResult == null) { return null; @@ -1842,7 +1873,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { || keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt") || keyword.startsWith("Transfigure") || keyword.startsWith("Aura swap") || keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling") - || keyword.startsWith("Encore")) { + || keyword.startsWith("Encore") || keyword.startsWith("Mutate")) { // keyword parsing takes care of adding a proper description } else if (keyword.startsWith("CantBeBlockedByAmount")) { sbLong.append(getName()).append(" can't be blocked "); diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 202aae365ed..cfa82df2d7a 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -4273,6 +4273,22 @@ public class CardFactoryUtil { } else { 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")) { final String[] k = keyword.split(":"); final String manacost = k[1]; diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index 4a024109640..e0fc65032c7 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -507,6 +507,17 @@ public class CardView extends GameEntityView { return get(TrackableProperty.EncodedCards); } + public FCollectionView getMergedCards() { + return get(TrackableProperty.MergedCards); + } + public boolean hasMergedCards() { + return getMergedCards() != null; + } + + public CardView getMergedTo() { + return get(TrackableProperty.MergedTo); + } + public GameEntityView getEntityAttachedTo() { return get(TrackableProperty.EntityAttachedTo); } diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 96a11fb2ef4..51a1303c061 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -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."), 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."), + 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."), 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."), diff --git a/forge-game/src/main/java/forge/game/spellability/AlternativeCost.java b/forge-game/src/main/java/forge/game/spellability/AlternativeCost.java index 7f23a7fdbc6..c3965363e72 100644 --- a/forge-game/src/main/java/forge/game/spellability/AlternativeCost.java +++ b/forge-game/src/main/java/forge/game/spellability/AlternativeCost.java @@ -11,6 +11,7 @@ public enum AlternativeCost { Flashback, Foretold, Madness, + Mutate, Offering, Outlast, // ActivatedAbility Prowl, diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 11fef9d1084..cd7b9efcc66 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -1219,6 +1219,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return isAlternativeCost(AlternativeCost.Madness); } + public final boolean isMutate() { + return isAlternativeCost(AlternativeCost.Mutate); + } + public final boolean isProwl() { return isAlternativeCost(AlternativeCost.Prowl); } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerMutates.java b/forge-game/src/main/java/forge/game/trigger/TriggerMutates.java new file mode 100644 index 00000000000..b49b11ef54d --- /dev/null +++ b/forge-game/src/main/java/forge/game/trigger/TriggerMutates.java @@ -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 params, final Card host, final boolean intrinsic) { + super(params, host, intrinsic); + } + + @Override + public boolean performTest(Map runParams) { + if (hasParam("ValidCard")) { + return matchesValid(runParams.get(AbilityKey.Card), getParam("ValidCard").split(","), + this.getHostCard()); + } + + return true; + } + + @Override + public void setTriggeringObjects(SpellAbility sa, Map 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(); + } +} diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerType.java b/forge-game/src/main/java/forge/game/trigger/TriggerType.java index bd3175bf094..938e88c4c72 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerType.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerType.java @@ -71,6 +71,7 @@ public enum TriggerType { LifeGained(TriggerLifeGained.class), LifeLost(TriggerLifeLost.class), LosesGame(TriggerLosesGame.class), + Mutates(TriggerMutates.class), NewGame(TriggerNewGame.class), PayCumulativeUpkeep(TriggerPayCumulativeUpkeep.class), PayEcho(TriggerPayEcho.class), diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index 21dcc1c897b..29656dbdc1c 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -470,6 +470,9 @@ public class MagicStack /* extends MyObservable */ implements Iterable 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()) { final Iterable enchants = card.getAttachedCards(); for (final CardView e : enchants) { @@ -699,11 +714,13 @@ public class PlayArea extends CardPanelContainer implements CardPanelMouseListen } } + // Treat merged cards like attached cards 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()); - } - else { + } else { attachedToPanel = null; } if (toPanel.getAttachedToPanel() != attachedToPanel) { diff --git a/forge-gui-mobile/src/forge/screens/match/views/VCardDisplayArea.java b/forge-gui-mobile/src/forge/screens/match/views/VCardDisplayArea.java index 6af3cc4c31f..e0d48ca63d7 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VCardDisplayArea.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VCardDisplayArea.java @@ -286,6 +286,17 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH attachedPanels.clear(); + // Treat merged cards like attached cards + if (card.hasMergedCards()) { + final Iterable merged = card.getMergedCards(); + for (final CardView c : merged) { + final CardAreaPanel cardC = CardAreaPanel.get(c); + if (cardC != null) { + attachedPanels.add(cardC); + } + } + } + if (card.hasCardAttachments()) { final Iterable enchants = card.getAttachedCards(); 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())); } else { diff --git a/forge-gui/src/main/java/forge/card/CardDetailUtil.java b/forge-gui/src/main/java/forge/card/CardDetailUtil.java index 9708175d5d1..6dc860e2e14 100644 --- a/forge-gui/src/main/java/forge/card/CardDetailUtil.java +++ b/forge-gui/src/main/java/forge/card/CardDetailUtil.java @@ -536,6 +536,20 @@ public class CardDetailUtil { 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 if (card.getMustBlockCards() != null) { if (area.length() != 0) {