diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d72d9d61d33..9d7eb4bcad6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -26,7 +26,8 @@ jobs: close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' stale-issue-label: 'no-issue-activity' stale-pr-label: 'no-pr-activity' - exempt-pr-labels: 'awaiting-approval,work-in-progress' + exempt-issue-labels: 'keep' + exempt-pr-labels: 'awaiting-approval,work-in-progress,keep' days-before-issue-stale: 30 days-before-pr-stale: 45 days-before-issue-close: 5 diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 04d69d7ad41..fb05d635995 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -655,10 +655,11 @@ public class AiController { List all = ComputerUtilAbility.getSpellAbilities(cards, player); try { - Collections.sort(all, saComparator); // put best spells first + Collections.sort(all, ComputerUtilAbility.saEvaluator); // put best spells first + ComputerUtilAbility.sortCreatureSpells(all); } catch (IllegalArgumentException ex) { System.err.println(ex.getMessage()); - String assertex = ComparatorUtil.verifyTransitivity(saComparator, all); + String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all); Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex); } @@ -1016,167 +1017,6 @@ public class AiController { return false; } - // not sure "playing biggest spell" matters? - private final static Comparator saComparator = new Comparator() { - @Override - public int compare(final SpellAbility a, final SpellAbility b) { - // sort from highest cost to lowest - // we want the highest costs first - int a1 = a.getPayCosts().getTotalMana().getCMC(); - int b1 = b.getPayCosts().getTotalMana().getCMC(); - - // deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs - if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) { - return 1; - } else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) { - return -1; - } - - // 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; - } - - // deprioritize pump spells with pure energy cost (can be activated last, - // since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler) - int a2 = 0, b2 = 0; - if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) { - if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { - a2 = a.getPayCosts().getCostEnergy().convertAmount(); - } - } - if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) { - if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { - b2 = b.getPayCosts().getCostEnergy().convertAmount(); - } - } - if (a2 == 0 && b2 > 0) { - return -1; - } else if (b2 == 0 && a2 > 0) { - return 1; - } - - // cast 0 mana cost spells first (might be a Mox) - if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) { - return -1; - } else if (a1 > 0 && b1 == 0 && ApiType.Mana != b.getApi()) { - return 1; - } - - if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) { - return -1; - } else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) { - return 1; - } - - if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) { - // Cheaper Spectacle costs should be preferred - // FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not? - // (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs) - if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) { - return 1; - } else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) { - return 1; - } - } - - // If both are permanent creature spells, prefer the one that evaluates higher - if (a1 == b1 && a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { - int evalA = ComputerUtilCard.evaluateCreature(a); - int evalB = ComputerUtilCard.evaluateCreature(b); - if (evalA > evalB) { - a1++; - } else if (evalB > evalA) { - b1++; - } - } - - a1 += getSpellAbilityPriority(a); - b1 += getSpellAbilityPriority(b); - - return b1 - a1; - } - - private int getSpellAbilityPriority(SpellAbility sa) { - int p = 0; - Card source = sa.getHostCard(); - final Player ai = source == null ? sa.getActivatingPlayer() : source.getController(); - if (ai == null) { - System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa); - return 0; - } - final boolean noCreatures = ai.getCreaturesInPlay().isEmpty(); - - if (source != null) { - // puts creatures in front of spells - if (source.isCreature()) { - p += 1; - } - if (source.hasSVar("AIPriorityModifier")) { - p += Integer.parseInt(source.getSVar("AIPriorityModifier")); - } - if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) { - p -= 10; - } - // don't play equipments before having any creatures - if (source.isEquipment() && noCreatures) { - p -= 9; - } - // don't equip stuff in main 2 if there's more stuff to cast at the moment - if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) { - p -= 1; - } - // 1. increase chance of using Surge effects - // 2. non-surged versions are usually inefficient - if (source.getOracleText().contains("surge cost") && !sa.isSurged()) { - p -= 9; - } - // move snap-casted spells to front - if (source.isInZone(ZoneType.Graveyard)) { - if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) { - p += 50; - } - } - // if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count - if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) { - p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS)); - } - } - - // use Surge and Prowl costs when able to - if (sa.isSurged() || sa.isProwl()) { - p += 9; - } - // sort planeswalker abilities with most costly first - if (sa.isPwAbility()) { - final CostPart cost = sa.getPayCosts().getCostParts().get(0); - if (cost instanceof CostRemoveCounter) { - p += cost.convertAmount() == null ? 1 : cost.convertAmount(); - } else if (cost instanceof CostPutCounter) { - p -= cost.convertAmount(); - } - if (sa.hasParam("Ultimate")) { - p += 9; - } - } - - if (ApiType.DestroyAll == sa.getApi()) { - p += 4; - } else if (ApiType.Mana == sa.getApi()) { - p -= 9; - } - - // try to cast mana ritual spells before casting spells to maximize potential mana - if ("ManaRitual".equals(sa.getParam("AILogic"))) { - p += 9; - } - - return p; - } - }; - public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa) { return getCardsToDiscard(numDiscard, uTypes, sa, CardCollection.EMPTY); } @@ -1744,10 +1584,11 @@ public class AiController { return null; try { - Collections.sort(all, saComparator); // put best spells first + Collections.sort(all, ComputerUtilAbility.saEvaluator); // put best spells first + ComputerUtilAbility.sortCreatureSpells(all); } catch (IllegalArgumentException ex) { System.err.println(ex.getMessage()); - String assertex = ComparatorUtil.verifyTransitivity(saComparator, all); + String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all); Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex); } @@ -2292,14 +2133,14 @@ public class AiController { } // TODO move to more common place - private List filterList(List input, Predicate pred) { + private static List filterList(List input, Predicate pred) { List filtered = Lists.newArrayList(Iterables.filter(input, pred)); input.removeAll(filtered); return filtered; } // TODO move to more common place - private List filterListByApi(List input, ApiType type) { + public static List filterListByApi(List input, ApiType type) { return filterList(input, SpellAbilityPredicates.isApi(type)); } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index 3eca0f0fcad..af10923a66a 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -1,5 +1,7 @@ package forge.ai; +import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -15,6 +17,12 @@ import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardPredicates.Presets; +import forge.game.cost.CostPart; +import forge.game.cost.CostPayEnergy; +import forge.game.cost.CostPutCounter; +import forge.game.cost.CostRemoveCounter; +import forge.game.keyword.Keyword; +import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.OptionalCostValue; import forge.game.spellability.SpellAbility; @@ -223,4 +231,183 @@ public class ComputerUtilAbility { } return true; } + + public final static saComparator saEvaluator = new saComparator(); + + // not sure "playing biggest spell" matters? + public final static class saComparator implements Comparator { + @Override + public int compare(final SpellAbility a, final SpellAbility b) { + return compareEvaluator(a, b, false); + } + public int compareEvaluator(final SpellAbility a, final SpellAbility b, boolean safeToEvaluateCreatures) { + // sort from highest cost to lowest + // we want the highest costs first + int a1 = a.getPayCosts().getTotalMana().getCMC(); + int b1 = b.getPayCosts().getTotalMana().getCMC(); + + // deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs + if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) { + return 1; + } else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) { + return -1; + } + + // 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; + } + + // deprioritize pump spells with pure energy cost (can be activated last, + // since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler) + int a2 = 0, b2 = 0; + if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) { + if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { + a2 = a.getPayCosts().getCostEnergy().convertAmount(); + } + } + if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) { + if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { + b2 = b.getPayCosts().getCostEnergy().convertAmount(); + } + } + if (a2 == 0 && b2 > 0) { + return -1; + } else if (b2 == 0 && a2 > 0) { + return 1; + } + + // cast 0 mana cost spells first (might be a Mox) + if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) { + return -1; + } else if (a1 > 0 && b1 == 0 && ApiType.Mana != b.getApi()) { + return 1; + } + + if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) { + return -1; + } else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) { + return 1; + } + + if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) { + // Cheaper Spectacle costs should be preferred + // FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not? + // (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs) + if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) { + return 1; + } else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) { + return 1; + } + } + + a1 += getSpellAbilityPriority(a); + b1 += getSpellAbilityPriority(b); + + // If both are creature spells sort them after + if (safeToEvaluateCreatures) { + a1 += Math.round(ComputerUtilCard.evaluateCreature(a) / 100f); + b1 += Math.round(ComputerUtilCard.evaluateCreature(b) / 100f); + } + + return b1 - a1; + } + + private static int getSpellAbilityPriority(SpellAbility sa) { + int p = 0; + Card source = sa.getHostCard(); + final Player ai = source == null ? sa.getActivatingPlayer() : source.getController(); + if (ai == null) { + System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa); + return 0; + } + final boolean noCreatures = ai.getCreaturesInPlay().isEmpty(); + + if (source != null) { + // puts creatures in front of spells + if (source.isCreature()) { + p += 1; + } + if (source.hasSVar("AIPriorityModifier")) { + p += Integer.parseInt(source.getSVar("AIPriorityModifier")); + } + if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) { + p -= 10; + } + // don't play equipments before having any creatures + if (source.isEquipment() && noCreatures) { + p -= 9; + } + // don't equip stuff in main 2 if there's more stuff to cast at the moment + if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + p -= 1; + } + // 1. increase chance of using Surge effects + // 2. non-surged versions are usually inefficient + if (source.getOracleText().contains("surge cost") && !sa.isSurged()) { + p -= 9; + } + // move snap-casted spells to front + if (source.isInZone(ZoneType.Graveyard)) { + if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) { + p += 50; + } + } + // if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count + if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) { + p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS)); + } + } + + // use Surge and Prowl costs when able to + if (sa.isSurged() || sa.isProwl()) { + p += 9; + } + // sort planeswalker abilities with most costly first + if (sa.isPwAbility()) { + final CostPart cost = sa.getPayCosts().getCostParts().get(0); + if (cost instanceof CostRemoveCounter) { + p += cost.convertAmount() == null ? 1 : cost.convertAmount(); + } else if (cost instanceof CostPutCounter) { + p -= cost.convertAmount(); + } + if (sa.hasParam("Ultimate")) { + p += 9; + } + } + + if (ApiType.DestroyAll == sa.getApi()) { + p += 4; + } else if (ApiType.Mana == sa.getApi()) { + p -= 9; + } + + // try to cast mana ritual spells before casting spells to maximize potential mana + if ("ManaRitual".equals(sa.getParam("AILogic"))) { + p += 9; + } + + return p; + } + }; + + public static List sortCreatureSpells(final List all) { + // try to smoothen power creep by making CMC less of a factor + final List creatures = AiController.filterListByApi(Lists.newArrayList(all), ApiType.PermanentCreature); + if (creatures.size() <= 1) { + return all; + } + // TODO this doesn't account for nearly identical creatures where one is a newer but more cost efficient variant + Collections.sort(creatures, ComputerUtilCard.EvaluateCreatureSpellComparator); + int idx = 0; + for (int i = 0; i < all.size(); i++) { + if (all.get(i).getApi() == ApiType.PermanentCreature) { + all.set(i, creatures.get(idx)); + idx++; + } + } + return all; + } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 464909d9148..9127805e45d 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -567,6 +567,13 @@ public class ComputerUtilCard { return evaluateCreature(b) - evaluateCreature(a); } }; + public static final Comparator EvaluateCreatureSpellComparator = new Comparator() { + @Override + public int compare(final SpellAbility a, final SpellAbility b) { + // TODO ideally we could reuse the value from the previous pass with false + return ComputerUtilAbility.saEvaluator.compareEvaluator(a, b, true); + } + }; private static final CreatureEvaluator creatureEvaluator = new CreatureEvaluator(); private static final LandEvaluator landEvaluator = new LandEvaluator(); @@ -596,7 +603,7 @@ public class ComputerUtilCard { host.setState(sa.getCardStateName(), false); } - int eval = creatureEvaluator.evaluateCreature(host); + int eval = evaluateCreature(host); if (currentState != null) { host.setState(currentState, false); diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index 3f31a6d7431..232080c5dde 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -471,6 +471,13 @@ public class CountersPutAi extends CountersAi { if (sacSelf && c.equals(source)) { return false; } + if ("NoCounterOfType".equals(sa.getParam("AILogic"))) { + for (String ctrType : types) { + if (c.getCounters(CounterType.getType(ctrType)) > 0) { + return false; + } + } + } return sa.canTarget(c) && c.canReceiveCounters(CounterType.getType(type)); } }); diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 4ecb72a7584..9b0b9fe566f 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -590,9 +590,7 @@ public class GameAction { // 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())) { - copied.addChangedCardKeywordsInternal(ImmutableList.of(cause.getKeyword()), null, false, game.getNextTimestamp(), 0, false); - // update Keyword Cache - copied.updateKeywords(); + copied.addChangedCardKeywordsInternal(ImmutableList.of(cause.getKeyword()), null, false, game.getNextTimestamp(), 0, true); } } diff --git a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java index 73d05760ed8..03b5da87f07 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -636,10 +636,8 @@ public abstract class SpellAbilityEffect { combat.initConstraints(); if (sa.hasParam("ChoosePlayerOrPlaneswalker")) { PlayerCollection defendingPlayers = AbilityUtils.getDefinedPlayers(sa.hasParam("ForEach") ? c : host, attacking, sa); - defs = new FCollection<>(); - for (Player p : defendingPlayers) { - defs.addAll(combat.getDefendersControlledBy(p)); - } + defs = new FCollection<>(defendingPlayers); + defs.addAll(Iterables.filter(combat.getDefendingPlaneswalkers(), CardPredicates.isControlledByAnyOf(defendingPlayers))); } else if ("True".equalsIgnoreCase(attacking)) { defs = (FCollection) combat.getDefenders(); } else { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java index fd15e1b68f5..4d69a2a07f3 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ConniveEffect.java @@ -66,7 +66,7 @@ public class ConniveEffect extends SpellAbilityEffect { for (final Player p : controllers) { CardCollection connivers = CardLists.filterControlledBy(toConnive, p); - while (connivers.size() > 0) { + while (!connivers.isEmpty()) { GameEntityCounterTable table = new GameEntityCounterTable(); final CardZoneTable triggerList = new CardZoneTable(); Map discardedMap = Maps.newHashMap(); @@ -76,18 +76,18 @@ public class ConniveEffect extends SpellAbilityEffect { Card conniver = connivers.size() > 1 ? p.getController().chooseSingleEntityForEffect(connivers, sa, Localizer.getInstance().getMessage("lblChooseConniver"), null) : connivers.get(0); + connivers.remove(conniver); p.drawCards(num, sa, moveParams); - CardCollection validDisards = - CardLists.filter(p.getCardsIn(ZoneType.Hand), CardPredicates.Presets.NON_TOKEN); - if (validDisards.isEmpty() || !p.canDiscardBy(sa, true)) { // hand being empty unlikely, just to be safe + CardCollection validDiscards = CardLists.filter(p.getCardsIn(ZoneType.Hand), CardPredicates.Presets.NON_TOKEN); + if (validDiscards.isEmpty() || !p.canDiscardBy(sa, true)) { // hand being empty unlikely, just to be safe continue; } - int amt = Math.min(validDisards.size(), num); + int amt = Math.min(validDiscards.size(), num); CardCollectionView toBeDiscarded = amt == 0 ? CardCollection.EMPTY : - p.getController().chooseCardsToDiscardFrom(p, sa, validDisards, amt, amt); + p.getController().chooseCardsToDiscardFrom(p, sa, validDiscards, amt, amt); if (toBeDiscarded.size() > 1) { toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa); @@ -101,7 +101,6 @@ public class ConniveEffect extends SpellAbilityEffect { if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.equalsWithTimestamp(conniver)) { conniver.addCounter(CounterEnumType.P1P1, numCntrs, p, table); } - connivers.remove(conniver); discardedMap.put(p, CardCollection.getView(toBeDiscarded)); discard(sa, triggerList, true, discardedMap, moveParams); table.replaceCounterEffect(game, sa, true); 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 bce92e01f46..8b2f0f7b938 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -4721,7 +4721,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { final List keywords, final List removeKeywords, final boolean removeAllKeywords, final long timestamp, final long staticId, final boolean updateView) { - final KeywordsChange newCks = new KeywordsChange(keywords, removeKeywords, removeAllKeywords); changedCardKeywords.put(timestamp, staticId, newCks); 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 f71b96b6e0a..b802b5c9faa 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -124,62 +124,49 @@ public class CardFactory { final Card source = sourceSA.getHostCard(); final Card original = targetSA.getHostCard(); final Game game = source.getGame(); - final Card c = new Card(game.nextCardId(), original.getPaperCard(), game); - copyCopiableCharacteristics(original, c, sourceSA, targetSA); + int id = game.nextCardId(); - if (sourceSA.hasParam("NonLegendary")) { - c.removeType(CardType.Supertype.Legendary); - } + // need to create a physical card first, i need the original card faces + final Card copy = CardFactory.getCard(original.getPaperCard(), controller, id, game); - if (sourceSA.hasParam("CopySetPower")) { - c.setBasePower(Integer.parseInt(sourceSA.getParam("CopySetPower"))); - } - - if (sourceSA.hasParam("CopySetToughness")) { - c.setBaseToughness(Integer.parseInt(sourceSA.getParam("CopySetToughness"))); - } - - if (sourceSA.hasParam("CopySetLoyalty")) { - c.setBaseLoyalty(AbilityUtils.calculateAmount(source, sourceSA.getParam("CopySetLoyalty"), sourceSA)); - } - - if (sourceSA.hasParam("CopyAddTypes")) { - c.addType(Arrays.asList(sourceSA.getParam("CopyAddTypes").split(" & "))); - } - - // change the color of the copy (eg: Fork) - if (sourceSA.hasParam("CopyIsColor")) { - ColorSet finalColors; - final String newColor = sourceSA.getParam("CopyIsColor"); - if (newColor.equals("ChosenColor")) { - finalColors = ColorSet.fromNames(source.getChosenColors()); - } else { - finalColors = ColorSet.fromNames(newColor.split(",")); + if (original.isTransformable()) { + // 707.8a If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced card not on the battlefield, + // the resulting token is a transforming token that has both a front face and a back face. + // The characteristics of each face are determined by the copiable values of the same face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent. + // If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield with its back face up. + // This rule does not apply to tokens that are created with their own set of characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect. + copy.setBackSide(original.isBackSide()); + if (original.isTransformed()) { + copy.incrementTransformedTimestamp(); } - - c.addColor(finalColors, !sourceSA.hasParam("OverwriteColors"), c.getTimestamp(), 0, false); } - c.clearControllers(); - c.setOwner(controller); - c.setCopiedSpell(true); - c.setCopiedPermanent(original); + copy.setStates(getCloneStates(original, copy, sourceSA)); + // force update the now set State + if (original.isTransformable()) { + copy.setState(original.isTransformed() ? CardStateName.Transformed : CardStateName.Original, true, true); + } else { + copy.setState(copy.getCurrentStateName(), true, true); + } - c.setXManaCostPaidByColor(original.getXManaCostPaidByColor()); - c.setKickerMagnitude(original.getKickerMagnitude()); + copy.setCopiedSpell(true); + copy.setCopiedPermanent(original); + + copy.setXManaCostPaidByColor(original.getXManaCostPaidByColor()); + copy.setKickerMagnitude(original.getKickerMagnitude()); for (OptionalCost cost : original.getOptionalCostsPaid()) { - c.addOptionalCostPaid(cost); + copy.addOptionalCostPaid(cost); } if (targetSA.isBestow()) { - c.animateBestow(); + copy.animateBestow(); } if (sourceSA.hasParam("RememberNewCard")) { - source.addRemembered(c); + source.addRemembered(copy); } - return c; + return copy; } /** @@ -525,6 +512,7 @@ public class CardFactory { * @param from the {@link Card} to copy from. * @param to the {@link Card} to copy to. */ + @Deprecated public static void copyCopiableCharacteristics(final Card from, final Card to, SpellAbility sourceSA, SpellAbility targetSA) { final boolean toIsFaceDown = to.isFaceDown(); if (toIsFaceDown) { @@ -753,7 +741,10 @@ public class CardFactory { final CardState ret2 = new CardState(out, CardStateName.Adventure); ret2.copyFrom(in.getState(CardStateName.Adventure), false, sa); result.put(CardStateName.Adventure, ret2); - } else if (in.isTransformable() && sa instanceof SpellAbility && ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi())) { + } else if (in.isTransformable() && sa instanceof SpellAbility && ( + ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi()) || + ApiType.CopySpellAbility.equals(((SpellAbility)sa).getApi()) + )) { // CopyPermanent can copy token final CardState ret1 = new CardState(out, CardStateName.Original); ret1.copyFrom(in.getState(CardStateName.Original), false, sa); @@ -820,7 +811,7 @@ public class CardFactory { } if (state.getType().isPlaneswalker() && sa.hasParam("SetLoyalty")) { - state.setBaseLoyalty(String.valueOf(sa.getParam("SetLoyalty"))); + state.setBaseLoyalty(String.valueOf(AbilityUtils.calculateAmount(host, sa.getParam("SetLoyalty"), sa))); } // Planning a Vizier of Many Faces rework; always might come in handy diff --git a/forge-game/src/main/java/forge/game/combat/CombatUtil.java b/forge-game/src/main/java/forge/game/combat/CombatUtil.java index a84b3b5bcb1..e64d682faf7 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -239,7 +239,7 @@ public class CombatUtil { if (!defender.equals(ge) && ge instanceof Player) { // found a player which does not goad that creature // and creature can attack this player or planeswalker - if (!attacker.isGoadedBy((Player) ge) && canAttack(attacker, ge)) { + if (!attacker.isGoadedBy((Player) ge) && !ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) { return false; } } @@ -251,7 +251,7 @@ public class CombatUtil { if (defender != null && defender.hasKeyword("Creatures your opponents control attack a player other than you if able.")) { for (GameEntity ge : getAllPossibleDefenders(attacker.getController())) { if (!defender.equals(ge) && ge instanceof Player) { - if (canAttack(attacker, ge)) { + if (!ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) { return false; } } diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/black_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/black_castle.tmx index eab33989f10..5ab0337f3f2 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/black_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/black_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/blue_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/blue_castle.tmx index b6694251364..326323b2e20 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/blue_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/blue_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/green_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/green_castle.tmx index 78178402e21..56cc33c9a80 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/green_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/green_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/red_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/red_castle.tmx index c28d295a9fe..65dc919a18c 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/red_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/red_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/templeofchandra.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/templeofchandra.tmx index c11b4b257d9..f178b01e1a1 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/templeofchandra.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/templeofchandra.tmx @@ -357,12 +357,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the east", + "text":"You hear some loud sounds coming from the east", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":2}}], "action":[{"deleteMapObject":18}], @@ -379,12 +379,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the east", + "text":"You hear some loud sounds coming from the east", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":2}}], "action":[{"deleteMapObject":18}], @@ -401,11 +401,11 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the north", - "name":"flip the switch" + "text":"You hear some loud sounds coming from the north", + "name":"flip the switch" "options":[{ "action":[{"deleteMapObject":17}], "name":"ok" }] @@ -444,11 +444,11 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { "text":"You hear some rumbling and commotion from the south. The final gate appears to be open.", - "name":"flip the switch" + "name":"flip the switch" "options":[{ "action":[{"deleteMapObject":6}], "name":"ok" }] @@ -502,7 +502,7 @@ { "text":"The connected machinery whirrs to life, and the gate to your east opens.", "action":[{"deleteMapObject":-1}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "action":[{"deleteMapObject":11}], "name":"ok" }] diff --git a/forge-gui/res/adventure/Shandalar/maps/map/main_story/white_castle.tmx b/forge-gui/res/adventure/Shandalar/maps/map/main_story/white_castle.tmx index 3473ba1bd37..48842fb4ace 100644 --- a/forge-gui/res/adventure/Shandalar/maps/map/main_story/white_castle.tmx +++ b/forge-gui/res/adventure/Shandalar/maps/map/main_story/white_castle.tmx @@ -68,7 +68,7 @@ [{ - "text":"A gate is blocking the path. I looks like it is open elsewhere", + "text":"A gate is blocking the path. It looks like it is opened elsewhere.", "options":[ { "name":"continue" } ] @@ -84,12 +84,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -106,12 +106,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], @@ -128,12 +128,12 @@ [ { - "text":"Hmm a big switch is embedded into the wall", + "text":"Hmm... A big switch is embedded into the wall.", "options":[ { - "text":"You hear some loud sounds from the center", + "text":"You hear some loud sounds coming from the center.", "action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}], - "name":"flip the switch" + "name":"flip the switch" "options":[{ "condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}], "action":[{"deleteMapObject":55}], diff --git a/forge-gui/res/cardsfolder/d/donal_herald_of_wings.txt b/forge-gui/res/cardsfolder/d/donal_herald_of_wings.txt index f2ce0bd82eb..2388fe1dc4b 100644 --- a/forge-gui/res/cardsfolder/d/donal_herald_of_wings.txt +++ b/forge-gui/res/cardsfolder/d/donal_herald_of_wings.txt @@ -3,7 +3,7 @@ ManaCost:2 U U Types:Legendary Creature Human Wizard PT:3/3 T:Mode$ SpellCast | TriggerZones$ Battlefield | ValidCard$ Creature.withFlying+nonLegendary | ValidActivatingPlayer$ You | ResolvedLimit$ 1 | Execute$ TrigCopy | OptionalDecider$ You | TriggerDescription$ Whenever you cast a nonlegendary creature spell with flying, you may copy it, except the copy is a 1/1 Spirit in addition to its other types. Do this only once each turn. (The copy becomes a token.) -SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | CopySetPower$ 1 | CopySetToughness$ 1 | CopyAddTypes$ Spirit +SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | SetPower$ 1 | SetToughness$ 1 | AddTypes$ Spirit DeckHas:Ability$Token DeckHints:Keyword$Flying SVar:BuffedBy:Creature.withFlying diff --git a/forge-gui/res/cardsfolder/f/fork.txt b/forge-gui/res/cardsfolder/f/fork.txt index ee58faa4933..365c0d61cc4 100644 --- a/forge-gui/res/cardsfolder/f/fork.txt +++ b/forge-gui/res/cardsfolder/f/fork.txt @@ -1,5 +1,5 @@ Name:Fork ManaCost:R R Types:Instant -A:SP$ CopySpellAbility | Cost$ R R | ValidTgts$ Instant,Sorcery | TargetType$ Spell | CopyIsColor$ Red | OverwriteColors$ True | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy. +A:SP$ CopySpellAbility | Cost$ R R | ValidTgts$ Instant,Sorcery | TargetType$ Spell | SetColor$ Red | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy. Oracle:Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy. diff --git a/forge-gui/res/cardsfolder/i/invasion_of_fiora_marchesa_resolute_monarch.txt b/forge-gui/res/cardsfolder/i/invasion_of_fiora_marchesa_resolute_monarch.txt index 29014d1491c..bea3c7f53cd 100644 --- a/forge-gui/res/cardsfolder/i/invasion_of_fiora_marchesa_resolute_monarch.txt +++ b/forge-gui/res/cardsfolder/i/invasion_of_fiora_marchesa_resolute_monarch.txt @@ -19,7 +19,7 @@ PT:3/6 K:Menace K:Deathtouch T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigRemoveCounter | TriggerDescription$ Whenever CARDNAME attacks, remove all counters from up to one target permanent. -SVar:TrigRemoveCounter:DB$ RemoveCounter | ValidTgts$ Permanent | CounterType$ All | CounterNum$ All +SVar:TrigRemoveCounter:DB$ RemoveCounter | ValidTgts$ Permanent | TargetMin$ 0 | TargetMax$ 1 | CounterType$ All | CounterNum$ All T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | CheckSVar$ X | SVarCompare$ EQ0 | Execute$ TrigDraw | TriggerDescription$ At the beginning of your upkeep, if you haven't been dealt combat damage since your last turn, you draw a card and you lose 1 life. SVar:TrigDraw:DB$ Draw | SubAbility$ DBLoseLife SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 1 diff --git a/forge-gui/res/cardsfolder/l/lorehold_command.txt b/forge-gui/res/cardsfolder/l/lorehold_command.txt index e4b007264a7..73574b42718 100644 --- a/forge-gui/res/cardsfolder/l/lorehold_command.txt +++ b/forge-gui/res/cardsfolder/l/lorehold_command.txt @@ -3,10 +3,10 @@ ManaCost:3 W R Types:Instant A:SP$ Charm | Cost$ 3 W R | Choices$ DBSpirit,DBIndestructible,DBHelix,DBSacrifice | CharmNum$ 2 SVar:DBSpirit:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_3_2_spirit | TokenOwner$ You | SpellDescription$ Create a 3/2 red and white Spirit token. -SVar:DBIndestructible:DB$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | KW$ Indestructible & haste | SpellDescription$ • Creatures you control get +1/+0 and gain indestructible and haste until end of turn. -SVar:DBHelix:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ • CARDNAME deals 3 damage to any target. Target player gains 3 life. -SVar:DBGainLife:DB$ GainLife | ValidTgts$ Player | TgtPrompt$ Select target player (to gain 3 life) | LifeAmount$ 3 | SpellDescription$ Target player gains 3 life. -SVar:DBSacrifice:DB$ Sacrifice | Defined$ You | SacValid$ Permanent | SubAbility$ DBDraw -SVar:DBDraw:DB$ Draw | NumCards$ 2 | SpellDescription$ Sacrifice a permanent,draw two cards. +SVar:DBIndestructible:DB$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | KW$ Indestructible & Haste | SpellDescription$ Creatures you control get +1/+0 and gain indestructible and haste until end of turn. +SVar:DBHelix:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ CARDNAME deals 3 damage to any target. Target player gains 3 life. +SVar:DBGainLife:DB$ GainLife | ValidTgts$ Player | TgtPrompt$ Select target player (to gain 3 life) | LifeAmount$ 3 +SVar:DBSacrifice:DB$ Sacrifice | Defined$ You | SacValid$ Permanent | SpellDescription$ Sacrifice a permanent, then draw two cards. | SubAbility$ DBDraw +SVar:DBDraw:DB$ Draw | NumCards$ 2 DeckHas:Ability$Token|LifeGain Oracle:Choose two —\n• Create a 3/2 red and white Spirit creature token.\n• Creatures you control get +1/+0 and gain indestructible and haste until end of turn.\n• Lorehold Command deals 3 damage to any target. Target player gains 3 life.\n• Sacrifice a permanent, then draw two cards. diff --git a/forge-gui/res/cardsfolder/n/nahiri_forged_in_fury.txt b/forge-gui/res/cardsfolder/n/nahiri_forged_in_fury.txt index a57126348c7..6466b08c2d8 100644 --- a/forge-gui/res/cardsfolder/n/nahiri_forged_in_fury.txt +++ b/forge-gui/res/cardsfolder/n/nahiri_forged_in_fury.txt @@ -4,13 +4,10 @@ Types:Legendary Creature Kor Artificer PT:5/4 K:Affinity:Artifact.Equipment:equipment T:Mode$ Attacks | ValidCard$ Creature.equipped+YouCtrl | TriggerZones$ Battlefield | Execute$ TrigExile | TriggerDescription$ Whenever an equipped creature you control attacks, exile the top card of your library. You may play that card this turn. You may cast Equipment spells this way without paying their mana costs. -SVar:TrigExile:DB$ Dig | Defined$ You | DigNum$ 1 | ChangeNum$ All | DestinationZone$ Exile | RememberChanged$ True | SubAbility$ DBBranch -SVar:DBBranch:DB$ Branch | BranchConditionSVar$ X | TrueSubAbility$ DBEffect2 | FalseSubAbility$ DBEffect | SubAbility$ DBCleanup -SVar:DBEffect2:DB$ Effect | StaticAbilities$ STPlay2 | RememberObjects$ Remembered | ForgetOnMoved$ Exile -SVar:STPlay2:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may play that card this turn without paying its mana cost. -SVar:DBEffect:DB$ Effect | StaticAbilities$ STPlay | RememberObjects$ Remembered | ForgetOnMoved$ Exile +SVar:TrigExile:DB$ Dig | Defined$ You | DigNum$ 1 | ChangeNum$ All | DestinationZone$ Exile | RememberChanged$ True | SubAbility$ DBEffect +SVar:DBEffect:DB$ Effect | StaticAbilities$ STPlay,STPlay2 | RememberObjects$ Remembered | ForgetOnMoved$ Exile | SubAbility$ DBCleanup SVar:STPlay:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may play that card this turn. +SVar:STPlay2:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Equipment.IsRemembered | ValidAfterStack$ Spell.Equipment | AffectedZone$ Exile | Description$ You may cast Equipment spells this way without paying their mana costs. SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True -SVar:X:Remembered$Valid Equipment DeckNeeds:Type$Equipment Oracle:Affinity for Equipment\nWhenever an equipped creature you control attacks, exile the top card of your library. You may play that card this turn. You may cast Equipment spells this way without paying their mana costs. diff --git a/forge-gui/res/cardsfolder/n/nissa_genesis_mage.txt b/forge-gui/res/cardsfolder/n/nissa_genesis_mage.txt index 0b774712eb4..12a9947f6bc 100644 --- a/forge-gui/res/cardsfolder/n/nissa_genesis_mage.txt +++ b/forge-gui/res/cardsfolder/n/nissa_genesis_mage.txt @@ -3,7 +3,7 @@ ManaCost:5 G G Types:Legendary Planeswalker Nissa Loyalty:5 A:AB$ Untap | Cost$ AddCounter<2/LOYALTY> | ValidTgts$ Creature | TargetMin$ 0 | TargetMax$ 2 | Planeswalker$ True | SubAbility$ DBUntap | TgtPrompt$ Select target creature | SpellDescription$ Untap up to two target creatures and up to two target lands. -SVar:DBUntap:DB$ Untap | ValidTgts$ Land | TargetMin$ 0 | TargetMax$ 2 | TgtPrompt$ Select target Land +SVar:DBUntap:DB$ Untap | ValidTgts$ Land | TargetMin$ 0 | TargetMax$ 2 | AILogic$ Always | TgtPrompt$ Select target Land A:AB$ Pump | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ +5 | NumDef$ +5 | SpellDescription$ Target creature gets +5/+5 until end of turn. A:AB$ Dig | Cost$ SubCounter<10/LOYALTY> | DigNum$ 10 | AnyNumber$ True | ChangeValid$ Creature,Land | DestinationZone$ Battlefield | Ultimate$ True | Planeswalker$ True | DestinationZone2$ Library | LibraryPosition$ -1 | RestRandomOrder$ True | SpellDescription$ Look at the top ten cards of your library. You may put any number of creature and/or land cards from among them onto the battlefield. Put the rest on the bottom of your library in a random order. DeckHints:Name$Nissa's Encouragement|Brambleweft Behemoth|Forest diff --git a/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt b/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt index d79a62c6984..308d6c9f108 100644 --- a/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt +++ b/forge-gui/res/cardsfolder/n/niv_mizzet_supreme.txt @@ -4,7 +4,8 @@ Types:Legendary Creature Dragon Avatar PT:5/5 K:Flying K:Hexproof:Card.MonoColor:monocolored -S:Mode$ Continuous | Affected$ Instant.YouCtrl+numColorsEQ2,Sorcery.YouCtrl+numColorsEQ2 | AffectedZone$ Graveyard | AddKeyword$ Jump-start | Description$ Each instant and sorcery card in your graveyard that's exactly two colors has jump-start. +# TODO the AffectedZone Stack is needed for now but should be handled by the engine instead +S:Mode$ Continuous | Affected$ Instant.YouCtrl+numColorsEQ2,Sorcery.YouCtrl+numColorsEQ2 | AffectedZone$ Graveyard,Stack | AddKeyword$ Jump-start | Description$ Each instant and sorcery card in your graveyard that's exactly two colors has jump-start. DeckHints:Type$Instant|Sorcery & Ability$Mill|Graveyard DeckHas:Ability$Graveyard|Discard Oracle:Flying, hexproof from monocolored\nEach instant and sorcery card in your graveyard that's exactly two colors has jump-start. diff --git a/forge-gui/res/cardsfolder/o/ob_nixilis_the_adversary.txt b/forge-gui/res/cardsfolder/o/ob_nixilis_the_adversary.txt index d51f0da7ee9..3020d4faec7 100644 --- a/forge-gui/res/cardsfolder/o/ob_nixilis_the_adversary.txt +++ b/forge-gui/res/cardsfolder/o/ob_nixilis_the_adversary.txt @@ -2,7 +2,7 @@ Name:Ob Nixilis, the Adversary ManaCost:1 B R Types:Legendary Planeswalker Nixilis Loyalty:3 -K:Casualty:X:NonLegendary$ True | CopySetLoyalty$ Casualty:The copy isn't legendary and has starting loyalty X. +K:Casualty:X:NonLegendary$ True | SetLoyalty$ Casualty:The copy isn't legendary and has starting loyalty X. A:AB$ RepeatEach | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | RepeatPlayers$ Opponent | RepeatSubAbility$ DBDrain | SubAbility$ DBGainLife | SpellDescription$ Each opponent loses 2 life unless they discard a card. If you control a Demon or Devil, you gain 2 life. SVar:DBDrain:DB$ LoseLife | Defined$ Player.IsRemembered | LifeAmount$ 2 | UnlessCost$ Discard<1/Card> | UnlessPayer$ Player.IsRemembered SVar:DBGainLife:DB$ GainLife | LifeAmount$ 2 | ConditionPresent$ Demon.YouCtrl,Devil.YouCtrl | StackDescription$ None diff --git a/forge-gui/res/cardsfolder/o/outlaws_merriment.txt b/forge-gui/res/cardsfolder/o/outlaws_merriment.txt index 573679d31e6..07e2beeb4a0 100644 --- a/forge-gui/res/cardsfolder/o/outlaws_merriment.txt +++ b/forge-gui/res/cardsfolder/o/outlaws_merriment.txt @@ -1,8 +1,8 @@ Name:Outlaws' Merriment ManaCost:1 R W W Types:Enchantment -T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, choose one at random. Create a red and white creature token with those characteristics. -SVar:TrigCharm:DB$ Charm | Random$ True | Choices$ DBToken1,DBToken2,DBToken3 +T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, ABILITY +SVar:TrigCharm:DB$ Charm | Random$ True | Choices$ DBToken1,DBToken2,DBToken3 | AdditionalDescription$ Create a red and white creature token with those characteristics. SVar:DBToken1:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_3_1_human_warrior_trample_haste | TokenOwner$ You | SpellDescription$ 3/1 Human Warrior with trample and haste. SVar:DBToken2:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_2_1_human_cleric_lifelink_haste | TokenOwner$ You | SpellDescription$ 2/1 Human Cleric with lifelink and haste. SVar:DBToken3:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_1_2_human_rogue_haste_damage | TokenOwner$ You | SpellDescription$ 1/2 Human Rogue with haste and "When this creature enters the battlefield, it deals 1 damage to any target." diff --git a/forge-gui/res/cardsfolder/r/radha_heir_to_keld.txt b/forge-gui/res/cardsfolder/r/radha_heir_to_keld.txt index f3077bfb5b1..91b85c4c69f 100644 --- a/forge-gui/res/cardsfolder/r/radha_heir_to_keld.txt +++ b/forge-gui/res/cardsfolder/r/radha_heir_to_keld.txt @@ -3,6 +3,6 @@ ManaCost:R G Types:Legendary Creature Elf Warrior PT:2/2 A:AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}. -T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigMana | TriggerDescription$ Whenever CARDNAME attacks, add R R. +T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigMana | TriggerDescription$ Whenever CARDNAME attacks, add {R}{R}. SVar:TrigMana:DB$ Mana | Produced$ R | Amount$ 2 | SpellDescription$ Add {R}{R}. Oracle:Whenever Radha, Heir to Keld attacks, you may add {R}{R}.\n{T}: Add {G}. diff --git a/forge-gui/res/cardsfolder/t/tawnos_the_toymaker.txt b/forge-gui/res/cardsfolder/t/tawnos_the_toymaker.txt index 061d010ce07..3612f9f6a9b 100644 --- a/forge-gui/res/cardsfolder/t/tawnos_the_toymaker.txt +++ b/forge-gui/res/cardsfolder/t/tawnos_the_toymaker.txt @@ -3,7 +3,7 @@ ManaCost:3 G U Types:Legendary Creature Human Artificer PT:3/5 T:Mode$ SpellCast | TriggerZones$ Battlefield | OptionalDecider$ You | ValidCard$ Creature.Bird,Creature.Beast | ValidActivatingPlayer$ You | NoResolvingCheck$ True | Execute$ TrigCopy | TriggerDescription$ Whenever you cast a Beast or Bird creature spell, you may copy it, except it's an artifact in addition to its other types. (The copy becomes a token.) -SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | CopyAddTypes$ Artifact +SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | AddTypes$ Artifact DeckNeeds:Type$Beast|Bird DeckHas:Ability$Token & Type$Artifact Oracle:Whenever you cast a Beast or Bird creature spell, you may copy it, except it's an artifact in addition to its other types. (The copy becomes a token.) diff --git a/forge-gui/res/cardsfolder/t/tempered_veteran.txt b/forge-gui/res/cardsfolder/t/tempered_veteran.txt index 052f3f53b25..61a18be49b7 100644 --- a/forge-gui/res/cardsfolder/t/tempered_veteran.txt +++ b/forge-gui/res/cardsfolder/t/tempered_veteran.txt @@ -3,6 +3,6 @@ ManaCost:1 W Types:Creature Human Knight PT:1/2 A:AB$ PutCounter | Cost$ W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature.counters_GE1_P1P1 | TgtPrompt$ Select target creature with a +1/+1 counter | SpellDescription$ Put a +1/+1 counter on target creature with a +1/+1 counter on it. -A:AB$ PutCounter | Cost$ 4 W W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Put a +1/+1 counter on target creature. +A:AB$ PutCounter | Cost$ 4 W W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | AILogic$ NoCounterOfType | SpellDescription$ Put a +1/+1 counter on target creature. DeckHas:Ability$Counters Oracle:{W}, {T}: Put a +1/+1 counter on target creature with a +1/+1 counter on it.\n{4}{W}{W}, {T}: Put a +1/+1 counter on target creature. diff --git a/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletUtil.java b/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletUtil.java index aea1a343f0d..da64461ec20 100644 --- a/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletUtil.java @@ -126,12 +126,14 @@ public class GauntletUtil { break; case COMMANDER_DECK: deck = DeckgenUtil.getCommanderDeck(); - eventNames.add(deck.getName()); + if (deck != null) + eventNames.add(deck.getName()); break; default: continue; } - decks.add(deck); + if (deck != null) + decks.add(deck); } gauntlet.setDecks(decks); diff --git a/forge-gui/src/main/java/forge/model/FModel.java b/forge-gui/src/main/java/forge/model/FModel.java index adc72e64e2e..7029daf8f7c 100644 --- a/forge-gui/src/main/java/forge/model/FModel.java +++ b/forge-gui/src/main/java/forge/model/FModel.java @@ -164,6 +164,8 @@ public final class FModel { if (new AutoUpdater(true).attemptToUpdate()) { // } + // load types before loading cards + loadDynamicGamedata(); //load card database final CardStorageReader reader = new CardStorageReader(ForgeConstants.CARD_DATA_DIR, progressBarBridge, @@ -243,8 +245,6 @@ public final class FModel { Spell.setPerformanceMode(preferences.getPrefBoolean(FPref.PERFORMANCE_MODE)); - loadDynamicGamedata(); - if (progressBar != null) { FThreads.invokeInEdtLater(new Runnable() { @Override