diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index e565ef58c76..0655364b8d2 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -30,7 +30,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import forge.ai.AiCardMemory.MemorySet; -import forge.ai.ability.ChooseGenericEffectAi; import forge.ai.ability.ProtectAi; import forge.ai.ability.TokenAi; import forge.card.CardStateName; @@ -290,7 +289,7 @@ public class ComputerUtil { SpellAbility newSA = sa.copyWithNoManaCost(); newSA.setActivatingPlayer(ai, true); - if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) { + if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA, false) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) { return false; } @@ -1176,7 +1175,7 @@ public class ComputerUtil { return true; } - if (cardState.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) { + if (cardState.hasKeyword(Keyword.RIOT) && SpecialAiLogic.preferHasteForRiot(sa, ai)) { // Planning to choose Haste for Riot, so do this in Main 1 return true; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index d8750ad2e02..13fbee4c46c 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -255,10 +255,25 @@ public class ComputerUtilAbility { } // deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True - if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { - return 1; - } else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { - return -1; + if (ApiType.RollPlanarDice == a.getApi() || ApiType.RollPlanarDice == b.getApi()) { + Card hostCardForGame = a.getHostCard(); + if (hostCardForGame == null) { + if (b.getHostCard() != null) { + hostCardForGame = b.getHostCard(); + } else { + return 0; // fallback if neither SA have a host card somehow + } + } + Game game = hostCardForGame.getGame(); + for (Card c : game.getActivePlanes()) { + if (c.hasSVar("AIRollPlanarDieParams") && c.getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { + if (ApiType.RollPlanarDice == a.getApi()) { + return 1; + } else { + return -1; + } + } + } } // deprioritize pump spells with pure energy cost (can be activated last, diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index 7cbcacda85a..b2a6d08235d 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -1257,7 +1257,7 @@ public class ComputerUtilCombat { sa.setActivatingPlayer(source.getController(), true); if (sa.hasParam("Cost")) { - if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) { continue; } } @@ -1454,7 +1454,7 @@ public class ComputerUtilCombat { continue; } if (sa.hasParam("Cost")) { - if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) { continue; } } @@ -1488,7 +1488,7 @@ public class ComputerUtilCombat { continue; } if (sa.hasParam("Cost")) { - if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) { continue; } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index a6dee98dfc1..d391af96602 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -636,7 +636,7 @@ public class ComputerUtilCost { } return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect) - && CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa); + && CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, effect); } public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView payers) { @@ -760,6 +760,8 @@ public class ComputerUtilCost { int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList); return evalToken < evalCounter; + } else if ("Riot".equals(aiLogic)) { + return !SpecialAiLogic.preferHasteForRiot(sa, payer); } // Check for shocklands and similar ETB replacement effects diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 45089d914d5..fbeadcbfe89 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -976,7 +976,7 @@ public class ComputerUtilMana { // Check if AI can still play this mana ability ma.setActivatingPlayer(ai, true); // if the AI can't pay the additional costs skip the mana ability - if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) { + if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma, false)) { return false; } else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) { return false; @@ -1517,7 +1517,7 @@ public class ComputerUtilMana { if (cost != null) { // if the AI can't pay the additional costs skip the mana ability m.setActivatingPlayer(ai, true); - if (!CostPayment.canPayAdditionalCosts(m.getPayCosts(), m)) { + if (!CostPayment.canPayAdditionalCosts(m.getPayCosts(), m, false)) { continue; } diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 853571bac00..7d0e6ecab39 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1063,6 +1063,9 @@ public class PlayerControllerAi extends PlayerController { final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } }; emptyAbility.setActivatingPlayer(player, true); emptyAbility.setTriggeringObjects(sa.getTriggeringObjects()); + emptyAbility.setReplacingObjects(sa.getReplacingObjects()); + emptyAbility.setTrigger(sa.getTrigger()); + emptyAbility.setReplacementEffect(sa.getReplacementEffect()); emptyAbility.setSVars(sa.getSVars()); emptyAbility.setCardState(sa.getCardState()); emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid()); diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java index 4ea6cb514a7..a4241778f79 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java +++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java @@ -2,6 +2,8 @@ package forge.ai; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + import forge.ai.ability.TokenAi; import forge.game.Game; import forge.game.ability.AbilityUtils; @@ -13,6 +15,7 @@ import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; import forge.util.Aggregates; import forge.util.Expressions; @@ -398,4 +401,48 @@ public class SpecialAiLogic { } return willPlay; } + + public static boolean preferHasteForRiot(SpellAbility sa, Player player) { + // returning true means preferring Haste, returning false means preferring a +1/+1 counter + final Card host = sa.getHostCard(); + final Game game = host.getGame(); + final Card copy = CardUtil.getLKICopy(host); + copy.setLastKnownZone(player.getZone(ZoneType.Battlefield)); + + // check state it would have on the battlefield + CardCollection preList = new CardCollection(copy); + game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList); + // reset again? + game.getAction().checkStaticAbilities(false); + + // can't gain counters, use Haste + if (!copy.canReceiveCounters(CounterEnumType.P1P1)) { + return true; + } + + // already has Haste, use counter + if (copy.hasKeyword(Keyword.HASTE)) { + return false; + } + + // not AI turn + if (!game.getPhaseHandler().isPlayerTurn(player)) { + return false; + } + + // not before Combat + if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { + return false; + } + + // TODO check other opponents too if able + final Player opp = player.getWeakestOpponent(); + if (opp != null) { + // TODO add predict Combat Damage? + return opp.getLife() < copy.getNetPower(); + } + + // haste might not be good enough? + return false; + } } \ No newline at end of file diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java index 9edb682eabc..b63244ce892 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java @@ -3,7 +3,6 @@ package forge.ai.ability; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; @@ -11,9 +10,7 @@ import forge.ai.SpellApiToAi; import forge.card.MagicColor; import forge.game.Game; import forge.game.card.*; -import forge.game.combat.Combat; import forge.game.cost.Cost; -import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -33,8 +30,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) { return true; - } else if ("Riot".equals(aiLogic)) { - return true; } else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) { for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) { if (SpellApiToAi.Converter.get(sb.getApi()).canPlayAIWithSubs(ai, sb)) { @@ -86,9 +81,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells, Map params) { Card host = sa.getHostCard(); - final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final Game game = host.getGame(); - final Combat combat = game.getCombat(); final String logic = sa.getParam("AILogic"); if (logic == null) { return spells.get(0); @@ -287,54 +280,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { if (!filtered.isEmpty()) { return filtered.get(0); } - } else if ("Riot".equals(logic)) { - SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1); - return preferHasteForRiot(sa, player) ? hasteSA : counterSA; } return spells.get(0); // return first choice if no logic found } - - public static boolean preferHasteForRiot(SpellAbility sa, Player player) { - // returning true means preferring Haste, returning false means preferring a +1/+1 counter - final Card host = sa.getHostCard(); - final Game game = host.getGame(); - final Card copy = CardUtil.getLKICopy(host); - copy.setLastKnownZone(player.getZone(ZoneType.Battlefield)); - - // check state it would have on the battlefield - CardCollection preList = new CardCollection(copy); - game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList); - // reset again? - game.getAction().checkStaticAbilities(false); - - // can't gain counters, use Haste - if (!copy.canReceiveCounters(CounterEnumType.P1P1)) { - return true; - } - - // already has Haste, use counter - if (copy.hasKeyword(Keyword.HASTE)) { - return false; - } - - // not AI turn - if (!game.getPhaseHandler().isPlayerTurn(player)) { - return false; - } - - // not before Combat - if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { - return false; - } - - // TODO check other opponents too if able - final Player opp = player.getWeakestOpponent(); - if (opp != null) { - // TODO add predict Combat Damage? - return opp.getLife() < copy.getNetPower(); - } - - // haste might not be good enough? - return false; - } } diff --git a/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java b/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java index ca686734f15..32b21c964cb 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java @@ -19,9 +19,16 @@ public class RollPlanarDiceAi extends SpellAbilityAi { */ @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); - Card plane = sa.getHostCard(); + for (Card c : ai.getGame().getActivePlanes()) { + if (willRollOnPlane(ai, c)) { + return true; + } + } + return false; + } + private boolean willRollOnPlane(Player ai, Card plane) { + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); boolean decideToRoll = false; boolean rollInMain1 = false; String modeName = "never"; diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 8d131659da9..7574e7716e8 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -253,7 +253,6 @@ public class Game { public List getLeftBattlefieldThisTurn() { return leftBattlefieldThisTurn; } - public List getLeftGraveyardThisTurn() { return leftGraveyardThisTurn; } @@ -261,7 +260,6 @@ public class Game { public void clearLeftBattlefieldThisTurn() { leftBattlefieldThisTurn.clear(); } - public void clearLeftGraveyardThisTurn() { leftGraveyardThisTurn.clear(); } diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 5c24ac41b08..0304efda411 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -583,14 +583,19 @@ public class GameAction { } } - // Need to apply any static effects to produce correct triggers - checkStaticAbilities(); - - if (table.replaceCounterEffect(game, null, true, true, params)) { - // update static abilities after etb counters have been placed + if (!table.isEmpty()) { + // we don't want always trigger before counters are placed + game.getTriggerHandler().suppressMode(TriggerType.Always); + // Need to apply any static effects to produce correct triggers checkStaticAbilities(); } + table.replaceCounterEffect(game, null, true, true, params); + + // update static abilities after etb counters have been placed + game.getTriggerHandler().clearSuppression(TriggerType.Always); + checkStaticAbilities(); + // 400.7g try adding keyword back into card if it doesn't already have it if (zoneTo.is(ZoneType.Stack) && cause != null && cause.isSpell() && !cause.isIntrinsic() && c.equals(cause.getHostCard())) { if (cause.getKeyword() != null && !copied.getKeywords().contains(cause.getKeyword())) { @@ -2085,6 +2090,9 @@ public class GameAction { // if (game.getRules().hasAppliedVariant(GameType.Planechase)) { first.initPlane(); + for (final Player p1 : game.getPlayers()) { + p1.createPlanechaseEffects(game); + } } first = runOpeningHandActions(first); diff --git a/forge-game/src/main/java/forge/game/PlanarDice.java b/forge-game/src/main/java/forge/game/PlanarDice.java index 7f0b92c7f60..11dcc7d442b 100644 --- a/forge-game/src/main/java/forge/game/PlanarDice.java +++ b/forge-game/src/main/java/forge/game/PlanarDice.java @@ -87,6 +87,11 @@ public enum PlanarDice { runParams.put(AbilityKey.Result, Arrays.asList(0)); roller.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDieOnce, runParams, false); + if (res == Chaos) { + runParams = AbilityKey.mapFromPlayer(roller); + roller.getGame().getTriggerHandler().runTrigger(TriggerType.ChaosEnsues, runParams, false); + } + return res; } diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 9beb675215f..b14163f6a40 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -1691,9 +1691,6 @@ public class AbilityUtils { if (root.isTrigger()) { Trigger t = root.getTrigger(); - if (t == null) { - return doXMath(0, expr, c, ctb); - } // ImmediateTrigger should check for the Ability which created the trigger if (t.getSpawningAbility() != null) { @@ -2083,7 +2080,6 @@ public class AbilityUtils { for (Mana m : paidMana) { if (m.toString().equals(type)) { count++; - } } return doXMath(count, expr, c, ctb); @@ -2168,11 +2164,9 @@ public class AbilityUtils { if (sq[0].startsWith("RememberedSize")) { return doXMath(c.getRememberedCount(), expr, c, ctb); } - if (sq[0].startsWith("ChosenSize")) { return doXMath(c.getChosenCards().size(), expr, c, ctb); } - if (sq[0].startsWith("ImprintedSize")) { return doXMath(c.getImprintedCards().size(), expr, c, ctb); } @@ -2714,54 +2708,35 @@ public class AbilityUtils { return MyRandom.getRandom().nextInt(1+max-min) + min; } + String[] paidparts = l[0].split("\\$", 2); + Iterable someCards = null; + // Count$ThisTurnCast // Count$LastTurnCast if (sq[0].startsWith("ThisTurnCast") || sq[0].startsWith("LastTurnCast")) { - String[] paidparts = l[0].split("\\$", 2); final String[] workingCopy = paidparts[0].split("_"); final String validFilter = workingCopy[1]; - List res; if (workingCopy[0].contains("This")) { - res = CardUtil.getThisTurnCast(validFilter, c, ctb, player); + someCards = CardUtil.getThisTurnCast(validFilter, c, ctb, player); } else { - res = CardUtil.getLastTurnCast(validFilter, c, ctb, player); + someCards = CardUtil.getLastTurnCast(validFilter, c, ctb, player); } - - int num; - if (paidparts.length > 1) { - num = handlePaid(res, paidparts[1], c, ctb); - } else { - num = res.size(); - } - - return doXMath(num, expr, c, ctb); } // Count$ThisTurnEntered [from ] - if (sq[0].startsWith("ThisTurnEntered")) { - final String[] workingCopy = l[0].split("_", 5); - + if (sq[0].startsWith("ThisTurnEntered") || sq[0].startsWith("LastTurnEntered")) { + final String[] workingCopy = paidparts[0].split("_"); ZoneType destination = ZoneType.smartValueOf(workingCopy[1]); final boolean hasFrom = workingCopy[2].equals("from"); ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null; String validFilter = workingCopy[hasFrom ? 4 : 2]; - final List res = CardUtil.getThisTurnEntered(destination, origin, validFilter, c, ctb, player); - return doXMath(res.size(), expr, c, ctb); - } - - // Count$LastTurnEntered [from ] - if (sq[0].startsWith("LastTurnEntered")) { - final String[] workingCopy = l[0].split("_", 5); - - ZoneType destination = ZoneType.smartValueOf(workingCopy[1]); - final boolean hasFrom = workingCopy[2].equals("from"); - ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null; - String validFilter = workingCopy[hasFrom ? 4 : 2]; - - final List res = CardUtil.getLastTurnEntered(destination, origin, validFilter, c, ctb, player); - return doXMath(res.size(), expr, c, ctb); + if (workingCopy[0].contains("This")) { + someCards = CardUtil.getThisTurnEntered(destination, origin, validFilter, c, ctb, player); + } else { + someCards = CardUtil.getLastTurnEntered(destination, origin, validFilter, c, ctb, player); + } } if (sq[0].startsWith("CountersAddedThisTurn")) { @@ -2779,7 +2754,6 @@ public class AbilityUtils { // count valid cards in any specified zone/s if (sq[0].startsWith("Valid")) { - String[] paidparts = l[0].split("\\$", 2); String[] lparts = paidparts[0].split(" ", 2); CardCollectionView cardsInZones = null; @@ -2805,13 +2779,7 @@ public class AbilityUtils { } } - int cnt; - if (paidparts.length > 1) { - cnt = handlePaid(CardLists.getValidCards(cardsInZones, lparts[1], player, c, ctb), paidparts[1], c, ctb); - } else { - cnt = CardLists.getValidCardCount(cardsInZones, lparts[1], player, c, ctb); - } - return doXMath(cnt, expr, c, ctb); + someCards = CardLists.getValidCards(cardsInZones, lparts[1], player, c, ctb); } if (sq[0].startsWith("MostCardName")) { @@ -2891,23 +2859,27 @@ public class AbilityUtils { return doXMath(Iterables.size(powers), expr, c, ctb); } if (sq[0].startsWith("DifferentCounterKinds_")) { - final List kinds = Lists.newArrayList(); + final Set kinds = Sets.newHashSet(); final String rest = l[0].substring(22); CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb); for (final Card card : list) { - for (final Map.Entry map : card.getCounters().entrySet()) { - if (!kinds.contains(map.getKey())) { - kinds.add(map.getKey()); - } - } + kinds.addAll(card.getCounters().keySet()); } return doXMath(kinds.size(), expr, c, ctb); } // Complex counting methods - CardCollectionView someCards = getCardListForXCount(c, player, sq, ctb); + Integer num = null; + if (someCards == null) { + someCards = getCardListForXCount(c, player, sq, ctb); + } else if (paidparts.length > 1) { + num = handlePaid(someCards, paidparts[1], c, ctb); + } + if (num == null) { + num = Iterables.size(someCards); + } - return doXMath(someCards.size(), expr, c, ctb); + return doXMath(num, expr, c, ctb); } public static final void applyManaColorConversion(ManaConversionMatrix matrix, String conversion) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java index b9b14b9b0b1..1f8b462ea58 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java @@ -192,7 +192,7 @@ public class ControlGainEffect extends SpellAbilityEffect { tgtC.addChangedCardKeywords(kws, Lists.newArrayList(), false, tStamp, 0); game.fireEvent(new GameEventCardStatsChanged(tgtC)); } - if (hiddenKws.isEmpty()) { + if (!hiddenKws.isEmpty()) { tgtC.addHiddenExtrinsicKeywords(tStamp, 0, hiddenKws); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ProtectEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ProtectEffect.java index a4e09a5b5f4..f75f5df741b 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ProtectEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ProtectEffect.java @@ -5,9 +5,12 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; +import org.apache.commons.lang3.StringUtils; + import com.google.common.collect.Lists; import forge.GameCommand; +import forge.card.CardType; import forge.card.MagicColor; import forge.game.Game; import forge.game.ability.AbilityUtils; @@ -129,8 +132,12 @@ public class ProtectEffect extends SpellAbilityEffect { } List gainsKWList = Lists.newArrayList(); - for (String color : gains) { - gainsKWList.add(TextUtil.concatWithSpace("Protection from", color)); + for (String type : gains) { + if (isChoice && sa.getParam("Choices").equals("CardType")) { + gainsKWList.add("Protection:" + type); + } else { + gainsKWList.add(TextUtil.concatWithSpace("Protection from", type)); + } } tgtCards.addAll(CardUtil.getRadiance(sa)); @@ -176,6 +183,8 @@ public class ProtectEffect extends SpellAbilityEffect { if (choices.contains("AnyColor")) { gains.addAll(MagicColor.Constant.ONLY_COLORS); choices = choices.replaceAll("AnyColor,?", ""); + } else if (choices.contains("CardType")) { + choices = StringUtils.join(CardType.getAllCardTypes(), ","); } // Add any remaining choices if (choices.length() > 0) { diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 34e48d2cd28..41c223ea615 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -5532,6 +5532,10 @@ public class Card extends GameEntity implements Comparable, IHasSVars { // KeywordCollection#getAmount final String[] parse = kw.contains(":") ? kw.split(":") : kw.split(" "); + if (parse.length < 2) { + count++; + continue; + } final String s = parse[1]; if (StringUtils.isNumeric(s)) { count += Integer.parseInt(s); diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index 7c3be8266c2..0624524101e 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -318,42 +318,11 @@ public class CardFactory { // ****************************************************************** // ************** Link to different CardFactories ******************* - if (card.isPlane()) { - buildPlaneAbilities(card); - } buildBattleAbilities(card); CardFactoryUtil.setupKeywordedAbilities(card); // Should happen AFTER setting left/right split abilities to set Fuse ability to both sides card.updateStateForView(); } - private static void buildPlaneAbilities(Card card) { - String trigger = "Mode$ PlanarDice | Result$ Planeswalk | TriggerZones$ Command | Secondary$ True | " + - "TriggerDescription$ Whenever you roll the Planeswalker symbol on the planar die, planeswalk."; - - String rolledWalk = "DB$ Planeswalk | Cause$ PlanarDie"; - - Trigger planesWalkTrigger = TriggerHandler.parseTrigger(trigger, card, true); - planesWalkTrigger.setOverridingAbility(AbilityFactory.getAbility(rolledWalk, card)); - card.addTrigger(planesWalkTrigger); - - String chaosTrig = "Mode$ PlanarDice | Result$ Chaos | TriggerZones$ Command | Static$ True"; - - String rolledChaos = "DB$ ChaosEnsues"; - - Trigger chaosTrigger = TriggerHandler.parseTrigger(chaosTrig, card, true); - chaosTrigger.setOverridingAbility(AbilityFactory.getAbility(rolledChaos, card)); - card.addTrigger(chaosTrigger); - - String specialA = "ST$ RollPlanarDice | Cost$ X | SorcerySpeed$ True | Activator$ Player | SpecialAction$ True" + - " | ActivationZone$ Command | SpellDescription$ Roll the planar dice. X is equal to the number of " + - "times you have previously taken this action this turn. | CostDesc$ {X}: "; - - SpellAbility planarRoll = AbilityFactory.getAbility(specialA, card); - planarRoll.setSVar("X", "Count$PlanarDiceSpecialActionThisTurn"); - - card.addSpellAbility(planarRoll); - } - private static void buildBattleAbilities(Card card) { if (!card.isBattle()) { return; diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 097220edac6..b097d3a5666 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -2445,21 +2445,11 @@ public class CardFactoryUtil { ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, repeatSA, false, true, intrinsic, "Card.Self", ""); inst.addReplacement(cardre); - } else if (keyword.startsWith("Riot")) { - final String choose = "DB$ GenericChoice | AILogic$ Riot | SpellDescription$ Riot"; + } else if (keyword.equals("Riot")) { + final String hasteStr = "DB$ Animate | Defined$ Self | Keywords$ Haste | Duration$ Permanent | UnlessCost$ AddCounter<1/P1P1> | UnlessPayer$ You | UnlessAI$ Riot | SpellDescription$ Riot"; - final String counter = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | ETB$ True | CounterNum$ 1" + - " | SpellDescription$ Put a +1/+1 counter on it."; - final String haste = "DB$ Animate | Defined$ Self | Keywords$ Haste | Duration$ Permanent | SpellDescription$ Haste"; - - SpellAbility saChoose = AbilityFactory.getAbility(choose, card); - - List list = Lists.newArrayList(); - list.add((AbilitySub)AbilityFactory.getAbility(counter, card)); - list.add((AbilitySub)AbilityFactory.getAbility(haste, card)); - saChoose.setAdditionalAbilityList("Choices", list); - - ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, saChoose, false, true, intrinsic, "Card.Self", ""); + final SpellAbility hasteSa = AbilityFactory.getAbility(hasteStr, card); + ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, hasteSa, false, true, intrinsic, "Card.Self", ""); inst.addReplacement(cardre); } else if (keyword.equals("Sunburst")) { diff --git a/forge-game/src/main/java/forge/game/cost/Cost.java b/forge-game/src/main/java/forge/game/cost/Cost.java index 9f110f71700..de6cafa4dc8 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -297,7 +297,7 @@ public class Cost implements Serializable { final String[] splitStr = abCostParse(parse, 5); final String type = splitStr.length > 2 ? splitStr[2] : "CARDNAME"; final String description = splitStr.length > 3 ? splitStr[3] : null; - final ZoneType zone = splitStr.length > 4 ? ZoneType.smartValueOf(splitStr[4]) : ZoneType.Battlefield; + final List zone = splitStr.length > 4 ? ZoneType.listValueOf(splitStr[4]) : Lists.newArrayList(ZoneType.Battlefield); boolean oneOrMore = false; if (splitStr[0].equals("X1+")) { oneOrMore = true; @@ -979,7 +979,7 @@ public class Cost implements Serializable { if (counters < 0) { costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription())); } else { - costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription(), ZoneType.Battlefield, false)); + costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription(), Lists.newArrayList(ZoneType.Battlefield) , false)); } } else { continue; diff --git a/forge-game/src/main/java/forge/game/cost/CostPayment.java b/forge-game/src/main/java/forge/game/cost/CostPayment.java index 0ce3be371f0..e04b0e7870a 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPayment.java +++ b/forge-game/src/main/java/forge/game/cost/CostPayment.java @@ -95,13 +95,13 @@ public class CostPayment extends ManaConversionMatrix { * a {@link forge.game.spellability.SpellAbility} object. * @return a boolean. */ - public static boolean canPayAdditionalCosts(Cost cost, final SpellAbility ability) { + public static boolean canPayAdditionalCosts(Cost cost, final SpellAbility ability, final boolean effect) { if (cost == null) { return true; } cost = CostAdjustment.adjust(cost, ability); - return cost.canPay(ability, false); + return cost.canPay(ability, effect); } /** diff --git a/forge-game/src/main/java/forge/game/cost/CostPutCounter.java b/forge-game/src/main/java/forge/game/cost/CostPutCounter.java index 5d2b2f610da..cbadfcda3bc 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPutCounter.java +++ b/forge-game/src/main/java/forge/game/cost/CostPutCounter.java @@ -19,13 +19,21 @@ package forge.game.cost; import java.util.List; +import com.google.common.collect.Sets; + +import forge.game.Game; import forge.game.GameEntityCounterTable; +import forge.game.ability.AbilityKey; import forge.game.card.Card; +import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.card.CardPredicates; +import forge.game.card.CardUtil; import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.player.Player; +import forge.game.replacement.ReplacementEffect; +import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; @@ -140,15 +148,36 @@ public class CostPutCounter extends CostPartWithList { @Override public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) { final Card source = ability.getHostCard(); + final Game game = source.getGame(); if (this.payCostFromSource()) { - return source.isInPlay() && (getAbilityAmount(ability) == 0 || source.canReceiveCounters(this.counter)); + if (isETBReplacement(ability, effect)) { + final Card copy = CardUtil.getLKICopy(source); + copy.setLastKnownZone(payer.getZone(ZoneType.Battlefield)); + + // check state it would have on the battlefield + CardCollection preList = new CardCollection(copy); + game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList); + // reset again? + game.getAction().checkStaticAbilities(false); + if (copy.canReceiveCounters(getCounter())) { + return true; + } + } else { + if (!source.isInPlay()) { + return false; + } + if (source.canReceiveCounters(getCounter())) { + return true; + } + } + return getAbilityAmount(ability) == 0; } // 3 Cards have Put a -1/-1 Counter on a Creature you control. List typeList = CardLists.getValidCards(source.getGame().getCardsIn(ZoneType.Battlefield), this.getType().split(";"), payer, source, ability); - typeList = CardLists.filter(typeList, CardPredicates.canReceiveCounters(this.counter)); + typeList = CardLists.filter(typeList, CardPredicates.canReceiveCounters(getCounter())); return !typeList.isEmpty(); } @@ -172,8 +201,12 @@ public class CostPutCounter extends CostPartWithList { @Override protected Card doPayment(Player payer, SpellAbility ability, Card targetCard, final boolean effect) { - final int i = this.getAbilityAmount(ability); - targetCard.addCounter(this.getCounter(), i, payer, counterTable); + final int i = getAbilityAmount(ability); + if (isETBReplacement(ability, effect)) { + targetCard.addEtbCounter(getCounter(), i, payer); + } else { + targetCard.addCounter(getCounter(), i, payer, counterTable); + } return targetCard; } @@ -208,4 +241,27 @@ public class CostPutCounter extends CostPartWithList { counterTable.clear(); } + protected boolean isETBReplacement(final SpellAbility ability, final boolean effect) { + if (!effect) { + return false; + } + // only for itself? + if (!payCostFromSource()) { + return false; + } + if (ability == null) { + return false; + } + if (!ability.isReplacementAbility()) { + return false; + } + ReplacementEffect re = ability.getReplacementEffect(); + if (re.getMode() != ReplacementType.Moved) { + return false; + } + if (!ability.getHostCard().equals(ability.getReplacingObject(AbilityKey.Card))) { + return false; + } + return true; + } } diff --git a/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java b/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java index 4f234f24f86..4c284381471 100644 --- a/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java +++ b/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java @@ -41,7 +41,7 @@ public class CostRemoveCounter extends CostPart { */ private static final long serialVersionUID = 1L; public final CounterType counter; - public final ZoneType zone; + public final List zone; public final Boolean oneOrMore; /** @@ -57,7 +57,7 @@ public class CostRemoveCounter extends CostPart { * the description * @param zone the zone. */ - public CostRemoveCounter(final String amount, final CounterType counter, final String type, final String description, final ZoneType zone, final boolean oneOrMore) { + public CostRemoveCounter(final String amount, final CounterType counter, final String type, final String description, final List zone, final boolean oneOrMore) { super(amount, type, description); this.counter = counter; @@ -73,23 +73,24 @@ public class CostRemoveCounter extends CostPart { final CounterType cntrs = this.counter; final Card source = ability.getHostCard(); final String type = this.getType(); + if (this.payCostFromSource()) { return source.getCounters(cntrs); - } else { - List typeList; - if (type.equals("OriginalHost")) { - typeList = Lists.newArrayList(ability.getOriginalHost()); - } else { - typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability); - } - - // Single Target - int maxcount = 0; - for (Card c : typeList) { - maxcount = Math.max(maxcount, c.getCounters(cntrs)); - } - return maxcount; } + + List typeList; + if (type.equals("OriginalHost")) { + typeList = Lists.newArrayList(ability.getOriginalHost()); + } else { + typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability); + } + + // Single Target + int maxcount = 0; + for (Card c : typeList) { + maxcount = Math.max(maxcount, c.getCounters(cntrs)); + } + return maxcount; } /* @@ -146,26 +147,24 @@ public class CostRemoveCounter extends CostPart { final int amount; if (getAmount().equals("All")) { amount = source.getCounters(cntrs); - } - else { + } else { amount = getAbilityAmount(ability); } if (this.payCostFromSource()) { return !source.isPhasedOut() && (source.getCounters(cntrs) - amount) >= 0; } - else { - List typeList; - if (type.equals("OriginalHost")) { - typeList = Lists.newArrayList(ability.getOriginalHost()); - } else { - typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability); - } - // (default logic) remove X counters from a single permanent - for (Card c : typeList) { - if (c.getCounters(cntrs) - amount >= 0) { - return true; - } + List typeList; + if (type.equals("OriginalHost")) { + typeList = Lists.newArrayList(ability.getOriginalHost()); + } else { + typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability); + } + + // (default logic) remove X counters from a single permanent + for (Card c : typeList) { + if (c.getCounters(cntrs) - amount >= 0) { + return true; } } diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 4a7d46da450..5480961d611 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -175,7 +175,7 @@ public enum Keyword { STRIVE("Strive", KeywordWithCost.class, false, "CARDNAME costs %s more to cast for each target beyond the first."), SUNBURST("Sunburst", SimpleKeyword.class, false, "This enters the battlefield with either a +1/+1 or charge counter on it for each color of mana spent to cast it based on whether it's a creature."), SURGE("Surge", KeywordWithCost.class, false, "You may cast this spell for its surge cost if you or a teammate has cast another spell this turn."), - SUSPEND("Suspend", Suspend.class, false, "Rather than cast this card from your hand, you may pay %s and exile it with {%d:time counter} on it. At the beginning of your upkeep, remove a time counter. When the last is removed, cast it without paying its mana cost."), + SUSPEND("Suspend", Suspend.class, false, "If you could begin to cast this card by putting it onto the stack from your hand, you may pay %s and exile it with {%d:time counter} on it. At the beginning of your upkeep, remove a time counter. When the last is removed, play it without paying its mana cost. If you cast a creature spell this way, it gains haste until you lose control of the spell or the permanent it becomes."), TOTEM_ARMOR("Totem armor", SimpleKeyword.class, true, "If enchanted permanent would be destroyed, instead remove all damage marked on it and destroy this Aura."), TOXIC("Toxic", KeywordWithAmount.class, false, "Players dealt combat damage by this creature also get {%d:poison counter}."), TRAINING("Training", SimpleKeyword.class, false, "Whenever this creature attacks with another creature with greater power, put a +1/+1 counter on this creature."), diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 9b9bc6cb576..230ae9e9e71 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -3176,6 +3176,36 @@ public class Player extends GameEntity implements Comparable { return eff; } + public void createPlanechaseEffects(Game game) { + final PlayerZone com = getZone(ZoneType.Command); + final String name = "Planar Dice"; + final Card eff = new Card(game.nextCardId(), game); + eff.setTimestamp(game.getNextTimestamp()); + eff.setName(name); + eff.setOwner(this); + eff.setImmutable(true); + String image = ImageKeys.getTokenKey("planechase"); + eff.setImageKey(image); + + String trigger = "Mode$ PlanarDice | Result$ Planeswalk | TriggerZones$ Command | ValidPlayer$ You | Secondary$ True | " + + "TriggerDescription$ Whenever you roll the Planeswalker symbol on the planar die, planeswalk."; + String rolledWalk = "DB$ Planeswalk | Cause$ PlanarDie"; + Trigger planesWalkTrigger = TriggerHandler.parseTrigger(trigger, eff, true); + planesWalkTrigger.setOverridingAbility(AbilityFactory.getAbility(rolledWalk, eff)); + eff.addTrigger(planesWalkTrigger); + + String specialA = "ST$ RollPlanarDice | Cost$ X | SorcerySpeed$ True | Activator$ Player | SpecialAction$ True" + + " | ActivationZone$ Command | SpellDescription$ Roll the planar dice. X is equal to the number of " + + "times you have previously taken this action this turn. | CostDesc$ {X}: "; + SpellAbility planarRoll = AbilityFactory.getAbility(specialA, eff); + planarRoll.setSVar("X", "Count$PlanarDiceSpecialActionThisTurn"); + eff.addSpellAbility(planarRoll); + + eff.updateStateForView(); + com.add(eff); + this.updateZoneForView(com); + } + public void createTheRing(Card host) { final PlayerZone com = getZone(ZoneType.Command); if (theRing == null) { diff --git a/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java b/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java index 5008719eb52..60112d3784c 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java @@ -90,7 +90,7 @@ public abstract class AbilityActivated extends SpellAbility implements Cloneable return false; } - return CostPayment.canPayAdditionalCosts(this.getPayCosts(), this); + return CostPayment.canPayAdditionalCosts(this.getPayCosts(), this, false); } /** {@inheritDoc} */ diff --git a/forge-game/src/main/java/forge/game/spellability/Spell.java b/forge-game/src/main/java/forge/game/spellability/Spell.java index 17071958e61..27d30f323bd 100644 --- a/forge-game/src/main/java/forge/game/spellability/Spell.java +++ b/forge-game/src/main/java/forge/game/spellability/Spell.java @@ -112,7 +112,7 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable return false; } - if (!CostPayment.canPayAdditionalCosts(this.getPayCosts(), this)) { + if (!CostPayment.canPayAdditionalCosts(this.getPayCosts(), this, false)) { return false; } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 797c2d547d0..9363684128f 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -827,6 +827,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void setReplacingObject(final AbilityKey type, final Object o) { replacingObjects.put(type, o); } + public void setReplacingObjects(final Map repParams) { + replacingObjects = AbilityKey.newMap(repParams); + } public void setReplacingObjectsFrom(final Map repParams, final AbilityKey... types) { int typesLength = types.length; for (int i = 0; i < typesLength; i += 1) { diff --git a/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java b/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java index 487b5afc4cb..23b9d2c5487 100644 --- a/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java +++ b/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java @@ -373,8 +373,14 @@ public class AdventureEventData implements Serializable { } } - for (String restricted : Config.instance().getConfigData().restrictedEditions) { - legalBlocks.removeIf(q -> q.getName().equals(restricted)); + ConfigData configData = Config.instance().getConfigData(); + if (configData.allowedEditions != null) { + List allowed = Arrays.asList(configData.allowedEditions); + legalBlocks.removeIf(q -> !allowed.contains(q.getName())); + } else { + for (String restricted : configData.restrictedEditions) { + legalBlocks.removeIf(q -> q.getName().equals(restricted)); + } } return legalBlocks.isEmpty() ? null : Aggregates.random(legalBlocks); } @@ -388,8 +394,14 @@ public class AdventureEventData implements Serializable { legalBlocks.add(b); } } - for (String restricted : Config.instance().getConfigData().restrictedEditions) { - legalBlocks.removeIf(q -> q.getName().equals(restricted)); + ConfigData configData = Config.instance().getConfigData(); + if (configData.allowedEditions != null) { + List allowed = Arrays.asList(configData.allowedEditions); + legalBlocks.removeIf(q -> !allowed.contains(q.getName())); + } else { + for (String restricted : configData.restrictedEditions) { + legalBlocks.removeIf(q -> q.getName().equals(restricted)); + } } return legalBlocks.isEmpty()?null:Aggregates.random(legalBlocks); } diff --git a/forge-gui-mobile/src/forge/adventure/data/ConfigData.java b/forge-gui-mobile/src/forge/adventure/data/ConfigData.java index bf18d7326c6..453affb632e 100644 --- a/forge-gui-mobile/src/forge/adventure/data/ConfigData.java +++ b/forge-gui-mobile/src/forge/adventure/data/ConfigData.java @@ -22,4 +22,5 @@ public class ConfigData { public RewardData legalCards; public String[] restrictedCards; public String[] restrictedEditions; + public String[] allowedEditions; } diff --git a/forge-gui-mobile/src/forge/adventure/data/RewardData.java b/forge-gui-mobile/src/forge/adventure/data/RewardData.java index a1db60d757e..3b7fcef20dd 100644 --- a/forge-gui-mobile/src/forge/adventure/data/RewardData.java +++ b/forge-gui-mobile/src/forge/adventure/data/RewardData.java @@ -86,7 +86,8 @@ public class RewardData implements Serializable { private static Iterable allEnemyCards; static private void initializeAllCards(){ - RewardData legals = Config.instance().getConfigData().legalCards; + ConfigData configData = Config.instance().getConfigData(); + RewardData legals = configData.legalCards; if(legals==null) allCards = CardUtil.getFullCardPool(false); // we need unique cards only here, so that a unique card can be chosen before a set variant is determined @@ -100,11 +101,14 @@ public class RewardData implements Serializable { return false; if(input.getRules().getAiHints().getRemNonCommanderDecks()) return false; - if(Arrays.asList(Config.instance().getConfigData().restrictedEditions).contains(input.getEdition())) + if(configData.allowedEditions != null) { + if (!Arrays.asList(configData.allowedEditions).contains(input.getEdition())) + return false; + } else if(Arrays.asList(configData.restrictedEditions).contains(input.getEdition())) return false; if(input.getRules().isCustom()) return false; - return !Arrays.asList(Config.instance().getConfigData().restrictedCards).contains(input.getName()); + return !Arrays.asList(configData.restrictedCards).contains(input.getName()); }); //Filter AI cards for enemies. allEnemyCards=Iterables.filter(allCards, input -> { diff --git a/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java b/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java index 08b8f87a906..a7e26681b73 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java @@ -11,6 +11,7 @@ import com.github.tommyettinger.textra.TextraButton; import com.github.tommyettinger.textra.TextraLabel; import forge.Forge; import forge.StaticData; +import forge.adventure.data.ConfigData; import forge.adventure.data.RewardData; import forge.adventure.util.*; import forge.card.CardEdition; @@ -151,7 +152,10 @@ public class SpellSmithScene extends UIScene { .filter(input2 -> input2.getEdition().equals(input.getCode())).collect(Collectors.toList()); if (it.size() == 0) return false; - return (!Arrays.asList(Config.instance().getConfigData().restrictedEditions).contains(input.getCode())); + ConfigData configData = Config.instance().getConfigData(); + if (configData.allowedEditions != null) + return Arrays.asList(configData.allowedEditions).contains(input.getCode()); + return (!Arrays.asList(configData.restrictedEditions).contains(input.getCode())); }).sorted(new Comparator() { @Override public int compare(CardEdition e1, CardEdition e2) { diff --git a/forge-gui-mobile/src/forge/adventure/stage/MapStage.java b/forge-gui-mobile/src/forge/adventure/stage/MapStage.java index bd3e696c298..767f5995145 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/MapStage.java +++ b/forge-gui-mobile/src/forge/adventure/stage/MapStage.java @@ -480,8 +480,11 @@ public class MapStage extends GameStage { if (enemy != null && !enemy.toString().isEmpty()) { EnemyData EN = WorldData.getEnemy(enemy.toString()); if (EN == null) { - System.err.printf("Enemy \"%s\" not found.", enemy); - break; + System.err.printf("Enemy \"%s\" not found, choosing a random one for current biome\n", enemy); + forge.adventure.world.World world = Current.world(); + Vector2 poiPos = AdventureQuestController.instance().mostRecentPOI.getPosition(); + int currentBiome = forge.adventure.world.World.highestBiome(world.getBiome((int) poiPos.x / world.getTileSize(), (int) poiPos.y / world.getTileSize())); + EN = world.getData().GetBiomes().get(currentBiome).getEnemy(1.0f); } EnemySprite mob = new EnemySprite(id, EN); Object dialogObject = prop.get("dialog"); //Check if the enemy has a dialogue attached to it. diff --git a/forge-gui-mobile/src/forge/adventure/util/CardUtil.java b/forge-gui-mobile/src/forge/adventure/util/CardUtil.java index 67a03becec3..a8c6edf766a 100644 --- a/forge-gui-mobile/src/forge/adventure/util/CardUtil.java +++ b/forge-gui-mobile/src/forge/adventure/util/CardUtil.java @@ -6,6 +6,7 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import forge.StaticData; +import forge.adventure.data.ConfigData; import forge.adventure.data.GeneratedDeckData; import forge.adventure.data.GeneratedDeckTemplateData; import forge.adventure.data.RewardData; @@ -23,7 +24,6 @@ import forge.item.generation.UnOpenedProduct; import forge.model.FModel; import forge.util.Aggregates; -import java.io.File; import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Pattern; @@ -714,7 +714,7 @@ public class CardUtil { public static Deck getDeck(String path, boolean forAI, boolean isFantasyMode, String colors, boolean isTheme, boolean useGeneticAI, CardEdition starterEdition, boolean discourageDuplicates) { if(path.endsWith(".dck")) - return DeckSerializer.fromFile(new File(Config.instance().getCommonFilePath(path))); + return DeckSerializer.fromFile(Config.instance().getFile(path).file()); if(forAI && (isFantasyMode||useGeneticAI)) { Deck deck = DeckgenUtil.getRandomOrPreconOrThemeDeck(colors, forAI, isTheme, useGeneticAI); @@ -755,8 +755,12 @@ public class CardUtil { } public static Deck generateBoosterPackAsDeck(String code){ - - if (Arrays.asList(Config.instance().getConfigData().restrictedEditions).contains(code)){ + ConfigData configData = Config.instance().getConfigData(); + if (configData.allowedEditions != null) { + if (!Arrays.asList(configData.allowedEditions).contains(code)){ + System.err.println("Cannot generate booster pack, '" + code + "' is not an allowed edition"); + } + } else if (Arrays.asList(configData.restrictedEditions).contains(code)){ System.err.println("Cannot generate booster pack, '" + code + "' is a restricted edition"); } diff --git a/forge-gui/res/cardsfolder/c/churning_reservoir.txt b/forge-gui/res/cardsfolder/c/churning_reservoir.txt index 73c14d7cd26..a75fcb16827 100644 --- a/forge-gui/res/cardsfolder/c/churning_reservoir.txt +++ b/forge-gui/res/cardsfolder/c/churning_reservoir.txt @@ -5,6 +5,6 @@ T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigPutCounter | Tri SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Artifact.nonToken+YouCtrl+Other,Creature.YouCtrl+Other+nonToken | CounterType$ OIL | CounterNum$ 1 A:AB$ Token | Cost$ 2 T | TokenScript$ r_1_1_phyrexian_goblin | CheckSVar$ CountCountersRemoved | CheckSVarCompare$ GE1 | SpellDescription$ Create a 1/1 red Phyrexian Goblin creature token. Activate only if an oil counter was removed from a permanent you controlled this turn or a permanent with an oil counter on it was put into a graveyard this turn. SVar:CountCountersRemoved:Count$CountersRemovedThisTurn OIL Card.YouCtrl+inRealZoneBattlefield/Plus.X -SVar:X:Count$ThisTurnEntered_Graveyard_from_Battlefield_Permanent.YouCtrl+counters_GE1_OIL +SVar:X:Count$ThisTurnEntered_Graveyard_from_Battlefield_Permanent.counters_GE1_OIL DeckHas:Ability$Token/Counters & Type$Goblin|Phyrexian Oracle:At the beginning of your upkeep, put an oil counter on another target nontoken artifact or creature you control.\n{2}, {T}: Create a 1/1 red Phyrexian Goblin creature token. Activate only if an oil counter was removed from a permanent you controlled this turn or a permanent with an oil counter on it was put into a graveyard this turn. diff --git a/forge-gui/res/cardsfolder/d/diseased_vermin.txt b/forge-gui/res/cardsfolder/d/diseased_vermin.txt index b0e60831e11..aca1338b198 100644 --- a/forge-gui/res/cardsfolder/d/diseased_vermin.txt +++ b/forge-gui/res/cardsfolder/d/diseased_vermin.txt @@ -3,11 +3,8 @@ ManaCost:2 B Types:Creature Rat PT:1/1 T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigPutCounter | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, put an infection counter on it. -SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ INFECTION | CounterNum$ 1 | SubAbility$ DBRemember -SVar:DBRemember:DB$ Pump | RememberObjects$ Opponent | StackDescription$ None +SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ INFECTION | CounterNum$ 1 T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ DBDisease | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, Diseased Vermin deals X damage to target opponent previously dealt damage by it, where X is the number of infection counters on it. -SVar:DBDisease:DB$ DealDamage | ValidTgts$ Opponent.IsRemembered | NumDmg$ X +SVar:DBDisease:DB$ DealDamage | ValidTgts$ Opponent.wasDealtDamageThisGameBy Self | NumDmg$ X SVar:X:Count$CardCounters.INFECTION -T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ DBCleanup | Static$ True -SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True Oracle:Whenever Diseased Vermin deals combat damage to a player, put an infection counter on it.\nAt the beginning of your upkeep, Diseased Vermin deals X damage to target opponent previously dealt damage by it, where X is the number of infection counters on it. diff --git a/forge-gui/res/cardsfolder/g/gandalf_the_grey.txt b/forge-gui/res/cardsfolder/g/gandalf_the_grey.txt index b0d5d3497ba..2bbdbe46c60 100644 --- a/forge-gui/res/cardsfolder/g/gandalf_the_grey.txt +++ b/forge-gui/res/cardsfolder/g/gandalf_the_grey.txt @@ -6,7 +6,7 @@ T:Mode$ SpellCast | ValidCard$ Instant,Sorcery | ValidActivatingPlayer$ You | Tr SVar:TrigCharm:DB$ Charm | Choices$ DBTapUntap,DBDamage,DBCopy,DBChangeZone | ChoiceRestriction$ ThisGame | CharmNum$ 1 SVar:DBTapUntap:DB$ TapOrUntap | ValidTgts$ Permanent | TgtPrompt$ Select target permanent to tap or untap | SpellDescription$ You may tap or untap target permanent. SVar:DBDamage:DB$ DealDamage | Defined$ Opponent | NumDmg$ 3 | SpellDescription$ CARDNAME deals 3 damage to each opponent. -SVar:DBCopy:DB$ CopySpellAbility | ValidTgts$ Instant.YouCtrl,Sorcery.YouCtrl | TgtPrompt$ Select target instant or sorcery spell you control | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell you control. You may choose new targets for the copy. +SVar:DBCopy:DB$ CopySpellAbility | ValidTgts$ Instant.YouCtrl,Sorcery.YouCtrl | TargetType$ Spell | TgtPrompt$ Select target instant or sorcery spell you control | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell you control. You may choose new targets for the copy. SVar:DBChangeZone:DB$ ChangeZone | Origin$ Battlefield | Destination$ Library | LibraryPosition$ 0 | SpellDescription$ Put NICKNAME on top of its owner's library. DeckNeeds:Type$Instant|Sorcery Oracle:Whenever you cast an instant or sorcery spell, choose one that hasn't been chosen —\n• You may tap or untap target permanent.\n• Gandalf the Grey deals 3 damage to each opponent.\n• Copy target instant or sorcery spell you control. You may choose new targets for the copy.\n• Put Gandalf on top of its owner's library. diff --git a/forge-gui/res/cardsfolder/m/magmatic_galleon.txt b/forge-gui/res/cardsfolder/m/magmatic_galleon.txt index e6d837f5fc7..e1656fdf595 100644 --- a/forge-gui/res/cardsfolder/m/magmatic_galleon.txt +++ b/forge-gui/res/cardsfolder/m/magmatic_galleon.txt @@ -4,7 +4,7 @@ Types:Artifact Vehicle PT:5/5 T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDamage | TriggerDescription$ When CARDNAME enters the battlefield, it deals 5 damage to target creature an opponent controls. SVar:TrigDamage:DB$ DealDamage | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls | NumDmg$ 5 -T:Mode$ ExcessDamageAll | ValidTarget$ Creature.OppCtrl | CombatDamage$ False | Execute$ TrigTreasure | TriggerDescription$ Whenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token. +T:Mode$ ExcessDamageAll | ValidTarget$ Creature.OppCtrl | CombatDamage$ False | TriggerZones$ Battlefield | Execute$ TrigTreasure | TriggerDescription$ Whenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token. SVar:TrigTreasure:DB$ Token | TokenScript$ c_a_treasure_sac K:Crew:2 Oracle:When Magmatic Galleon enters the battlefield, it deals 5 damage to target creature an opponent controls.\nWhenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token.\nCrew 2 diff --git a/forge-gui/res/cardsfolder/m/me_the_immortal.txt b/forge-gui/res/cardsfolder/m/me_the_immortal.txt index a554bcae7f9..46bb9a3ec46 100644 --- a/forge-gui/res/cardsfolder/m/me_the_immortal.txt +++ b/forge-gui/res/cardsfolder/m/me_the_immortal.txt @@ -3,7 +3,7 @@ ManaCost:2 G U R Types:Legendary Creature Human Rogue PT:3/3 T:Mode$ Phase | Phase$ BeginCombat | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigCounter | TriggerDescription$ At the beginning of combat on your turn, put your choice of a +1/+1, first strike, vigilance, or menace counter on CARDNAME. -SVar:TrigCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1,First strike,Vigilance,Menace | CounterNum$ 1 +SVar:TrigCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1,First Strike,Vigilance,Menace | CounterNum$ 1 K:Counters remain on CARDNAME as it moves to any zone other than a player's hand or library. SVar:AltCost:Cost$ 2 G U R Discard<2/Card> | ActivationZone$ Graveyard | Description$ You may cast NICKNAME from your graveyard by discarding two cards in addition to paying its other costs. DeckHas:Ability$Counters|Discard diff --git a/forge-gui/res/cardsfolder/p/pippin_guard_of_the_citadel.txt b/forge-gui/res/cardsfolder/p/pippin_guard_of_the_citadel.txt index 8b83603c367..c9854be4a34 100644 --- a/forge-gui/res/cardsfolder/p/pippin_guard_of_the_citadel.txt +++ b/forge-gui/res/cardsfolder/p/pippin_guard_of_the_citadel.txt @@ -4,5 +4,5 @@ Types:Legendary Creature Halfling Soldier PT:2/2 K:Vigilance K:Ward:1 -A:AB$ Protection | Cost$ T | ValidTgts$ Creature.YouCtrl+Other | TgtPrompt$ Select another target creature you control | Gains$ Choice | Choices$ artifacts,enchantments,creatures,battles,instants,sorceries,planeswalkers,lands | SpellDescription$ Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.) +A:AB$ Protection | Cost$ T | ValidTgts$ Creature.YouCtrl+Other | TgtPrompt$ Select another target creature you control | Gains$ Choice | Choices$ CardType | SpellDescription$ Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.) Oracle:Vigilance, ward {1}\n{T}: Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.) diff --git a/forge-gui/res/cardsfolder/r/ravens_run.txt b/forge-gui/res/cardsfolder/r/ravens_run.txt index 1d1b83ed5c0..9a569083e30 100644 --- a/forge-gui/res/cardsfolder/r/ravens_run.txt +++ b/forge-gui/res/cardsfolder/r/ravens_run.txt @@ -2,7 +2,7 @@ Name:Raven's Run ManaCost:no cost Types:Plane Shadowmoor S:Mode$ Continuous | EffectZone$ Command | Affected$ Creature | AddKeyword$ Wither | Description$ All Creatures have Wither (They deal damage to creatures in the form of -1/-1 counters.) -T:Mode$ ChaosEnsues | OptionalDecider$ You | TriggerZones$ Command | Execute$ TrigPutCounter | TriggerDescription$ Whenever chaos ensues, put a -1/-1 counter on target creature, two -1/-1 counters on another target creature, and three -1/-1 counters on a third target creature. +T:Mode$ ChaosEnsues | TriggerZones$ Command | Execute$ TrigPutCounter | TriggerDescription$ Whenever chaos ensues, put a -1/-1 counter on target creature, two -1/-1 counters on another target creature, and three -1/-1 counters on a third target creature. SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Creature | TargetUnique$ True | CounterType$ M1M1 | SubAbility$ DBPutTwo SVar:DBPutTwo:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select another target creature | TargetUnique$ True | CounterType$ M1M1 | CounterNum$ 2 | SubAbility$ DBPutThree SVar:DBPutThree:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select a third target creature | TargetUnique$ True | CounterType$ M1M1 | CounterNum$ 3 diff --git a/forge-gui/res/cardsfolder/r/rhuk_hexgold_nabber.txt b/forge-gui/res/cardsfolder/r/rhuk_hexgold_nabber.txt index e20997d2055..41a71913429 100644 --- a/forge-gui/res/cardsfolder/r/rhuk_hexgold_nabber.txt +++ b/forge-gui/res/cardsfolder/r/rhuk_hexgold_nabber.txt @@ -4,8 +4,8 @@ Types:Legendary Creature Goblin Rebel PT:2/2 K:Trample K:Haste -T:Mode$ Attacks | ValidCard$ Creature.equipped+Other | Execute$ TrigAttach | TriggerZones$ Battlefield | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. -T:Mode$ ChangesZone | ValidCard$ Creature.equipped+Other | Origin$ Battlefield | Destination$ Graveyard | Execute$ TrigAttach | TriggerZones$ Battlefield | OptionalDecider$ You | Secondary$ True | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. +T:Mode$ Attacks | ValidCard$ Creature.equipped+YouCtrl+Other | Execute$ TrigAttach | TriggerZones$ Battlefield | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. +T:Mode$ ChangesZone | ValidCard$ Creature.equipped+YouCtrl+Other | Origin$ Battlefield | Destination$ Graveyard | Execute$ TrigAttach | TriggerZones$ Battlefield | OptionalDecider$ You | Secondary$ True | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. SVar:TrigAttach:DB$ Attach | Object$ AttachedTo TriggeredCard.Equipment | Defined$ Self DeckNeeds:Type$Equipment Oracle:Trample, haste\nWhenever an equipped creature you control other than Rhuk, Hexgold Nabber attacks or dies, you may attach all Equipment attached to that creature to Rhuk. diff --git a/forge-gui/res/cardsfolder/r/rift_elemental.txt b/forge-gui/res/cardsfolder/r/rift_elemental.txt index 2d513992aee..8c14619128a 100644 --- a/forge-gui/res/cardsfolder/r/rift_elemental.txt +++ b/forge-gui/res/cardsfolder/r/rift_elemental.txt @@ -2,7 +2,6 @@ Name:Rift Elemental ManaCost:R Types:Creature Elemental PT:1/1 -A:AB$ Pump | Cost$ 1 R SubCounter<1/TIME/Permanent/permanent you control> | Defined$ Self | NumAtt$ 2 | SpellDescription$ CARDNAME gets +2/+0 until end of turn. -A:AB$ Pump | Cost$ 1 R SubCounter<1/TIME/Card.suspended/suspended card you own/Exile> | Defined$ Self | NumAtt$ 2 | SpellDescription$ CARDNAME gets +2/+0 until end of turn. +A:AB$ Pump | Cost$ 1 R SubCounter<1/TIME/Permanent.inZoneBattlefield;Card.suspended/a permanent you control or suspended card you own/Battlefield,Exile> | Defined$ Self | NumAtt$ 2 | SpellDescription$ CARDNAME gets +2/+0 until end of turn. AI:RemoveDeck:All Oracle:{1}{R}, Remove a time counter from a permanent you control or suspended card you own: Rift Elemental gets +2/+0 until end of turn. diff --git a/forge-gui/res/cardsfolder/s/sycorax_commander.txt b/forge-gui/res/cardsfolder/s/sycorax_commander.txt new file mode 100644 index 00000000000..eabf8d7546b --- /dev/null +++ b/forge-gui/res/cardsfolder/s/sycorax_commander.txt @@ -0,0 +1,16 @@ +Name:Sycorax Commander +ManaCost:2 B R +Types:Creature Alien Soldier +PT:4/2 +K:First Strike +K:Haste +T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigChoice | TriggerDescription$ Sanctified Rules of Combat — When CARDNAME enters the battlefield, each opponent faces a villainous choice — That opponent discards all the cards in their hand, then draws that many cards minus one, or CARDNAME deals damage to that player equal to the number of cards in their hand. +SVar:TrigChoice:DB$ VillainousChoice | Defined$ Opponent | Choices$ DBDiscard,DBDamage +SVar:DBDiscard:DB$ Discard | Defined$ Player.IsRemembered | Mode$ Hand | RememberDiscarded$ True | SubAbility$ DBDraw | SpellDescription$ Opponent discards their hand, then draws that many cards minus one. +SVar:DBDraw:DB$ Draw | NumCards$ X | Defined$ Player.IsRemembered | SubAbility$ CleanDrawn +SVar:CleanDrawn:DB$ Cleanup | ClearRemembered$ True +# This calculation isn't considering the remembered player, only the remembered cards +SVar:X:Remembered$Amount/Minus.1 +SVar:DBDamage:DB$ DealDamage | Defined$ Player.IsRemembered | NumDmg$ Y | SpellDescription$ CARDNAME deals damage to opponent equal to the number of cards in their hand. +SVar:Y:Count$ValidHand Card.OwnedBy Player.IsRemembered +Oracle:First strike, haste\nSanctified Rules of Combat — When Sycorax Commander enters the battlefield, each opponent faces a villainous choice — That opponent discards all the cards in their hand, then draws that many cards minus one, or Sycorax Commander deals damage to that player equal to the number of cards in their hand. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/t/the_dalek_emperor.txt b/forge-gui/res/cardsfolder/t/the_dalek_emperor.txt new file mode 100644 index 00000000000..944bc456248 --- /dev/null +++ b/forge-gui/res/cardsfolder/t/the_dalek_emperor.txt @@ -0,0 +1,13 @@ +Name:The Dalek Emperor +ManaCost:5 B R +Types:Legendary Artifact Creature Dalek +PT:6/6 +K:Affinity:Dalek +S:Mode$ Continuous | Affected$ Dalek.Other+YouCtrl | AddKeyword$ Haste | Description$ Other Daleks you control have haste. +T:Mode$ Phase | Phase$ BeginCombat | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigChoice | TriggerDescription$ At the beginning of combat on your turn, each opponent faces a villainous choice — That player sacrifices a creature they control, or you create a 3/3 black Dalek artifact creature token with menace. +SVar:TrigChoice:DB$ VillainousChoice | Defined$ Opponent | Choices$ DBSacrifice,DBToken +SVar:DBSacrifice:DB$ Sacrifice | Defined$ Remembered | SacValid$ Creature | SacMessage$ Creature | SpellDescription$ Opponent sacrifices a creature. +SVar:DBToken:DB$ Token | TokenAmount$ 1 | TokenScript$ b_3_3_a_dalek_menace | TokenOwner$ You | SpellDescription$ You create a 3/3 black Dalek artifact creature token with menace. +DeckHints:Type$Dalek +DeckHas:Ability$Token +Oracle:Affinity for Daleks (This spell costs {1} less to cast for each Dalek you control.)\nOther Daleks you control have haste.\nAt the beginning of combat on your turn, each opponent faces a villainous choice — That player sacrifices a creature they control, or you create a 3/3 black Dalek artifact creature token with menace. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/t/the_master_gallifreys_end.txt b/forge-gui/res/cardsfolder/t/the_master_gallifreys_end.txt new file mode 100644 index 00000000000..9ff3b433323 --- /dev/null +++ b/forge-gui/res/cardsfolder/t/the_master_gallifreys_end.txt @@ -0,0 +1,15 @@ +Name:The Master, Gallifrey's End +ManaCost:2 B R +Types:Legendary Creature Time Lord Rogue +PT:4/3 +T:Mode$ ChangesZone | ValidCard$ Creature.Artifact+nonToken+YouCtrl | Origin$ Battlefield | Destination$ Graveyard | OptionalDecider$ You | Execute$ TrigExile | TriggerZones$ Battlefield | TriggerDescription$ Make Them Pay — Whenever a nontoken artifact creature you control dies, you may exile it. If you do, choose an opponent with the most life among your opponents. That player faces a villainous choice — They lose 4 life, or you create a token that's a copy of that card. +SVar:TrigExile:DB$ ChangeZone | RememberChanged$ True | Origin$ Graveyard | Destination$ Exile | Defined$ TriggeredNewCardLKICopy | SubAbility$ DBChoosePlayer +SVar:DBChoosePlayer:DB$ ChoosePlayer | Defined$ You | Choices$ Player.Opponent+lifeEQX | SubAbility$ DBChoice +SVar:X:Count$OppGreatestLifeTotal +SVar:DBChoice:DB$ VillainousChoice | Defined$ ChosenPlayer | Choices$ DBLoseLife,DBCopy | SubAbility$ DBCleanup +SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 4 | Defined$ Player.IsRemembered | SpellDescription$ Opponent loses 4 life. +SVar:DBCopy:DB$ CopyPermanent | Defined$ Remembered | SubAbility$ DBCleanup | SpellDescription$ You create a token that's a copy of the exiled card. +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True +DeckHints:Type$Artifact +DeckHas:Ability$Token +Oracle:Make Them Pay — Whenever a nontoken artifact creature you control dies, you may exile it. If you do, choose an opponent with the most life among your opponents. That player faces a villainous choice — They lose 4 life, or you create a token that's a copy of that card. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/t/the_night_of_the_doctor.txt b/forge-gui/res/cardsfolder/t/the_night_of_the_doctor.txt index 5eeb21ee932..248e1a068a4 100644 --- a/forge-gui/res/cardsfolder/t/the_night_of_the_doctor.txt +++ b/forge-gui/res/cardsfolder/t/the_night_of_the_doctor.txt @@ -4,6 +4,6 @@ Types:Enchantment Saga K:Chapter:2:DBDestroyAll,DBChangeZone SVar:DBDestroyAll:DB$ DestroyAll | ValidCards$ Creature | SpellDescription$ Destroy all creatures. SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.Legendary+YouCtrl | TgtPrompt$ Choose target legendary creature card in your graveyard | SubAbility$ DBPutCounter | RememberChanged$ True | SpellDescription$ Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it. -SVar:DBPutCounter:DB$ PutCounter | Choices$ Creature.IsRemembered | CounterType$ First strike,Vigilance,Lifelink | CounterNum$ 1| SubAbility$ DBCleanup +SVar:DBPutCounter:DB$ PutCounter | Choices$ Creature.IsRemembered | CounterType$ First Strike,Vigilance,Lifelink | CounterNum$ 1| SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after II.)\nI — Destroy all creatures.\nII — Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it. diff --git a/forge-gui/res/cardsfolder/t/this_is_how_it_ends.txt b/forge-gui/res/cardsfolder/t/this_is_how_it_ends.txt new file mode 100644 index 00000000000..406ed4b2f74 --- /dev/null +++ b/forge-gui/res/cardsfolder/t/this_is_how_it_ends.txt @@ -0,0 +1,8 @@ +Name:This Is How It Ends +ManaCost:3 B +Types:Instant +A:SP$ ChangeZone | ValidTgts$ Creature | TgtPrompt$ Select target creature | Origin$ Battlefield | Destination$ Library | Shuffle$ True | SubAbility$ DBChoice | SpellDescription$ The owner of target creature shuffles it into their library, +SVar:DBChoice:DB$ VillainousChoice | Defined$ TargetedOwner | Choices$ DBLoseLife,DBShuffleAnother | SpellDescription$ then faces a villainous choice — They lose 5 life, or they shuffle another creature they own into their library. +SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 5 | Defined$ Remembered | SpellDescription$ Targeted creature's owner loses 5 life. +SVar:DBShuffleAnother:DB$ ChangeZone | ChangeType$ Creature.OwnedBy Remembered | DefinedPlayer$ Remembered | Chooser$ Remembered | ChangeNum$ 1 | Mandatory$ True | Origin$ Battlefield | Destination$ Library | Shuffle$ True | Hidden$ True | SpellDescription$ Targeted creature's owner shuffles another creature they own into their library. +Oracle:Target creature's owner shuffles it into their library, then faces a villainous choice — They lose 5 life, or they shuffle another creature they own into their library. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/v/veil_of_secrecy.txt b/forge-gui/res/cardsfolder/v/veil_of_secrecy.txt index 5b94eb0d24e..3de962e2c61 100644 --- a/forge-gui/res/cardsfolder/v/veil_of_secrecy.txt +++ b/forge-gui/res/cardsfolder/v/veil_of_secrecy.txt @@ -3,7 +3,7 @@ ManaCost:1 U Types:Instant Arcane K:Splice:Arcane:Return<1/Creature.Blue/blue creature> A:SP$ Pump | ValidTgts$ Creature | KW$ Shroud | SubAbility$ DBUnblockable | StackDescription$ REP Target creature_{c:Targeted} | SpellDescription$ Target creature gains shroud until end of turn and can't be blocked this turn. (A creature with shroud can't be the target of spells or abilities.) -SVar:DBUnblockable:DB$ Effect | RememberObjects$ Self | ExileOnMoved$ Battlefield | StaticAbilities$ Unblockable +SVar:DBUnblockable:DB$ Effect | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | StaticAbilities$ Unblockable SVar:Unblockable:Mode$ CantBlockBy | ValidAttacker$ Card.IsRemembered | Description$ This creature can't be blocked this turn. AI:RemoveDeck:All DeckHints:Type$Arcane diff --git a/forge-gui/res/cardsfolder/w/witchking_of_angmar.txt b/forge-gui/res/cardsfolder/w/witch_king_of_angmar.txt similarity index 100% rename from forge-gui/res/cardsfolder/w/witchking_of_angmar.txt rename to forge-gui/res/cardsfolder/w/witch_king_of_angmar.txt diff --git a/forge-gui/src/main/java/forge/player/HumanCostDecision.java b/forge-gui/src/main/java/forge/player/HumanCostDecision.java index 9d3f15cc8db..3354ba0b7ea 100644 --- a/forge-gui/src/main/java/forge/player/HumanCostDecision.java +++ b/forge-gui/src/main/java/forge/player/HumanCostDecision.java @@ -1036,7 +1036,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { } final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, validCards, ability); - inp.setMessage(Localizer.getInstance().getMessage("lblRemoveCountersFromAInZoneCard", cost.zone.getTranslatedName())); + inp.setMessage(Localizer.getInstance().getMessage("lblRemoveCountersFromAInZoneCard", Lang.joinHomogenous(cost.zone, ZoneType.Accessors.GET_TRANSLATED_NAME))); inp.setCancelAllowed(true); inp.showAndWait();