Merge remote-tracking branch 'upstream/master' into collector-number-in-card-list-and-card-db-refactoring

This commit is contained in:
leriomaggio
2021-07-21 17:32:02 +01:00
488 changed files with 2679 additions and 1017 deletions

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.43-SNAPSHOT</version>
<version>1.6.44-SNAPSHOT</version>
</parent>
<artifactId>forge-game</artifactId>

View File

@@ -705,17 +705,20 @@ public class GameAction {
}
}
if (zoneFrom == null) {
c.setCastFrom(null);
c.setCastSA(null);
} else if (zoneTo.is(ZoneType.Stack)) {
c.setCastFrom(zoneFrom.getZoneType());
if (zoneTo.is(ZoneType.Stack)) {
// zoneFrom maybe null if the spell is cast from "Ouside the game", ex. ability of Garth One-Eye
if (zoneFrom == null) {
c.setCastFrom(null);
} else {
c.setCastFrom(zoneFrom.getZoneType());
}
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard()) && !c.isCopiedSpell()) {
c.setCastSA(cause);
} else {
c.setCastSA(null);
}
} else if (!(zoneTo.is(ZoneType.Battlefield) && zoneFrom.is(ZoneType.Stack))) {
} else if (zoneFrom == null || !(zoneFrom.is(ZoneType.Stack) &&
(zoneTo.is(ZoneType.Battlefield) || zoneTo.is(ZoneType.Merged)))) {
c.setCastFrom(null);
c.setCastSA(null);
}

View File

