mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-14 09:48:02 +00:00
Mutate second step
This commit is contained in:
@@ -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())) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
9
forge-gui/res/cardsfolder/d/dreamtail_heron.txt
Normal file
9
forge-gui/res/cardsfolder/d/dreamtail_heron.txt
Normal 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.
|
||||
7
forge-gui/res/cardsfolder/p/parcelbeast.txt
Normal file
7
forge-gui/res/cardsfolder/p/parcelbeast.txt
Normal 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.
|
||||
9
forge-gui/res/cardsfolder/v/vulpikeet.txt
Normal file
9
forge-gui/res/cardsfolder/v/vulpikeet.txt
Normal 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.
|
||||
Reference in New Issue
Block a user