From 66a7d6a83bed3b6dbde634c9c20aa91cb7cc89fb Mon Sep 17 00:00:00 2001 From: Lyu Zong-Hong Date: Sat, 6 Feb 2021 22:37:07 +0900 Subject: [PATCH] Mutate second step --- forge-game/src/main/java/forge/game/Game.java | 2 +- .../src/main/java/forge/game/GameAction.java | 9 +- .../game/ability/effects/MutateEffect.java | 83 +++++++++++++++---- .../src/main/java/forge/game/card/Card.java | 7 ++ .../main/java/forge/game/card/CardState.java | 45 ++++++++++ .../main/java/forge/game/player/Player.java | 2 +- .../java/forge/game/player/PlayerView.java | 2 +- .../main/java/forge/game/zone/MagicStack.java | 5 +- .../main/java/forge/game/zone/PlayerZone.java | 2 +- .../game/zone/PlayerZoneBattlefield.java | 15 ++-- .../src/main/java/forge/game/zone/Zone.java | 4 +- .../res/cardsfolder/d/dreamtail_heron.txt | 9 ++ forge-gui/res/cardsfolder/p/parcelbeast.txt | 7 ++ forge-gui/res/cardsfolder/v/vulpikeet.txt | 9 ++ 14 files changed, 169 insertions(+), 32 deletions(-) create mode 100644 forge-gui/res/cardsfolder/d/dreamtail_heron.txt create mode 100644 forge-gui/res/cardsfolder/p/parcelbeast.txt create mode 100644 forge-gui/res/cardsfolder/v/vulpikeet.txt diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 58d844b2911..8288ddeabc2 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -636,7 +636,7 @@ public class Game { if (!visitor.visitAll(player.getZone(ZoneType.Library).getCards())) { return; } - if (!visitor.visitAll(player.getZone(ZoneType.Battlefield).getCards(false))) { + if (!visitor.visitAll(player.getZone(ZoneType.Battlefield).getCards(false, true))) { return; } if (!visitor.visitAll(player.getZone(ZoneType.Exile).getCards())) { diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 98640a1ce6e..9fc00f539a0 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -120,7 +120,7 @@ public class GameAction { game.addChangeZoneLKIInfo(c); } - boolean suppress = !c.isToken() && zoneFrom.equals(zoneTo); + boolean suppress = (!c.isToken() && zoneFrom.equals(zoneTo)) || c.isMerged(); Card copied = null; Card lastKnownInfo = null; @@ -339,6 +339,13 @@ public class GameAction { // update state for view copied.updateStateForView(); + if (copied.isMerged()) { + if (copied.getMergedToCard() != null) { + copied.getMergedToCard().updateStateForView(); + } else { + copied.getMergedCards().get(0).updateStateForView(); + } + } if (fromBattlefield) { copied.setDamage(0); //clear damage after a card leaves the battlefield 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 index 4c4c1d8e0e8..01b023ba780 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/MutateEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/MutateEffect.java @@ -3,34 +3,56 @@ package forge.game.ability.effects; import java.util.*; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import forge.game.Game; 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.card.*; 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(); + private void migrateTopCard(final Card host, final Card target) { + // Copy all status from target card and migrate all counters + // Also update all reference of target card to new top card + + // TODO: find out all necessary status that should be copied + host.setTapped(target.isTapped()); + host.setSickness(target.isFirstTurnControlled()); + host.setFlipped(target.isFlipped()); + host.setDamage(target.getDamage()); + host.setMonstrous(target.isMonstrous()); + host.setRenowned(target.isRenowned()); + + // Migrate counters + Map counters = target.getCounters(); + if (!counters.isEmpty()) { + host.setCounters(Maps.newHashMap(counters)); + } + target.clearCounters(); + + // Migrate attached cards + CardCollectionView attached = target.getAttachedCards(); + for (final Card c : attached) { + c.setEntityAttachedTo(host); + } + target.setAttachedCards(null); + host.setAttachedCards(attached); + + // TODO: move all remembered, imprinted objects to new top card + // and possibly many other needs to be migrated. } - + @Override public void resolve(SpellAbility sa) { final Player p = sa.getActivatingPlayer(); final Card host = sa.getHostCard(); + final Game game = host.getGame(); // There shouldn't be any mutate abilities, but for now. if (sa.isSpell()) { host.setController(p, 0); @@ -45,13 +67,13 @@ public class MutateEffect extends SpellAbilityEffect { } final List targets = getDefinedOrTargeted(sa, "Defined"); - Card target = (Card)targets.get(0); + final Card target = (Card)targets.get(0); CardCollectionView view = CardCollection.getView(Lists.newArrayList(host, target)); - Card topCard = host.getController().getController().chooseSingleEntityForEffect( + final Card topCard = host.getController().getController().chooseSingleEntityForEffect( view, sa, - "Choose which creature to be the top", + "Choose which creature to be on top", false, new HashMap<>() ); @@ -66,11 +88,36 @@ public class MutateEffect extends SpellAbilityEffect { target.addMergedCard(host); host.setMergedToCard(target); } + + // Now the top card always have all abilities from bottom cards + final Long ts = game.getNextTimestamp(); + if (topCard == target) { + final CardCloneStates cloneStates = CardFactory.getCloneStates(target, target, sa); + final CardState targetState = cloneStates.get(target.getCurrentStateName()); + final CardState newState = host.getCurrentState(); + targetState.addAbilitiesFrom(newState, false); + target.addCloneState(cloneStates, ts); + // Re-register triggers for target card + game.getTriggerHandler().clearActiveTriggers(target, null); + game.getTriggerHandler().registerActiveTrigger(target, false); + } else { + final CardCloneStates cloneStates = CardFactory.getCloneStates(host, host, sa); + final CardState newState = cloneStates.get(host.getCurrentStateName()); + final CardState targetState = target.getCurrentState(); + newState.addAbilitiesFrom(targetState, false); + host.addCloneState(cloneStates, ts); + } - final Card c = p.getGame().getAction().moveToPlay(host, p, sa); - sa.setHostCard(c); + game.getAction().moveToPlay(host, p, sa); - p.getGame().getTriggerHandler().runTrigger(TriggerType.Mutates, AbilityKey.mapFromCard(c), false); + if (topCard == host) { + migrateTopCard(host, target); + } else { + host.setTapped(target.isTapped()); + host.setFlipped(target.isFlipped()); + } + + game.getTriggerHandler().runTrigger(TriggerType.Mutates, AbilityKey.mapFromCard(topCard), 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 ccd964bdb35..4d8ed12491b 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -1025,6 +1025,10 @@ public class Card extends GameEntity implements Comparable, IHasSVars { mergedTo = view.setCard(mergedTo, c, TrackableProperty.MergedTo); } + public final boolean isMerged() { + return !getMergedCards().isEmpty() || getMergedToCard() != null; + } + public final String getFlipResult(final Player flipper) { if (flipResult == null) { return null; @@ -3716,6 +3720,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars { if (tapped == tapped0) { return; } tapped = tapped0; view.updateTapped(this); + for (final Card c : getMergedCards()) { + c.setTapped(tapped0); + } } public final void tap() { diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index f304cfb409d..2098be3399b 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -563,6 +563,51 @@ public class CardState extends GameObject implements IHasSVars { } } + public final void addAbilitiesFrom(final CardState source, final boolean lki) { + // TODO: what happens if SVar has the same name ? + sVars.putAll(source.getSVars()); + + for (SpellAbility sa : source.manaAbilities) { + if (sa.isIntrinsic()) { + manaAbilities.add(sa.copy(card, lki)); + } + } + + for (SpellAbility sa : source.nonManaAbilities) { + if (sa.isIntrinsic()) { + nonManaAbilities.add(sa.copy(card, lki)); + } + } + + for (KeywordInterface k : source.intrinsicKeywords) { + intrinsicKeywords.insert(k.copy(card, lki)); + } + + for (Trigger tr : source.triggers) { + if (tr.isIntrinsic()) { + triggers.add(tr.copy(card, lki)); + } + } + + for (ReplacementEffect re : source.replacementEffects) { + if (re.isIntrinsic()) { + replacementEffects.add(re.copy(card, lki)); + } + } + + staticAbilities.clear(); + for (StaticAbility sa : source.staticAbilities) { + if (sa.isIntrinsic()) { + staticAbilities.add(sa.copy(card, lki)); + } + } + + // Not sure if this is needed + if (lki && source.loyaltyRep != null) { + this.loyaltyRep = source.loyaltyRep.copy(card, lki); + } + } + public CardState copy(final Card host, CardStateName name, final boolean lki) { CardState result = new CardState(host, name); result.copyFrom(this, lki); diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index c5a0287007f..e9b83b611fe 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -1497,7 +1497,7 @@ public class Player extends GameEntity implements Comparable { } PlayerZone zone = getZone(zoneType); - return zone == null ? CardCollection.EMPTY : zone.getCards(filterOutPhasedOut); + return zone == null ? CardCollection.EMPTY : zone.getCards(filterOutPhasedOut, true); } public final CardCollectionView getCardsIncludePhasingIn(final ZoneType zone) { diff --git a/forge-game/src/main/java/forge/game/player/PlayerView.java b/forge-game/src/main/java/forge/game/player/PlayerView.java index 19e6e1cb6fd..6aa3db8dae3 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerView.java +++ b/forge-game/src/main/java/forge/game/player/PlayerView.java @@ -470,7 +470,7 @@ public class PlayerView extends GameEntityView { void updateZone(PlayerZone zone) { TrackableProperty prop = getZoneProp(zone.getZoneType()); if (prop == null) { return; } - set(prop, CardView.getCollection(zone.getCards(false))); + set(prop, CardView.getCollection(zone.getCards(false, false))); //update delirium if (ZoneType.Graveyard == zone.getZoneType()) 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 29656dbdc1c..4e691cf4739 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -471,8 +471,11 @@ public class MagicStack /* extends MyObservable */ implements Iterable { } public final CardCollectionView getCards() { - return getCards(true); + return getCards(true, true); } - public CardCollectionView getCards(final boolean filter) { + public CardCollectionView getCards(final boolean filterOutPhasedOut, final boolean filterOutMerged) { return cardList; // Non-Battlefield PlayerZones don't care about the filter } diff --git a/forge-gui/res/cardsfolder/d/dreamtail_heron.txt b/forge-gui/res/cardsfolder/d/dreamtail_heron.txt new file mode 100644 index 00000000000..48b9d16d0f0 --- /dev/null +++ b/forge-gui/res/cardsfolder/d/dreamtail_heron.txt @@ -0,0 +1,9 @@ +Name:Dreamtail Heron +ManaCost:4 U +Types:Creature Elemental Bird +PT:3/4 +K:Mutate:3 U +K:Flying +T:Mode$ Mutates | ValidCard$ Card.Self | Execute$ TrigDraw | TriggerDescription$ Whenever this creature mutates, draw a card. +SVar:TrigDraw:DB$ Draw | NumCards$ 1 +Oracle:Mutate {3}{U} (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 from under it.)\nFlying\nWhenever this creature mutates, draw a card. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/p/parcelbeast.txt b/forge-gui/res/cardsfolder/p/parcelbeast.txt new file mode 100644 index 00000000000..62d45a87269 --- /dev/null +++ b/forge-gui/res/cardsfolder/p/parcelbeast.txt @@ -0,0 +1,7 @@ +Name:Parcelbeast +ManaCost:2 G U +Types:Creature Elemental Beast +PT:2/4 +K:Mutate:G U +A:AB$ Dig | Cost$ 1 T | DigNum$ 1 | ChangeNum$ 1 | ChangeValid$ Land | Optional$ True | DestinationZone$ Battlefield | DestinationZone2$ Hand | StackDescription$ SpellDescription | SpellDescription$ Look at the top card of your library. If it's a land card, you may put it onto the battlefield. If you don't put the card onto the battlefield, put it into your hand. +Oracle:Mutate {G}{U} (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 from under it.)\n{1}, {T}: Look at the top card of your library. If it's a land card, you may put it onto the battlefield. If you don't put the card onto the battlefield, put it into your hand. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/v/vulpikeet.txt b/forge-gui/res/cardsfolder/v/vulpikeet.txt new file mode 100644 index 00000000000..e8c991b1e1e --- /dev/null +++ b/forge-gui/res/cardsfolder/v/vulpikeet.txt @@ -0,0 +1,9 @@ +Name:Vulpikeet +ManaCost:3 W +Types:Creature Fox Bird +PT:2/3 +K:Mutate:2 W +K:Flying +T:Mode$ Mutates | ValidCard$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever this creature mutates, put a +1/+1 counter on it. +SVar:TrigPutCounter:DB$ PutCounter | Defined$ TriggeredCardLKICopy | CounterType$ P1P1 | CounterNum$ 1 +Oracle:Mutate {2}{W} (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 from under it.)\nFlying\nWhenever this creature mutates, put a +1/+1 counter on it. \ No newline at end of file