@@ -153,8 +153,7 @@ public final class GameActionUtil {
final StringBuilder sb = new StringBuilder(sa.getDescription());
if (!source.equals(host)) {
sb.append(" by ");
if ((host.isEmblem() || host.getType().hasSubtype("Effect"))
&& host.getEffectSource() != null) {
if ((host.isImmutable()) && host.getEffectSource() != null) {
sb.append(host.getEffectSource());
} else {
sb.append(host);
@@ -542,7 +541,6 @@ public final class GameActionUtil {
final Card eff = new Card(game.nextCardId(), game);
eff.setTimestamp(game.getNextTimestamp());
eff.setName(sourceCard.getName() + "'s Effect");
eff.addType("Effect");
eff.setOwner(controller);
eff.setImageKey(sourceCard.getImageKey());

View File

@@ -1,5 +1,6 @@
package forge.game;
import java.util.Arrays;
import java.util.Map;
import com.google.common.collect.ImmutableList;
@@ -8,7 +9,7 @@ import forge.game.ability.AbilityKey;
import forge.game.player.Player;
import forge.game.trigger.TriggerType;
/**
/**
* Represents the planar dice for Planechase games.
*
*/
@@ -16,7 +17,7 @@ public enum PlanarDice {
Planeswalk,
Chaos,
Blank;
public static PlanarDice roll(Player roller, PlanarDice riggedResult)
{
PlanarDice res = Blank;
@@ -27,25 +28,37 @@ public enum PlanarDice {
res = Planeswalk;
else if (i == 1)
res = Chaos;
PlanarDice trigRes = res;
if(roller.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.blankIsChaos)
&& res == Blank)
{
trigRes = Chaos;
}
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, roller);
runParams.put(AbilityKey.Result, trigRes);
roller.getGame().getTriggerHandler().runTrigger(TriggerType.PlanarDice, runParams,false);
// Also run normal RolledDie and RolledDieOnce triggers
runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, roller);
runParams.put(AbilityKey.Sides, 6);
runParams.put(AbilityKey.Result, 0);
roller.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDie, runParams, false);
runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, roller);
runParams.put(AbilityKey.Sides, 6);
runParams.put(AbilityKey.Result, Arrays.asList(0));
roller.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDieOnce, runParams, false);
return res;
}
/**
* Parses a string into an enum member.
* @param string to parse

View File

@@ -31,6 +31,7 @@ import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
import forge.card.mana.ManaCostShard;
import forge.game.CardTraitBase;
import forge.game.Direction;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
@@ -111,9 +112,9 @@ public class AbilityUtils {
// Probably will move to One function solution sometime in the future
public static CardCollection getDefinedCards(final Card hostCard, final String def, final CardTraitBase sa) {
CardCollection cards = new CardCollection();
String defined = (def == null) ? "Self" : applyAbilityTextChangeEffects(def, sa); // default to Self
final String[] incR = defined.split("\\.", 2);
defined = incR[0];
String changedDef = (def == null) ? "Self" : applyAbilityTextChangeEffects(def, sa); // default to Self
final String[] incR = changedDef.split("\\.", 2);
String defined = incR[0];
final Game game = hostCard.getGame();
Card c = null;
@@ -131,7 +132,7 @@ public class AbilityUtils {
}
}
else if (defined.equals("EffectSource")) {
if (hostCard.isEmblem() || hostCard.getType().hasSubtype("Effect")) {
if (hostCard.isImmutable()) {
c = findEffectRoot(hostCard);
}
}
@@ -313,6 +314,10 @@ public class AbilityUtils {
for (final Card imprint : hostCard.getImprintedCards()) {
cards.add(game.getCardState(imprint));
}
} else if (defined.equals("UntilLeavesBattlefield")) {
for (final Card ulb : hostCard.getUntilLeavesBattlefield()) {
cards.add(game.getCardState(ulb));
}
} else if (defined.startsWith("ThisTurnEntered")) {
final String[] workingCopy = defined.split("_");
ZoneType destination, origin;
@@ -345,6 +350,23 @@ public class AbilityUtils {
cards.add(game.getCardState(cardByID));
}
}
} else if (defined.startsWith("Valid")) {
Iterable<Card> candidates;
String validDefined;
if (defined.startsWith("Valid ")) {
candidates = game.getCardsIn(ZoneType.Battlefield);
validDefined = changedDef.substring("Valid ".length());
} else if (defined.startsWith("ValidAll ")) {
candidates = game.getCardsInGame();
validDefined = changedDef.substring("ValidAll ".length());
} else {
String[] s = changedDef.split(" ", 2);
String zone = s[0].substring("Valid".length());
candidates = game.getCardsIn(ZoneType.smartValueOf(zone));
validDefined = s[1];
}
cards.addAll(CardLists.getValidCards(candidates, validDefined.split(","), hostCard.getController(), hostCard, sa));
return cards;
} else {
CardCollection list = null;
if (sa instanceof SpellAbility) {
@@ -376,19 +398,6 @@ public class AbilityUtils {
}
}
if (defined.startsWith("Valid ")) {
String validDefined = defined.substring("Valid ".length());
list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), validDefined.split(","), hostCard.getController(), hostCard, sa);
} else if (defined.startsWith("ValidAll ")) {
String validDefined = defined.substring("ValidAll ".length());
list = CardLists.getValidCards(game.getCardsInGame(), validDefined.split(","), hostCard.getController(), hostCard, sa);
} else if (defined.startsWith("Valid")) {
String[] s = defined.split(" ");
String zone = s[0].substring("Valid".length());
String validDefined = s[1];
list = CardLists.getValidCards(game.getCardsIn(ZoneType.smartValueOf(zone)), validDefined.split(","), hostCard.getController(), hostCard, sa);
}
if (list != null) {
cards.addAll(list);
}
@@ -420,7 +429,7 @@ public class AbilityUtils {
private static Card findEffectRoot(Card startCard) {
Card cc = startCard.getEffectSource();
if (cc != null) {
if (cc.isEmblem() || cc.getType().hasSubtype("Effect")) {
if (cc.isImmutable()) {
return findEffectRoot(cc);
}
return cc;
@@ -1268,6 +1277,9 @@ public class AbilityUtils {
}
else if (defined.equals("Opponent")) {
players.addAll(player.getOpponents());
} else if (defined.startsWith("NextPlayerToYour")) {
Direction dir = defined.substring(16).equals("Left") ? Direction.Left : Direction.Right;
players.add(game.getNextPlayerAfter(player, dir));
}
else {
for (Player p : game.getPlayersInTurnOrder()) {

View File

@@ -142,6 +142,7 @@ public enum ApiType {
ReplaceEffect (ReplaceEffect.class),
ReplaceMana (ReplaceManaEffect.class),
ReplaceDamage (ReplaceDamageEffect.class),
ReplaceToken (ReplaceTokenEffect.class),
ReplaceSplitDamage (ReplaceSplitDamageEffect.class),
RestartGame (RestartGameEffect.class),
Reveal (RevealEffect.class),

View File

@@ -13,7 +13,6 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Table;
import forge.GameCommand;
import forge.card.CardType;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameEntity;
@@ -457,25 +456,19 @@ public abstract class SpellAbilityEffect {
final Card eff = new Card(game.nextCardId(), game);
eff.setTimestamp(game.getNextTimestamp());
eff.setName(name);
eff.setColor(hostCard.determineColor().getColor());
// if name includes emblem then it should be one
eff.addType(name.startsWith("Emblem") ? "Emblem" : "Effect");
// add Planeswalker types into Emblem for fun
if (name.startsWith("Emblem") && hostCard.isPlaneswalker()) {
for (final String type : hostCard.getType().getSubtypes()) {
if (CardType.isAPlaneswalkerType(type)) {
eff.addType(type);
}
}
if (name.startsWith("Emblem")) {
eff.setEmblem(true);
// Emblem needs to be colorless
eff.setColor(MagicColor.COLORLESS);
}
eff.setOwner(controller);
eff.setSVars(sa.getSVars());
eff.setImageKey(image);
if (eff.getType().hasType(CardType.CoreType.Emblem)) {
eff.setColor(MagicColor.COLORLESS);
} else {
eff.setColor(hostCard.determineColor().getColor());
}
eff.setImmutable(true);
eff.setEffectSource(sa);

View File

@@ -16,7 +16,6 @@ import forge.game.card.CardPredicates;
import forge.game.card.CardZoneTable;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.card.token.TokenInfo;
import forge.game.event.GameEventCombatChanged;
import forge.game.event.GameEventTokenCreated;
import forge.game.player.Player;
@@ -53,37 +52,24 @@ public class AmassEffect extends TokenEffectBase {
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa);
final boolean remember = sa.hasParam("RememberAmass");
boolean useZoneTable = true;
CardZoneTable triggerList = sa.getChangeZoneTable();
if (triggerList == null) {
triggerList = new CardZoneTable();
useZoneTable = false;
}
if (sa.hasParam("ChangeZoneTable")) {
sa.setChangeZoneTable(triggerList);
useZoneTable = true;
}
MutableBoolean combatChanged = new MutableBoolean(false);
// create army token if needed
if (CardLists.count(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Army")) == 0) {
final String tokenScript = "b_0_0_zombie_army";
CardZoneTable triggerList = new CardZoneTable();
MutableBoolean combatChanged = new MutableBoolean(false);
final Card prototype = TokenInfo.getProtoType(tokenScript, sa, false);
makeTokenTable(makeTokenTableInternal(activator, "b_0_0_zombie_army", 1, sa), false, triggerList, combatChanged, sa);
makeTokens(prototype, activator, sa, 1, true, false, triggerList, combatChanged);
triggerList.triggerChangesZoneAll(game, sa);
triggerList.clear();
if (!useZoneTable) {
triggerList.triggerChangesZoneAll(game, sa);
triggerList.clear();
}
game.fireEvent(new GameEventTokenCreated());
if (combatChanged.isTrue()) {
game.updateCombatForView();
game.fireEvent(new GameEventCombatChanged());
}
}
if (combatChanged.isTrue()) {
game.updateCombatForView();
game.fireEvent(new GameEventCombatChanged());
}
Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", CounterType.get(CounterEnumType.P1P1));
params.put("Amount", 1);

View File

@@ -190,11 +190,6 @@ public class AnimateEffect extends AnimateEffectBase {
}
}
// Restore immutable to effect
if (sa.hasParam("Immutable")) {
c.setImmutable(true);
}
game.fireEvent(new GameEventCardStatsChanged(c));
}

View File

@@ -164,14 +164,6 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
final Trigger parsedTrigger = TriggerHandler.parseTrigger(AbilityUtils.getSVar(sa, s), c, false, sa);
addedTriggers.add(parsedTrigger);
}
if (sa.hasParam("GainsTriggeredAbilitiesOf")) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("GainsTriggeredAbilitiesOf"), sa);
for (final Card card : cards) {
for (Trigger t : card.getTriggers()) {
addedTriggers.add(t.copy(c, false));
}
}
}
// give replacement effects
final List<ReplacementEffect> addedReplacements = Lists.newArrayList();

View File

@@ -119,6 +119,12 @@ public class ChooseCardEffect extends SpellAbilityEffect {
Aggregates.random(choices, validAmount, chosen);
} else {
String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChooseaCard") + " ";
if (sa.hasParam ("ChoiceTitleAppendDefined")) {
String defined = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("ChoiceTitleAppendDefined"), sa).toString();
final StringBuilder sb = new StringBuilder();
sb.append(title).append(" ").append(defined);
title = sb.toString();
}
chosen.addAll(p.getController().chooseCardsForEffect(choices, sa, title, minAmount, validAmount, !sa.hasParam("Mandatory"), null));
}
}

View File

@@ -72,8 +72,7 @@ public class ChooseTypeEffect extends SpellAbilityEffect {
}
}
}
}
else {
} else {
throw new InvalidParameterException(sa.getHostCard() + "'s ability resulted in no types to choose from");
}
}

View File

@@ -67,7 +67,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
if (tapOnLose) {
c.tap();
}
} // if
}
host.removeGainControlTargets(c);
}
@@ -91,7 +91,12 @@ public class ControlGainEffect extends SpellAbilityEffect {
if (sa.hasParam("ControlledByTarget")) {
tgtCards = CardLists.filterControlledBy(tgtCards, getTargetPlayers(sa));
}
}
// in case source was LKI or still resolving
if (source.isLKI() || source.getZone().is(ZoneType.Stack)) {
source = game.getCardState(source);
}
// check for lose control criteria right away
if (lose != null && lose.contains("LeavesPlay") && !source.isInZone(ZoneType.Battlefield)) {
@@ -103,7 +108,6 @@ public class ControlGainEffect extends SpellAbilityEffect {
boolean combatChanged = false;
for (Card tgtC : tgtCards) {
if (!tgtC.isInPlay() || !tgtC.canBeControlledBy(newController)) {
continue;
}

View File

@@ -20,6 +20,7 @@ import forge.game.card.CardFactory;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardZoneTable;
import forge.game.card.TokenCreateTable;
import forge.game.event.GameEventCombatChanged;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
@@ -177,15 +178,18 @@ public class CopyPermanentEffect extends TokenEffectBase {
}
MutableBoolean combatChanged = new MutableBoolean(false);
TokenCreateTable tokenTable = new TokenCreateTable();
for (final Card c : tgtCards) {
// if it only targets player, it already got all needed cards from defined
if (sa.usesTargeting() && !sa.getTargetRestrictions().canTgtPlayer() && !c.canBeTargetedBy(sa)) {
continue;
}
makeTokens(getProtoType(sa, c), controller, sa, numCopies, true, true, triggerList, combatChanged);
tokenTable.put(controller, getProtoType(sa, c, controller), numCopies);
} // end foreach Card
makeTokenTable(tokenTable, true, triggerList, combatChanged, sa);
if (!useZoneTable) {
triggerList.triggerChangesZoneAll(game, sa);
triggerList.clear();
@@ -196,9 +200,8 @@ public class CopyPermanentEffect extends TokenEffectBase {
}
} // end resolve
private Card getProtoType(final SpellAbility sa, final Card original) {
private Card getProtoType(final SpellAbility sa, final Card original, final Player newOwner) {
final Card host = sa.getHostCard();
final Player newOwner = sa.getActivatingPlayer();
int id = newOwner == null ? 0 : newOwner.getGame().nextCardId();
final Card copy = new Card(id, original.getPaperCard(), host.getGame());
copy.setOwner(newOwner);

View File

@@ -12,7 +12,6 @@ public class DetachedCardEffect extends Card {
card = card0;
setName(name0);
addType("Effect");
setOwner(card0.getOwner());
setImmutable(true);

View File

@@ -140,10 +140,6 @@ public class EffectEffect extends SpellAbilityEffect {
final Card eff = createEffect(sa, controller, name, image);
eff.setSetCode(sa.getHostCard().getSetCode());
eff.setRarity(sa.getHostCard().getRarity());
// For Raging River effect to add attacker "left" or "right" pile later
if (sa.hasParam("Mutable")) {
eff.setImmutable(false);
}
// Abilities and triggers work the same as they do for Token
// Grant abilities

View File

@@ -38,7 +38,6 @@ public class FightEffect extends DamageBaseEffect {
return sb.toString();
}
/* (non-Javadoc)
* @see forge.game.ability.SpellAbilityEffect#resolve(forge.game.spellability.SpellAbility)
*/
@@ -77,6 +76,7 @@ public class FightEffect extends DamageBaseEffect {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Fighters, fighters);
game.getTriggerHandler().runTrigger(TriggerType.FightOnce, runParams, false);
}
private static List<Card> getFighters(SpellAbility sa) {

View File

@@ -6,7 +6,6 @@ import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardZoneTable;
import forge.game.card.token.TokenInfo;
import forge.game.event.GameEventCombatChanged;
import forge.game.event.GameEventTokenCreated;
import forge.game.player.Player;
@@ -36,14 +35,13 @@ public class InvestigateEffect extends TokenEffectBase {
final int amount = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("Num", "1"), sa);
final String tokenScript = "c_a_clue_draw";
final Card prototype = TokenInfo.getProtoType(tokenScript, sa, false);
// Investigate in Sequence
for (final Player p : getTargetPlayers(sa)) {
for (int i = 0; i < amount; i++) {
CardZoneTable triggerList = new CardZoneTable();
MutableBoolean combatChanged = new MutableBoolean(false);
makeTokens(prototype, p, sa, 1, true, false, triggerList, combatChanged);
makeTokenTable(makeTokenTableInternal(p, "c_a_clue_draw", 1, sa), false, triggerList, combatChanged, sa);
triggerList.triggerChangesZoneAll(game, sa);
p.addInvestigatedThisTurn();

View File

@@ -1,7 +1,10 @@
package forge.game.ability.effects;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
@@ -69,6 +72,8 @@ public class PlayEffect extends SpellAbilityEffect {
final boolean optional = sa.hasParam("Optional");
boolean remember = sa.hasParam("RememberPlayed");
int amount = 1;
boolean hasTotalCMCLimit = sa.hasParam("WithTotalCMC");
int totalCMCLimit = Integer.MAX_VALUE;
if (sa.hasParam("Amount") && !sa.getParam("Amount").equals("All")) {
amount = AbilityUtils.calculateAmount(source, sa.getParam("Amount"), sa);
}
@@ -163,13 +168,15 @@ public class PlayEffect extends SpellAbilityEffect {
if (sa.hasParam("ValidSA")) {
final String valid[] = {sa.getParam("ValidSA")};
List<Card> toRemove = Lists.newArrayList();
for (Card c : tgtCards) {
Iterator<Card> it = tgtCards.iterator();
while (it.hasNext()) {
Card c = it.next();
if (!Iterables.any(AbilityUtils.getBasicSpellsFromPlayEffect(c, controller), SpellAbilityPredicates.isValid(valid, controller , c, sa))) {
toRemove.add(c);
// it.remove will only remove item from the list part of CardCollection
tgtCards.asSet().remove(c);
it.remove();
}
}
tgtCards.removeAll(toRemove);
if (tgtCards.isEmpty()) {
return;
}
@@ -179,15 +186,37 @@ public class PlayEffect extends SpellAbilityEffect {
amount = tgtCards.size();
}
if (hasTotalCMCLimit) {
totalCMCLimit = AbilityUtils.calculateAmount(source, sa.getParam("WithTotalCMC"), sa);
}
if (controlledByPlayer != null) {
activator.addController(controlledByTimeStamp, controlledByPlayer);
}
boolean singleOption = tgtCards.size() == 1 && amount == 1 && optional;
Map<String, Object> params = hasTotalCMCLimit ? new HashMap<>() : null;
while (!tgtCards.isEmpty() && amount > 0 && totalCMCLimit >= 0) {
if (hasTotalCMCLimit) {
// filter out cards with mana value greater than limit
Iterator<Card> it = tgtCards.iterator();
final String [] valid = {"Spell.cmcLE"+totalCMCLimit};
while (it.hasNext()) {
Card c = it.next();
if (!Iterables.any(AbilityUtils.getBasicSpellsFromPlayEffect(c, controller), SpellAbilityPredicates.isValid(valid, controller , c, sa))) {
// it.remove will only remove item from the list part of CardCollection
tgtCards.asSet().remove(c);
it.remove();
}
}
if (tgtCards.isEmpty())
break;
params.put("CMCLimit", totalCMCLimit);
}
while (!tgtCards.isEmpty() && amount > 0) {
activator.getController().tempShowCards(showCards);
Card tgtCard = controller.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblSelectCardToPlay"), !singleOption && optional, null);
Card tgtCard = controller.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblSelectCardToPlay"), !singleOption && optional, params);
activator.getController().endTempShowCards();
if (tgtCard == null) {
break;
@@ -248,6 +277,14 @@ public class PlayEffect extends SpellAbilityEffect {
final String valid[] = {sa.getParam("ValidSA")};
sas = Lists.newArrayList(Iterables.filter(sas, SpellAbilityPredicates.isValid(valid, controller , source, sa)));
}
if (hasTotalCMCLimit) {
Iterator<SpellAbility> it = sas.iterator();
while (it.hasNext()) {
SpellAbility s = it.next();
if (s.getPayCosts().getTotalMana().getCMC() > totalCMCLimit)
it.remove();
}
}
if (sas.isEmpty()) {
continue;
@@ -276,6 +313,8 @@ public class PlayEffect extends SpellAbilityEffect {
continue;
}
final int tgtCMC = tgtSA.getPayCosts().getTotalMana().getCMC();
if (sa.hasParam("WithoutManaCost")) {
tgtSA = tgtSA.copyWithNoManaCost();
} else if (sa.hasParam("PlayCost")) {
@@ -341,6 +380,7 @@ public class PlayEffect extends SpellAbilityEffect {
}
amount--;
totalCMCLimit -= tgtCMC;
}
// Remove controlled by player if any

View File

@@ -121,7 +121,7 @@ public class PumpEffect extends SpellAbilityEffect {
final Card host = sa.getHostCard();
//if host is not on the battlefield don't apply
// Suspend should does Affect the Stack
if ((sa.hasParam("UntilLoseControlOfHost") || sa.hasParam("UntilHostLeavesPlay"))
if (("UntilHostLeavesPlay".equals(sa.getParam("Duration")) || "UntilLoseControlOfHost".equals(sa.getParam("Duration")))
&& !(host.isInPlay() || host.isInZone(ZoneType.Stack))) {
return;
}
@@ -158,7 +158,6 @@ public class PumpEffect extends SpellAbilityEffect {
}
if (tgts.size() > 0) {
for (final GameEntity c : tgts) {
sb.append(c).append(" ");
}

View File

@@ -37,7 +37,7 @@ public class RegenerationEffect extends SpellAbilityEffect {
// Play the Regen sound
game.fireEvent(new GameEventCardRegenerated());
if (host.getType().hasStringType("Effect")) {
if (host.isImmutable()) {
c.subtractShield(host);
host.removeRemembered(c);
}

View File

@@ -49,7 +49,7 @@ public class RepeatEffect extends SpellAbilityEffect {
// TODO Replace Infinite Loop Break with a game draw. Here are the scenarios that can cause this:
// Helm of Obedience vs Graveyard to Library replacement effect
if(source.getName().equals("Helm of Obedience")) {
if (source.getName().equals("Helm of Obedience")) {
StringBuilder infLoop = new StringBuilder(sa.getHostCard().toString());
infLoop.append(" - To avoid an infinite loop, this repeat has been broken ");
infLoop.append(" and the game will now continue in the current state, ending the loop early. ");

View File

@@ -47,7 +47,7 @@ public class ReplaceDamageEffect extends SpellAbilityEffect {
prevent -= n;
if (!StringUtils.isNumeric(varValue) && card.getSVar(varValue).startsWith("Number$")) {
if (card.getType().hasStringType("Effect") && prevent <= 0) {
if (card.isImmutable() && prevent <= 0) {
game.getAction().exile(card, null);
} else {
card.setSVar(varValue, "Number$" + prevent);

View File

@@ -3,12 +3,6 @@ package forge.game.ability.effects;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Maps;
import forge.game.Game;
import forge.game.GameLogEntryType;
import forge.game.GameObject;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
@@ -17,25 +11,21 @@ import forge.game.card.Card;
import forge.game.card.token.TokenInfo;
import forge.game.player.Player;
import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.util.TextUtil;
public class ReplaceEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
final Game game = card.getGame();
final AbilityKey varName = AbilityKey.fromString(sa.getParam("VarName"));
final String varValue = sa.getParam("VarValue");
final String type = sa.getParamOrDefault("VarType", "amount");
final ReplacementType retype = sa.getReplacementEffect().getMode();
@SuppressWarnings("unchecked")
Map<AbilityKey, Object> originalParams = (Map<AbilityKey, Object>) sa.getReplacingObject(AbilityKey.OriginalParams);
Map<AbilityKey, Object> params = Maps.newHashMap(originalParams);
Map<AbilityKey, Object> params = (Map<AbilityKey, Object>) sa.getReplacingObject(AbilityKey.OriginalParams);
if ("Card".equals(type)) {
List<Card> list = AbilityUtils.getDefinedCards(card, varValue, sa);
@@ -53,7 +43,7 @@ public class ReplaceEffect extends SpellAbilityEffect {
params.put(varName, list.get(0));
}
} else if ("TokenScript".equals(type)) {
final Card protoType = TokenInfo.getProtoType(varValue, sa);
final Card protoType = TokenInfo.getProtoType(varValue, sa, sa.getActivatingPlayer());
if (protoType != null) {
params.put(varName, protoType);
}
@@ -61,42 +51,7 @@ public class ReplaceEffect extends SpellAbilityEffect {
params.put(varName, AbilityUtils.calculateAmount(card, varValue, sa));
}
if (params.containsKey(AbilityKey.EffectOnly)) {
params.put(AbilityKey.EffectOnly, true);
}
if (retype == ReplacementType.DamageDone) {
for (Map.Entry<AbilityKey, Object> e : params.entrySet()) {
originalParams.put(e.getKey(), e.getValue());
}
originalParams.put(AbilityKey.ReplacementResult, ReplacementResult.Updated);
return;
}
// need to log Updated events there, or the log is wrong order
String message = sa.getReplacementEffect().toString();
if ( !StringUtils.isEmpty(message)) {
message = TextUtil.fastReplace(message, "CARDNAME", card.getName());
game.getGameLog().add(GameLogEntryType.EFFECT_REPLACED, message);
}
//try to call replacementHandler with new Params
ReplacementResult result = game.getReplacementHandler().run(retype, params);
switch (result) {
case NotReplaced:
case Updated: {
for (Map.Entry<AbilityKey, Object> e : params.entrySet()) {
originalParams.put(e.getKey(), e.getValue());
}
// effect was updated
originalParams.put(AbilityKey.ReplacementResult, ReplacementResult.Updated);
break;
}
default:
// effect was replaced with something else
originalParams.put(AbilityKey.ReplacementResult, result);
break;
}
params.put(AbilityKey.ReplacementResult, ReplacementResult.Updated);
}
}

View File

@@ -4,20 +4,15 @@ import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Maps;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameLogEntryType;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.util.TextUtil;
public class ReplaceManaEffect extends SpellAbilityEffect {
@@ -25,17 +20,14 @@ public class ReplaceManaEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
final Player player = sa.getActivatingPlayer();
final Game game = card.getGame();
// outside of Replacement Effect, unwanted result
if (!sa.isReplacementAbility()) {
return;
}
final ReplacementType event = sa.getReplacementEffect().getMode();
@SuppressWarnings("unchecked")
Map<AbilityKey, Object> originalParams = (Map<AbilityKey, Object>) sa.getReplacingObject(AbilityKey.OriginalParams);
Map<AbilityKey, Object> params = Maps.newHashMap(originalParams);
Map<AbilityKey, Object> params = (Map<AbilityKey, Object>) sa.getReplacingObject(AbilityKey.OriginalParams);
String replaced = (String)sa.getReplacingObject(AbilityKey.Mana);
if (sa.hasParam("ReplaceMana")) {
@@ -79,31 +71,8 @@ public class ReplaceManaEffect extends SpellAbilityEffect {
replaced = StringUtils.repeat(replaced, " ", Integer.valueOf(sa.getParam("ReplaceAmount")));
}
params.put(AbilityKey.Mana, replaced);
// need to log Updated events there, or the log is wrong order
String message = sa.getReplacementEffect().toString();
if (!StringUtils.isEmpty(message)) {
message = TextUtil.fastReplace(message, "CARDNAME", card.getName());
game.getGameLog().add(GameLogEntryType.EFFECT_REPLACED, message);
}
//try to call replacementHandler with new Params
ReplacementResult result = game.getReplacementHandler().run(event, params);
switch (result) {
case NotReplaced:
case Updated: {
for (Map.Entry<AbilityKey, Object> e : params.entrySet()) {
originalParams.put(e.getKey(), e.getValue());
}
// effect was updated
originalParams.put(AbilityKey.ReplacementResult, ReplacementResult.Updated);
break;
}
default:
// effect was replaced with something else
originalParams.put(AbilityKey.ReplacementResult, result);
break;
}
// effect was updated
params.put(AbilityKey.ReplacementResult, ReplacementResult.Updated);
}
}

View File

@@ -45,7 +45,7 @@ public class ReplaceSplitDamageEffect extends SpellAbilityEffect {
dmg -= n;
prevent -= n;
if (card.getType().hasStringType("Effect") && prevent <= 0) {
if (card.isImmutable() && prevent <= 0) {
game.getAction().exile(card, null);
} else if (!StringUtils.isNumeric(varValue)) {
sa.setSVar(varValue, "Number$" + prevent);

View File

@@ -0,0 +1,131 @@
package forge.game.ability.effects;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.ObjectUtils;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.TokenCreateTable;
import forge.game.card.token.TokenInfo;
import forge.game.player.Player;
import forge.game.replacement.ReplacementResult;
import forge.game.spellability.SpellAbility;
public class ReplaceTokenEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card card = sa.getHostCard();
final Player p = sa.getActivatingPlayer();
final Game game = card.getGame();
// ReplaceToken Effect only applies to one Player
Player affected = (Player) sa.getReplacingObject(AbilityKey.Player);
TokenCreateTable table = (TokenCreateTable) sa.getReplacingObject(AbilityKey.Token);
@SuppressWarnings("unchecked")
Map<AbilityKey, Object> originalParams = (Map<AbilityKey, Object>) sa
.getReplacingObject(AbilityKey.OriginalParams);
// currently the only ones that changes the amount does double it
if ("Amount".equals(sa.getParam("Type"))) {
for (Map.Entry<Card, Integer> e : table.row(affected).entrySet()) {
if (!sa.matchesValidParam("ValidCard", e.getKey())) {
continue;
}
// currently the amount is only doubled
table.put(affected, e.getKey(), e.getValue() * 2);
}
} else if ("AddToken".equals(sa.getParam("Type"))) {
long timestamp = game.getNextTimestamp();
Map<Player, Integer> byController = Maps.newHashMap();
for (Map.Entry<Card, Integer> e : table.row(affected).entrySet()) {
if (!sa.matchesValidParam("ValidCard", e.getKey())) {
continue;
}
Player contoller = e.getKey().getController();
int old = ObjectUtils.defaultIfNull(byController.get(contoller), 0);
byController.put(contoller, old + e.getValue());
}
if (!byController.isEmpty()) {
// for Xorn, might matter if you could somehow create Treasure under multiple players control
if (sa.hasParam("Amount")) {
int i = AbilityUtils.calculateAmount(card, sa.getParam("Amount"), sa);
for (Map.Entry<Player, Integer> e : byController.entrySet()) {
e.setValue(i);
}
}
for (Map.Entry<Player, Integer> e : byController.entrySet()) {
for (String script : sa.getParam("TokenScript").split(",")) {
final Card token = TokenInfo.getProtoType(script, sa, p);
if (token == null) {
throw new RuntimeException("don't find Token for TokenScript: " + script);
}
token.setController(e.getKey(), timestamp);
table.put(p, token, e.getValue());
}
}
}
} else if ("ReplaceToken".equals(sa.getParam("Type"))) {
long timestamp = game.getNextTimestamp();
Map<Player, Integer> toInsertMap = Maps.newHashMap();
Set<Card> toRemoveSet = Sets.newHashSet();
for (Map.Entry<Card, Integer> e : table.row(affected).entrySet()) {
if (!sa.matchesValidParam("ValidCard", e.getKey())) {
continue;
}
Player controller = e.getKey().getController();
int old = ObjectUtils.defaultIfNull(toInsertMap.get(controller), 0);
toInsertMap.put(controller, old + e.getValue());
toRemoveSet.add(e.getKey());
}
// remove replaced tokens
table.row(affected).keySet().removeAll(toRemoveSet);
// insert new tokens
for (Map.Entry<Player, Integer> pe : toInsertMap.entrySet()) {
if (pe.getValue() <= 0) {
continue;
}
for (String script : sa.getParam("TokenScript").split(",")) {
final Card token = TokenInfo.getProtoType(script, sa, pe.getKey());
if (token == null) {
throw new RuntimeException("don't find Token for TokenScript: " + script);
}
token.setController(pe.getKey(), timestamp);
table.put(affected, token, pe.getValue());
}
}
} else if ("ReplaceController".equals(sa.getParam("Type"))) {
long timestamp = game.getNextTimestamp();
Player newController = sa.getActivatingPlayer();
if (sa.hasParam("NewController")) {
newController = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("NewController"), sa).get(0);
}
for (Map.Entry<Card, Integer> c : table.row(affected).entrySet()) {
if (!sa.matchesValidParam("ValidCard", c.getKey())) {
continue;
}
c.getKey().setController(newController, timestamp);
}
}
// effect was updated
originalParams.put(AbilityKey.ReplacementResult, ReplacementResult.Updated);
}
}

View File

@@ -23,10 +23,8 @@ import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardZoneTable;
import forge.game.card.token.TokenInfo;
import forge.game.event.GameEventCombatChanged;
import forge.game.event.GameEventTokenCreated;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class TokenEffect extends TokenEffectBase {
@@ -36,20 +34,6 @@ public class TokenEffect extends TokenEffectBase {
return sa.getDescription();
}
public Card loadTokenPrototype(SpellAbility sa) {
if (!sa.hasParam("TokenScript")) {
return null;
}
final Card result = TokenInfo.getProtoType(sa.getParam("TokenScript"), sa);
if (result == null) {
throw new RuntimeException("don't find Token for TokenScript: " + sa.getParam("TokenScript"));
}
return result;
}
@Override
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
@@ -67,9 +51,8 @@ public class TokenEffect extends TokenEffectBase {
}
}
Card prototype = loadTokenPrototype(sa);
final int finalAmount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("TokenAmount", "1"), sa);
MutableBoolean combatChanged = new MutableBoolean(false);
boolean useZoneTable = true;
CardZoneTable triggerList = sa.getChangeZoneTable();
@@ -82,10 +65,8 @@ public class TokenEffect extends TokenEffectBase {
useZoneTable = true;
}
MutableBoolean combatChanged = new MutableBoolean(false);
for (final Player owner : AbilityUtils.getDefinedPlayers(host, sa.getParamOrDefault("TokenOwner", "You"), sa)) {
makeTokens(prototype, owner, sa, finalAmount, true, false, triggerList, combatChanged);
}
makeTokenTable(AbilityUtils.getDefinedPlayers(host, sa.getParamOrDefault("TokenOwner", "You"), sa),
sa.getParam("TokenScript").split(","), finalAmount, false, triggerList, combatChanged, sa);
if (!useZoneTable) {
triggerList.triggerChangesZoneAll(game, sa);

View File

@@ -2,123 +2,212 @@ package forge.game.ability.effects;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.mutable.MutableBoolean;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import forge.GameCommand;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardFactory;
import forge.game.card.CardUtil;
import forge.game.card.CardZoneTable;
import forge.game.card.CounterType;
import forge.game.card.TokenCreateTable;
import forge.game.card.token.TokenInfo;
import forge.game.event.GameEventCardStatsChanged;
import forge.game.player.Player;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.zone.ZoneType;
public abstract class TokenEffectBase extends SpellAbilityEffect {
protected List<Card> makeTokens(final Card prototype, final Player creator, final SpellAbility sa, int finalAmount,
boolean applyMultiplier, boolean clone, CardZoneTable triggerList, MutableBoolean combatChanged) {
protected TokenCreateTable createTokenTable(Iterable<Player> players, String[] tokenScripts, final int finalAmount, final SpellAbility sa) {
TokenCreateTable tokenTable = new TokenCreateTable();
for (final Player owner : players) {
for (String script : tokenScripts) {
final Card result = TokenInfo.getProtoType(script, sa, owner);
if (result == null) {
throw new RuntimeException("don't find Token for TokenScript: " + script);
}
// set owner
result.setOwner(owner);
tokenTable.put(owner, result, finalAmount);
}
}
return tokenTable;
}
protected TokenCreateTable makeTokenTableInternal(Player owner, String script, final int finalAmount, final SpellAbility sa) {
TokenCreateTable tokenTable = new TokenCreateTable();
final Card result = TokenInfo.getProtoType(script, sa, owner, false);
if (result == null) {
throw new RuntimeException("don't find Token for TokenScript: " + script);
}
// set owner
result.setOwner(owner);
tokenTable.put(owner, result, finalAmount);
return tokenTable;
}
protected TokenCreateTable makeTokenTable(Iterable<Player> players, String[] tokenScripts, final int finalAmount, final boolean clone,
CardZoneTable triggerList, MutableBoolean combatChanged, final SpellAbility sa) {
return makeTokenTable(createTokenTable(players, tokenScripts, finalAmount, sa), clone, triggerList, combatChanged, sa);
}
protected TokenCreateTable makeTokenTable(TokenCreateTable tokenTable, final boolean clone, CardZoneTable triggerList, MutableBoolean combatChanged, final SpellAbility sa) {
final Card host = sa.getHostCard();
final Game game = host.getGame();
final long timestamp = game.getNextTimestamp();
long timestamp = game.getNextTimestamp();
// support PlayerCollection for affected
Set<Player> toRemove = Sets.newHashSet();
for (Player p : tokenTable.rowKeySet()) {
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(p);
repParams.put(AbilityKey.Token, tokenTable);
repParams.put(AbilityKey.EffectOnly, true); // currently only effects can create tokens?
switch (game.getReplacementHandler().run(ReplacementType.CreateToken, repParams)) {
case NotReplaced:
break;
case Updated: {
tokenTable = (TokenCreateTable) repParams.get(AbilityKey.Token);
break;
}
default:
toRemove.add(p);
}
}
tokenTable.rowKeySet().removeAll(toRemove);
final List<String> pumpKeywords = Lists.newArrayList();
if (sa.hasParam("PumpKeywords")) {
pumpKeywords.addAll(Arrays.asList(sa.getParam("PumpKeywords").split(" & ")));
}
List<Card> allTokens = Lists.newArrayList();
for (Card tok : TokenInfo.makeTokensFromPrototype(prototype, creator, finalAmount, applyMultiplier)) {
if (sa.hasParam("TokenTapped")) {
tok.setTapped(true);
}
if (!sa.hasParam("AttachAfter") && sa.hasParam("AttachedTo") && !attachTokenTo(tok, sa)) {
continue;
}
if (sa.hasParam("WithCounters")) {
String[] parse = sa.getParam("WithCounters").split("_");
tok.addEtbCounter(CounterType.getType(parse[0]), Integer.parseInt(parse[1]), creator);
}
if (sa.hasParam("WithCountersType")) {
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
int cAmount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("WithCountersAmount", "1"), sa);
tok.addEtbCounter(cType, cAmount, creator);
}
if (clone) {
tok.setCopiedPermanent(prototype);
}
// Should this be catching the Card that's returned?
Card c = game.getAction().moveToPlay(tok, sa);
if (c == null || c.getZone() == null) {
// in case token can't enter the battlefield, it isn't created
triggerList.put(ZoneType.None, ZoneType.None, c);
continue;
}
triggerList.put(ZoneType.None, c.getZone().getZoneType(), c);
creator.addTokensCreatedThisTurn();
if (clone) {
c.setCloneOrigin(host);
}
if (!pumpKeywords.isEmpty()) {
c.addChangedCardKeywords(pumpKeywords, Lists.newArrayList(), false, false, timestamp);
addPumpUntil(sa, c, timestamp);
}
if (sa.hasParam("AtEOTTrig")) {
addSelfTrigger(sa, sa.getParam("AtEOTTrig"), c);
}
if (addToCombat(c, tok.getController(), sa, "TokenAttacking", "TokenBlocking")) {
combatChanged.setTrue();
}
if (sa.hasParam("AttachAfter") && sa.hasParam("AttachedTo")) {
attachTokenTo(tok, sa);
}
c.updateStateForView();
if (sa.hasParam("RememberTokens")) {
host.addRemembered(c);
}
if (sa.hasParam("ImprintTokens")) {
host.addImprintedCard(c);
}
if (sa.hasParam("RememberSource")) {
c.addRemembered(host);
}
if (sa.hasParam("TokenRemembered")) {
final String remembered = sa.getParam("TokenRemembered");
for (final Object o : AbilityUtils.getDefinedObjects(host, remembered, sa)) {
c.addRemembered(o);
for (final Table.Cell<Player, Card, Integer> c : tokenTable.cellSet()) {
Card prototype = c.getColumnKey();
Player creator = c.getRowKey();
Player controller = prototype.getController();
int cellAmount = c.getValue();
for (int i = 0; i < cellAmount; i++) {
Card tok = CardFactory.copyCard(prototype, true);
// Crafty Cutpurse would change under which control it does enter,
// but it shouldn't change who creates the token
tok.setOwner(creator);
if (creator != controller) {
tok.setController(controller, timestamp);
}
tok.setTimestamp(timestamp);
tok.setToken(true);
// do effect stuff with the token
if (sa.hasParam("TokenTapped")) {
tok.setTapped(true);
}
if (!sa.hasParam("AttachAfter") && sa.hasParam("AttachedTo") && !attachTokenTo(tok, sa)) {
continue;
}
if (sa.hasParam("WithCounters")) {
String[] parse = sa.getParam("WithCounters").split("_");
tok.addEtbCounter(CounterType.getType(parse[0]), Integer.parseInt(parse[1]), creator);
}
if (sa.hasParam("WithCountersType")) {
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
int cAmount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("WithCountersAmount", "1"), sa);
tok.addEtbCounter(cType, cAmount, creator);
}
if (sa.hasParam("AddTriggersFrom")) {
final List<Card> cards = AbilityUtils.getDefinedCards(host, sa.getParam("AddTriggersFrom"), sa);
for (final Card card : cards) {
for (final Trigger trig : card.getTriggers()) {
tok.addTrigger(trig.copy(tok, false));
}
}
}
if (clone) {
tok.setCopiedPermanent(prototype);
}
// Should this be catching the Card that's returned?
Card moved = game.getAction().moveToPlay(tok, sa);
if (moved == null || moved.getZone() == null) {
// in case token can't enter the battlefield, it isn't created
triggerList.put(ZoneType.None, ZoneType.None, moved);
continue;
}
triggerList.put(ZoneType.None, moved.getZone().getZoneType(), moved);
creator.addTokensCreatedThisTurn();
if (clone) {
moved.setCloneOrigin(host);
}
if (!pumpKeywords.isEmpty()) {
moved.addChangedCardKeywords(pumpKeywords, Lists.newArrayList(), false, false, timestamp);
addPumpUntil(sa, moved, timestamp);
}
if (sa.hasParam("AtEOTTrig")) {
addSelfTrigger(sa, sa.getParam("AtEOTTrig"), moved);
}
if (addToCombat(moved, tok.getController(), sa, "TokenAttacking", "TokenBlocking")) {
combatChanged.setTrue();
}
if (sa.hasParam("AttachAfter") && sa.hasParam("AttachedTo")) {
attachTokenTo(tok, sa);
}
moved.updateStateForView();
if (sa.hasParam("RememberTokens")) {
host.addRemembered(moved);
}
if (sa.hasParam("ImprintTokens")) {
host.addImprintedCard(moved);
}
if (sa.hasParam("RememberSource")) {
moved.addRemembered(host);
}
if (sa.hasParam("TokenRemembered")) {
final String remembered = sa.getParam("TokenRemembered");
for (final Object o : AbilityUtils.getDefinedObjects(host, remembered, sa)) {
moved.addRemembered(o);
}
}
allTokens.add(moved);
}
allTokens.add(c);
}
if (sa.hasParam("AtEOT")) {
registerDelayedTrigger(sa, sa.getParam("AtEOT"), allTokens);
}
return allTokens;
return tokenTable;
}
private boolean attachTokenTo(Card tok, SpellAbility sa) {

View File

@@ -206,6 +206,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// for Vanguard / Manapool / Emblems etc.
private boolean isImmutable = false;
private boolean isEmblem = false;
private int exertThisTurn = 0;
private PlayerCollection exertedByPlayer = new PlayerCollection();
@@ -1629,7 +1630,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return exiledWith;
}
public final void setExiledWith(final Card e) {
exiledWith = e;
exiledWith = view.setCard(exiledWith, e, TrackableProperty.ExiledWith);
}
public final void cleanupExiledWith() {
@@ -1738,6 +1739,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public void setCurrentRoom(String room) {
currentRoom = room;
view.updateCurrentRoom(this);
view.getCurrentState().updateAbilityText(this, getCurrentState());
}
public boolean isInLastRoom() {
for (final Trigger t : getTriggers()) {
@@ -2154,6 +2156,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public String getAbilityText(final CardState state) {
final String linebreak = "\r\n\r\n";
final String grayTag = "<span style=\"color:gray;\">";
final String endTag = "</span>";
final CardTypeView type = state.getType();
final StringBuilder sb = new StringBuilder();
@@ -2199,9 +2204,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// Get original description since text might be translated
if (replacementEffect.hasParam("Description") &&
replacementEffect.getParam("Description").contains("enters the battlefield")) {
sb.append(text).append("\r\n");
sb.append(text).append(linebreak);
} else {
replacementEffects.append(text).append("\r\n");
replacementEffects.append(text).append(linebreak);
}
}
}
@@ -2213,7 +2218,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
Cost cost = first.getPayCosts();
if (cost != null && !cost.isOnlyManaCost()) {
sb.append(cost.toString());
sb.append("\r\n");
sb.append(linebreak);
}
}
}
@@ -2233,15 +2238,22 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
// Give spellText line breaks for easier reading
sb.append("\r\n");
sb.append(text.replaceAll("\\\\r\\\\n", "\r\n"));
sb.append("\r\n");
sb.append(linebreak);
// Triggered abilities
for (final Trigger trig : state.getTriggers()) {
if (!trig.isSecondary() && !trig.isClassAbility()) {
boolean disabled = false;
// Disable text of other rooms
if (type.isDungeon() && !trig.getOverridingAbility().getParam("RoomName").equals(getCurrentRoom())) {
disabled = true;
}
String trigStr = trig.replaceAbilityText(trig.toString(), state);
sb.append(trigStr.replaceAll("\\\\r\\\\n", "\r\n")).append("\r\n");
if (disabled) sb.append(grayTag);
sb.append(trigStr.replaceAll("\\\\r\\\\n", "\r\n"));
if (disabled) sb.append(endTag);
sb.append(linebreak);
}
}
@@ -2253,7 +2265,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
if (!stAb.isSecondary() && !stAb.isClassAbility()) {
final String stAbD = stAb.toString();
if (!stAbD.equals("")) {
sb.append(stAbD).append("\r\n");
sb.append(stAbD).append(linebreak);
}
}
}
@@ -2326,7 +2338,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
desc = TextUtil.fastReplace(desc, "EFFECTSOURCE", host.getEffectSource().getName());
}
sb.append(desc);
sb.append("\r\n");
sb.append(linebreak);
}
}
}
@@ -2334,13 +2346,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// Class Abilities
if (isClassCard()) {
String linebreak = "\r\n\r\n";
sb.append(linebreak);
// Currently the maximum levels of all Class cards are all 3
for (int level = 1; level <= 3; ++level) {
boolean disabled = level > getClassLevel() && isInZone(ZoneType.Battlefield);
final String grayTag = "<span style=\"color:gray;\">";
final String endTag = "</span>";
for (final Trigger trig : state.getTriggers()) {
if (trig.isClassLevelNAbility(level) && !trig.isSecondary()) {
if (disabled) sb.append(grayTag);
@@ -4532,15 +4541,13 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final boolean isPermanent() {
return !isImmutable && (isInZone(ZoneType.Battlefield) || getType().isPermanent());
return !isImmutable() && (isInZone(ZoneType.Battlefield) || getType().isPermanent());
}
public final boolean isSpell() {
return (isInstant() || isSorcery() || (isAura() && !isInZone((ZoneType.Battlefield))));
}
public final boolean isEmblem() { return getType().isEmblem(); }
public final boolean isLand() { return getType().isLand(); }
public final boolean isBasicLand() { return getType().isBasicLand(); }
public final boolean isSnow() { return getType().isSnow(); }
@@ -4799,11 +4806,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// Takes one argument like Permanent.Blue+withFlying
@Override
public final boolean isValid(final String restriction, final Player sourceController, final Card source, CardTraitBase spellAbility) {
if (isImmutable() && source != null && !source.isRemembered(this) &&
!(restriction.startsWith("Emblem") || restriction.startsWith("Effect"))) { // special case exclusion
return false;
}
// Inclusive restrictions are Card types
final String[] incR = restriction.split("\\.", 2);
@@ -4813,14 +4815,27 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
incR[0] = incR[0].substring(1); // consume negation sign
}
if (incR[0].equals("Spell") && !isSpell()) {
return testFailed;
}
if (incR[0].equals("Permanent") && !isPermanent()) {
return testFailed;
}
if (!incR[0].equals("card") && !incR[0].equals("Card") && !incR[0].equals("Spell")
&& !incR[0].equals("Permanent") && !getType().hasStringType(incR[0])) {
if (incR[0].equals("Spell")) {
if (!isSpell()) {
return testFailed;
}
} else if (incR[0].equals("Permanent")) {
if (!isPermanent()) {
return testFailed;
}
} else if (incR[0].equals("Effect")) {
if (!isImmutable()) {
return testFailed;
}
} else if (incR[0].equals("Emblem")) {
if (!isEmblem()) {
return testFailed;
}
} else if (incR[0].equals("card") || incR[0].equals("Card")) {
if (isImmutable()) {
return testFailed;
}
} else if (!getType().hasStringType(incR[0])) {
return testFailed; // Check for wrong type
}
@@ -4828,15 +4843,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
final String excR = incR[1];
final String[] exRs = excR.split("\\+"); // Exclusive Restrictions are ...
for (String exR : exRs) {
if (exR.startsWith("!")) {
exR = exR.substring(1);
if (hasProperty(exR, sourceController, source, spellAbility)) {
return testFailed;
}
} else {
if (!hasProperty(exR, sourceController, source, spellAbility)) {
return testFailed;
}
if (!hasProperty(exR, sourceController, source, spellAbility)) {
return testFailed;
}
}
}
@@ -4846,6 +4854,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// Takes arguments like Blue or withFlying
@Override
public boolean hasProperty(final String property, final Player sourceController, final Card source, CardTraitBase spellAbility) {
if (property.startsWith("!")) {
return !CardProperty.cardHasProperty(this, property.substring(1), sourceController, source, spellAbility);
}
return CardProperty.cardHasProperty(this, property, sourceController, source, spellAbility);
}
@@ -4854,6 +4865,15 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final void setImmutable(final boolean isImmutable0) {
isImmutable = isImmutable0;
view.updateImmutable(this);
}
public final boolean isEmblem() {
return isEmblem;
}
public final void setEmblem(final boolean isEmblem0) {
isEmblem = isEmblem0;
view.updateEmblem(this);
}
/*
@@ -5969,6 +5989,19 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public void setCastFrom(final ZoneType castFrom0) {
castFrom = castFrom0;
}
public boolean wasCast() {
if (hasMergedCard()) {
boolean wasCast = false;
for (Card c : getMergedCards()) {
if (null != c.getCastFrom()) {
wasCast = true;
break;
}
}
return wasCast;
}
return getCastFrom() != null;
}
public SpellAbility getCastSA() {
return castSA;

View File

@@ -350,7 +350,7 @@ public class CardProperty {
return false;
}
} else if (property.equals("EffectSource")) {
if (!source.isEmblem() && !source.getType().hasSubtype("Effect")) {
if (!source.isImmutable()) {
return false;
}
@@ -446,6 +446,9 @@ public class CardProperty {
}
} else if (property.startsWith("CanEnchant")) {
final String restriction = property.substring(10);
if (restriction.equals("EquippedBy")) {
if (!source.getEquipping().canBeAttached(card)) return false;
}
if (restriction.equals("Remembered")) {
for (final Object rem : source.getRemembered()) {
if (!(rem instanceof Card) || !((Card) rem).canBeAttached(card))
@@ -1658,6 +1661,10 @@ public class CardProperty {
if (source.hasImprintedCard(card)) {
return false;
}
} else if (property.equals("IsGoaded")) {
if (!card.isGoaded()) {
return false;
}
} else if (property.equals("NoAbilities")) {
if (!card.hasNoAbilities()) {
return false;
@@ -1680,16 +1687,15 @@ public class CardProperty {
return false;
}
} else if (property.equals("wasCast")) {
if (null == card.getCastFrom()) {
if (!card.wasCast()) {
return false;
}
} else if (property.equals("wasNotCast")) {
if (null != card.getCastFrom()) {
if (card.wasCast()) {
return false;
}
} else if (property.startsWith("wasCastFrom")) {
// How are we getting in here with a comma?
final String strZone = property.split(",")[0].substring(11);
final String strZone = property.substring(11);
final ZoneType realZone = ZoneType.smartValueOf(strZone);
if (realZone != card.getCastFrom()) {
return false;

View File

@@ -120,7 +120,7 @@ public final class CardUtil {
List<Card> res = Lists.newArrayList();
final Game game = src.getGame();
if (to != ZoneType.Stack) {
for (Player p : game.getPlayers()) {
for (Player p : game.getRegisteredPlayers()) {
res.addAll(p.getZone(to).getCardsAddedThisTurn(from));
}
}
@@ -242,6 +242,7 @@ public final class CardUtil {
newCopy.setToken(in.isToken());
newCopy.setCopiedSpell(in.isCopiedSpell());
newCopy.setImmutable(in.isImmutable());
newCopy.setEmblem(in.isEmblem());
// lock in the current P/T
newCopy.setBasePower(in.getCurrentPower());
@@ -530,6 +531,7 @@ public final class CardUtil {
// however, due to the changes necessary for SA_Requirements this is much
// different than the original
public static List<Card> getValidCardsToTarget(TargetRestrictions tgt, SpellAbility ability) {
Card activatingCard = ability.getHostCard();
final Game game = ability.getActivatingPlayer().getGame();
final List<ZoneType> zone = tgt.getZone();
@@ -539,7 +541,6 @@ public final class CardUtil {
if (canTgtStack) {
// Since getTargetableCards doesn't have additional checks if one of the Zones is stack
// Remove the activating card from targeting itself if its on the Stack
Card activatingCard = ability.getHostCard();
if (activatingCard.isInZone(ZoneType.Stack)) {
choices.remove(ability.getHostCard());
}
@@ -561,7 +562,7 @@ public final class CardUtil {
final List<Card> choicesCopy = Lists.newArrayList(choices);
for (final Card c : choicesCopy) {
if (c.getCMC() > tgt.getMaxTotalCMC(c, ability) - totalCMCTargeted) {
if (c.getCMC() > tgt.getMaxTotalCMC(activatingCard, ability) - totalCMCTargeted) {
choices.remove(c);
}
}
@@ -576,7 +577,7 @@ public final class CardUtil {
final List<Card> choicesCopy = Lists.newArrayList(choices);
for (final Card c : choicesCopy) {
if (c.getNetPower() > tgt.getMaxTotalPower(c, ability) - totalPowerTargeted) {
if (c.getNetPower() > tgt.getMaxTotalPower(activatingCard, ability) - totalPowerTargeted) {
choices.remove(c);
}
}

View File

@@ -233,6 +233,20 @@ public class CardView extends GameEntityView {
set(TrackableProperty.Token, c.isToken());
}
public boolean isImmutable() {
return get(TrackableProperty.IsImmutable);
}
public void updateImmutable(Card c) {
set(TrackableProperty.IsImmutable, c.isImmutable());
}
public boolean isEmblem() {
return get(TrackableProperty.IsEmblem);
}
public void updateEmblem(Card c) {
set(TrackableProperty.IsEmblem, c.isEmblem());
}
public boolean isTokenCard() { return get(TrackableProperty.TokenCard); }
void updateTokenCard(Card c) { set(TrackableProperty.TokenCard, c.isTokenCard()); }
@@ -585,6 +599,10 @@ public class CardView extends GameEntityView {
return get(TrackableProperty.CloneOrigin);
}
public CardView getExiledWith() {
return get(TrackableProperty.ExiledWith);
}
public FCollectionView<CardView> getImprintedCards() {
return get(TrackableProperty.ImprintedCards);
}

View File

@@ -65,6 +65,8 @@ public enum CounterEnumType {
COIN("COIN",255,215,0),
COMPONENT("COMPN", 224, 160, 48),
CORPSE("CRPSE", 230, 186, 209),
CORRUPTION("CRPTN", 210, 121, 210),

View File

@@ -0,0 +1,95 @@
package forge.game.card;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ObjectUtils;
import com.google.common.collect.ForwardingTable;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Table;
import forge.game.CardTraitBase;
import forge.game.GameObjectPredicates;
import forge.game.player.Player;
public class TokenCreateTable extends ForwardingTable<Player, Card, Integer> {
Table<Player, Card, Integer> dataMap = HashBasedTable.create();
public TokenCreateTable() {
}
@Override
protected Table<Player, Card, Integer> delegate() {
return dataMap;
}
public int add(Player p, Card c, int i) {
int old = ObjectUtils.defaultIfNull(this.get(p, c), 0);
int newValue = old + i;
this.put(p, c, newValue);
return newValue;
}
public int getFilterAmount(String validOwner, String validToken, final CardTraitBase ctb) {
final Card host = ctb.getHostCard();
int result = 0;
List<Card> filteredCards = null;
List<Player> filteredPlayer = null;
if (validOwner == null && validToken == null) {
for (Integer i : values()) {
result += i;
}
return result;
}
if (validOwner != null) {
filteredPlayer = Lists.newArrayList(Iterables.filter(rowKeySet(),
GameObjectPredicates.restriction(validOwner.split(","), host.getController(), host, ctb)));
if (filteredPlayer.isEmpty()) {
return 0;
}
}
if (validToken != null) {
filteredCards = CardLists.getValidCardsAsList(columnKeySet(), validToken, host.getController(), host, ctb);
if (filteredCards.isEmpty()) {
return 0;
}
}
if (filteredPlayer == null) {
for (Map.Entry<Card, Map<Player, Integer>> e : columnMap().entrySet()) {
for (Integer i : e.getValue().values()) {
result += i;
}
}
return result;
}
if (filteredCards == null) {
for (Map.Entry<Player, Map<Card, Integer>> e : rowMap().entrySet()) {
for (Integer i : e.getValue().values()) {
result += i;
}
}
return result;
}
for (Table.Cell<Player, Card, Integer> c : this.cellSet()) {
if (!filteredPlayer.contains(c.getRowKey())) {
continue;
}
if (!filteredCards.contains(c.getColumnKey())) {
continue;
}
result += c.getValue();
}
return result;
}
}

View File

@@ -15,15 +15,12 @@ import forge.StaticData;
import forge.card.CardType;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardFactory;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardUtil;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.item.PaperToken;
@@ -140,59 +137,6 @@ public class TokenInfo {
return sb.toString();
}
public static List<Card> makeToken(final Card prototype, final Player owner,
final boolean applyMultiplier, final int num) {
final List<Card> list = Lists.newArrayList();
final Game game = owner.getGame();
int multiplier = num;
Player player = owner;
Card proto = prototype;
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(player);
repParams.put(AbilityKey.Token, prototype);
repParams.put(AbilityKey.TokenNum, multiplier);
repParams.put(AbilityKey.EffectOnly, applyMultiplier);
switch (game.getReplacementHandler().run(ReplacementType.CreateToken, repParams)) {
case NotReplaced:
break;
case Updated: {
multiplier = (int) repParams.get(AbilityKey.TokenNum);
player = (Player) repParams.get(AbilityKey.Affected);
proto = (Card) repParams.get(AbilityKey.Token);
break;
}
default:
multiplier = 0;
break;
}
if (multiplier <= 0) {
return list;
}
long timestamp = game.getNextTimestamp();
for (int i = 0; i < multiplier; i++) {
// need to set owner or copyCard will fail with assign new ID
proto.setOwner(owner);
Card copy = CardFactory.copyCard(proto, true);
// need to assign player after token is copied
if (player != owner) {
copy.setController(player, timestamp);
}
copy.setTimestamp(timestamp);
copy.setToken(true);
list.add(copy);
}
return list;
}
static public List<Card> makeTokensFromPrototype(Card prototype, final Player owner, int amount, final boolean applyMultiplier) {
return makeToken(prototype, owner, applyMultiplier, amount);
}
public Card makeOneToken(final Player controller) {
final Game game = controller.getGame();
final Card c = toCard(game);
@@ -321,10 +265,10 @@ public class TokenInfo {
result.getCurrentState().changeTextIntrinsic(colorMap, typeMap);
}
static public Card getProtoType(final String script, final SpellAbility sa) {
return getProtoType(script, sa, true);
static public Card getProtoType(final String script, final SpellAbility sa, final Player owner) {
return getProtoType(script, sa, owner, true);
}
static public Card getProtoType(final String script, final SpellAbility sa, boolean applyTextChange) {
static public Card getProtoType(final String script, final SpellAbility sa, final Player owner, boolean applyTextChange) {
// script might be null, or sa might be null
if (script == null || sa == null) {
return null;
@@ -338,7 +282,7 @@ public class TokenInfo {
if (token == null) {
return null;
}
final Card result = Card.fromPaperCard(token, null, game);
final Card result = Card.fromPaperCard(token, owner, game);
if (sa.hasParam("TokenPower")) {
String str = sa.getParam("TokenPower");

View File

@@ -1080,7 +1080,7 @@ public class CombatUtil {
return canAttackerBeBlockedWithAmount(attacker, amount, combat != null ? combat.getDefenderPlayerByAttacker(attacker) : null);
}
public static boolean canAttackerBeBlockedWithAmount(Card attacker, int amount, Player defender) {
if(amount == 0 )
if (amount == 0)
return false; // no block
List<String> restrictions = Lists.newArrayList();
@@ -1093,7 +1093,7 @@ public class CombatUtil {
restrictions.add("LT2");
}
}
for ( String res : restrictions ) {
for (String res : restrictions) {
int operand = Integer.parseInt(res.substring(2));
String operator = res.substring(0,2);
if (Expressions.compare(amount, operator, operand) )
@@ -1118,7 +1118,7 @@ public class CombatUtil {
}
}
int minBlockers = 1;
for ( String res : restrictions ) {
for (String res : restrictions) {
int operand = Integer.parseInt(res.substring(2));
String operator = res.substring(0, 2);
if (operator.equals("LT") || operator.equals("GE")) {

View File

@@ -363,6 +363,13 @@ public class Cost implements Serializable {
return new CostFlipCoin(splitStr[0]);
}
if (parse.startsWith("RollDice<")) {
// RollDice<NumDice/Sides/ResultSVar>
final String[] splitStr = abCostParse(parse, 4);
final String description = splitStr.length > 3 ? splitStr[3] : null;
return new CostRollDice(splitStr[0], splitStr[1], splitStr[2], description);
}
if (parse.startsWith("Discard<")) {
// Discard<NumCards/Type>
final String[] splitStr = abCostParse(parse, 3);

View File

@@ -211,7 +211,7 @@ public class CostPayment extends ManaConversionMatrix {
return false;
}
// abilities care what was used to pay for them
if( part instanceof CostPartWithList ) {
if (part instanceof CostPartWithList) {
((CostPartWithList) part).resetLists();
}

View File

@@ -0,0 +1,68 @@
package forge.game.cost;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
/**
* This is for the "RollDice" Cost
*/
public class CostRollDice extends CostPart {
/**
* Serializables need a version ID.
*/
private static final long serialVersionUID = 1L;
private final String resultSVar;
/**
* Instantiates a new cost RollDice.
*
* @param amount
* the amount
*/
public CostRollDice(final String amount, final String sides, final String resultSVar, final String description) {
super(amount, sides, description);
this.resultSVar = resultSVar;
}
/*
* (non-Javadoc)
*
* @see
* forge.card.cost.CostPart#canPay(forge.card.spellability.SpellAbility,
* forge.Card, forge.Player, forge.card.cost.Cost)
*/
@Override
public final boolean canPay(final SpellAbility ability, final Player payer) {
return true;
}
@Override
public final String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("Roll ").append(getAmount());
if (this.getTypeDescription() == null) {
sb.append("d").append(getType());
} else {
sb.append(" ").append(this.getTypeDescription());
}
return sb.toString();
}
@Override
public boolean payAsDecided(Player payer, PaymentDecision pd, SpellAbility sa) {
int sides = Integer.parseInt(getType());
int result = RollDiceEffect.rollDiceForPlayer(sa, payer, pd.c, sides);
sa.setSVar(resultSVar, Integer.toString(result));
return true;
}
public <T> T accept(ICostVisitor<T> visitor) {
return visitor.visit(this);
}
}

View File

@@ -12,6 +12,7 @@ public interface ICostVisitor<T> {
T visit(CostExiledMoveToGrave cost);
T visit(CostExert cost);
T visit(CostFlipCoin cost);
T visit(CostRollDice cost);
T visit(CostMill cost);
T visit(CostAddMana cost);
T visit(CostPayLife cost);
@@ -84,6 +85,11 @@ public interface ICostVisitor<T> {
return null;
}
@Override
public T visit(CostRollDice cost) {
return null;
}
@Override
public T visit(CostMill cost) {
return null;
@@ -173,7 +179,7 @@ public interface ICostVisitor<T> {
public T visit(CostUnattach cost) {
return null;
}
@Override
public T visit(CostTapType cost) {
return null;

View File

@@ -2217,6 +2217,9 @@ public class Player extends GameEntity implements Comparable<Player> {
@Override
public final boolean hasProperty(final String property, final Player sourceController, final Card source, CardTraitBase spellAbility) {
if (property.startsWith("!")) {
return !PlayerProperty.playerHasProperty(this, property.substring(1), sourceController, source, spellAbility);
}
return PlayerProperty.playerHasProperty(this, property, sourceController, source, spellAbility);
}
@@ -3173,6 +3176,7 @@ public class Player extends GameEntity implements Comparable<Player> {
if (monarchEffect == null) {
monarchEffect = new Card(game.nextCardId(), null, game);
monarchEffect.setOwner(this);
monarchEffect.setImmutable(true);
if (set != null) {
monarchEffect.setImageKey("t:monarch_" + set.toLowerCase());
monarchEffect.setSetCode(set);
@@ -3180,7 +3184,6 @@ public class Player extends GameEntity implements Comparable<Player> {
monarchEffect.setImageKey("t:monarch");
}
monarchEffect.setName("The Monarch");
monarchEffect.addType("Effect");
{
final String drawTrig = "Mode$ Phase | Phase$ End of Turn | TriggerZones$ Command | " +
@@ -3277,8 +3280,7 @@ public class Player extends GameEntity implements Comparable<Player> {
blessingEffect.setOwner(this);
blessingEffect.setImageKey("t:blessing");
blessingEffect.setName("City's Blessing");
blessingEffect.addType("Effect");
blessingEffect.setImmutable(true);
blessingEffect.updateStateForView();
@@ -3350,7 +3352,6 @@ public class Player extends GameEntity implements Comparable<Player> {
keywordEffect.setOwner(this);
keywordEffect.setName("Keyword Effects");
keywordEffect.setImageKey(ImageKeys.HIDDEN_CARD);
keywordEffect.addType("Effect");
keywordEffect.updateStateForView();

View File

@@ -4,6 +4,7 @@ import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.card.TokenCreateTable;
import forge.game.spellability.SpellAbility;
/**
@@ -27,9 +28,11 @@ public class ReplaceToken extends ReplacementEffect {
*/
@Override
public boolean canReplace(Map<AbilityKey, Object> runParams) {
/*
if (((int) runParams.get(AbilityKey.TokenNum)) <= 0) {
return false;
}
//*/
if (hasParam("EffectOnly")) {
final Boolean effectOnly = (Boolean) runParams.get(AbilityKey.EffectOnly);
@@ -41,10 +44,16 @@ public class ReplaceToken extends ReplacementEffect {
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) {
return false;
}
/*/
if (!matchesValidParam("ValidToken", runParams.get(AbilityKey.Token))) {
return false;
}
//*/
if (filterAmount((TokenCreateTable) runParams.get(AbilityKey.Token)) <= 0) {
return false;
}
return true;
}
@@ -54,8 +63,13 @@ public class ReplaceToken extends ReplacementEffect {
*/
@Override
public void setReplacingObjects(Map<AbilityKey, Object> runParams, SpellAbility sa) {
sa.setReplacingObject(AbilityKey.TokenNum, runParams.get(AbilityKey.TokenNum));
sa.setReplacingObject(AbilityKey.TokenNum, filterAmount((TokenCreateTable) runParams.get(AbilityKey.Token)));
sa.setReplacingObject(AbilityKey.Token, runParams.get(AbilityKey.Token));
sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Affected));
}
public int filterAmount(final TokenCreateTable table) {
return table.getFilterAmount(getParamOrDefault("ValidPlayer", null), getParamOrDefault("ValidToken", null), this);
}
}

View File

@@ -220,7 +220,7 @@ public abstract class ReplacementEffect extends TriggerReplacementBase {
if (hasParam("Description") && !this.isSuppressed()) {
String desc = AbilityUtils.applyDescriptionTextChangeEffects(getParam("Description"), this);
String currentName;
if (this.isIntrinsic() && !this.getHostCard().isMutated() && cardState != null) {
if (this.isIntrinsic() && cardState != null && cardState.getCard() == getHostCard()) {
currentName = cardState.getName();
}
else {

View File

@@ -28,6 +28,7 @@ import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import forge.game.CardTraitBase;
@@ -274,20 +275,45 @@ public class ReplacementHandler {
chosenRE.setOtherChoices(null);
return res;
}
// Log there
String message = chosenRE.getDescription();
if (!StringUtils.isEmpty(message)) {
if (chosenRE.getHostCard() != null) {
message = TextUtil.fastReplace(message, "CARDNAME", chosenRE.getHostCard().getName());
}
game.getGameLog().add(GameLogEntryType.EFFECT_REPLACED, message);
}
// if its updated, try to call event again
if (res == ReplacementResult.Updated) {
Map<AbilityKey, Object> params = Maps.newHashMap(runParams);
if (params.containsKey(AbilityKey.EffectOnly)) {
params.put(AbilityKey.EffectOnly, true);
}
ReplacementResult result = run(event, params);
switch (result) {
case NotReplaced:
case Updated: {
for (Map.Entry<AbilityKey, Object> e : params.entrySet()) {
runParams.put(e.getKey(), e.getValue());
}
// effect was updated
runParams.put(AbilityKey.ReplacementResult, ReplacementResult.Updated);
break;
}
default:
// effect was replaced with something else
runParams.put(AbilityKey.ReplacementResult, result);
break;
}
}
chosenRE.setHasRun(false);
hasRun.remove(chosenRE);
chosenRE.setOtherChoices(null);
// Updated Replacements need to be logged elsewhere because its otherwise in the wrong order
if (res != ReplacementResult.Updated) {
String message = chosenRE.getDescription();
if (!StringUtils.isEmpty(message))
if (chosenRE.getHostCard() != null) {
message = TextUtil.fastReplace(message, "CARDNAME", chosenRE.getHostCard().getName());
}
game.getGameLog().add(GameLogEntryType.EFFECT_REPLACED, message);
}
return res;
}
@@ -300,7 +326,6 @@ public class ReplacementHandler {
*/
private ReplacementResult executeReplacement(final Map<AbilityKey, Object> runParams,
final ReplacementEffect replacementEffect, final Player decider, final Game game) {
final Map<String, String> mapParams = replacementEffect.getMapParams();
SpellAbility effectSA = null;
@@ -311,10 +336,9 @@ public class ReplacementHandler {
host = game.getCardState(host);
}
if (replacementEffect.getOverridingAbility() == null && mapParams.containsKey("ReplaceWith")) {
final String effectSVar = mapParams.get("ReplaceWith");
if (replacementEffect.getOverridingAbility() == null && replacementEffect.hasParam("ReplaceWith")) {
// TODO: the source of replacement effect should be the source of the original effect
effectSA = AbilityFactory.getAbility(host, effectSVar, replacementEffect);
effectSA = AbilityFactory.getAbility(host, replacementEffect.getParam("ReplaceWith"), replacementEffect);
//replacementEffect.setOverridingAbility(effectSA);
//effectSA.setTrigger(true);
} else if (replacementEffect.getOverridingAbility() != null) {
@@ -342,10 +366,10 @@ public class ReplacementHandler {
// Decider gets to choose whether or not to apply the replacement.
if (replacementEffect.hasParam("Optional")) {
Player optDecider = decider;
if (mapParams.containsKey("OptionalDecider") && (effectSA != null)) {
if (replacementEffect.hasParam("OptionalDecider") && (effectSA != null)) {
effectSA.setActivatingPlayer(host.getController());
optDecider = AbilityUtils.getDefinedPlayers(host,
mapParams.get("OptionalDecider"), effectSA).get(0);
replacementEffect.getParam("OptionalDecider"), effectSA).get(0);
}
Card cardForUi = host.getCardForUi();
@@ -360,12 +384,12 @@ public class ReplacementHandler {
}
}
boolean isPrevent = mapParams.containsKey("Prevent") && mapParams.get("Prevent").equals("True");
if (isPrevent || mapParams.containsKey("PreventionEffect")) {
boolean isPrevent = "True".equals(replacementEffect.getParam("Prevent"));
if (isPrevent || replacementEffect.hasParam("PreventionEffect")) {
if (Boolean.TRUE.equals(runParams.get(AbilityKey.NoPreventDamage))) {
// If can't prevent damage, result is not replaced
// But still put "prevented" amount for buffered SA
if (mapParams.containsKey("AlwaysReplace")) {
if (replacementEffect.hasParam("AlwaysReplace")) {
runParams.put(AbilityKey.PreventedAmount, runParams.get(AbilityKey.DamageAmount));
} else {
runParams.put(AbilityKey.PreventedAmount, 0);
@@ -377,10 +401,8 @@ public class ReplacementHandler {
}
}
if (mapParams.containsKey("Skip")) {
if (mapParams.get("Skip").equals("True")) {
return ReplacementResult.Skipped; // Event is skipped.
}
if ("True".equals(replacementEffect.getParam("Skip"))) {
return ReplacementResult.Skipped; // Event is skipped.
}
Player player = host.getController();
@@ -394,6 +416,11 @@ public class ReplacementHandler {
// The SA if buffered, but replacement result should be set to Replaced
runParams.put(AbilityKey.ReplacementResult, ReplacementResult.Replaced);
}
// these ones are special for updating
if (apiType == ApiType.ReplaceToken || apiType == ApiType.ReplaceEffect || apiType == ApiType.ReplaceMana) {
runParams.put(AbilityKey.ReplacementResult, ReplacementResult.Updated);
}
}
// if the spellability is a replace effect then its some new logic

View File

@@ -95,8 +95,7 @@ public class LandAbility extends Ability {
Card source = sta.getHostCard();
if (!source.equals(getHostCard())) {
sb.append(" by ");
if ((source.isEmblem() || source.getType().hasSubtype("Effect"))
&& source.getEffectSource() != null) {
if (source.isImmutable() && source.getEffectSource() != null) {
sb.append(source.getEffectSource());
} else {
sb.append(source);

View File

@@ -854,7 +854,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
if (node.getHostCard() != null) {
String currentName;
// if alternate state is viewed while card uses original
if (node.isIntrinsic() && !node.getHostCard().isMutated() && node.cardState != null) {
if (node.isIntrinsic() && node.cardState != null && node.cardState.getCard() == node.getHostCard()) {
currentName = node.cardState.getName();
}
else {

View File

@@ -209,7 +209,7 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
public final String toString() {
if (hasParam("Description") && !this.isSuppressed()) {
String currentName;
if (this.isIntrinsic() && !this.getHostCard().isMutated() && cardState != null) {
if (this.isIntrinsic() && cardState != null && cardState.getCard() == getHostCard()) {
currentName = cardState.getName();
}
else {

View File

@@ -92,7 +92,7 @@ public class StaticAbilityCantAttackBlock {
}
/**
* returns true if attacker can be blocked by blocker
* returns true if attacker can't be blocked by blocker
* @param stAb
* @param attacker
* @param blocker
@@ -104,9 +104,10 @@ public class StaticAbilityCantAttackBlock {
return false;
}
if (stAb.hasParam("ValidBlocker")) {
boolean stillblock = true;
for (final String v : stAb.getParam("ValidBlocker").split(",")) {
if (blocker.isValid(v, host.getController(), host, stAb)) {
boolean stillblock = false;
stillblock = false;
//Dragon Hunter check
if (v.contains("withoutReach") && blocker.hasStartOfKeyword("IfReach")) {
for (KeywordInterface inst : blocker.getKeywords()) {
@@ -120,13 +121,14 @@ public class StaticAbilityCantAttackBlock {
}
}
}
if (stillblock) {
return false;
if (!stillblock) {
break;
}
} else {
return false;
}
}
if (stillblock) {
return false;
}
}
// relative valid relative to each other
if (!stAb.matchesValidParam("ValidAttackerRelative", attacker, blocker)) {

View File

@@ -132,7 +132,7 @@ public abstract class Trigger extends TriggerReplacementBase {
StringBuilder sb = new StringBuilder();
String currentName;
if (this.isIntrinsic() && !this.getHostCard().isMutated() && cardState != null) {
if (this.isIntrinsic() && cardState != null && cardState.getCard() == getHostCard()) {
currentName = cardState.getName();
}
else {

View File

@@ -31,8 +31,6 @@ import forge.game.IHasSVars;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.*;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
@@ -346,8 +344,7 @@ public class TriggerHandler {
}
}
private boolean runNonStaticTriggersForPlayer(final Player player, final TriggerWaiting wt, final List<Trigger> delayedTriggersWorkingCopy ) {
private boolean runNonStaticTriggersForPlayer(final Player player, final TriggerWaiting wt, final List<Trigger> delayedTriggersWorkingCopy) {
final TriggerType mode = wt.getMode();
final Map<AbilityKey, Object> runParams = wt.getParams();
final List<Trigger> triggers = wt.getTriggers() != null ? wt.getTriggers() : activeTriggers;
@@ -503,7 +500,6 @@ public class TriggerHandler {
// runs it if so.
// Return true if the trigger went off, false otherwise.
private void runSingleTriggerInternal(final Trigger regtrig, final Map<AbilityKey, Object> runParams) {
// All tests passed, execute ability.
if (regtrig instanceof TriggerTapsForMana) {
final SpellAbility abMana = (SpellAbility) runParams.get(AbilityKey.AbilityMana);
@@ -519,8 +515,7 @@ public class TriggerHandler {
if (sa == null) {
if (!regtrig.hasParam("Execute")) {
sa = new SpellAbility.EmptySa(host);
}
else {
} else {
String name = regtrig.getParam("Execute");
if (!host.getCurrentState().hasSVar(name)) {
System.err.println("Warning: tried to run a trigger for card " + host + " referencing a SVar " + name + " not present on the current state " + host.getCurrentState() + ". Aborting trigger execution to prevent a crash.");
@@ -577,12 +572,6 @@ public class TriggerHandler {
}
sa.setStackDescription(sa.toString());
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
if (!CharmEffect.makeChoices(sa)) {
// 603.3c If no mode is chosen, the ability is removed from the stack.
return;
}
}
Player decider = null;
boolean isMandatory = false;
@@ -592,8 +581,7 @@ public class TriggerHandler {
}
else if (sa instanceof AbilitySub || !sa.hasParam("Cost") || sa.getParam("Cost").equals("0")) {
isMandatory = true;
}
else { // triggers with a cost can't be mandatory
} else { // triggers with a cost can't be mandatory
sa.setOptionalTrigger(true);
decider = sa.getActivatingPlayer();
}
@@ -605,8 +593,7 @@ public class TriggerHandler {
wrapperAbility.setLastStateBattlefield(game.getLastStateBattlefield());
if (regtrig.isStatic()) {
wrapperAbility.getActivatingPlayer().getController().playTrigger(host, wrapperAbility, isMandatory);
}
else {
} else {
game.getStack().addSimultaneousStackEntry(wrapperAbility);
}

View File

@@ -44,6 +44,11 @@ public class TriggerRolledDie extends Trigger {
}
return false;
}
if (hasParam("ValidSides")) {
final int validSides = Integer.parseInt(getParam("ValidSides"));
final int sides = (int) runParams.get(AbilityKey.Sides);
if (sides == validSides) return true;
}
return true;
}

View File

@@ -232,8 +232,8 @@ public class WrappedAbility extends Ability {
if (regtrig == null) return "";
final StringBuilder sb = new StringBuilder(regtrig.replaceAbilityText(regtrig.toString(true), this));
List<TargetChoices> allTargets = sa.getAllTargetChoices();
if (!allTargets.isEmpty()) {
sb.append(" (Targeting ");
if (!allTargets.isEmpty() && !ApiType.Charm.equals(sa.getApi())) {
sb.append(" (Targeting: ");
sb.append(allTargets);
sb.append(")");
}
@@ -549,7 +549,6 @@ public class WrappedAbility extends Ability {
sa.setXManaCostPaid(n);
}
public CardState getCardState() {
return sa.getCardState();
}

View File

@@ -40,6 +40,7 @@ import forge.game.GameObject;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardUtil;
@@ -224,8 +225,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
undoStackOwner = activator;
}
undoStack.push(sp);
}
else {
} else {
clearUndoStack();
}
@@ -628,8 +628,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
if (sa.usesTargeting()) {
if (sa.isZeroTargets()) {
// Nothing targeted, and nothing needs to be targeted.
}
else {
} else {
// Some targets were chosen, fizzling for this subability is now possible
//fizzle = true;
// With multi-targets, as long as one target is still legal,
@@ -675,8 +674,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
}
else if (sa.getTargetCard() != null) {
fizzle = !sa.canTarget(sa.getTargetCard());
}
else {
} else {
// Set fizzle to the same as the parent if there's no target info
fizzle = parentFizzled;
}
@@ -799,7 +797,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
result |= chooseOrderOfSimultaneousStackEntry(whoAddsToStack);
// 2014-08-10 Fix infinite loop when a player dies during a multiplayer game during their turn
whoAddsToStack = game.getNextPlayerAfter(whoAddsToStack);
} while( whoAddsToStack != null && whoAddsToStack != playerTurn);
} while (whoAddsToStack != null && whoAddsToStack != playerTurn);
return result;
}
@@ -809,9 +807,19 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
}
final List<SpellAbility> activePlayerSAs = Lists.newArrayList();
final List<SpellAbility> failedSAs = Lists.newArrayList();
for (int i = 0; i < simultaneousStackEntryList.size(); i++) {
SpellAbility sa = simultaneousStackEntryList.get(i);
Player activator = sa.getActivatingPlayer();
if (sa.getApi() == ApiType.Charm) {
if (!CharmEffect.makeChoices(sa)) {
// 603.3c If no mode is chosen, the ability is removed from the stack.
failedSAs.add(sa);
continue;
}
}
if (activator == null) {
if (sa.getHostCard().getController().equals(activePlayer)) {
activePlayerSAs.add(sa);
@@ -823,6 +831,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
}
}
simultaneousStackEntryList.removeAll(activePlayerSAs);
simultaneousStackEntryList.removeAll(failedSAs);
if (activePlayerSAs.isEmpty()) {
return false;
@@ -875,8 +884,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
public final void addCastCommand(final String valid, final GameCommand c) {
if (commandList.containsKey(valid)) {
commandList.get(valid).add(0, c);
}
else {
} else {
commandList.put(valid, Lists.newArrayList(c));
}
}

View File

@@ -24,6 +24,9 @@ public enum TrackableProperty {
Controller(TrackableTypes.PlayerViewType),
Zone(TrackableTypes.EnumType(ZoneType.class)),
IsImmutable(TrackableTypes.BooleanType),
IsEmblem(TrackableTypes.BooleanType),
Flipped(TrackableTypes.BooleanType),
Facedown(TrackableTypes.BooleanType),
Foretold(TrackableTypes.BooleanType),
@@ -74,6 +77,7 @@ public enum TrackableProperty {
UntilLeavesBattlefield(TrackableTypes.CardViewCollectionType),
GainControlTargets(TrackableTypes.CardViewCollectionType),
CloneOrigin(TrackableTypes.CardViewType),
ExiledWith(TrackableTypes.CardViewType),
ImprintedCards(TrackableTypes.CardViewCollectionType),
HauntedBy(TrackableTypes.CardViewCollectionType),

View File

@@ -53,6 +53,7 @@ public class MessageUtil {
case Protection:
return Localizer.getInstance().getMessage("lblPlayerChooseValue", choser, value);
case RollDice:
case PutCounter:// For Clay Golem cost text
return value;
case Vote:
String chooser = StringUtils.capitalize(mayBeYou(player, target));