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())) {
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())) {

View File

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

View File

@@ -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<GameObject> 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<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
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<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));
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);
}
}

View File

@@ -1025,6 +1025,10 @@ public class Card extends GameEntity implements Comparable<Card>, 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<Card>, IHasSVars {
if (tapped == tapped0) { return; }
tapped = tapped0;
view.updateTapped(this);
for (final Card c : getMergedCards()) {
c.setTapped(tapped0);
}
}
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) {
CardState result = new CardState(host, name);
result.copyFrom(this, lki);

View File

@@ -1497,7 +1497,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
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) {

View File

@@ -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())

View File

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

View File

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

View File

@@ -62,7 +62,7 @@ public class PlayerZoneBattlefield extends PlayerZone {
super.add(c, position, latestState);
if (trigger) {
if (trigger && !c.isMerged()) {
c.setSickness(true); // summoning sickness
c.runComesIntoPlayCommands();
}
@@ -84,18 +84,20 @@ public class PlayerZoneBattlefield extends PlayerZone {
}
@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
// 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);
if (!filter) {
CardCollectionView cards = super.getCards(false, false);
if (!filterOutPhasedOut && !filterOutMerged) {
return cards;
}
boolean hasFilteredCard = false;
for (Card c : cards) {
if (c.isPhasedOut()) {
if (filterOutPhasedOut && c.isPhasedOut() ||
filterOutMerged && c.getMergedToCard() != null) {
hasFilteredCard = true;
break;
}
@@ -104,7 +106,8 @@ public class PlayerZoneBattlefield extends PlayerZone {
if (hasFilteredCard) {
CardCollection filteredCollection = new CardCollection();
for (Card c : cards) {
if (!c.isPhasedOut()) {
if ((!filterOutPhasedOut || !c.isPhasedOut()) &&
(!filterOutMerged || c.getMergedToCard() == null)) {
filteredCollection.add(c);
}
}

View File

@@ -190,10 +190,10 @@ public class Zone implements java.io.Serializable, Iterable<Card> {
}
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
}

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.