Mutate second step

This commit is contained in:
Lyu Zong-Hong
2021-02-06 22:37:07 +09:00
parent c845d0566f
commit 66a7d6a83b
14 changed files with 169 additions and 32 deletions

View File

@@ -636,7 +636,7 @@ public class Game {
if (!visitor.visitAll(player.getZone(ZoneType.Library).getCards())) { if (!visitor.visitAll(player.getZone(ZoneType.Library).getCards())) {
return; return;
} }
if (!visitor.visitAll(player.getZone(ZoneType.Battlefield).getCards(false))) { if (!visitor.visitAll(player.getZone(ZoneType.Battlefield).getCards(false, true))) {
return; return;
} }
if (!visitor.visitAll(player.getZone(ZoneType.Exile).getCards())) { if (!visitor.visitAll(player.getZone(ZoneType.Exile).getCards())) {

View File

@@ -120,7 +120,7 @@ public class GameAction {
game.addChangeZoneLKIInfo(c); game.addChangeZoneLKIInfo(c);
} }
boolean suppress = !c.isToken() && zoneFrom.equals(zoneTo); boolean suppress = (!c.isToken() && zoneFrom.equals(zoneTo)) || c.isMerged();
Card copied = null; Card copied = null;
Card lastKnownInfo = null; Card lastKnownInfo = null;
@@ -339,6 +339,13 @@ public class GameAction {
// update state for view // update state for view
copied.updateStateForView(); copied.updateStateForView();
if (copied.isMerged()) {
if (copied.getMergedToCard() != null) {
copied.getMergedToCard().updateStateForView();
} else {
copied.getMergedCards().get(0).updateStateForView();
}
}
if (fromBattlefield) { if (fromBattlefield) {
copied.setDamage(0); //clear damage after a card leaves the battlefield copied.setDamage(0); //clear damage after a card leaves the battlefield

View File

@@ -3,13 +3,13 @@ package forge.game.ability.effects;
import java.util.*; import java.util.*;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.game.Game;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
@@ -17,20 +17,42 @@ import forge.util.Lang;
public class MutateEffect extends SpellAbilityEffect { public class MutateEffect extends SpellAbilityEffect {
@Override private void migrateTopCard(final Card host, final Card target) {
public String getStackDescription(SpellAbility sa) { // Copy all status from target card and migrate all counters
final StringBuilder sb = new StringBuilder(); // Also update all reference of target card to new top card
final List<GameObject> targets = getTargets(sa);
sb.append(" Mutates with "); // TODO: find out all necessary status that should be copied
sb.append(Lang.joinHomogenous(targets)); host.setTapped(target.isTapped());
return sb.toString(); host.setSickness(target.isFirstTurnControlled());
host.setFlipped(target.isFlipped());
host.setDamage(target.getDamage());
host.setMonstrous(target.isMonstrous());
host.setRenowned(target.isRenowned());
// Migrate counters
Map<CounterType, Integer> 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 @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Player p = sa.getActivatingPlayer(); final Player p = sa.getActivatingPlayer();
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
final Game game = host.getGame();
// There shouldn't be any mutate abilities, but for now. // There shouldn't be any mutate abilities, but for now.
if (sa.isSpell()) { if (sa.isSpell()) {
host.setController(p, 0); host.setController(p, 0);
@@ -45,13 +67,13 @@ public class MutateEffect extends SpellAbilityEffect {
} }
final List<GameObject> targets = getDefinedOrTargeted(sa, "Defined"); final List<GameObject> 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)); CardCollectionView view = CardCollection.getView(Lists.newArrayList(host, target));
Card topCard = host.getController().getController().chooseSingleEntityForEffect( final Card topCard = host.getController().getController().chooseSingleEntityForEffect(
view, view,
sa, sa,
"Choose which creature to be the top", "Choose which creature to be on top",
false, false,
new HashMap<>() new HashMap<>()
); );
@@ -67,10 +89,35 @@ public class MutateEffect extends SpellAbilityEffect {
host.setMergedToCard(target); host.setMergedToCard(target);
} }
final Card c = p.getGame().getAction().moveToPlay(host, p, sa); // Now the top card always have all abilities from bottom cards
sa.setHostCard(c); 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);
}
p.getGame().getTriggerHandler().runTrigger(TriggerType.Mutates, AbilityKey.mapFromCard(c), false); game.getAction().moveToPlay(host, p, sa);
if (topCard == host) {
migrateTopCard(host, target);
} else {
host.setTapped(target.isTapped());
host.setFlipped(target.isFlipped());
}
game.getTriggerHandler().runTrigger(TriggerType.Mutates, AbilityKey.mapFromCard(topCard), false);
} }
} }

View File

@@ -1025,6 +1025,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
mergedTo = view.setCard(mergedTo, c, TrackableProperty.MergedTo); mergedTo = view.setCard(mergedTo, c, TrackableProperty.MergedTo);
} }
public final boolean isMerged() {
return !getMergedCards().isEmpty() || getMergedToCard() != null;
}
public final String getFlipResult(final Player flipper) { public final String getFlipResult(final Player flipper) {
if (flipResult == null) { if (flipResult == null) {
return null; return null;
@@ -3716,6 +3720,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
if (tapped == tapped0) { return; } if (tapped == tapped0) { return; }
tapped = tapped0; tapped = tapped0;
view.updateTapped(this); view.updateTapped(this);
for (final Card c : getMergedCards()) {
c.setTapped(tapped0);
}
} }
public final void tap() { public final void tap() {

View File

@@ -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) { public CardState copy(final Card host, CardStateName name, final boolean lki) {
CardState result = new CardState(host, name); CardState result = new CardState(host, name);
result.copyFrom(this, lki); result.copyFrom(this, lki);

View File

@@ -1497,7 +1497,7 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
PlayerZone zone = getZone(zoneType); 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) { public final CardCollectionView getCardsIncludePhasingIn(final ZoneType zone) {

View File

@@ -470,7 +470,7 @@ public class PlayerView extends GameEntityView {
void updateZone(PlayerZone zone) { void updateZone(PlayerZone zone) {
TrackableProperty prop = getZoneProp(zone.getZoneType()); TrackableProperty prop = getZoneProp(zone.getZoneType());
if (prop == null) { return; } if (prop == null) { return; }
set(prop, CardView.getCollection(zone.getCards(false))); set(prop, CardView.getCollection(zone.getCards(false, false)));
//update delirium //update delirium
if (ZoneType.Graveyard == zone.getZoneType()) if (ZoneType.Graveyard == zone.getZoneType())

View File

@@ -471,8 +471,11 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
game.fireEvent(new GameEventCardStatsChanged(source)); game.fireEvent(new GameEventCardStatsChanged(source));
AbilityUtils.resolve(first); AbilityUtils.resolve(first);
} else if (sa.isMutate()) { } else if (sa.isMutate()) {
SpellAbility first = source.getFirstSpellAbility();
// need to set activating player
first.setActivatingPlayer(sa.getActivatingPlayer());
game.fireEvent(new GameEventCardStatsChanged(source)); game.fireEvent(new GameEventCardStatsChanged(source));
AbilityUtils.resolve(sa.getHostCard().getFirstSpellAbility()); AbilityUtils.resolve(first);
} 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

@@ -106,7 +106,7 @@ public class PlayerZone extends Zone {
} }
public CardCollectionView getCardsPlayerCanActivate(Player who) { public CardCollectionView getCardsPlayerCanActivate(Player who) {
CardCollectionView cl = getCards(false); CardCollectionView cl = getCards(false, true);
boolean checkingForOwner = who == player; boolean checkingForOwner = who == player;
if (checkingForOwner && (is(ZoneType.Battlefield) || is(ZoneType.Hand))) { if (checkingForOwner && (is(ZoneType.Battlefield) || is(ZoneType.Hand))) {

View File

@@ -62,7 +62,7 @@ public class PlayerZoneBattlefield extends PlayerZone {
super.add(c, position, latestState); super.add(c, position, latestState);
if (trigger) { if (trigger && !c.isMerged()) {
c.setSickness(true); // summoning sickness c.setSickness(true); // summoning sickness
c.runComesIntoPlayCommands(); c.runComesIntoPlayCommands();
} }
@@ -84,18 +84,20 @@ public class PlayerZoneBattlefield extends PlayerZone {
} }
@Override @Override
public final CardCollectionView getCards(final boolean filter) { public final CardCollectionView getCards(final boolean filterOutPhasedOut, final boolean filterOutMerged) {
// Battlefield filters out Phased Out cards by default. Needs to call // Battlefield filters out Phased Out cards by default. Needs to call
// getCards(false) to get Phased Out cards // getCards(false) to get Phased Out cards
// For merged permanent, also filter out all merged cards except the top one
CardCollectionView cards = super.getCards(false); CardCollectionView cards = super.getCards(false, false);
if (!filter) { if (!filterOutPhasedOut && !filterOutMerged) {
return cards; return cards;
} }
boolean hasFilteredCard = false; boolean hasFilteredCard = false;
for (Card c : cards) { for (Card c : cards) {
if (c.isPhasedOut()) { if (filterOutPhasedOut && c.isPhasedOut() ||
filterOutMerged && c.getMergedToCard() != null) {
hasFilteredCard = true; hasFilteredCard = true;
break; break;
} }
@@ -104,7 +106,8 @@ public class PlayerZoneBattlefield extends PlayerZone {
if (hasFilteredCard) { if (hasFilteredCard) {
CardCollection filteredCollection = new CardCollection(); CardCollection filteredCollection = new CardCollection();
for (Card c : cards) { for (Card c : cards) {
if (!c.isPhasedOut()) { if ((!filterOutPhasedOut || !c.isPhasedOut()) &&
(!filterOutMerged || c.getMergedToCard() == null)) {
filteredCollection.add(c); filteredCollection.add(c);
} }
} }

View File

@@ -190,10 +190,10 @@ public class Zone implements java.io.Serializable, Iterable<Card> {
} }
public final CardCollectionView getCards() { 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 return cardList; // Non-Battlefield PlayerZones don't care about the filter
} }

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.