diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7824f..6a8ca9e1859 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,6 +4,7 @@ about: Create a report to help us improve title: '' labels: '' assignees: '' +type: 'Bug' --- @@ -31,7 +32,6 @@ If applicable, add screenshots to help explain your problem. **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7d615..8520249f9cb 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,6 +4,7 @@ about: Suggest an idea for this project title: '' labels: '' assignees: '' +type: 'Feature' --- diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 808043bb503..877b16b0f1e 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -129,7 +129,9 @@ jobs: makeLatest: true - name: 🔧 Install XML tools - run: sudo apt-get install -y libxml2-utils + run: | + sudo apt-get update + sudo apt-get install -y libxml2-utils - name: 🔼 Bump versionCode in root POM id: bump_version diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml index 59aaaffdc04..c293c074119 100644 --- a/.github/workflows/test-build.yaml +++ b/.github/workflows/test-build.yaml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '17' ] + java: ['17', '21'] name: Test with Java ${{ matrix.Java }} steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index a57534b0d4a..eb48c74dba1 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,9 @@ forge-gui-mobile-dev/testAssets forge-gui/res/cardsfolder/*.bat +# Generated changelog file +forge-gui/release-files/CHANGES.txt + forge-gui/res/PerSetTrackingResults forge-gui/res/decks forge-gui/res/layouts diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md deleted file mode 100644 index c2804f08ab3..00000000000 --- a/.gitlab/issue_templates/Bug.md +++ /dev/null @@ -1,33 +0,0 @@ -Summary - -(Summarize the bug encountered concisely) - - -Steps to reproduce - -(How one can reproduce the issue - this is very important. Specific cards and specific actions especially) - - -Which version of Forge are you on (Release, Snapshot? Desktop, Android?) - - -What is the current bug behavior? - -(What actually happens) - - -What is the expected correct behavior? - -(What you should see instead) - - -Relevant logs and/or screenshots - -(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state. - - -Possible fixes - -(If you can, link to the line of code that might be responsible for the problem) - -/label ~needs-investigation \ No newline at end of file diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md deleted file mode 100644 index 712f6a75065..00000000000 --- a/.gitlab/issue_templates/Feature.md +++ /dev/null @@ -1,15 +0,0 @@ -Summary - -(Summarize the feature you wish concisely) - - -Example screenshots - -(If this is a UI change, please provide an example screenshot of how this feature might work) - - -Feature type - -(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.) - -/label ~feature request \ No newline at end of file diff --git a/adventure-editor/src/main/java/forge/adventure/Main.java b/adventure-editor/src/main/java/forge/adventure/Main.java index 946c88cae70..d3efde2b85e 100644 --- a/adventure-editor/src/main/java/forge/adventure/Main.java +++ b/adventure-editor/src/main/java/forge/adventure/Main.java @@ -15,7 +15,7 @@ public class Main { public static void main(String[] args) { GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/")); - GuiBase.setDeviceInfo(null, 0, 0); + GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/"); new EditorMainWindow(Config.instance()); } } diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 7f09eb3cfab..c9d698c7582 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -97,6 +97,7 @@ public class AiController { private int lastAttackAggression; private boolean useLivingEnd; private List skipped; + private boolean timeoutReached; public AiController(final Player computerPlayer, final Game game0) { player = computerPlayer; @@ -886,27 +887,8 @@ public class AiController { private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) { final Card host = sa.getHostCard(); - // Check a predefined condition - if (sa.hasParam("AICheckSVar")) { - final String svarToCheck = sa.getParam("AICheckSVar"); - String comparator = "GE"; - int compareTo = 1; - - if (sa.hasParam("AISVarCompare")) { - final String fullCmp = sa.getParam("AISVarCompare"); - comparator = fullCmp.substring(0, 2); - final String strCmpTo = fullCmp.substring(2); - try { - compareTo = Integer.parseInt(strCmpTo); - } catch (final Exception ignored) { - compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa); - } - } - - int left = AbilityUtils.calculateAmount(host, svarToCheck, sa); - if (!Expressions.compare(left, comparator, compareTo)) { - return AiPlayDecision.AnotherTime; - } + if (sa.hasParam("AICheckSVar") && !aiShouldRun(sa, sa, host, null)) { + return AiPlayDecision.AnotherTime; } // this is the "heaviest" check, which also sets up targets, defines X, etc. @@ -924,7 +906,7 @@ public class AiController { // check if enough left (pass memory indirectly because we don't want to include those) Set tappedForMana = AiCardMemory.getMemorySet(player, MemorySet.PAYS_TAP_COST); - if (tappedForMana != null && tappedForMana.isEmpty() && + if (tappedForMana != null && !tappedForMana.isEmpty() && !ComputerUtilCost.checkTapTypeCost(player, sa.getPayCosts(), host, sa, new CardCollection(tappedForMana))) { return AiPlayDecision.CantAfford; } @@ -1664,6 +1646,9 @@ public class AiController { Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex); } + // in case of infinite loop reset below would not be reached + timeoutReached = false; + FutureTask future = new FutureTask<>(() -> { //avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast... boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0); @@ -1673,6 +1658,11 @@ public class AiController { continue; } + if (timeoutReached) { + timeoutReached = false; + break; + } + if (sa.getHostCard().hasKeyword(Keyword.STORM) && sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell && player.getZone(ZoneType.Hand).contains( @@ -1752,7 +1742,10 @@ public class AiController { t.stop(); } catch (UnsupportedOperationException ex) { // Android and Java 20 dropped support to stop so sadly thread will keep running + timeoutReached = true; future.cancel(true); + // TODO wait a few more seconds to try and exit at a safe point before letting the engine continue + // TODO mark some as skipped to increase chance to find something playable next priority } return null; } @@ -1805,14 +1798,9 @@ public class AiController { * @param sa the sa * @return true, if successful */ - public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) { - Card hostCard = effect.getHostCard(); - if (hostCard.hasAlternateState()) { - hostCard = game.getCardState(hostCard); - } - + public final boolean aiShouldRun(final CardTraitBase effect, final SpellAbility sa, final Card host, final GameEntity affected) { if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) { - final Player controller = hostCard.getController(); + final Player controller = host.getController(); if (affected instanceof Player) { return !((Player) affected).isOpponentOf(controller); } @@ -1821,7 +1809,6 @@ public class AiController { } } if (effect.hasParam("AICheckSVar")) { - System.out.println("aiShouldRun?" + sa); final String svarToCheck = effect.getParam("AICheckSVar"); String comparator = "GE"; int compareTo = 1; @@ -1834,9 +1821,9 @@ public class AiController { compareTo = Integer.parseInt(strCmpTo); } catch (final Exception ignored) { if (sa == null) { - compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect); + compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), effect); } else { - compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa); + compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa); } } } @@ -1844,13 +1831,12 @@ public class AiController { int left = 0; if (sa == null) { - left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect); + left = AbilityUtils.calculateAmount(host, svarToCheck, effect); } else { - left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa); + left = AbilityUtils.calculateAmount(host, svarToCheck, sa); } - System.out.println("aiShouldRun?" + left + comparator + compareTo); return Expressions.compare(left, comparator, compareTo); - } else if (effect.hasParam("AICheckDredge")) { + } else if (effect.isKeyword(Keyword.DREDGE)) { return player.getCardsIn(ZoneType.Library).size() > 8 || player.isCardInPlay("Laboratory Maniac"); } else return sa != null && doTrigger(sa, false); } diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index 52d9ad4f90a..88de7ba4eb4 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -29,12 +29,15 @@ public class AiCostDecision extends CostDecisionMakerBase { private final CardCollection tapped; public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) { + this(ai0, sa, effect, false); + } + public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect, final boolean payMana) { super(ai0, effect, sa, sa.getHostCard()); discarded = new CardCollection(); tapped = new CardCollection(); Set tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST); - if (tappedForMana != null) { + if (!payMana && tappedForMana != null) { tapped.addAll(tappedForMana); } } @@ -110,7 +113,7 @@ public class AiCostDecision extends CostDecisionMakerBase { randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability); } return PaymentDecision.card(randomSubset); - } else if (type.equals("DifferentNames")) { + } else if (type.contains("+WithDifferentNames")) { CardCollection differentNames = new CardCollection(); CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe")); while (c > 0) { @@ -563,7 +566,7 @@ public class AiCostDecision extends CostDecisionMakerBase { int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove); if (thisRemove > 0) { removed += thisRemove; - table.put(null, prefCard, CounterType.get(cType), thisRemove); + table.put(null, prefCard, cType, thisRemove); } } } @@ -573,7 +576,7 @@ public class AiCostDecision extends CostDecisionMakerBase { @Override public PaymentDecision visit(CostRemoveAnyCounter cost) { final int c = cost.getAbilityAmount(ability); - final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source); + final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source); if (c <= 0) { return null; @@ -716,7 +719,7 @@ public class AiCostDecision extends CostDecisionMakerBase { int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove); if (over > 0) { toRemove += over; - table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over); + table.put(null, crd, CounterEnumType.QUEST, over); } } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 7a95f488277..bad4b3c3bbc 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -767,7 +767,7 @@ public class ComputerUtil { public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) { CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa); - typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN))); + typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN)); if (untap) { typeList.remove(activate); @@ -2542,7 +2542,7 @@ public class ComputerUtil { boolean opponent = controller.isOpponentOf(ai); - final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1); + final CounterType p1p1Type = CounterEnumType.P1P1; if (!sa.hasParam("AILogic")) { return Aggregates.random(options); @@ -3104,41 +3104,38 @@ public class ComputerUtil { public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) { final Card source = sa.getHostCard(); - if (source == null) { return srcList; } - - if (sa.hasParam("AITgts")) { - CardCollection list; - String aiTgts = sa.getParam("AITgts"); - if (aiTgts.startsWith("BetterThan")) { - int value = 0; - if (aiTgts.endsWith("Source")) { - value = ComputerUtilCard.evaluateCreature(source); - if (source.isEnchanted()) { - for (Card enc : source.getEnchantedBy()) { - if (enc.getController().equals(ai)) { - value += 100; // is 100 per AI's own aura enough? - } - } - } - } else if (aiTgts.contains("EvalRating.")) { - value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa); - } else { - System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa); - value = ComputerUtilCard.evaluateCreature(source); - } - final int totalValue = value; - list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30); - } else { - list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa); - } - - if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) { - return list; - } else { - return srcList; - } + if (source == null || !sa.hasParam("AITgts")) { + return srcList; } + CardCollection list; + String aiTgts = sa.getParam("AITgts"); + if (aiTgts.startsWith("BetterThan")) { + int value = 0; + if (aiTgts.endsWith("Source")) { + value = ComputerUtilCard.evaluateCreature(source); + if (source.isEnchanted()) { + for (Card enc : source.getEnchantedBy()) { + if (enc.getController().equals(ai)) { + value += 100; // is 100 per AI's own aura enough? + } + } + } + } else if (aiTgts.contains("EvalRating.")) { + value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa); + } else { + System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa); + value = ComputerUtilCard.evaluateCreature(source); + } + final int totalValue = value; + list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30); + } else { + list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa); + } + + if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) { + return list; + } return srcList; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index 7e5f7144482..602d8a23f07 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -177,16 +177,16 @@ public class ComputerUtilCombat { public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) { int damage = attacker.getNetCombatDamage(); int sum = 0; - if (attacked instanceof Player player && !player.canLoseLife()) { - return 0; - } - - // ask ReplacementDamage directly - if (isCombatDamagePrevented(attacker, attacked, damage)) { + if (attacked instanceof Player p && !p.canLoseLife()) { return 0; } if (!attacker.hasKeyword(Keyword.INFECT)) { + // ask ReplacementDamage directly + if (isCombatDamagePrevented(attacker, attacked, damage)) { + return 0; + } + damage += predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities); sum = predictDamageTo(attacked, damage, attacker, true); if (attacker.hasDoubleStrike()) { @@ -974,17 +974,13 @@ public class ComputerUtilCombat { continue; } + int pBonus = 0; if (ability.getApi() == ApiType.Pump) { if (!ability.hasParam("NumAtt")) { continue; } - if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) { - int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability); - if (pBonus > 0) { - power += pBonus; - } - } + pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability); } else if (ability.getApi() == ApiType.PutCounter) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { continue; @@ -998,12 +994,11 @@ public class ComputerUtilCombat { continue; } - if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) { - int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); - if (pBonus > 0) { - power += pBonus; - } - } + pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); + } + + if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) { + power += pBonus; } } @@ -1107,17 +1102,13 @@ public class ComputerUtilCombat { continue; } + int tBonus = 0; if (ability.getApi() == ApiType.Pump) { if (!ability.hasParam("NumDef")) { continue; } - if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) { - int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability); - if (tBonus > 0) { - toughness += tBonus; - } - } + tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability); } else if (ability.getApi() == ApiType.PutCounter) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { continue; @@ -1131,12 +1122,11 @@ public class ComputerUtilCombat { continue; } - if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) { - int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); - if (tBonus > 0) { - toughness += tBonus; - } - } + tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); + } + + if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) { + toughness += tBonus; } } return toughness; @@ -1305,6 +1295,7 @@ public class ComputerUtilCombat { continue; } + int pBonus = 0; if (ability.getApi() == ApiType.Pump) { if (!ability.hasParam("NumAtt")) { continue; @@ -1314,11 +1305,8 @@ public class ComputerUtilCombat { continue; } - if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) { - int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability); - if (pBonus > 0) { - power += pBonus; - } + if (!ability.getPayCosts().hasTapCost()) { + pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability); } } else if (ability.getApi() == ApiType.PutCounter) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { @@ -1333,13 +1321,14 @@ public class ComputerUtilCombat { continue; } - if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) { - int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); - if (pBonus > 0) { - power += pBonus; - } + if (!ability.getPayCosts().hasTapCost()) { + pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); } } + + if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) { + power += pBonus; + } } return power; } @@ -1530,16 +1519,14 @@ public class ComputerUtilCombat { if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) { continue; } - if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) { - continue; - } + int tBonus = 0; if (ability.getApi() == ApiType.Pump) { if (!ability.hasParam("NumDef")) { continue; } - toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true); + tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true); } else if (ability.getApi() == ApiType.PutCounter) { if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) { continue; @@ -1553,10 +1540,11 @@ public class ComputerUtilCombat { continue; } - int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); - if (tBonus > 0) { - toughness += tBonus; - } + tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability); + } + + if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) { + toughness += tBonus; } } return toughness; @@ -2020,35 +2008,35 @@ public class ComputerUtilCombat { * * @param self * a {@link forge.game.player.Player} object. - * @param attacker + * @param combatant * a {@link forge.game.card.Card} object. - * @param block + * @param opposedCombatants * @param dmgCanDeal * a int. * @param defender * @param overrideOrder overriding combatant order */ - public static Map distributeAIDamage(final Player self, final Card attacker, final CardCollectionView block, final CardCollectionView remaining, int dmgCanDeal, GameEntity defender, boolean overrideOrder) { + public static Map distributeAIDamage(final Player self, final Card combatant, CardCollectionView opposedCombatants, final CardCollectionView remaining, int dmgCanDeal, GameEntity defender, boolean overrideOrder) { Map damageMap = Maps.newHashMap(); - Combat combat = attacker.getGame().getCombat(); + Combat combat = combatant.getGame().getCombat(); boolean isAttacking = defender != null; // Check for Banding, Defensive Formation - boolean isAttackingMe = isAttacking && combat.getDefenderPlayerByAttacker(attacker).equals(self); - boolean isBlockingMyBand = attacker.getController().isOpponentOf(self) && AttackingBand.isValidBand(block, true); + boolean isAttackingMe = isAttacking && combat.getDefenderPlayerByAttacker(combatant).equals(self); + boolean isBlockingMyBand = combatant.getController().isOpponentOf(self) && AttackingBand.isValidBand(opposedCombatants, true); final boolean aiDistributesBandingDmg = isAttackingMe || isBlockingMyBand; - final boolean hasTrample = attacker.hasKeyword(Keyword.TRAMPLE); + final boolean hasTrample = combatant.hasKeyword(Keyword.TRAMPLE); - if (combat != null && remaining != null && hasTrample && attacker.isAttacking() && !aiDistributesBandingDmg) { + if (combat != null && remaining != null && hasTrample && combatant.isAttacking() && !aiDistributesBandingDmg) { // if attacker has trample and some of its blockers are also blocking others it's generally a good idea // to assign those without trample first so we can maximize the damage to the defender for (final Card c : remaining) { - if (c == attacker || c.hasKeyword(Keyword.TRAMPLE)) { + if (c == combatant || c.hasKeyword(Keyword.TRAMPLE)) { continue; } - final CardCollection sharedBlockers = new CardCollection(block); + final CardCollection sharedBlockers = new CardCollection(opposedCombatants); sharedBlockers.retainAll(combat.getBlockers(c)); if (!sharedBlockers.isEmpty()) { // signal skip for now @@ -2058,12 +2046,21 @@ public class ComputerUtilCombat { // TODO sort remaining tramplers for DamageDone triggers } - if (block.size() == 1) { - final Card blocker = block.getFirst(); + // Order the combatants in preferred order in case legacy ordering is disabled + if (!self.getGame().getRules().hasOrderCombatants()) { + if (combatant.isAttacking()) { + opposedCombatants = AiBlockController.orderBlockers(combatant, new CardCollection(opposedCombatants)); + } else { + opposedCombatants = AiBlockController.orderAttackers(combatant, new CardCollection(opposedCombatants)); + } + } + + if (opposedCombatants.size() == 1) { + final Card blocker = opposedCombatants.getFirst(); int dmgToBlocker = dmgCanDeal; if (hasTrample && isAttacking && !aiDistributesBandingDmg) { // otherwise no entity to deliver damage via trample - dmgToBlocker = getEnoughDamageToKill(blocker, dmgCanDeal, attacker, true); + dmgToBlocker = getEnoughDamageToKill(blocker, dmgCanDeal, combatant, true); if (dmgCanDeal < dmgToBlocker) { // can't kill so just put the lowest legal amount @@ -2082,9 +2079,9 @@ public class ComputerUtilCombat { // Does the attacker deal lethal damage to all blockers //Blocking Order now determined after declare blockers Card lastBlocker = null; - for (final Card b : block) { + for (final Card b : opposedCombatants) { lastBlocker = b; - final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, attacker, true); + final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, combatant, true); if (dmgToKill <= dmgCanDeal) { damageMap.put(b, dmgToKill); dmgCanDeal -= dmgToKill; @@ -2109,15 +2106,15 @@ public class ComputerUtilCombat { } else { // In the event of Banding or Defensive Formation, assign max damage to the blocker who // can tank all the damage or to the worst blocker to lose as little as possible - for (final Card b : block) { - final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, attacker, true); + for (final Card b : opposedCombatants) { + final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, combatant, true); if (dmgToKill > dmgCanDeal) { damageMap.put(b, dmgCanDeal); break; } } if (damageMap.isEmpty()) { - damageMap.put(ComputerUtilCard.getWorstCreatureAI(block), dmgCanDeal); + damageMap.put(ComputerUtilCard.getWorstCreatureAI(opposedCombatants), dmgCanDeal); } } return damageMap; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index ad00bc9c9c8..60af06cc6ba 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -287,7 +287,9 @@ public class ComputerUtilMana { continue; } - if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) { + int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1; + if (amount <= 0) { + // wrong gamestate for variable amount continue; } @@ -351,9 +353,14 @@ public class ComputerUtilMana { continue; } + // these should come last since they reserve the paying cards + // (this means if a mana ability has both parts it doesn't currently undo reservations if the second part fails) if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma, ma.isTrigger())) { continue; } + if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) { + continue; + } return paymentChoice; } @@ -443,7 +450,6 @@ public class ComputerUtilMana { manaProduced = manaProduced.replace(s, color); } } else if (saMana.hasParam("ReplaceColor")) { - // replace color String color = saMana.getParam("ReplaceColor"); if ("Chosen".equals(color)) { if (card.hasChosenColor()) { @@ -704,9 +710,9 @@ public class ComputerUtilMana { if (hasConverge && (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) { final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION; - for (final byte b : ColorSet.fromMask(unpaidColors)) { + for (final MagicColor.Color b : ColorSet.fromMask(unpaidColors)) { // try and pay other colors for converge - final ManaCostShard shard = ManaCostShard.valueOf(b); + final ManaCostShard shard = ManaCostShard.valueOf(b.getColorMask()); saList = sourcesForShards.get(shard); if (saList != null && !saList.isEmpty()) { toPay = shard; @@ -735,7 +741,8 @@ public class ComputerUtilMana { if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) { if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) { - saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for + // not a good idea to sac a card that you're targeting with the SA you're paying for + saExcludeList.add(saPayment); continue; } } @@ -809,11 +816,11 @@ public class ComputerUtilMana { String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay); payMultipleMana(cost, manaProduced, ai); - // remove from available lists + // remove to prevent re-usage since resources don't get consumed sourcesForShards.values().removeIf(CardTraitPredicates.isHostCard(saPayment.getHostCard())); } else { final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment); - if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect))) { + if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect, true))) { saList.remove(saPayment); continue; } @@ -822,8 +829,10 @@ public class ComputerUtilMana { // subtract mana from mana pool manapool.payManaFromAbility(sa, cost, saPayment); - // no need to remove abilities from resource map, - // once their costs are paid and consume resources, they can not be used again + // need to consider if another use is now prevented + if (!cost.isPaid() && saPayment.isActivatedAbility() && !saPayment.getRestrictions().canPlay(saPayment.getHostCard(), saPayment)) { + sourcesForShards.values().removeIf(s -> s == saPayment); + } if (hasConverge) { // hack to prevent converge re-using sources @@ -887,7 +896,8 @@ public class ComputerUtilMana { if (hasConverge) { // add extra colors for paying converge final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION; - for (final byte b : ColorSet.fromMask(unpaidColors)) { + for (final MagicColor.Color color : ColorSet.fromMask(unpaidColors)) { + final byte b = color.getColorMask(); final ManaCostShard shard = ManaCostShard.valueOf(b); if (!sourcesForShards.containsKey(shard)) { if (ai.getManaPool().canPayForShardWithColor(shard, b)) { @@ -914,7 +924,7 @@ public class ComputerUtilMana { ColorSet shared = ColorSet.fromMask(toPay.getColorMask()).getSharedColors(ColorSet.fromNames(m.getComboColors(saPayment).split(" "))); // but other effects might still lead to a more permissive payment if (!shared.isColorless()) { - m.setExpressChoice(ColorSet.fromMask(shared.iterator().next())); + m.setExpressChoice(shared.iterator().next().getShortName()); } getComboManaChoice(ai, saPayment, sa, cost); } @@ -1089,7 +1099,7 @@ public class ComputerUtilMana { // * pay hybrids // * pay phyrexian, keep mana for colorless // * pay generic - return cost.getShardToPayByPriority(shardsToPay, ColorSet.ALL_COLORS.getColor()); + return cost.getShardToPayByPriority(shardsToPay, ColorSet.WUBRG.getColor()); } private static void adjustManaCostToAvoidNegEffects(ManaCostBeingPaid cost, final Card card, Player ai) { @@ -1496,7 +1506,7 @@ public class ComputerUtilMana { } if (!cost.isReusuableResource()) { - for(CostPart part : cost.getCostParts()) { + for (CostPart part : cost.getCostParts()) { if (part instanceof CostSacrifice && !part.payCostFromSource()) { unpreferredCost = true; } @@ -1587,10 +1597,8 @@ public class ComputerUtilMana { // don't use abilities with dangerous drawbacks AbilitySub sub = m.getSubAbility(); - if (sub != null) { - if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) { - continue; - } + if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) { + continue; } manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list @@ -1658,7 +1666,6 @@ public class ComputerUtilMana { if (replaced.contains("C")) { manaMap.put(ManaAtom.COLORLESS, m); } - } } } diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 6cd6815c86f..05f92ee72f2 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -460,7 +460,11 @@ public class PlayerControllerAi extends PlayerController { @Override public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) { - return brains.aiShouldRun(replacementEffect, effectSA, affected); + Card host = replacementEffect.getHostCard(); + if (host.hasAlternateState()) { + host = host.getGame().getCardState(host); + } + return brains.aiShouldRun(replacementEffect, effectSA, host, affected); } @Override @@ -1024,13 +1028,13 @@ public class PlayerControllerAi extends PlayerController { if ((colors.getColor() & chosenColorMask) != 0) { return chosenColorMask; } - return Iterables.getFirst(colors, (byte)0); + return Iterables.getFirst(colors, MagicColor.Color.COLORLESS).getColorMask(); } @Override public byte chooseColor(String message, SpellAbility sa, ColorSet colors) { if (colors.countColors() < 2) { - return Iterables.getFirst(colors, MagicColor.WHITE); + return Iterables.getFirst(colors, MagicColor.Color.WHITE).getColorMask(); } // You may switch on sa.getApi() here and use sa.getParam("AILogic") CardCollectionView hand = player.getCardsIn(ZoneType.Hand); @@ -1043,7 +1047,7 @@ public class PlayerControllerAi extends PlayerController { if ((colors.getColor() & chosenColorMask) != 0) { return chosenColorMask; } - return Iterables.getFirst(colors, MagicColor.WHITE); + return Iterables.getFirst(colors, MagicColor.Color.WHITE).getColorMask(); } @Override @@ -1347,6 +1351,11 @@ public class PlayerControllerAi extends PlayerController { // Ai won't understand that anyway } + @Override + public void revealUnsupported(Map> unsupported) { + // Ai won't understand that anyway + } + @Override public Map> complainCardsCantPlayWell(Deck myDeck) { // TODO check if profile detection set to Auto diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java index 036641d7f6b..9b514a68343 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java +++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java @@ -171,7 +171,7 @@ public class SpecialAiLogic { final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); - if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) { lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent } @@ -277,7 +277,7 @@ public class SpecialAiLogic { final boolean isInfect = source.hasKeyword(Keyword.INFECT); int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); - if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) { lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java index 49cf157d4c8..590c4285a96 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -101,11 +101,7 @@ public class ChangeZoneAi extends SpellAbilityAi { sa.getHostCard().removeSVar("AIPreferenceOverride"); } - if (aiLogic.equals("BeforeCombat")) { - if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) { - return false; - } - } else if (aiLogic.equals("SurpriseBlock")) { + if (aiLogic.equals("SurpriseBlock")) { if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { return false; } @@ -298,13 +294,7 @@ public class ChangeZoneAi extends SpellAbilityAi { boolean activateForCost = ComputerUtil.activateForCost(sa, ai); if (sa.hasParam("Origin")) { - try { - origin = ZoneType.listValueOf(sa.getParam("Origin")); - } catch (IllegalArgumentException ex) { - // This happens when Origin is something like - // "Graveyard,Library" (Doomsday) - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } + origin = ZoneType.listValueOf(sa.getParam("Origin")); } final String destination = sa.getParam("Destination"); @@ -771,6 +761,8 @@ public class ChangeZoneAi extends SpellAbilityAi { return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN); } else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) { return true; + } else if (aiLogic.equals("BeforeCombat")) { + return !ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN); } if (sa.isHidden()) { @@ -897,9 +889,6 @@ public class ChangeZoneAi extends SpellAbilityAi { CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa); list = ComputerUtil.filterAITgts(sa, ai, list, true); - if (sa.hasParam("AITgtsOnlyBetterThanSelf")) { - list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30); - } if (source.isInZone(ZoneType.Hand)) { list = CardLists.filter(list, CardPredicates.nameNotEquals(source.getName())); // Don't get the same card back. @@ -908,8 +897,6 @@ public class ChangeZoneAi extends SpellAbilityAi { list.remove(source); // spells can't target their own source, because it's actually in the stack zone } - // list = CardLists.canSubsequentlyTarget(list, sa); - if (sa.hasParam("AttachedTo")) { list = CardLists.filter(list, c -> { for (Card card : game.getCardsIn(ZoneType.Battlefield)) { @@ -1252,53 +1239,12 @@ public class ChangeZoneAi extends SpellAbilityAi { } } - // if max CMC exceeded, do not choose this card (but keep looking for other options) - if (sa.hasParam("MaxTotalTargetCMC")) { - if (choice.getCMC() > sa.getTargetRestrictions().getMaxTotalCMC(choice, sa) - sa.getTargets().getTotalTargetedCMC()) { - list.remove(choice); - continue; - } - } - - // if max power exceeded, do not choose this card (but keep looking for other options) - if (sa.hasParam("MaxTotalTargetPower")) { - if (choice.getNetPower() > sa.getTargetRestrictions().getMaxTotalPower(choice, sa) -sa.getTargets().getTotalTargetedPower()) { - list.remove(choice); - continue; - } - } - - // honor the Same Creature Type restriction - if (sa.getTargetRestrictions().isWithSameCreatureType()) { - Card firstTarget = sa.getTargetCard(); - if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) { - list.remove(choice); - continue; - } - } - list.remove(choice); if (sa.canTarget(choice)) { sa.getTargets().add(choice); } } - // Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card. - // TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard - // vs. two cards in the second player's graveyard, which cards are more relevant to be targeted, etc.). Consider improving. - if (sa.getTargetRestrictions().isSingleZone()) { - Card firstTgt = sa.getTargetCard(); - CardCollection toRemove = new CardCollection(); - if (firstTgt != null) { - for (Card t : sa.getTargets().getTargetCards()) { - if (!t.getController().equals(firstTgt.getController())) { - toRemove.add(t); - } - } - sa.getTargets().removeAll(toRemove); - } - } - return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java index 519f26bdd3b..b7199681f47 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java @@ -96,6 +96,10 @@ public class CloneAi extends SpellAbilityAi { if (sa.usesTargeting()) { chance = cloneTgtAI(sa); } else { + if (sa.isReplacementAbility() && host.isCloned()) { + // prevent StackOverflow from infinite loop copying another ETB RE + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } if (sa.hasParam("Choices")) { CardCollectionView choices = CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield), sa.getParam("Choices"), host.getController(), host, sa); @@ -188,7 +192,7 @@ public class CloneAi extends SpellAbilityAi { final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary")); String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary" - : "Permanent.YouDontCtrl+notnamed" + name + ",Permanent.nonLegendary+notnamed" + name; + : "Permanent.YouDontCtrl+!named" + name + ",Permanent.nonLegendary+!named" + name; // TODO: rewrite this block so that this is done somehow more elegantly if (canCloneLegendary) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ConniveAi.java b/forge-ai/src/main/java/forge/ai/ability/ConniveAi.java index 3beb8d09463..dc7abc8cdf0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ConniveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ConniveAi.java @@ -119,7 +119,7 @@ public class ConniveAi extends SpellAbilityAi { } } return new AiAbilityDecision( - sa.isTargetNumberValid() && !sa.getTargets().isEmpty() ? 100 : 0, + sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed ); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java index 09faaaee18c..57262dd602a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java @@ -53,17 +53,15 @@ public class ControlExchangeAi extends SpellAbilityAi { if (mandatory) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - } else { - if (mandatory) { - AiAbilityDecision decision = chkDrawback(sa, aiPlayer); - if (sa.isTargetNumberValid()) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } - - return decision; - } else { - return canPlay(aiPlayer, sa); + } else if (mandatory) { + AiAbilityDecision decision = chkDrawback(sa, aiPlayer); + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } + + return decision; + } else { + return canPlay(aiPlayer, sa); } return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } diff --git a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java index 779d6a02e15..7fb91ce52d8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java @@ -44,7 +44,7 @@ public class CopyPermanentAi extends SpellAbilityAi { // Not at EOT phase return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn); } - } if ("DuplicatePerms".equals(aiLogic)) { + } else if ("DuplicatePerms".equals(aiLogic)) { final List valid = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); if (valid.size() < 2) { return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); @@ -212,7 +212,7 @@ public class CopyPermanentAi extends SpellAbilityAi { if (mandatory) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { - return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java index dc9f5b16e02..f8c6060117a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java @@ -135,9 +135,7 @@ public class CounterAi extends SpellAbilityAi { if (sa.hasParam("AILogic")) { String logic = sa.getParam("AILogic"); - if ("Never".equals(logic)) { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } else if (logic.startsWith("MinCMC.")) { // TODO fix Daze and fold into AITgts + if (logic.startsWith("MinCMC.")) { // TODO fix Daze and fold into AITgts int minCMC = Integer.parseInt(logic.substring(7)); if (tgtCMC < minCMC) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java index ea7a896b62a..e8df15e7f5d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java @@ -102,7 +102,7 @@ public abstract class CountersAi extends SpellAbilityAi { } else if (type.equals("DIVINITY")) { final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0); choice = ComputerUtilCard.getMostExpensivePermanentAI(boon); - } else if (CounterType.get(type).isKeywordCounter()) { + } else if (CounterType.getType(type).isKeywordCounter()) { choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type)); } else { // The AI really should put counters on cards that can use it. diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java index 6d6cc83b351..d102ea2065d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java @@ -424,16 +424,20 @@ public class CountersMoveAi extends SpellAbilityAi { // move counter to opponents creature but only if you can not steal them // try to move to something useless or something that would leave play - List oppList = CardLists.filterControlledBy(tgtCards, ai.getOpponents()); - if (!oppList.isEmpty()) { - List best = CardLists.filter(oppList, card -> { + boolean isNegative = ComputerUtil.isNegativeCounter(cType, src); + List filteredTgtList; + filteredTgtList = isNegative ? CardLists.filterControlledBy(tgtCards, ai.getOpponents()) : + CardLists.filterControlledBy(tgtCards, ai.getYourTeam()); + + if (!filteredTgtList.isEmpty()) { + List best = CardLists.filter(filteredTgtList, card -> { // gain from useless - if (!ComputerUtilCard.isUselessCreature(ai, card)) { + if (isNegative && !ComputerUtilCard.isUselessCreature(ai, card)) { return true; } // source would leave the game - if (!card.hasSVar("EndOfTurnLeavePlay")) { + if (isNegative && !card.hasSVar("EndOfTurnLeavePlay")) { return true; } @@ -441,7 +445,7 @@ public class CountersMoveAi extends SpellAbilityAi { }); if (best.isEmpty()) { - best = oppList; + best = filteredTgtList; } Card card = ComputerUtilCard.getBestCreatureAI(best); diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java index c3c80df3d04..5564cfd0b59 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java @@ -154,7 +154,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { } if (counterType == null || counterType.is(type)) { - addTargetsByCounterType(ai, sa, aiList, CounterType.get(type)); + addTargetsByCounterType(ai, sa, aiList, type); } } } @@ -163,7 +163,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { if (!oppList.isEmpty()) { // not enough targets if (sa.canAddMoreTarget()) { - final CounterType type = CounterType.get(CounterEnumType.M1M1); + final CounterType type = CounterEnumType.M1M1; if (counterType == null || counterType == type) { addTargetsByCounterType(ai, sa, oppList, type); } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java index 7ac79b97ac7..ef11dc61c80 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java @@ -110,7 +110,7 @@ public class CountersProliferateAi extends SpellAbilityAi { public T chooseSingleEntity(Player ai, SpellAbility sa, Collection options, boolean isOptional, Player targetedPlayer, Map params) { // Proliferate is always optional for all, no need to select best - final CounterType poison = CounterType.get(CounterEnumType.POISON); + final CounterType poison = CounterEnumType.POISON; boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO); // because countertype can't be chosen anymore, only look for poison counters 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 c0a846e603b..77dfc9c9443 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -92,9 +92,8 @@ public class CountersPutAi extends CountersAi { return false; } return chance > MyRandom.getRandom().nextFloat(); - } else { - return false; } + return false; } if (sa.isKeyword(Keyword.LEVEL_UP)) { @@ -124,7 +123,6 @@ public class CountersPutAi extends CountersAi { final Cost abCost = sa.getPayCosts(); final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); - CardCollection list; Card choice = null; final String amountStr = sa.getParamOrDefault("CounterNum", "1"); final boolean divided = sa.isDividedAsYouChoose(); @@ -170,7 +168,7 @@ public class CountersPutAi extends CountersAi { CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1)); oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING); - oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1))); + oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterEnumType.M1M1)); Card best = ComputerUtilCard.getBestAI(oppCreatM1); if (best != null) { @@ -292,10 +290,8 @@ public class CountersPutAi extends CountersAi { if (willActivate) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("ChargeToBestCMC")) { return doChargeToCMCLogic(ai, sa); } else if (logic.equals("ChargeToBestOppControlledCMC")) { @@ -336,7 +332,7 @@ public class CountersPutAi extends CountersAi { Game game = ai.getGame(); Combat combat = game.getCombat(); - if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) { + if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return doCombatAdaptLogic(source, amount, combat); @@ -348,7 +344,7 @@ public class CountersPutAi extends CountersAi { if (type.equals("P1P1")) { nPump = amount; } - return FightAi.canFightAi(ai, sa, nPump, nPump); + return FightAi.canFight(ai, sa, nPump, nPump); } if (amountStr.equals("X")) { @@ -442,17 +438,16 @@ public class CountersPutAi extends CountersAi { } sa.addDividedAllocation(c, amount); return decision; - } else { - if (!hasSacCost) { - // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies - return decision; - } + } else if (!hasSacCost) { + // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies + return decision; } } } sa.resetTargets(); + CardCollection list; if (sa.isCurse()) { list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); } else { @@ -608,7 +603,21 @@ public class CountersPutAi extends CountersAi { return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } - final int currCounters = cards.get(0).getCounters(CounterType.get(type)); + final int currCounters = cards.get(0).getCounters(CounterType.getType(type)); + + // adding counters would cause counter amount to overflow + if (Integer.MAX_VALUE - currCounters <= amount) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + if (type.equals("P1P1")) { + if (Integer.MAX_VALUE - cards.get(0).getNetPower() <= amount) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + if (Integer.MAX_VALUE - cards.get(0).getNetToughness() <= amount) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } + // each non +1/+1 counter on the card is a 10% chance of not // activating this ability. @@ -623,7 +632,7 @@ public class CountersPutAi extends CountersAi { } // Useless since the card already has the keyword (or for another reason) - if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) { + if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -670,14 +679,12 @@ public class CountersPutAi extends CountersAi { || (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger()); if (sa.usesTargeting()) { - CardCollection list = null; - + CardCollection list; if (sa.isCurse()) { list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); } else { list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); } - list = CardLists.getTargetableCards(list, sa); if (list.isEmpty() && isMandatoryTrigger) { @@ -693,9 +700,8 @@ public class CountersPutAi extends CountersAi { || sa.getTargets().isEmpty()) { sa.resetTargets(); return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); - } else { - break; } + break; } if (sa.isCurse()) { @@ -737,9 +743,7 @@ public class CountersPutAi extends CountersAi { protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) { final SpellAbility root = sa.getRootAbility(); final Card source = sa.getHostCard(); - final String aiLogic = sa.getParamOrDefault("AILogic", ""); - boolean preferred = true; - CardCollection list; + final String aiLogic = sa.getParam("AILogic"); final String amountStr = sa.getParamOrDefault("CounterNum", "1"); final boolean divided = sa.isDividedAsYouChoose(); final int amount = AbilityUtils.calculateAmount(source, amountStr, sa); @@ -758,14 +762,10 @@ public class CountersPutAi extends CountersAi { } if ("ChargeToBestCMC".equals(aiLogic)) { - AiAbilityDecision decision = doChargeToCMCLogic(ai, sa); - if (decision.willingToPlay()) { - return decision; - } else if (mandatory) { + if (mandatory) { return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } + return doChargeToCMCLogic(ai, sa); } if (!sa.usesTargeting()) { @@ -789,7 +789,6 @@ public class CountersPutAi extends CountersAi { // things like Powder Keg, which are way too complex for the AI } } else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) { - // can only target opponent PlayerCollection playerList = new PlayerCollection(IterableUtil.filter( sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class)); @@ -804,34 +803,32 @@ public class CountersPutAi extends CountersAi { sa.getTargets().add(choice); } } else { - String logic = sa.getParam("AILogic"); - if ("Fight".equals(logic) || "PowerDmg".equals(logic)) { + if ("Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic)) { int nPump = 0; if (type.equals("P1P1")) { nPump = amount; } - AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump); + AiAbilityDecision decision = FightAi.canFight(ai, sa, nPump, nPump); if (decision.willingToPlay()) { return decision; } } - if (sa.isCurse()) { - list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); - } else { - list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); - } - list = CardLists.getTargetableCards(list, sa); - - // Filter AI-specific targets if provided - list = ComputerUtil.filterAITgts(sa, ai, list, false); - - int totalTargets = list.size(); - sa.resetTargets(); + + Iterable filteredField; + if (sa.isCurse()) { + filteredField = ai.getOpponents().getCardsIn(ZoneType.Battlefield); + } else { + filteredField = ai.getCardsIn(ZoneType.Battlefield); + } + CardCollection list = CardLists.getTargetableCards(filteredField, sa); + list = ComputerUtil.filterAITgts(sa, ai, list, false); + int totalTargets = list.size(); + boolean preferred = true; + while (sa.canAddMoreTarget()) { if (mandatory) { - // When things are mandatory, gotta handle a little differently if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) { return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } @@ -856,7 +853,7 @@ public class CountersPutAi extends CountersAi { return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi); } - Card choice = null; + Card choice; // Choose targets here: if (sa.isCurse()) { @@ -865,33 +862,27 @@ public class CountersPutAi extends CountersAi { if (choice == null && mandatory) { choice = Aggregates.random(list); } + } else if (type.equals("M1M1")) { + choice = ComputerUtilCard.getWorstCreatureAI(list); } else { - if (type.equals("M1M1")) { - choice = ComputerUtilCard.getWorstCreatureAI(list); - } else { - choice = Aggregates.random(list); - } + choice = Aggregates.random(list); } + } else if (preferred) { + list = ComputerUtil.getSafeTargets(ai, sa, list); + choice = chooseBoonTarget(list, type); + if (choice == null && mandatory) { + choice = Aggregates.random(list); + } + } else if (type.equals("P1P1")) { + choice = ComputerUtilCard.getWorstCreatureAI(list); } else { - if (preferred) { - list = ComputerUtil.getSafeTargets(ai, sa, list); - choice = chooseBoonTarget(list, type); - if (choice == null && mandatory) { - choice = Aggregates.random(list); - } - } else { - if (type.equals("P1P1")) { - choice = ComputerUtilCard.getWorstCreatureAI(list); - } else { - choice = Aggregates.random(list); - } - } + choice = Aggregates.random(list); } if (choice != null && divided) { - int alloc = Math.max(amount / totalTargets, 1); if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) { sa.addDividedAllocation(choice, left); } else { + int alloc = Math.max(amount / totalTargets, 1); sa.addDividedAllocation(choice, alloc); left -= alloc; } @@ -961,8 +952,8 @@ public class CountersPutAi extends CountersAi { protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) { // Bolster does use this // TODO need more or less logic there? - final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1); - final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1); + final CounterType m1m1 = CounterEnumType.M1M1; + final CounterType p1p1 = CounterEnumType.P1P1; // no logic if there is no options or no to choice if (!isOptional && Iterables.size(options) <= 1) { @@ -981,9 +972,7 @@ public class CountersPutAi extends CountersAi { final String amountStr = sa.getParamOrDefault("CounterNum", "1"); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa); - final boolean isCurse = sa.isCurse(); - - if (isCurse) { + if (sa.isCurse()) { final CardCollection opponents = CardLists.filterControlledBy(options, ai.getOpponents()); if (!opponents.isEmpty()) { @@ -1080,11 +1069,10 @@ public class CountersPutAi extends CountersAi { Player ai = sa.getActivatingPlayer(); GameEntity e = (GameEntity) params.get("Target"); // for Card try to select not useless counter - if (e instanceof Card) { - Card c = (Card) e; + if (e instanceof Card c) { if (c.getController().isOpponentOf(ai)) { - if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) { - return CounterType.get(CounterEnumType.M1M1); + if (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) { + return CounterEnumType.M1M1; } for (CounterType type : options) { if (ComputerUtil.isNegativeCounter(type, c)) { @@ -1098,15 +1086,14 @@ public class CountersPutAi extends CountersAi { } } } - } else if (e instanceof Player) { - Player p = (Player) e; + } else if (e instanceof Player p) { if (p.isOpponentOf(ai)) { - if (options.contains(CounterType.get(CounterEnumType.POISON))) { - return CounterType.get(CounterEnumType.POISON); + if (options.contains(CounterEnumType.POISON)) { + return CounterEnumType.POISON; } } else { - if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) { - return CounterType.get(CounterEnumType.EXPERIENCE); + if (options.contains(CounterEnumType.EXPERIENCE)) { + return CounterEnumType.EXPERIENCE; } } @@ -1211,9 +1198,8 @@ public class CountersPutAi extends CountersAi { } if (numCtrs < optimalCMC) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) { @@ -1233,9 +1219,8 @@ public class CountersPutAi extends CountersAi { if (numCtrs < optimalCMC) { // If the AI has less counters than the optimal CMC, it should play the ability. return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - // If the AI has enough counters or more than the optimal CMC, it should not play the ability. - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } + // If the AI has enough counters or more than the optimal CMC, it should not play the ability. + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java index e440110daf0..83f4f59a784 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java @@ -218,18 +218,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { Card tgt = (Card) params.get("Target"); // planeswalker has high priority for loyalty counters - if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) { - return CounterType.get(CounterEnumType.LOYALTY); + if (tgt.isPlaneswalker() && options.contains(CounterEnumType.LOYALTY)) { + return CounterEnumType.LOYALTY; } if (tgt.getController().isOpponentOf(ai)) { // creatures with BaseToughness below or equal zero might be // killed if their counters are removed if (tgt.isCreature() && tgt.getBaseToughness() <= 0) { - if (options.contains(CounterType.get(CounterEnumType.P1P1))) { - return CounterType.get(CounterEnumType.P1P1); - } else if (options.contains(CounterType.get(CounterEnumType.M1M1))) { - return CounterType.get(CounterEnumType.M1M1); + if (options.contains(CounterEnumType.P1P1)) { + return CounterEnumType.P1P1; + } else if (options.contains(CounterEnumType.M1M1)) { + return CounterEnumType.M1M1; } } @@ -241,17 +241,17 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { } } else { // this counters are treat first to be removed - if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) { + if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterEnumType.ICE)) { CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage"); boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate) Card::ignoreLegendRule); if (maritEmpty) { - return CounterType.get(CounterEnumType.ICE); + return CounterEnumType.ICE; } - } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) { - return CounterType.get(CounterEnumType.P1P1); - } else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) { - return CounterType.get(CounterEnumType.M1M1); + } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterEnumType.P1P1)) { + return CounterEnumType.P1P1; + } else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterEnumType.M1M1)) { + return CounterEnumType.M1M1; } // fallback logic, select positive counter to add more diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java index 8641772befc..ea28a216b43 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java @@ -384,7 +384,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (targetCard.getController().isOpponentOf(ai)) { // if its a Planeswalker try to remove Loyality first if (targetCard.isPlaneswalker()) { - return CounterType.get(CounterEnumType.LOYALTY); + return CounterEnumType.LOYALTY; } for (CounterType type : options) { if (!ComputerUtil.isNegativeCounter(type, targetCard)) { @@ -392,10 +392,10 @@ public class CountersRemoveAi extends SpellAbilityAi { } } } else { - if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) { - return CounterType.get(CounterEnumType.M1M1); - } else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) { - return CounterType.get(CounterEnumType.P1P1); + if (options.contains(CounterEnumType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) { + return CounterEnumType.M1M1; + } else if (options.contains(CounterEnumType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) { + return CounterEnumType.P1P1; } for (CounterType type : options) { if (ComputerUtil.isNegativeCounter(type, targetCard)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java index b28b7e89d03..e01fceb598a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java @@ -133,9 +133,9 @@ public class DamageAllAi extends SpellAbilityAi { if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) { // When using Pestilence to hurt players, do it at // the end of the opponent's turn only - if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))) - || ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) - && (ai.getGame().getNonactivePlayers().contains(ai))))) + if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")) + || (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) + && !ai.getGame().getPhaseHandler().isPlayerTurn(ai))) // Need further improvement : if able to kill immediately with repeated activations, do not wait // for phases! Will also need to implement considering repeated activations for killed creatures! // || (ai.sa.getPayCosts(). ??? ) diff --git a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java index a6ff092ffbc..9a646b9e0d2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java @@ -37,10 +37,6 @@ public class DigMultipleAi extends SpellAbilityAi { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - if ("Never".equals(sa.getParam("AILogic"))) { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } - // don't deck yourself if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) { int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java index 4d0f5420928..0a9cc6285b7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -26,7 +26,6 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.Card; import forge.game.card.CounterEnumType; -import forge.game.card.CounterType; import forge.game.cost.*; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -55,7 +54,7 @@ public class DrawAi extends SpellAbilityAi { } if (ComputerUtil.playImmediately(ai, sa)) { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Don't tap creatures that may be able to block @@ -74,9 +73,10 @@ public class DrawAi extends SpellAbilityAi { // TODO: make this configurable in the AI profile return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* @@ -174,9 +174,8 @@ public class DrawAi extends SpellAbilityAi { public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) { if (targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay())) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } /** @@ -370,7 +369,7 @@ public class DrawAi extends SpellAbilityAi { // try to make opponent lose to poison // currently only Caress of Phyrexia - if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + if (getPoison != null && oppA.canReceiveCounters(CounterEnumType.POISON)) { if (oppA.getPoisonCounters() + numCards > 9) { sa.getTargets().add(oppA); return true; @@ -414,7 +413,7 @@ public class DrawAi extends SpellAbilityAi { } } - if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + if (getPoison != null && ai.canReceiveCounters(CounterEnumType.POISON)) { if (numCards + ai.getPoisonCounters() >= 8) { aiTarget = false; } @@ -472,7 +471,7 @@ public class DrawAi extends SpellAbilityAi { } // ally would lose because of poison - if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON)) && ally.getPoisonCounters() + numCards > 9) { + if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards > 9) { continue; } @@ -541,9 +540,8 @@ public class DrawAi extends SpellAbilityAi { if (targetAI(ai, sa, mandatory)) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } /* (non-Javadoc) diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java index 746a5b7623f..8e7b0d073bc 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -270,7 +270,7 @@ public class EffectAi extends SpellAbilityAi { } return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("Fight")) { - return FightAi.canFightAi(ai, sa, 0,0); + return FightAi.canFight(ai, sa, 0,0); } else if (logic.equals("Pump")) { sa.resetTargets(); List options = CardUtil.getValidCardsToTarget(sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/FightAi.java b/forge-ai/src/main/java/forge/ai/ability/FightAi.java index 083aa8c155b..ff15bb89907 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java @@ -177,7 +177,7 @@ public class FightAi extends SpellAbilityAi { * @param power bonus to power * @return true if fight effect should be played, false otherwise */ - public static AiAbilityDecision canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) { + public static AiAbilityDecision canFight(final Player ai, final SpellAbility sa, int power, int toughness) { final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); AbilitySub tgtFight = sa.getSubAbility(); diff --git a/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java b/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java index 885c09975a8..031f4077b9c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java @@ -12,15 +12,13 @@ import forge.game.spellability.SpellAbility; public class FlipACoinAi extends SpellAbilityAi { /* (non-Javadoc) - * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) + * @see forge.card.abilityfactory.SpellAiLogic#checkApiLogic(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { if (sa.hasParam("AILogic")) { String ailogic = sa.getParam("AILogic"); - if (ailogic.equals("Never")) { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } else if (ailogic.equals("PhaseOut")) { + if (ailogic.equals("PhaseOut")) { if (!ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(sa.getHostCard())) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ManaAi.java b/forge-ai/src/main/java/forge/ai/ability/ManaAi.java index 9abccca74f3..6bd4e24a37e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManaAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManaAi.java @@ -159,7 +159,7 @@ public class ManaAi extends SpellAbilityAi { int numCounters = 0; int manaSurplus = 0; if ("Count$xPaid".equals(host.getSVar("X")) && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) { - CounterType ctrType = CounterType.get(CounterEnumType.KI); // Petalmane Baku + CounterType ctrType = CounterEnumType.KI; // Petalmane Baku for (CostPart part : sa.getPayCosts().getCostParts()) { if (part instanceof CostRemoveCounter) { ctrType = ((CostRemoveCounter)part).counter; diff --git a/forge-ai/src/main/java/forge/ai/ability/MillAi.java b/forge-ai/src/main/java/forge/ai/ability/MillAi.java index 2f6ae8947a0..4fe634834e0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java @@ -24,12 +24,7 @@ public class MillAi extends SpellAbilityAi { @Override protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { - PhaseHandler ph = ai.getGame().getPhaseHandler(); - - if (aiLogic.equals("Main1")) { - return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases") - || ComputerUtil.castSpellInMain1(ai, sa); - } else if (aiLogic.equals("LilianaMill")) { + if (aiLogic.equals("LilianaMill")) { // TODO convert to AICheckSVar // Only mill if a "Raise Dead" target is available, in case of control decks with few creatures return CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES).size() >= 1; @@ -55,9 +50,10 @@ public class MillAi extends SpellAbilityAi { // because they are also potentially useful for combat return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai); } - return true; + return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases") + || ComputerUtil.castSpellInMain1(ai, sa); } - + @Override protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { /* diff --git a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java index 407f3e00eb0..e98c6924426 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java @@ -6,10 +6,10 @@ import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardLists; +import forge.game.card.CardUtil; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.keyword.Keyword; -import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -38,9 +38,6 @@ public class MustBlockAi extends SpellAbilityAi { if (!list.isEmpty()) { final Card blocker = ComputerUtilCard.getBestCreatureAI(list); - if (blocker == null) { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } sa.getTargets().add(blocker); return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @@ -63,11 +60,6 @@ public class MustBlockAi extends SpellAbilityAi { protected AiAbilityDecision doTriggerNoCost(final Player ai, SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); - // only use on creatures that can attack - if (!ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } - Card attacker = source; if (sa.hasParam("DefinedAttacker")) { final List cards = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedAttacker"), sa); @@ -81,13 +73,9 @@ public class MustBlockAi extends SpellAbilityAi { boolean chance = false; if (sa.usesTargeting()) { - final List list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true); - if (list.isEmpty()) { - if (sa.isTargetNumberValid()) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); - } + List list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true); + if (list.isEmpty() && mandatory) { + list = CardUtil.getValidCardsToTarget(sa); } final Card blocker = ComputerUtilCard.getBestCreatureAI(list); if (blocker == null) { diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java index 60f13d95328..200d5648d91 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java @@ -78,7 +78,7 @@ public class PermanentCreatureAi extends PermanentAi { || ph.getPhase().isBefore(PhaseType.END_OF_TURN)) && ai.getManaPool().totalMana() <= 0 && (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) - && (!card.hasETBTrigger(true) && !card.hasSVar("AmbushAI")) + && !card.hasETBTrigger(true) && !card.hasSVar("AmbushAI") && game.getStack().isEmpty() && !ComputerUtil.castPermanentInMain1(ai, sa)) { // AiPlayDecision.AnotherTime; diff --git a/forge-ai/src/main/java/forge/ai/ability/PhasesAi.java b/forge-ai/src/main/java/forge/ai/ability/PhasesAi.java index 5cdbaafdc06..02ae927180f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PhasesAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PhasesAi.java @@ -33,10 +33,8 @@ public class PhasesAi extends SpellAbilityAi { final boolean isThreatened = ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(source); if (isThreatened) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); diff --git a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java index 650af4ec3e8..66c177e0a53 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java @@ -6,7 +6,6 @@ import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.CounterEnumType; -import forge.game.card.CounterType; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.GameLossReason; @@ -65,7 +64,7 @@ public class PoisonAi extends SpellAbilityAi { boolean result; if (sa.usesTargeting()) { result = tgtPlayer(ai, sa, mandatory); - } else if (mandatory || !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + } else if (mandatory || !ai.canReceiveCounters(CounterEnumType.POISON)) { // mandatory or ai is uneffected result = true; } else { @@ -90,7 +89,7 @@ public class PoisonAi extends SpellAbilityAi { PlayerCollection betterTgts = tgts.filter(input -> { if (input.cantLoseCheck(GameLossReason.Poisoned)) { return false; - } else if (!input.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + } else if (!input.canReceiveCounters(CounterEnumType.POISON)) { return false; } return true; @@ -109,7 +108,7 @@ public class PoisonAi extends SpellAbilityAi { if (tgts.isEmpty()) { if (mandatory) { // AI is uneffected - if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterEnumType.POISON)) { sa.getTargets().add(ai); return true; } @@ -121,7 +120,7 @@ public class PoisonAi extends SpellAbilityAi { if (input.cantLoseCheck(GameLossReason.Poisoned)) { return true; } - return !input.canReceiveCounters(CounterType.get(CounterEnumType.POISON)); + return !input.canReceiveCounters(CounterEnumType.POISON); }); if (!betterAllies.isEmpty()) { allies = betterAllies; diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java index 3c0cec2634d..948337ed207 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -339,11 +339,11 @@ public class PumpAi extends PumpAiBase { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - if (!pumpTgtAI(ai, sa, defense, attack, false, false)) { - return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + if (pumpTgtAI(ai, sa, defense, attack, false, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory, @@ -453,7 +453,7 @@ public class PumpAi extends PumpAiBase { } if (isFight) { - return FightAi.canFightAi(ai, sa, attack, defense).willingToPlay(); + return FightAi.canFight(ai, sa, attack, defense).willingToPlay(); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java index 83f74410723..c082ea153d6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java @@ -131,7 +131,7 @@ public class PumpAllAi extends PumpAiBase { boolean result = ai.getCreaturesInPlay().anyMatch(c -> c.isValid(valid, source.getController(), source, sa) && ComputerUtilCard.shouldPumpCard(ai, sa, c, defense, power, keywords)); return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } // pumpAllCanPlayAI() + } @Override public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) { diff --git a/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java b/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java index adddae91d79..c36128335d0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java @@ -8,7 +8,6 @@ import forge.ai.SpellAbilityAi; import forge.game.card.Card; import forge.game.card.CardPredicates; import forge.game.card.CounterEnumType; -import forge.game.card.CounterType; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerController; @@ -40,7 +39,7 @@ public class TimeTravelAi extends SpellAbilityAi { // so removing them is good; stuff on the battlefield is usually stuff like Vanishing or As Foretold, which favors adding Time // counters for better effect, but exceptions should be added here). Card target = (Card)params.get("Target"); - return !ComputerUtil.isNegativeCounter(CounterType.get(CounterEnumType.TIME), target); + return !ComputerUtil.isNegativeCounter(CounterEnumType.TIME, target); } @Override diff --git a/forge-core/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java index 9e88336b61d..e6b2c166685 100644 --- a/forge-core/src/main/java/forge/ImageKeys.java +++ b/forge-core/src/main/java/forge/ImageKeys.java @@ -31,6 +31,8 @@ public final class ImageKeys { public static final String MONARCH_IMAGE = "monarch"; public static final String THE_RING_IMAGE = "the_ring"; public static final String RADIATION_IMAGE = "radiation"; + public static final String SPEED_IMAGE = "speed"; + public static final String MAX_SPEED_IMAGE = "max_speed"; public static final String BACKFACE_POSTFIX = "$alt"; public static final String SPECFACE_W = "$wspec"; diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java index 3ebb8092dad..249724aa50a 100644 --- a/forge-core/src/main/java/forge/StaticData.java +++ b/forge-core/src/main/java/forge/StaticData.java @@ -29,8 +29,6 @@ import java.util.stream.Collectors; public class StaticData { private final CardStorageReader cardReader; private final CardStorageReader tokenReader; - private final CardStorageReader customCardReader; - private final String blockDataFolder; private final CardDb commonCards; private final CardDb variantCards; @@ -79,7 +77,6 @@ public class StaticData { this.tokenReader = tokenReader; this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder))); this.blockDataFolder = blockDataFolder; - this.customCardReader = customCardReader; this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance; this.enableSmartCardArtSelection = enableSmartCardArtSelection; this.loadNonLegalCards = loadNonLegalCards; @@ -881,7 +878,7 @@ public class StaticData { } } } - // stream().toList() causes crash on Android, use Collectors.toList() + // stream().toList() causes crash on Android 8-13, use Collectors.toList() List NIF = new ArrayList<>(NIF_Q).stream().sorted().collect(Collectors.toList()); List CNI = new ArrayList<>(CNI_Q).stream().sorted().collect(Collectors.toList()); List TOK = new ArrayList<>(TOKEN_Q).stream().sorted().collect(Collectors.toList()); diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java index 296b985958c..4c4cc36242b 100644 --- a/forge-core/src/main/java/forge/card/CardDb.java +++ b/forge-core/src/main/java/forge/card/CardDb.java @@ -45,8 +45,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { public final static char NameSetSeparator = '|'; public final static String FlagPrefix = "#"; public static final String FlagSeparator = "\t"; - private final String exlcudedCardName = "Concentrate"; - private final String exlcudedCardSet = "DS0"; // need this to obtain cardReference by name+set+artindex private final ListMultimap allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList); @@ -303,7 +301,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { // create faces list from rules for (final CardRules rule : rules.values()) { - if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName())) + if (filteredCards.contains(rule.getName())) continue; for (ICardFace face : rule.getAllFaces()) { addFaceToDbNames(face); @@ -501,8 +499,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { } public void addCard(PaperCard paperCard) { - if (excludeCard(paperCard.getName(), paperCard.getEdition())) + if (filtered.contains(paperCard.getName())) { return; + } allCardsByName.put(paperCard.getName(), paperCard); @@ -523,17 +522,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { } } - private boolean excludeCard(String cardName, String cardEdition) { - if (filtered.isEmpty()) - return false; - if (filtered.contains(cardName)) { - if (exlcudedCardSet.equalsIgnoreCase(cardEdition) && exlcudedCardName.equalsIgnoreCase(cardName)) - return true; - else return !exlcudedCardName.equalsIgnoreCase(cardName); - } - return false; - } - private void reIndex() { uniqueCardsByName.clear(); for (Entry> kv : allCardsByName.asMap().entrySet()) { diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java index df16246453f..3a69cfd0b37 100644 --- a/forge-core/src/main/java/forge/card/CardEdition.java +++ b/forge-core/src/main/java/forge/card/CardEdition.java @@ -52,6 +52,14 @@ import java.util.stream.Collectors; */ public final class CardEdition implements Comparable { + public DraftOptions getDraftOptions() { + return draftOptions; + } + + public void setDraftOptions(DraftOptions draftOptions) { + this.draftOptions = draftOptions; + } + // immutable public enum Type { UNKNOWN, @@ -275,18 +283,22 @@ public final class CardEdition implements Comparable { // Booster/draft info private List boosterSlots = null; private boolean smallSetOverride = false; - private boolean foilAlwaysInCommonSlot = false; + private String additionalUnlockSet = ""; private FoilType foilType = FoilType.NOT_SUPPORTED; + + // Replace all of these things with booster slots + private boolean foilAlwaysInCommonSlot = false; private double foilChanceInBooster = 0; private double chanceReplaceCommonWith = 0; private String slotReplaceCommonWith = "Common"; private String additionalSheetForFoils = ""; - private String additionalUnlockSet = ""; private String boosterMustContain = ""; private String boosterReplaceSlotFromPrintSheet = ""; private String sheetReplaceCardFromSheet = ""; private String sheetReplaceCardFromSheet2 = ""; - private String doublePickDuringDraft = ""; + + // Draft options + private DraftOptions draftOptions = null; private String[] chaosDraftThemes = new String[0]; private final ListMultimap cardMap; @@ -373,7 +385,6 @@ public final class CardEdition implements Comparable { public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; } public String getAdditionalSheetForFoils() { return additionalSheetForFoils; } public String getAdditionalUnlockSet() { return additionalUnlockSet; } - public String getDoublePickDuringDraft() { return doublePickDuringDraft; } public String getBoosterMustContain() { return boosterMustContain; } public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; } public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; } @@ -619,7 +630,7 @@ public final class CardEdition implements Comparable { * functional variant name - grouping #9 */ // "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$" - "(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$" + "(^(.?[0-9A-Z-]+\\S*[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$" ); final Pattern tokenPattern = Pattern.compile( @@ -628,7 +639,7 @@ public final class CardEdition implements Comparable { * name - grouping #3 * artist name - grouping #5 */ - "(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$" + "(^(.?[0-9A-Z-]+\\S?[A-Z☇]*)\\s)?([^@]*)( @(.*))?$" ); ListMultimap cardMap = ArrayListMultimap.create(); @@ -649,31 +660,37 @@ public final class CardEdition implements Comparable { continue; } - // parse sections of the format " " - if (editionSectionsWithCollectorNumbers.contains(sectionName)) { - for(String line : contents.get(sectionName)) { - Matcher matcher = pattern.matcher(line); - - if (!matcher.matches()) { - continue; - } - - String collectorNumber = matcher.group(2); - CardRarity r = CardRarity.smartValueOf(matcher.group(4)); - String cardName = matcher.group(5); - String artistName = matcher.group(7); - String functionalVariantName = matcher.group(9); - EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName); - - cardMap.put(sectionName, cis); - } - } else if (boosterSlotsToParse.contains(sectionName)) { - // parse booster slots of the format "Base=N\n|Replace= " - boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName))); + if (sectionName.endsWith("Types")) { + CardType.Helper.parseTypes(sectionName, contents.get(sectionName)); } else { - // save custom print sheets of the format " ||" - // to parse later when printsheets are loaded lazily (and the cardpool is already initialized) - customPrintSheetsToParse.put(sectionName, contents.get(sectionName)); + // Parse cards + + // parse sections of the format " " + if (editionSectionsWithCollectorNumbers.contains(sectionName)) { + for(String line : contents.get(sectionName)) { + Matcher matcher = pattern.matcher(line); + + if (!matcher.matches()) { + continue; + } + + String collectorNumber = matcher.group(2); + CardRarity r = CardRarity.smartValueOf(matcher.group(4)); + String cardName = matcher.group(5); + String artistName = matcher.group(7); + String functionalVariantName = matcher.group(9); + EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName); + + cardMap.put(sectionName, cis); + } + } else if (boosterSlotsToParse.contains(sectionName)) { + // parse booster slots of the format "Base=N\n|Replace= " + boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName))); + } else { + // save custom print sheets of the format " ||" + // to parse later when printsheets are loaded lazily (and the cardpool is already initialized) + customPrintSheetsToParse.put(sectionName, contents.get(sectionName)); + } } } @@ -802,7 +819,6 @@ public final class CardEdition implements Comparable { res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral res.smallSetOverride = metadata.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon) - res.doublePickDuringDraft = metadata.get("DoublePick", ""); // "FirstPick" or "Always" res.boosterMustContain = metadata.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature res.boosterReplaceSlotFromPrintSheet = metadata.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card @@ -810,6 +826,23 @@ public final class CardEdition implements Comparable { res.sheetReplaceCardFromSheet2 = metadata.get("SheetReplaceCardFromSheet2", ""); res.chaosDraftThemes = metadata.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names + // Draft options + String doublePick = metadata.get("DoublePick", "Never"); + int maxPodSize = metadata.getInt("MaxPodSize", 8); + int recommendedPodSize = metadata.getInt("RecommendedPodSize", 8); + int maxMatchPlayers = metadata.getInt("MaxMatchPlayers", 2); + String deckType = metadata.get("DeckType", "Normal"); + String freeCommander = metadata.get("FreeCommander", ""); + + res.draftOptions = new DraftOptions( + doublePick, + maxPodSize, + recommendedPodSize, + maxMatchPlayers, + deckType, + freeCommander + ); + return res; } @@ -840,7 +873,7 @@ public final class CardEdition implements Comparable { @Override public void add(CardEdition item) { //Even though we want it to be read only, make an exception for custom content. if(lock) throw new UnsupportedOperationException("This is a read-only storage"); - else map.put(item.getName(), item); + else map.put(item.getCode(), item); } public void append(CardEdition.Collection C){ //Append custom editions if (lock) throw new UnsupportedOperationException("This is a read-only storage"); @@ -985,16 +1018,13 @@ public final class CardEdition implements Comparable { public static final Predicate HAS_BOOSTER_BOX = edition -> edition.getBoosterBoxCount() > 0; + @Deprecated //Use CardEdition::hasBasicLands and a nonnull test. public static final Predicate hasBasicLands = ed -> { if (ed == null) { // Happens for new sets with "???" code return false; } - for(String landName : MagicColor.Constant.BASIC_LANDS) { - if (null == StaticData.instance().getCommonCards().getCard(landName, ed.getCode(), 0)) - return false; - } - return true; + return ed.hasBasicLands(); }; } @@ -1015,7 +1045,7 @@ public final class CardEdition implements Comparable { public boolean hasBasicLands() { for(String landName : MagicColor.Constant.BASIC_LANDS) { - if (null == StaticData.instance().getCommonCards().getCard(landName, this.getCode(), 0)) + if (this.getCardInSet(landName).isEmpty()) return false; } return true; diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index b2b41a1bd03..3d3ffe0648c 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -53,6 +53,7 @@ public final class CardRules implements ICardCharacteristics { private boolean addsWildCardColor; private int setColorID; private boolean custom; + private boolean unsupported; private String path; public CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) { @@ -167,21 +168,7 @@ public final class CardRules implements ICardCharacteristics { } public boolean isTransformable() { - if (CardSplitType.Transform == getSplitType()) { - return true; - } - if (CardSplitType.Modal != getSplitType()) { - return false; - } - for (ICardFace face : getAllFaces()) { - for (String spell : face.getAbilities()) { - if (spell.contains("AB$ SetState") && spell.contains("Mode$ Transform")) { - return true; - } - } - // TODO check keywords if needed - } - return false; + return CardSplitType.Transform == getSplitType() || CardSplitType.Modal == getSplitType(); } public ICardFace getWSpecialize() { @@ -220,7 +207,9 @@ public final class CardRules implements ICardCharacteristics { } public boolean isCustom() { return custom; } - public void setCustom() { custom = true; } + public void setCustom() { custom = true; } + + public boolean isUnsupported() { return unsupported; } @Override public CardType getType() { @@ -335,6 +324,15 @@ public final class CardRules implements ICardCharacteristics { } if (hasKeyword("Friends forever") && b.hasKeyword("Friends forever")) { legal = true; // Stranger Things Secret Lair gimmick partner commander + } + if (hasKeyword("Partner - Survivors") && b.hasKeyword("Partner - Survivors")) { + legal = true; // The Last of Us Secret Lair gimmick partner commander + } + if (hasKeyword("Partner - Father & Son") && b.hasKeyword("Partner - Father & Son")) { + legal = true; // God of War Secret Lair gimmick partner commander + } + if (hasKeyword("Partner - Character select") && b.hasKeyword("Partner - Character select")) { + legal = true; // TMNT Commander deck gimmick partner commander } if (hasKeyword("Choose a Background") && b.canBeBackground() || b.hasKeyword("Choose a Background") && canBeBackground()) { @@ -353,6 +351,7 @@ public final class CardRules implements ICardCharacteristics { } return canBeCommander() && (hasKeyword("Partner") || !this.partnerWith.isEmpty() || hasKeyword("Friends forever") || hasKeyword("Choose a Background") || + hasKeyword("Partner - Father & Son") || hasKeyword("Partner - Survivors") || hasKeyword("Partner - Character select") || hasKeyword("Doctor's companion") || isDoctor()); } @@ -373,6 +372,9 @@ public final class CardRules implements ICardCharacteristics { public boolean canBeOathbreaker() { CardType type = mainPart.getType(); + if (mainPart.getOracleText().contains("can be your commander")) { + return true; + } return type.isPlaneswalker(); } @@ -825,6 +827,8 @@ public final class CardRules implements ICardCharacteristics { faces[0].assignMissingFields(); final CardRules result = new CardRules(faces, CardSplitType.None, cah); + result.unsupported = true; + return result; } diff --git a/forge-core/src/main/java/forge/card/CardRulesPredicates.java b/forge-core/src/main/java/forge/card/CardRulesPredicates.java index 505c70f86e8..ef658eff833 100644 --- a/forge-core/src/main/java/forge/card/CardRulesPredicates.java +++ b/forge-core/src/main/java/forge/card/CardRulesPredicates.java @@ -196,6 +196,31 @@ public final class CardRulesPredicates { return card -> card.getSplitType().equals(type); } + /** + * @return a Predicate that matches cards that are vanilla. + */ + public static Predicate isVanilla() { + return card -> { + if (!(card.getType().isCreature() || card.getType().isLand()) || + card.getSplitType() != CardSplitType.None || + card.hasFunctionalVariants()) { + return false; + } + + ICardFace mainPart = card.getMainPart(); + + boolean hasAny = + mainPart.getKeywords().iterator().hasNext() || + mainPart.getAbilities().iterator().hasNext() || + mainPart.getStaticAbilities().iterator().hasNext() || + mainPart.getTriggers().iterator().hasNext() || + (mainPart.getDraftActions() != null && mainPart.getDraftActions().iterator().hasNext()) || + mainPart.getReplacements().iterator().hasNext(); + + return !hasAny; + }; + } + /** * Checks for color. * diff --git a/forge-core/src/main/java/forge/card/CardType.java b/forge-core/src/main/java/forge/card/CardType.java index 84d9edc49d3..b8998629be2 100644 --- a/forge-core/src/main/java/forge/card/CardType.java +++ b/forge-core/src/main/java/forge/card/CardType.java @@ -1066,4 +1066,74 @@ public final class CardType implements Comparable, CardTypeView { return type; } + public static class Helper { + public static final void parseTypes(String sectionName, List content) { + Set addToSection = null; + + switch (sectionName) { + case "BasicTypes": + addToSection = CardType.Constant.BASIC_TYPES; + break; + case "LandTypes": + addToSection = CardType.Constant.LAND_TYPES; + break; + case "CreatureTypes": + addToSection = CardType.Constant.CREATURE_TYPES; + break; + case "SpellTypes": + addToSection = CardType.Constant.SPELL_TYPES; + break; + case "EnchantmentTypes": + addToSection = CardType.Constant.ENCHANTMENT_TYPES; + break; + case "ArtifactTypes": + addToSection = CardType.Constant.ARTIFACT_TYPES; + break; + case "WalkerTypes": + addToSection = CardType.Constant.WALKER_TYPES; + break; + case "DungeonTypes": + addToSection = CardType.Constant.DUNGEON_TYPES; + break; + case "BattleTypes": + addToSection = CardType.Constant.BATTLE_TYPES; + break; + case "PlanarTypes": + addToSection = CardType.Constant.PLANAR_TYPES; + break; + } + + if (addToSection == null) { + return; + } + + for(String line : content) { + if (line.length() == 0) continue; + + if (line.contains(":")) { + String[] k = line.split(":"); + + if (addToSection.contains(k[0])) { + continue; + } + + addToSection.add(k[0]); + CardType.Constant.pluralTypes.put(k[0], k[1]); + + if (k[0].contains(" ")) { + CardType.Constant.MultiwordTypes.add(k[0]); + } + } else { + if (addToSection.contains(line)) { + continue; + } + + addToSection.add(line); + if (line.contains(" ")) { + CardType.Constant.MultiwordTypes.add(line); + } + } + } + } + } } diff --git a/forge-core/src/main/java/forge/card/ColorSet.java b/forge-core/src/main/java/forge/card/ColorSet.java index abf25c085da..e4b3fe19a5f 100644 --- a/forge-core/src/main/java/forge/card/ColorSet.java +++ b/forge-core/src/main/java/forge/card/ColorSet.java @@ -17,15 +17,12 @@ */ package forge.card; -import com.google.common.collect.UnmodifiableIterator; import forge.card.MagicColor.Color; import forge.card.mana.ManaCost; -import forge.card.mana.ManaCostShard; import forge.util.BinaryUtil; import java.io.Serializable; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -38,28 +35,64 @@ import java.util.stream.Stream; * * */ -public final class ColorSet implements Comparable, Iterable, Serializable { - private static final long serialVersionUID = 794691267379929080L; +public enum ColorSet implements Iterable, Serializable { - private final byte myColor; + C(Color.COLORLESS), + W(Color.WHITE), + U(Color.BLUE), + WU(Color.WHITE, Color.BLUE), + B(Color.BLACK), + WB(Color.WHITE, Color.BLACK), + UB(Color.BLUE, Color.BLACK), + WUB(Color.WHITE, Color.BLUE, Color.BLACK), + R(Color.RED), + RW(Color.RED, Color.WHITE), + UR(Color.BLUE, Color.RED), + URW(Color.BLUE, Color.RED, Color.WHITE), + BR(Color.BLACK, Color.RED), + RWB(Color.RED, Color.WHITE, Color.BLACK), + UBR(Color.BLUE, Color.BLACK, Color.RED), + WUBR(Color.WHITE, Color.BLUE, Color.BLACK, Color.RED), + G(Color.GREEN), + GW(Color.GREEN, Color.WHITE), + GU(Color.GREEN, Color.BLUE), + GWU(Color.GREEN, Color.WHITE, Color.BLUE), + BG(Color.BLACK, Color.GREEN), + WBG(Color.WHITE, Color.BLACK, Color.GREEN), + BGU(Color.BLACK, Color.GREEN, Color.BLUE), + GWUB(Color.GREEN, Color.WHITE, Color.BLUE, Color.BLACK), + RG(Color.RED, Color.GREEN), + RGW(Color.RED, Color.GREEN, Color.WHITE), + GUR(Color.GREEN, Color.BLUE, Color.RED), + RGWU(Color.RED, Color.GREEN, Color.WHITE, Color.BLUE), + BRG(Color.BLACK, Color.RED, Color.GREEN), + BRGW(Color.BLACK, Color.RED, Color.GREEN, Color.WHITE), + UBRG(Color.BLUE, Color.BLACK, Color.RED, Color.GREEN), + WUBRG(Color.WHITE, Color.BLUE, Color.BLACK, Color.RED, Color.GREEN) + ; + + private static final long serialVersionUID = 794691267379929080L; + // needs to be before other static + + private final Collection orderedShards; private final float orderWeight; - private static final ColorSet[] cache = new ColorSet[32]; - - public static final ColorSet ALL_COLORS = fromMask(MagicColor.ALL_COLORS); - private static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS); - - private ColorSet(final byte mask) { - this.myColor = mask; - this.orderWeight = this.getOrderWeight(); + private ColorSet(final Color... ordered) { + this.orderedShards = Arrays.asList(ordered); + this.orderWeight = this.calcOrderWeight(); } public static ColorSet fromMask(final int mask) { final int mask32 = mask & MagicColor.ALL_COLORS; - if (cache[mask32] == null) { - cache[mask32] = new ColorSet((byte) mask32); + return values()[mask32]; + } + + public static ColorSet fromEnums(final Color... colors) { + byte mask = 0; + for (Color e : colors) { + mask |= e.getColorMask(); } - return cache[mask32]; + return fromMask(mask); } public static ColorSet fromNames(final String... colors) { @@ -98,7 +131,10 @@ public final class ColorSet implements Comparable, Iterable, Ser * @return true, if successful */ public boolean hasAnyColor(final int colormask) { - return (this.myColor & colormask) != 0; + return (this.ordinal() & colormask) != 0; + } + public boolean hasAnyColor(final Color c) { + return this.orderedShards.contains(c); } /** @@ -109,12 +145,12 @@ public final class ColorSet implements Comparable, Iterable, Ser * @return true, if successful */ public boolean hasAllColors(final int colormask) { - return (this.myColor & colormask) == colormask; + return (this.ordinal() & colormask) == colormask; } /** this has exactly the colors defined by operand. */ public boolean hasExactlyColor(final int colormask) { - return this.myColor == colormask; + return this.ordinal() == colormask; } /** this has no other colors except defined by operand. */ @@ -124,17 +160,17 @@ public final class ColorSet implements Comparable, Iterable, Ser /** this has no other colors except defined by operand. */ public boolean hasNoColorsExcept(final int colormask) { - return (this.myColor & ~colormask) == 0; + return (this.ordinal() & ~colormask) == 0; } /** This returns the colors that colormask contains that are not in color */ public ColorSet getMissingColors(final byte colormask) { - return fromMask(this.myColor & ~colormask); + return fromMask(this.ordinal() & ~colormask); } /** Operand has no other colors except defined by this. */ public boolean containsAllColorsFrom(final int colorProfile) { - return (~this.myColor & colorProfile) == 0; + return (~this.ordinal() & colorProfile) == 0; } /** @@ -143,7 +179,7 @@ public final class ColorSet implements Comparable, Iterable, Ser * @return the int */ public int countColors() { - return BinaryUtil.bitCount(this.myColor); + return BinaryUtil.bitCount(this.ordinal()); } // bit count // order has to be: W U B R G multi colorless - same as cards numbering @@ -153,7 +189,7 @@ public final class ColorSet implements Comparable, Iterable, Ser * * @return the order weight */ - private float getOrderWeight() { + private float calcOrderWeight() { float res = this.countColors(); if (hasWhite()) { res += 0.0005f; @@ -172,6 +208,10 @@ public final class ColorSet implements Comparable, Iterable, Ser } return res; } + public float getOrderWeight() + { + return orderWeight; + } /** * Checks if is colorless. @@ -179,7 +219,7 @@ public final class ColorSet implements Comparable, Iterable, Ser * @return true, if is colorless */ public boolean isColorless() { - return this.myColor == 0; + return this == C; } /** @@ -197,7 +237,7 @@ public final class ColorSet implements Comparable, Iterable, Ser * @return true, if is all colors */ public boolean isAllColors() { - return this == ALL_COLORS; + return this == WUBRG; } /** @@ -217,17 +257,7 @@ public final class ColorSet implements Comparable, Iterable, Ser * @return true, if is equal */ public boolean isEqual(final byte color) { - return color == this.myColor; - } - - /* - * (non-Javadoc) - * - * @see java.lang.Comparable#compareTo(java.lang.Object) - */ - @Override - public int compareTo(final ColorSet other) { - return Float.compare(this.orderWeight, other.orderWeight); + return color == this.ordinal(); } // Presets @@ -277,33 +307,13 @@ public final class ColorSet implements Comparable, Iterable, Ser } public ColorSet inverse() { - byte mask = this.myColor; + byte mask = (byte)this.ordinal(); mask ^= MagicColor.ALL_COLORS; return fromMask(mask); } public byte getColor() { - return myColor; - } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - final ManaCostShard[] orderedShards = getOrderedShards(); - return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining()); - } - - /** - * Gets the null color. - * - * @return the nullColor - */ - public static ColorSet getNullColor() { - return NO_COLORS; + return (byte)ordinal(); } /** @@ -313,7 +323,7 @@ public final class ColorSet implements Comparable, Iterable, Ser * @return true, if successful */ public boolean sharesColorWith(final ColorSet ccOther) { - return (this.myColor & ccOther.myColor) != 0; + return (this.ordinal() & ccOther.ordinal()) != 0; } public ColorSet getSharedColors(final ColorSet ccOther) { @@ -321,123 +331,24 @@ public final class ColorSet implements Comparable, Iterable, Ser } public ColorSet getOffColors(final ColorSet ccOther) { - return fromMask(~this.myColor & ccOther.myColor); + return fromMask(~this.ordinal() & ccOther.ordinal()); } public Set toEnumSet() { - if (isColorless()) { - return EnumSet.of(Color.COLORLESS); - } - List list = new ArrayList<>(); - for (Color c : Color.values()) { - if (hasAnyColor(c.getColormask())) { - list.add(c); - } - } - return EnumSet.copyOf(list); + return EnumSet.copyOf(orderedShards); } - @Override - public Iterator iterator() { - return new ColorIterator(); + //@Override + public Iterator iterator() { + return this.orderedShards.iterator(); } - private class ColorIterator extends UnmodifiableIterator { - int currentBit = -1; - - private int getIndexOfNextColor(){ - int nextBit = currentBit + 1; - while (nextBit < MagicColor.NUMBER_OR_COLORS) { - if ((myColor & MagicColor.WUBRG[nextBit]) != 0) { - break; - } - nextBit++; - } - return nextBit; - } - - @Override - public boolean hasNext() { - return getIndexOfNextColor() < MagicColor.NUMBER_OR_COLORS; - } - - @Override - public Byte next() { - currentBit = getIndexOfNextColor(); - if (currentBit >= MagicColor.NUMBER_OR_COLORS) { - throw new NoSuchElementException(); - } - - return MagicColor.WUBRG[currentBit]; - } - } - - public Stream stream() { - return this.toEnumSet().stream(); + public Stream stream() { + return this.orderedShards.stream(); } //Get array of mana cost shards for color set in the proper order - public ManaCostShard[] getOrderedShards() { - return shardOrderLookup[myColor]; - } - - private static final ManaCostShard[][] shardOrderLookup = new ManaCostShard[MagicColor.ALL_COLORS + 1][]; - static { - byte COLORLESS = MagicColor.COLORLESS; - byte WHITE = MagicColor.WHITE; - byte BLUE = MagicColor.BLUE; - byte BLACK = MagicColor.BLACK; - byte RED = MagicColor.RED; - byte GREEN = MagicColor.GREEN; - ManaCostShard C = ManaCostShard.COLORLESS; - ManaCostShard W = ManaCostShard.WHITE; - ManaCostShard U = ManaCostShard.BLUE; - ManaCostShard B = ManaCostShard.BLACK; - ManaCostShard R = ManaCostShard.RED; - ManaCostShard G = ManaCostShard.GREEN; - - //colorless - shardOrderLookup[COLORLESS] = new ManaCostShard[] { C }; - - //mono-color - shardOrderLookup[WHITE] = new ManaCostShard[] { W }; - shardOrderLookup[BLUE] = new ManaCostShard[] { U }; - shardOrderLookup[BLACK] = new ManaCostShard[] { B }; - shardOrderLookup[RED] = new ManaCostShard[] { R }; - shardOrderLookup[GREEN] = new ManaCostShard[] { G }; - - //two-color - shardOrderLookup[WHITE | BLUE] = new ManaCostShard[] { W, U }; - shardOrderLookup[WHITE | BLACK] = new ManaCostShard[] { W, B }; - shardOrderLookup[BLUE | BLACK] = new ManaCostShard[] { U, B }; - shardOrderLookup[BLUE | RED] = new ManaCostShard[] { U, R }; - shardOrderLookup[BLACK | RED] = new ManaCostShard[] { B, R }; - shardOrderLookup[BLACK | GREEN] = new ManaCostShard[] { B, G }; - shardOrderLookup[RED | GREEN] = new ManaCostShard[] { R, G }; - shardOrderLookup[RED | WHITE] = new ManaCostShard[] { R, W }; - shardOrderLookup[GREEN | WHITE] = new ManaCostShard[] { G, W }; - shardOrderLookup[GREEN | BLUE] = new ManaCostShard[] { G, U }; - - //three-color - shardOrderLookup[WHITE | BLUE | BLACK] = new ManaCostShard[] { W, U, B }; - shardOrderLookup[WHITE | BLACK | GREEN] = new ManaCostShard[] { W, B, G }; - shardOrderLookup[BLUE | BLACK | RED] = new ManaCostShard[] { U, B, R }; - shardOrderLookup[BLUE | RED | WHITE] = new ManaCostShard[] { U, R, W }; - shardOrderLookup[BLACK | RED | GREEN] = new ManaCostShard[] { B, R, G }; - shardOrderLookup[BLACK | GREEN | BLUE] = new ManaCostShard[] { B, G, U }; - shardOrderLookup[RED | GREEN | WHITE] = new ManaCostShard[] { R, G, W }; - shardOrderLookup[RED | WHITE | BLACK] = new ManaCostShard[] { R, W, B }; - shardOrderLookup[GREEN | WHITE | BLUE] = new ManaCostShard[] { G, W, U }; - shardOrderLookup[GREEN | BLUE | RED] = new ManaCostShard[] { G, U, R }; - - //four-color - shardOrderLookup[WHITE | BLUE | BLACK | RED] = new ManaCostShard[] { W, U, B, R }; - shardOrderLookup[BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { U, B, R, G }; - shardOrderLookup[BLACK | RED | GREEN | WHITE] = new ManaCostShard[] { B, R, G, W }; - shardOrderLookup[RED | GREEN | WHITE | BLUE] = new ManaCostShard[] { R, G, W, U }; - shardOrderLookup[GREEN | WHITE | BLUE | BLACK] = new ManaCostShard[] { G, W, U, B }; - - //five-color - shardOrderLookup[WHITE | BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { W, U, B, R, G }; + public Collection getOrderedColors() { + return orderedShards; } } diff --git a/forge-core/src/main/java/forge/card/DraftOptions.java b/forge-core/src/main/java/forge/card/DraftOptions.java new file mode 100644 index 00000000000..cdeca05203f --- /dev/null +++ b/forge-core/src/main/java/forge/card/DraftOptions.java @@ -0,0 +1,75 @@ +package forge.card; + +public class DraftOptions { + public enum DoublePick { + NEVER, + FIRST_PICK, // only first pick each pack + WHEN_POD_SIZE_IS_4, // only when pod size is 4, so you can pick two cards each time + ALWAYS // each time you receive a pack, you can pick two cards + }; + public enum DeckType { + Normal, // Standard deck, usually 40 cards + Commander // Special deck type for Commander format. Important for selection/construction + } + + private DoublePick doublePick = DoublePick.NEVER; + private final int maxPodSize; // Usually 8, but could be smaller for cubes. I guess it could be larger too + private final int recommendedPodSize; // Usually 8, but is 4 for new double pick + private final int maxMatchPlayers; // Usually 2, but 4 for things like Commander or Conspiracy + private final DeckType deckType; // Normal or Commander + private final String freeCommander; + + public DraftOptions(String doublePickOption, int maxPodSize, int recommendedPodSize, int maxMatchPlayers, String deckType, String freeCommander) { + this.maxPodSize = maxPodSize; + this.recommendedPodSize = recommendedPodSize; + this.maxMatchPlayers = maxMatchPlayers; + this.deckType = DeckType.valueOf(deckType); + this.freeCommander = freeCommander; + if (doublePickOption != null) { + switch (doublePickOption.toLowerCase()) { + case "firstpick": + doublePick = DoublePick.FIRST_PICK; + break; + case "always": + doublePick = DoublePick.ALWAYS; + break; + case "whenpodsizeis4": + doublePick = DoublePick.WHEN_POD_SIZE_IS_4; + break; + } + } + + } + public int getMaxPodSize() { + return maxPodSize; + } + public int getRecommendedPodSize() { + return recommendedPodSize; + } + public DoublePick getDoublePick() { + return doublePick; + } + + public DoublePick isDoublePick(int podSize) { + if (doublePick == DoublePick.WHEN_POD_SIZE_IS_4) { + if (podSize != 4) { + return DoublePick.NEVER; + } + // only when pod size is 4, so you can pick two cards each time + return DoublePick.ALWAYS; + } + + return doublePick; + } + + + public int getMaxMatchPlayers() { + return maxMatchPlayers; + } + public DeckType getDeckType() { + return deckType; + } + public String getFreeCommander() { + return freeCommander; + } +} diff --git a/forge-core/src/main/java/forge/card/ICardDatabase.java b/forge-core/src/main/java/forge/card/ICardDatabase.java index 1f516664621..537d3499648 100644 --- a/forge-core/src/main/java/forge/card/ICardDatabase.java +++ b/forge-core/src/main/java/forge/card/ICardDatabase.java @@ -75,8 +75,6 @@ public interface ICardDatabase extends Iterable { PaperCard getCardFromEditionsReleasedAfter(String cardName, CardArtPreference artPreference, int artIndex, Date releaseDate); PaperCard getCardFromEditionsReleasedAfter(String cardName, CardArtPreference artPreference, int artIndex, Date releaseDate, Predicate filter); - - /* CARDS COLLECTION RETRIEVAL METHODS * ================================== */ Collection getAllCards(); diff --git a/forge-core/src/main/java/forge/card/MagicColor.java b/forge-core/src/main/java/forge/card/MagicColor.java index f1e29ebf0e1..0eb9a7b6470 100644 --- a/forge-core/src/main/java/forge/card/MagicColor.java +++ b/forge-core/src/main/java/forge/card/MagicColor.java @@ -1,7 +1,9 @@ package forge.card; import com.google.common.collect.ImmutableList; -import forge.deck.DeckRecognizer; + +import forge.util.ITranslatable; +import forge.util.Localizer; /** * Holds byte values for each color magic has. @@ -157,21 +159,24 @@ public final class MagicColor { } } - public enum Color { - WHITE(Constant.WHITE, MagicColor.WHITE, "{W}"), - BLUE(Constant.BLUE, MagicColor.BLUE, "{U}"), - BLACK(Constant.BLACK, MagicColor.BLACK, "{B}"), - RED(Constant.RED, MagicColor.RED, "{R}"), - GREEN(Constant.GREEN, MagicColor.GREEN, "{G}"), - COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "{C}"); + public enum Color implements ITranslatable { + WHITE(Constant.WHITE, MagicColor.WHITE, "W", "lblWhite"), + BLUE(Constant.BLUE, MagicColor.BLUE, "U", "lblBlue"), + BLACK(Constant.BLACK, MagicColor.BLACK, "B", "lblBlack"), + RED(Constant.RED, MagicColor.RED, "R", "lblRed"), + GREEN(Constant.GREEN, MagicColor.GREEN, "G", "lblGreen"), + COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "C", "lblColorless"); - private final String name, symbol; + private final String name, shortName, symbol; + private final String label; private final byte colormask; - Color(String name0, byte colormask0, String symbol0) { + Color(String name0, byte colormask0, String shortName, String label) { name = name0; colormask = colormask0; - symbol = symbol0; + this.shortName = shortName; + symbol = "{" + shortName + "}"; + this.label = label; } public static Color fromByte(final byte color) { @@ -185,25 +190,25 @@ public final class MagicColor { } } + @Override public String getName() { return name; } - - public String getLocalizedName() { - //Should probably move some of this logic back here, or at least to a more general location. - return DeckRecognizer.getLocalisedMagicColorName(getName()); + public String getShortName() { + return shortName; } - public byte getColormask() { + @Override + public String getTranslatedName() { + return Localizer.getInstance().getMessage(label); + } + + public byte getColorMask() { return colormask; } public String getSymbol() { return symbol; } - @Override - public String toString() { - return name; - } } } diff --git a/forge-core/src/main/java/forge/card/mana/ManaCostShard.java b/forge-core/src/main/java/forge/card/mana/ManaCostShard.java index 1d558520a25..41b9b667256 100644 --- a/forge-core/src/main/java/forge/card/mana/ManaCostShard.java +++ b/forge-core/src/main/java/forge/card/mana/ManaCostShard.java @@ -17,6 +17,7 @@ */ package forge.card.mana; +import forge.card.ColorSet; import forge.util.BinaryUtil; /** @@ -34,52 +35,51 @@ public enum ManaCostShard { COLORLESS(ManaAtom.COLORLESS, "C"), /* Hybrid */ - WU(ManaAtom.WHITE | ManaAtom.BLUE, "W/U", "WU"), - WB(ManaAtom.WHITE | ManaAtom.BLACK, "W/B", "WB"), - UB(ManaAtom.BLUE | ManaAtom.BLACK, "U/B", "UB"), - UR(ManaAtom.BLUE | ManaAtom.RED, "U/R", "UR"), - BR(ManaAtom.BLACK | ManaAtom.RED, "B/R", "BR"), - BG(ManaAtom.BLACK | ManaAtom.GREEN, "B/G", "BG"), - RW(ManaAtom.RED | ManaAtom.WHITE, "R/W", "RW"), - RG(ManaAtom.RED | ManaAtom.GREEN, "R/G", "RG"), - GW(ManaAtom.GREEN | ManaAtom.WHITE, "G/W", "GW"), - GU(ManaAtom.GREEN | ManaAtom.BLUE, "G/U", "GU"), + WU(ManaAtom.WHITE | ManaAtom.BLUE, "W/U"), + WB(ManaAtom.WHITE | ManaAtom.BLACK, "W/B"), + UB(ManaAtom.BLUE | ManaAtom.BLACK, "U/B"), + UR(ManaAtom.BLUE | ManaAtom.RED, "U/R"), + BR(ManaAtom.BLACK | ManaAtom.RED, "B/R"), + BG(ManaAtom.BLACK | ManaAtom.GREEN, "B/G"), + RW(ManaAtom.RED | ManaAtom.WHITE, "R/W"), + RG(ManaAtom.RED | ManaAtom.GREEN, "R/G"), + GW(ManaAtom.GREEN | ManaAtom.WHITE, "G/W"), + GU(ManaAtom.GREEN | ManaAtom.BLUE, "G/U"), /* Or 2 generic */ - W2(ManaAtom.WHITE | ManaAtom.OR_2_GENERIC, "2/W", "2W"), - U2(ManaAtom.BLUE | ManaAtom.OR_2_GENERIC, "2/U", "2U"), - B2(ManaAtom.BLACK | ManaAtom.OR_2_GENERIC, "2/B", "2B"), - R2(ManaAtom.RED | ManaAtom.OR_2_GENERIC, "2/R", "2R"), - G2(ManaAtom.GREEN | ManaAtom.OR_2_GENERIC, "2/G", "2G"), + W2(ManaAtom.WHITE | ManaAtom.OR_2_GENERIC, "2/W"), + U2(ManaAtom.BLUE | ManaAtom.OR_2_GENERIC, "2/U"), + B2(ManaAtom.BLACK | ManaAtom.OR_2_GENERIC, "2/B"), + R2(ManaAtom.RED | ManaAtom.OR_2_GENERIC, "2/R"), + G2(ManaAtom.GREEN | ManaAtom.OR_2_GENERIC, "2/G"), /* Or Colorless */ - CW(ManaAtom.WHITE | ManaAtom.COLORLESS, "C/W", "CW"), - CU(ManaAtom.BLUE | ManaAtom.COLORLESS, "C/U", "CU"), - CB(ManaAtom.BLACK | ManaAtom.COLORLESS, "C/B", "CB"), - CR(ManaAtom.RED | ManaAtom.COLORLESS, "C/R", "CR"), - CG(ManaAtom.GREEN | ManaAtom.COLORLESS, "C/G", "CG"), + CW(ManaAtom.WHITE | ManaAtom.COLORLESS, "C/W"), + CU(ManaAtom.BLUE | ManaAtom.COLORLESS, "C/U"), + CB(ManaAtom.BLACK | ManaAtom.COLORLESS, "C/B"), + CR(ManaAtom.RED | ManaAtom.COLORLESS, "C/R"), + CG(ManaAtom.GREEN | ManaAtom.COLORLESS, "C/G"), // Snow and colorless S(ManaAtom.IS_SNOW, "S"), GENERIC(ManaAtom.GENERIC, "1"), - /* Phyrexian */ - WP(ManaAtom.WHITE | ManaAtom.OR_2_LIFE, "W/P", "WP"), - UP(ManaAtom.BLUE | ManaAtom.OR_2_LIFE, "U/P", "UP"), - BP(ManaAtom.BLACK | ManaAtom.OR_2_LIFE, "B/P", "BP"), - RP(ManaAtom.RED | ManaAtom.OR_2_LIFE, "R/P", "RP"), - GP(ManaAtom.GREEN | ManaAtom.OR_2_LIFE, "G/P", "GP"), - BGP(ManaAtom.BLACK | ManaAtom.GREEN | ManaAtom.OR_2_LIFE, "B/G/P", "BGP"), - BRP(ManaAtom.BLACK | ManaAtom.RED | ManaAtom.OR_2_LIFE, "B/R/P", "BRP"), - GUP(ManaAtom.GREEN | ManaAtom.BLUE | ManaAtom.OR_2_LIFE, "G/U/P", "GUP"), - GWP(ManaAtom.GREEN | ManaAtom.WHITE | ManaAtom.OR_2_LIFE, "G/W/P", "GWP"), - RGP(ManaAtom.RED | ManaAtom.GREEN | ManaAtom.OR_2_LIFE, "R/G/P", "RGP"), - RWP(ManaAtom.RED | ManaAtom.WHITE | ManaAtom.OR_2_LIFE, "R/W/P", "RWP"), - UBP(ManaAtom.BLUE | ManaAtom.BLACK | ManaAtom.OR_2_LIFE, "U/B/P", "UBP"), - URP(ManaAtom.BLUE | ManaAtom.RED | ManaAtom.OR_2_LIFE, "U/R/P", "URP"), - WBP(ManaAtom.WHITE | ManaAtom.BLACK | ManaAtom.OR_2_LIFE, "W/B/P", "WBP"), - WUP(ManaAtom.WHITE | ManaAtom.BLUE | ManaAtom.OR_2_LIFE, "W/U/P", "WUP"), + WP(ManaAtom.WHITE | ManaAtom.OR_2_LIFE, "W/P"), + UP(ManaAtom.BLUE | ManaAtom.OR_2_LIFE, "U/P"), + BP(ManaAtom.BLACK | ManaAtom.OR_2_LIFE, "B/P"), + RP(ManaAtom.RED | ManaAtom.OR_2_LIFE, "R/P"), + GP(ManaAtom.GREEN | ManaAtom.OR_2_LIFE, "G/P"), + BGP(ManaAtom.BLACK | ManaAtom.GREEN | ManaAtom.OR_2_LIFE, "B/G/P"), + BRP(ManaAtom.BLACK | ManaAtom.RED | ManaAtom.OR_2_LIFE, "B/R/P"), + GUP(ManaAtom.GREEN | ManaAtom.BLUE | ManaAtom.OR_2_LIFE, "G/U/P"), + GWP(ManaAtom.GREEN | ManaAtom.WHITE | ManaAtom.OR_2_LIFE, "G/W/P"), + RGP(ManaAtom.RED | ManaAtom.GREEN | ManaAtom.OR_2_LIFE, "R/G/P"), + RWP(ManaAtom.RED | ManaAtom.WHITE | ManaAtom.OR_2_LIFE, "R/W/P"), + UBP(ManaAtom.BLUE | ManaAtom.BLACK | ManaAtom.OR_2_LIFE, "U/B/P"), + URP(ManaAtom.BLUE | ManaAtom.RED | ManaAtom.OR_2_LIFE, "U/R/P"), + WBP(ManaAtom.WHITE | ManaAtom.BLACK | ManaAtom.OR_2_LIFE, "W/B/P"), + WUP(ManaAtom.WHITE | ManaAtom.BLUE | ManaAtom.OR_2_LIFE, "W/U/P"), X(ManaAtom.IS_X, "X"), @@ -108,26 +108,12 @@ public enum ManaCostShard { * the s value */ ManaCostShard(final int value, final String sValue) { - this(value, sValue, sValue); - } - - /** - * Instantiates a new card mana cost shard. - * - * @param value - * the value - * @param sValue - * the s value - * @param imgKey - * the img key - */ - ManaCostShard(final int value, final String sValue, final String imgKey) { this.shard = value; this.cmc = this.getCMC(); this.cmpc = this.getCmpCost(); this.stringValue = "{" + sValue + "}"; this.shortStringValue = sValue; - this.imageKey = imgKey; + this.imageKey = sValue.replace("/", ""); } public static final int COLORS_SUPERPOSITION = ManaAtom.WHITE | ManaAtom.BLUE | ManaAtom.BLACK | ManaAtom.RED | ManaAtom.GREEN; @@ -186,6 +172,10 @@ public enum ManaCostShard { return (byte)(this.shard & COLORS_SUPERPOSITION); } + public final ColorSet getColor() { + return ColorSet.fromMask(getColorMask()); + } + /** * Value of. * diff --git a/forge-core/src/main/java/forge/deck/Deck.java b/forge-core/src/main/java/forge/deck/Deck.java index 4d6b34e22a7..22f11d22c8f 100644 --- a/forge-core/src/main/java/forge/deck/Deck.java +++ b/forge-core/src/main/java/forge/deck/Deck.java @@ -115,6 +115,20 @@ public class Deck extends DeckBase implements Iterable> getValid() { + List unsupported = new ArrayList<>(); + for (Entry kv : parts.entrySet()) { + CardPool pool = kv.getValue(); + for (Entry pc : pool) { + if (pc.getKey().getRules() != null && pc.getKey().getRules().isUnsupported()) { + unsupported.add(pc.getKey()); + pool.remove(pc.getKey()); + } + } + } + return Pair.of(this, unsupported); + } + public List getCommanders() { List result = Lists.newArrayList(); final CardPool cp = get(DeckSection.Commander); diff --git a/forge-core/src/main/java/forge/deck/DeckRecognizer.java b/forge-core/src/main/java/forge/deck/DeckRecognizer.java index 2757f418b26..8ea96e9847a 100644 --- a/forge-core/src/main/java/forge/deck/DeckRecognizer.java +++ b/forge-core/src/main/java/forge/deck/DeckRecognizer.java @@ -22,6 +22,7 @@ import forge.StaticData; import forge.card.CardDb; import forge.card.CardEdition; import forge.card.CardType; +import forge.card.ColorSet; import forge.card.MagicColor; import forge.item.IPaperCard; import forge.item.PaperCard; @@ -49,6 +50,16 @@ public class DeckRecognizer { LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, + /** + * Valid card request, but can't be imported because the player does not have enough copies. + * Should be replaced with a different printing if possible. + */ + CARD_NOT_IN_INVENTORY, + /** + * Valid card request for a card that isn't in the player's inventory, but new copies can be acquired freely. + * Usually used for basic lands. Should be supplied to the import controller by the editor. + */ + FREE_CARD_NOT_IN_INVENTORY, // Warning messages WARNING_MESSAGE, UNKNOWN_CARD, @@ -63,10 +74,14 @@ public class DeckRecognizer { CARD_TYPE, CARD_RARITY, CARD_CMC, - MANA_COLOUR + MANA_COLOUR; + + public static final EnumSet CARD_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY); + public static final EnumSet IN_DECK_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, DECK_NAME, FREE_CARD_NOT_IN_INVENTORY); + public static final EnumSet CARD_PLACEHOLDER_TOKEN_TYPES = EnumSet.of(CARD_TYPE, CARD_RARITY, CARD_CMC, MANA_COLOUR); } - public enum LimitedCardType{ + public enum LimitedCardType { BANNED, RESTRICTED, } @@ -108,6 +123,10 @@ public class DeckRecognizer { return new Token(TokenType.CARD_FROM_INVALID_SET, count, card, cardRequestHasSetCode); } + public static Token NotInInventoryFree(final PaperCard card, final int count, final DeckSection section) { + return new Token(TokenType.FREE_CARD_NOT_IN_INVENTORY, count, card, section, true); + } + // WARNING MESSAGES // ================ public static Token UnknownCard(final String cardName, final String setCode, final int count) { @@ -126,6 +145,10 @@ public class DeckRecognizer { return new Token(TokenType.WARNING_MESSAGE, msg); } + public static Token NotInInventory(final PaperCard card, final int count, final DeckSection section) { + return new Token(TokenType.CARD_NOT_IN_INVENTORY, count, card, section, false); + } + /* ================================= * DECK SECTIONS * ================================= */ @@ -239,14 +262,11 @@ public class DeckRecognizer { /** * Filters all token types that have a PaperCard instance set (not null) * @return true for tokens of type: - * LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET. + * LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY. * False otherwise. */ public boolean isCardToken() { - return (this.type == TokenType.LEGAL_CARD || - this.type == TokenType.LIMITED_CARD || - this.type == TokenType.CARD_FROM_NOT_ALLOWED_SET || - this.type == TokenType.CARD_FROM_INVALID_SET); + return TokenType.CARD_TOKEN_TYPES.contains(this.type); } /** @@ -255,9 +275,7 @@ public class DeckRecognizer { * LEGAL_CARD, LIMITED_CARD, DECK_NAME; false otherwise. */ public boolean isTokenForDeck() { - return (this.type == TokenType.LEGAL_CARD || - this.type == TokenType.LIMITED_CARD || - this.type == TokenType.DECK_NAME); + return TokenType.IN_DECK_TOKEN_TYPES.contains(this.type); } /** @@ -266,7 +284,7 @@ public class DeckRecognizer { * False otherwise. */ public boolean isCardTokenForDeck() { - return (this.type == TokenType.LEGAL_CARD || this.type == TokenType.LIMITED_CARD); + return isCardToken() && isTokenForDeck(); } /** @@ -276,10 +294,7 @@ public class DeckRecognizer { * CARD_RARITY, CARD_CMC, CARD_TYPE, MANA_COLOUR */ public boolean isCardPlaceholder(){ - return (this.type == TokenType.CARD_RARITY || - this.type == TokenType.CARD_CMC || - this.type == TokenType.MANA_COLOUR || - this.type == TokenType.CARD_TYPE); + return TokenType.CARD_PLACEHOLDER_TOKEN_TYPES.contains(this.type); } /** Determines if current token is a Deck Section token @@ -536,7 +551,7 @@ public class DeckRecognizer { PaperCard tokenCard = token.getCard(); if (isAllowed(tokenSection)) { - if (!tokenSection.equals(referenceDeckSectionInParsing)) { + if (tokenSection != referenceDeckSectionInParsing) { Token sectionToken = Token.DeckSection(tokenSection.name(), this.allowedDeckSections); // just check that last token is stack is a card placeholder. // In that case, add the new section token before the placeholder @@ -575,7 +590,7 @@ public class DeckRecognizer { refLine = purgeAllLinks(refLine); String line; - if (StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER)) + if (refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER)) line = refLine.replaceAll(LINE_COMMENT_DELIMITER_OR_MD_HEADER, ""); else line = refLine.trim(); // Remove any trailing formatting @@ -584,7 +599,7 @@ public class DeckRecognizer { // Final fantasy cards like Summon: Choco/Mog should be ommited to be recognized. TODO: fix maybe for future cards if (!line.contains("Summon:")) line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // "); - if (StringUtils.startsWith(line, ASTERISK)) // markdown lists (tappedout md export) + if (line.startsWith(ASTERISK)) // Markdown lists (tappedout md export) line = line.substring(2); // == Patches to Corner Cases @@ -600,8 +615,8 @@ public class DeckRecognizer { Token result = recogniseCardToken(line, referenceSection); if (result == null) result = recogniseNonCardToken(line); - return result != null ? result : StringUtils.startsWith(refLine, DOUBLE_SLASH) || - StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER) ? + return result != null ? result : refLine.startsWith(DOUBLE_SLASH) || + refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER) ? new Token(TokenType.COMMENT, 0, refLine) : new Token(TokenType.UNKNOWN_TEXT, 0, refLine); } @@ -613,7 +628,7 @@ public class DeckRecognizer { while (m.find()) { line = line.replaceAll(m.group(), "").trim(); } - if (StringUtils.endsWith(line, "()")) + if (line.endsWith("()")) return line.substring(0, line.length()-2); return line; } @@ -741,21 +756,12 @@ public class DeckRecognizer { // This would save tons of time in parsing Input + would also allow to return UnsupportedCardTokens beforehand private DeckSection getTokenSection(String deckSec, DeckSection currentDeckSection, PaperCard card){ if (deckSec != null) { - DeckSection cardSection; - switch (deckSec.toUpperCase().trim()) { - case "MB": - cardSection = DeckSection.Main; - break; - case "SB": - cardSection = DeckSection.Sideboard; - break; - case "CM": - cardSection = DeckSection.Commander; - break; - default: - cardSection = DeckSection.matchingSection(card); - break; - } + DeckSection cardSection = switch (deckSec.toUpperCase().trim()) { + case "MB" -> DeckSection.Main; + case "SB" -> DeckSection.Sideboard; + case "CM" -> DeckSection.Commander; + default -> DeckSection.matchingSection(card); + }; if (cardSection.validate(card)) return cardSection; } @@ -988,80 +994,37 @@ public class DeckRecognizer { private static String getMagicColourLabel(MagicColor.Color magicColor) { if (magicColor == null) // Multicolour - return String.format("%s {W}{U}{B}{R}{G}", getLocalisedMagicColorName("Multicolour")); - return String.format("%s %s", magicColor.getLocalizedName(), magicColor.getSymbol()); + return String.format("%s {W}{U}{B}{R}{G}", Localizer.getInstance().getMessage("lblMulticolor")); + return String.format("%s %s", magicColor.getTranslatedName(), magicColor.getSymbol()); } - private static final HashMap manaSymbolsMap = new HashMap() {{ - put(MagicColor.WHITE | MagicColor.BLUE, "WU"); - put(MagicColor.BLUE | MagicColor.BLACK, "UB"); - put(MagicColor.BLACK | MagicColor.RED, "BR"); - put(MagicColor.RED | MagicColor.GREEN, "RG"); - put(MagicColor.GREEN | MagicColor.WHITE, "GW"); - put(MagicColor.WHITE | MagicColor.BLACK, "WB"); - put(MagicColor.BLUE | MagicColor.RED, "UR"); - put(MagicColor.BLACK | MagicColor.GREEN, "BG"); - put(MagicColor.RED | MagicColor.WHITE, "RW"); - put(MagicColor.GREEN | MagicColor.BLUE, "GU"); - }}; - private static String getMagicColourLabel(MagicColor.Color magicColor1, MagicColor.Color magicColor2){ + private static String getMagicColourLabel(MagicColor.Color magicColor1, MagicColor.Color magicColor2) { if (magicColor2 == null || magicColor2 == MagicColor.Color.COLORLESS || magicColor1 == MagicColor.Color.COLORLESS) return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2)); - String localisedName1 = magicColor1.getLocalizedName(); - String localisedName2 = magicColor2.getLocalizedName(); - String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask()); - return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol); + String localisedName1 = magicColor1.getTranslatedName(); + String localisedName2 = magicColor2.getTranslatedName(); + return String.format("%s/%s {%s}", localisedName1, localisedName2, ColorSet.fromEnums(magicColor1, magicColor2)); } private static MagicColor.Color getMagicColor(String colorName){ if (colorName.toLowerCase().startsWith("multi") || colorName.equalsIgnoreCase("m")) return null; // will be handled separately - - byte color = MagicColor.fromName(colorName.toLowerCase()); - switch (color) { - case MagicColor.WHITE: - return MagicColor.Color.WHITE; - case MagicColor.BLUE: - return MagicColor.Color.BLUE; - case MagicColor.BLACK: - return MagicColor.Color.BLACK; - case MagicColor.RED: - return MagicColor.Color.RED; - case MagicColor.GREEN: - return MagicColor.Color.GREEN; - default: - return MagicColor.Color.COLORLESS; - - } + return MagicColor.Color.fromByte(MagicColor.fromName(colorName.toLowerCase())); } public static String getLocalisedMagicColorName(String colorName){ Localizer localizer = Localizer.getInstance(); - switch(colorName.toLowerCase()){ - case MagicColor.Constant.WHITE: - return localizer.getMessage("lblWhite"); - - case MagicColor.Constant.BLUE: - return localizer.getMessage("lblBlue"); - - case MagicColor.Constant.BLACK: - return localizer.getMessage("lblBlack"); - - case MagicColor.Constant.RED: - return localizer.getMessage("lblRed"); - - case MagicColor.Constant.GREEN: - return localizer.getMessage("lblGreen"); - - case MagicColor.Constant.COLORLESS: - return localizer.getMessage("lblColorless"); - case "multicolour": - case "multicolor": - return localizer.getMessage("lblMulticolor"); - default: - return ""; - } + return switch (colorName.toLowerCase()) { + case MagicColor.Constant.WHITE -> localizer.getMessage("lblWhite"); + case MagicColor.Constant.BLUE -> localizer.getMessage("lblBlue"); + case MagicColor.Constant.BLACK -> localizer.getMessage("lblBlack"); + case MagicColor.Constant.RED -> localizer.getMessage("lblRed"); + case MagicColor.Constant.GREEN -> localizer.getMessage("lblGreen"); + case MagicColor.Constant.COLORLESS -> localizer.getMessage("lblColorless"); + case "multicolour", "multicolor" -> localizer.getMessage("lblMulticolor"); + default -> ""; + }; } /** @@ -1080,37 +1043,6 @@ public class DeckRecognizer { return ""; } - - - private static Pair getManaNameAndSymbol(String matchedMana) { - if (matchedMana == null) - return null; - - Localizer localizer = Localizer.getInstance(); - switch (matchedMana.toLowerCase()) { - case MagicColor.Constant.WHITE: - case "w": - return Pair.of(localizer.getMessage("lblWhite"), MagicColor.Color.WHITE.getSymbol()); - case MagicColor.Constant.BLUE: - case "u": - return Pair.of(localizer.getMessage("lblBlue"), MagicColor.Color.BLUE.getSymbol()); - case MagicColor.Constant.BLACK: - case "b": - return Pair.of(localizer.getMessage("lblBlack"), MagicColor.Color.BLACK.getSymbol()); - case MagicColor.Constant.RED: - case "r": - return Pair.of(localizer.getMessage("lblRed"), MagicColor.Color.RED.getSymbol()); - case MagicColor.Constant.GREEN: - case "g": - return Pair.of(localizer.getMessage("lblGreen"), MagicColor.Color.GREEN.getSymbol()); - case MagicColor.Constant.COLORLESS: - case "c": - return Pair.of(localizer.getMessage("lblColorless"), MagicColor.Color.COLORLESS.getSymbol()); - default: // Multicolour - return Pair.of(localizer.getMessage("lblMulticolor"), ""); - } - } - public static boolean isDeckName(final String lineAsIs) { if (lineAsIs == null) return false; diff --git a/forge-core/src/main/java/forge/item/IPaperCard.java b/forge-core/src/main/java/forge/item/IPaperCard.java index 9d16618c2c4..af12c23c78e 100644 --- a/forge-core/src/main/java/forge/item/IPaperCard.java +++ b/forge-core/src/main/java/forge/item/IPaperCard.java @@ -52,9 +52,4 @@ public interface IPaperCard extends InventoryItem, Serializable { default String getUntranslatedType() { return getRules().getType().toString(); } - - @Override - default String getUntranslatedOracle() { - return getRules().getOracleText(); - } } \ No newline at end of file diff --git a/forge-core/src/main/java/forge/item/PaperCard.java b/forge-core/src/main/java/forge/item/PaperCard.java index 5796def118e..63e5642ba3e 100644 --- a/forge-core/src/main/java/forge/item/PaperCard.java +++ b/forge-core/src/main/java/forge/item/PaperCard.java @@ -375,7 +375,8 @@ public class PaperCard implements Comparable, InventoryItemFromSet, System.out.println("PaperCard: " + name + " not found with set and index " + edition + ", " + artIndex); pc = readObjectAlternate(name, edition); if (pc == null) { - throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex))); + pc = StaticData.instance().getCommonCards().createUnsupportedCard(name); + //throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex))); } System.out.println("Alternate object found: " + pc.getName() + ", " + pc.getEdition() + ", " + pc.getArtIndex()); } @@ -592,7 +593,7 @@ public class PaperCard implements Comparable, InventoryItemFromSet, public PaperCardFlags withMarkedColors(ColorSet markedColors) { if(markedColors == null) - markedColors = ColorSet.getNullColor(); + markedColors = ColorSet.C; return new PaperCardFlags(this, markedColors, null); } diff --git a/forge-core/src/main/java/forge/item/PaperCardPredicates.java b/forge-core/src/main/java/forge/item/PaperCardPredicates.java index b685885baa1..449d191b9c6 100644 --- a/forge-core/src/main/java/forge/item/PaperCardPredicates.java +++ b/forge-core/src/main/java/forge/item/PaperCardPredicates.java @@ -57,6 +57,42 @@ public abstract class PaperCardPredicates { return new PredicateFoil(isFoil); } + /** + * Filters cards that were printed in any of the specified editions. + */ + public static Predicate printedInAnyEditions(final String[] editionCodes) { + Set editions = new HashSet<>(Arrays.asList(editionCodes)); + + return card -> StaticData.instance().getCommonCards().getAllCards(card.getName()).stream() + .map(PaperCard::getEdition).anyMatch(editionCode -> + editions.contains(editionCode) && + StaticData.instance().getCardEdition(editionCode).isCardObtainable(card.getName()) + ); + } + + /** + * Filters cards that only printed in any of the specified editions. + */ + public static Predicate onlyPrintedInEditions(final String[] editionCodes) { + Set editions = new HashSet<>(Arrays.asList(editionCodes)); + + return card -> StaticData.instance().getCommonCards().getAllCards(card.getName()).stream() + .map(PaperCard::getEdition).allMatch(editionCode -> + editions.contains(editionCode) && + StaticData.instance().getCardEdition(editionCode).isCardObtainable(card.getName()) + ); + } + + /** + * Filters cards that are obtainable in any edition. + */ + public static Predicate isObtainableAnyEdition() { + return card -> StaticData.instance().getCommonCards().getAllCards(card.getName()).stream() + .map(PaperCard::getEdition).anyMatch(editionCode -> + StaticData.instance().getCardEdition(editionCode).isCardObtainable(card.getName()) + ); + } + private static final class PredicatePrintedWithRarity implements Predicate { private final CardRarity matchingRarity; @@ -76,25 +112,19 @@ public abstract class PaperCardPredicates { } private static final class PredicateColor implements Predicate { - private final byte operand; + private final MagicColor.Color operand; - private PredicateColor(final byte color) { + private PredicateColor(final MagicColor.Color color) { this.operand = color; } @Override public boolean test(final PaperCard card) { - for (final byte color : card.getRules().getColor()) { - if (color == operand) { - return true; - } + if (card.getRules().getColor().hasAnyColor(operand)) { + return true; } - if (card.getRules().getType().hasType(CardType.CoreType.Land)) { - for (final byte color : card.getRules().getColorIdentity()) { - if (color == operand) { - return true; - } - } + if (card.getRules().getType().hasType(CardType.CoreType.Land) && card.getRules().getColorIdentity().hasAnyColor(operand)) { + return true; } return false; } @@ -199,11 +229,11 @@ public abstract class PaperCardPredicates { public static final Predicate IS_RARE_OR_MYTHIC = PaperCardPredicates.IS_RARE.or(PaperCardPredicates.IS_MYTHIC_RARE); public static final Predicate IS_SPECIAL = new PredicateRarity(CardRarity.Special); public static final Predicate IS_BASIC_LAND_RARITY = new PredicateRarity(CardRarity.BasicLand); - public static final Predicate IS_BLACK = new PredicateColor(MagicColor.BLACK); - public static final Predicate IS_BLUE = new PredicateColor(MagicColor.BLUE); - public static final Predicate IS_GREEN = new PredicateColor(MagicColor.GREEN); - public static final Predicate IS_RED = new PredicateColor(MagicColor.RED); - public static final Predicate IS_WHITE = new PredicateColor(MagicColor.WHITE); + public static final Predicate IS_BLACK = new PredicateColor(MagicColor.Color.BLACK); + public static final Predicate IS_BLUE = new PredicateColor(MagicColor.Color.BLUE); + public static final Predicate IS_GREEN = new PredicateColor(MagicColor.Color.GREEN); + public static final Predicate IS_RED = new PredicateColor(MagicColor.Color.RED); + public static final Predicate IS_WHITE = new PredicateColor(MagicColor.Color.WHITE); public static final Predicate IS_COLORLESS = paperCard -> paperCard.getRules().getColor().isColorless(); public static final Predicate IS_UNREBALANCED = PaperCard::isUnRebalanced; public static final Predicate IS_REBALANCED = PaperCard::isRebalanced; diff --git a/forge-core/src/main/java/forge/item/PaperToken.java b/forge-core/src/main/java/forge/item/PaperToken.java index 76f94634093..f60c5befe5c 100644 --- a/forge-core/src/main/java/forge/item/PaperToken.java +++ b/forge-core/src/main/java/forge/item/PaperToken.java @@ -156,7 +156,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard { return false; CardSplitType cst = this.cardRules.getSplitType(); //expand this on future for other tokens that has other backsides besides transform.. - return cst == CardSplitType.Transform; + return cst == CardSplitType.Transform || cst == CardSplitType.Modal; } @Override diff --git a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java index ad6951478a1..2a64d6154bd 100644 --- a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java +++ b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java @@ -633,7 +633,10 @@ public class BoosterGenerator { System.out.println("Parsing from main code: " + mainCode); String sheetName = StringUtils.strip(mainCode.substring(10), "()\" "); System.out.println("Attempting to lookup: " + sheetName); - src = tryGetStaticSheet(sheetName).toFlatList(); + PrintSheet fromSheet = tryGetStaticSheet(sheetName); + if (fromSheet == null) + throw new RuntimeException("PrintSheet Error: " + ps.getName() + " didn't find " + sheetName + " from " + mainCode); + src = fromSheet.toFlatList(); setPred = x -> true; } else if (mainCode.startsWith("promo") || mainCode.startsWith("name")) { // get exactly the named cards, that's a tiny inlined print sheet diff --git a/forge-core/src/main/java/forge/util/ITranslatable.java b/forge-core/src/main/java/forge/util/ITranslatable.java index 6bbba27afb3..6f75fcd96cc 100644 --- a/forge-core/src/main/java/forge/util/ITranslatable.java +++ b/forge-core/src/main/java/forge/util/ITranslatable.java @@ -10,13 +10,11 @@ public interface ITranslatable extends IHasName { default String getUntranslatedName() { return getName(); } + default String getTranslatedName() { + return getName(); + } default String getUntranslatedType() { return ""; } - - default String getUntranslatedOracle() { - return ""; - } - } diff --git a/forge-core/src/main/java/forge/util/ImageUtil.java b/forge-core/src/main/java/forge/util/ImageUtil.java index 358d85abb76..a6594512378 100644 --- a/forge-core/src/main/java/forge/util/ImageUtil.java +++ b/forge-core/src/main/java/forge/util/ImageUtil.java @@ -207,8 +207,6 @@ public class ImageUtil { else editionCode = cp.getEdition().toLowerCase(); String cardCollectorNumber = cp.getCollectorNumber(); - // Hack to account for variations in Arabian Nights - cardCollectorNumber = cardCollectorNumber.replace("+", "†"); // override old planechase sets from their modified id since scryfall move the planechase cards outside their original setcode if (cardCollectorNumber.startsWith("OHOP")) { editionCode = "ohop"; @@ -252,6 +250,11 @@ public class ImageUtil { : "&face=front"); } + if (cardCollectorNumber.endsWith("☇")) { + faceParam = "&face=back"; + cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1); + } + return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, encodeUtf8(cardCollectorNumber), langCode, versionParam, faceParam); } @@ -261,6 +264,10 @@ public class ImageUtil { if (!faceParam.isEmpty()) { faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front"); } + if (collectorNumber.endsWith("☇")) { + faceParam = "&face=back"; + collectorNumber = collectorNumber.substring(0, collectorNumber.length() - 1); + } return String.format("%s/%s/%s?format=image&version=%s%s", setCode, encodeUtf8(collectorNumber), langCode, versionParam, faceParam); } @@ -281,8 +288,7 @@ public class ImageUtil { char c; for (int i = 0; i < in.length(); i++) { c = in.charAt(i); - if ((c == '"') || (c == '/') || (c == ':') || (c == '?')) { - } else { + if ((c != '"') && (c != '/') && (c != ':') && (c != '?')) { out.append(c); } } diff --git a/forge-game/pom.xml b/forge-game/pom.xml index d4c360abda8..ce1a3dd95a5 100644 --- a/forge-game/pom.xml +++ b/forge-game/pom.xml @@ -32,7 +32,7 @@ io.sentry sentry-logback - 8.19.1 + 8.21.1 org.jgrapht diff --git a/forge-game/src/main/java/forge/game/CardTraitBase.java b/forge-game/src/main/java/forge/game/CardTraitBase.java index f707f2b0aea..4d84640e782 100644 --- a/forge-game/src/main/java/forge/game/CardTraitBase.java +++ b/forge-game/src/main/java/forge/game/CardTraitBase.java @@ -62,7 +62,9 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView, /** Keys of descriptive (text) parameters. */ private static final ImmutableList descriptiveKeys = ImmutableList.builder() - .add("Description", "SpellDescription", "StackDescription", "TriggerDescription").build(); + .add("Description", "SpellDescription", "StackDescription", "TriggerDescription") + .add("ChangeTypeDesc") + .build(); /** * Keys that should not changed diff --git a/forge-game/src/main/java/forge/game/ForgeScript.java b/forge-game/src/main/java/forge/game/ForgeScript.java index d7bfb7065f9..cf7a461f92b 100644 --- a/forge-game/src/main/java/forge/game/ForgeScript.java +++ b/forge-game/src/main/java/forge/game/ForgeScript.java @@ -35,7 +35,7 @@ public class ForgeScript { boolean withSource = property.endsWith("Source"); final ColorSet colors; if (withSource && StaticAbilityColorlessDamageSource.colorlessDamageSource(cardState)) { - colors = ColorSet.getNullColor(); + colors = ColorSet.C; } else { colors = cardState.getCard().getColor(cardState); } @@ -166,8 +166,6 @@ public class ForgeScript { Card source, CardTraitBase spellAbility) { if (property.equals("ManaAbility")) { return sa.isManaAbility(); - } else if (property.equals("nonManaAbility")) { - return !sa.isManaAbility(); } else if (property.equals("withoutXCost")) { return !sa.costHasManaX(); } else if (property.startsWith("XCost")) { @@ -195,7 +193,7 @@ public class ForgeScript { return sa.isKeyword(Keyword.SADDLE); } else if (property.equals("Station")) { return sa.isKeyword(Keyword.STATION); - }else if (property.equals("Cycling")) { + } else if (property.equals("Cycling")) { return sa.isCycling(); } else if (property.equals("Dash")) { return sa.isDash(); @@ -237,6 +235,8 @@ public class ForgeScript { return sa.isBoast(); } else if (property.equals("Exhaust")) { return sa.isExhaust(); + } else if (property.equals("Mayhem")) { + return sa.isMayhem(); } else if (property.equals("Mutate")) { return sa.isMutate(); } else if (property.equals("Ninjutsu")) { @@ -410,6 +410,8 @@ public class ForgeScript { return !sa.isPwAbility() && !sa.getRestrictions().isSorcerySpeed(); } return true; + } else if(property.startsWith("NamedAbility")) { + return sa.getName().equals(property.substring(12)); } else if (sa.getHostCard() != null) { return sa.getHostCard().hasProperty(property, sourceController, source, spellAbility); } diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 196dc753239..c9f8171f7c2 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -414,19 +414,6 @@ public class Game { return players; } - /** - * Gets the nonactive players who are still fighting to win, in turn order. - */ - public final PlayerCollection getNonactivePlayers() { - // Don't use getPlayersInTurnOrder to prevent copying the player collection twice - final PlayerCollection players = new PlayerCollection(ingamePlayers); - players.remove(phaseHandler.getPlayerTurn()); - if (!getTurnOrder().isDefaultDirection()) { - Collections.reverse(players); - } - return players; - } - /** * Gets the players who participated in match (regardless of outcome). * Use this in UI and after match calculations @@ -858,6 +845,8 @@ public class Game { p.revealFaceDownCards(); } + // TODO free any mindslaves + for (Card c : cards) { // CR 800.4d if card is controlled by opponent, LTB should trigger if (c.getOwner().equals(p) && c.getController().equals(p)) { @@ -893,8 +882,6 @@ public class Game { } triggerList.put(c.getZone().getZoneType(), null, c); getAction().ceaseToExist(c, false); - // CR 603.2f owner of trigger source lost game - getTriggerHandler().clearDelayedTrigger(c); } } else { // return stolen permanents diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 09faddb309b..0c985d805ea 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -57,6 +57,8 @@ import forge.item.PaperCard; import forge.util.*; import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; +import io.sentry.Breadcrumb; +import io.sentry.Sentry; import org.apache.commons.lang3.tuple.ImmutablePair; import org.jgrapht.alg.cycle.SzwarcfiterLauerSimpleCycles; import org.jgrapht.graph.DefaultDirectedGraph; @@ -220,10 +222,6 @@ public class GameAction { //copied.setGamePieceType(GamePieceType.COPIED_SPELL); } - if (c.isTransformed()) { - copied.incrementTransformedTimestamp(); - } - if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) { copied.setCastSA(cause); copied.setSplitStateToPlayAbility(cause); @@ -753,26 +751,29 @@ public class GameAction { public final Card moveTo(final ZoneType name, final Card c, final int libPosition, SpellAbility cause, Map params) { // Call specific functions to set PlayerZone, then move onto moveTo - switch(name) { - case Hand: return moveToHand(c, cause, params); - case Library: return moveToLibrary(c, libPosition, cause, params); - case Battlefield: return moveToPlay(c, c.getController(), cause, params); - case Graveyard: return moveToGraveyard(c, cause, params); - case Exile: - if (!c.canExiledBy(cause, true)) { - return null; - } - return exile(c, cause, params); - case Stack: return moveToStack(c, cause, params); - case PlanarDeck: - case SchemeDeck: - case AttractionDeck: - case ContraptionDeck: - return moveToVariantDeck(c, name, libPosition, cause, params); - case Junkyard: - return moveToJunkyard(c, cause, params); - default: // sideboard will also get there - return moveTo(c.getOwner().getZone(name), c, cause); + try { + return switch (name) { + case Hand -> moveToHand(c, cause, params); + case Library -> moveToLibrary(c, libPosition, cause, params); + case Battlefield -> moveToPlay(c, c.getController(), cause, params); + case Graveyard -> moveToGraveyard(c, cause, params); + case Exile -> !c.canExiledBy(cause, true) ? null : exile(c, cause, params); + case Stack -> moveToStack(c, cause, params); + case PlanarDeck, SchemeDeck, AttractionDeck, ContraptionDeck -> moveToVariantDeck(c, name, libPosition, cause, params); + case Junkyard -> moveToJunkyard(c, cause, params); + default -> moveTo(c.getOwner().getZone(name), c, cause); // sideboard will also get there + }; + } catch (Exception e) { + String msg = "GameAction:moveTo: Exception occured"; + + Breadcrumb bread = new Breadcrumb(msg); + bread.setData("Card", c.getName()); + bread.setData("SA", cause.toString()); + bread.setData("ZoneType", name.name()); + bread.setData("Player", c.getOwner()); + Sentry.addBreadcrumb(bread); + + throw new RuntimeException("Error in GameAction moveTo " + c.getName() + " to Player Zone " + name.name(), e); } } @@ -974,6 +975,7 @@ public class GameAction { // in some corner cases there's no zone yet (copied spell that failed targeting) if (z != null) { z.remove(c); + c.setZone(c.getOwner().getZone(ZoneType.None)); if (z.is(ZoneType.Battlefield)) { c.runLeavesPlayCommands(); } @@ -1600,9 +1602,7 @@ public class GameAction { } // recheck the game over condition at this point to make sure no other win conditions apply now. - if (!game.isGameOver()) { - checkGameOverCondition(); - } + checkGameOverCondition(); if (game.getAge() != GameStage.Play) { return false; @@ -1822,8 +1822,8 @@ public class GameAction { private boolean stateBasedAction704_5q(Card c) { boolean checkAgain = false; - final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1); - final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1); + final CounterType p1p1 = CounterEnumType.P1P1; + final CounterType m1m1 = CounterEnumType.M1M1; int plusOneCounters = c.getCounters(p1p1); int minusOneCounters = c.getCounters(m1m1); if (plusOneCounters > 0 && minusOneCounters > 0) { @@ -1843,7 +1843,7 @@ public class GameAction { return checkAgain; } private boolean stateBasedAction704_5r(Card c) { - final CounterType dreamType = CounterType.get(CounterEnumType.DREAM); + final CounterType dreamType = CounterEnumType.DREAM; int old = c.getCounters(dreamType); if (old <= 0) { @@ -1883,6 +1883,10 @@ public class GameAction { } public void checkGameOverCondition() { + if (game.isGameOver()) { + return; + } + // award loses as SBE GameEndReason reason = null; List losers = null; @@ -2222,6 +2226,13 @@ public class GameAction { } } + public void revealUnsupported(Map> unsupported) { + // Notify players + for (Player p : game.getPlayers()) { + p.getController().revealUnsupported(unsupported); + } + } + /** Delivers a message to all players. (use reveal to show Cards) */ public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) { if (saSource != null) { diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java index f9fd21b76c3..b7e2ec6d469 100644 --- a/forge-game/src/main/java/forge/game/GameActionUtil.java +++ b/forge-game/src/main/java/forge/game/GameActionUtil.java @@ -125,10 +125,22 @@ public final class GameActionUtil { // need to be done there before static abilities does reset the card // These Keywords depend on the Mana Cost of for Split Cards - if (sa.isBasicSpell() && !sa.isLandAbility()) { + if (sa.isBasicSpell()) { for (final KeywordInterface inst : source.getKeywords()) { final String keyword = inst.getOriginal(); + if (keyword.startsWith("Mayhem")) { + if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) { + continue; + } + + alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem)); + } + + if (sa.isLandAbility()) { + continue; + } + if (keyword.startsWith("Escape")) { if (!source.isInZone(ZoneType.Graveyard)) { continue; @@ -166,18 +178,6 @@ public final class GameActionUtil { } alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Flashback)); - } else if (keyword.startsWith("Mayhem")) { - if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) { - continue; - } - - // if source has No Mana cost, and Mayhem doesn't have own one, - // Mayhem can't work - if (keyword.equals("Mayhem") && source.getManaCost().isNoCost()) { - continue; - } - - alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem)); } else if (keyword.startsWith("Harmonize")) { if (!source.isInZone(ZoneType.Graveyard)) { continue; @@ -242,6 +242,7 @@ public final class GameActionUtil { } stackCopy.setLastKnownZone(game.getStackZone()); stackCopy.setCastFrom(oldZone); + stackCopy.setCastSA(sa); lkicheck = true; stackCopy.clearStaticChangedCardKeywords(false); @@ -992,9 +993,6 @@ public final class GameActionUtil { oldCard.setBackSide(false); oldCard.setState(oldCard.getFaceupCardStateName(), true); oldCard.unanimateBestow(); - if (ability.isDisturb() || ability.hasParam("CastTransformed")) { - oldCard.undoIncrementTransformedTimestamp(); - } if (ability.hasParam("Prototype")) { oldCard.removeCloneState(oldCard.getPrototypeTimestamp()); diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java index 966c4382799..1447b1d78ad 100644 --- a/forge-game/src/main/java/forge/game/GameEntity.java +++ b/forge-game/src/main/java/forge/game/GameEntity.java @@ -33,17 +33,18 @@ import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardPredicates; -import forge.game.card.CounterEnumType; import forge.game.card.CounterType; -import forge.game.event.GameEventCardAttachment; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; +import forge.game.keyword.KeywordWithType; import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbilityCantAttach; import forge.game.zone.ZoneType; +import forge.util.Lang; public abstract class GameEntity extends GameObject implements IIdentifiable { protected int id; @@ -197,14 +198,12 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { public final void addAttachedCard(final Card c) { if (attachedCards.add(c)) { updateAttachedCards(); - getGame().fireEvent(new GameEventCardAttachment(c, null, this)); } } public final void removeAttachedCard(final Card c) { if (attachedCards.remove(c)) { updateAttachedCards(); - getGame().fireEvent(new GameEventCardAttachment(c, this, null)); } } @@ -222,63 +221,83 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { return canBeAttached(attach, sa, false); } public boolean canBeAttached(final Card attach, SpellAbility sa, boolean checkSBA) { - // master mode - if (!attach.isAttachment() || (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) - || equals(attach)) { - return false; + return cantBeAttachedMsg(attach, sa, checkSBA) == null; + } + + public String cantBeAttachedMsg(final Card attach, SpellAbility sa) { + return cantBeAttachedMsg(attach, sa, false); + } + public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) { + if (!attach.isAttachment()) { + return attach.getName() + " is not an attachment"; + } + if (equals(attach)) { + return attach.getName() + " can't attach to itself"; + } + + if (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) { + return attach.getName() + " is a creature without reconfigure"; } if (attach.isPhasedOut()) { - return false; + return attach.getName() + " is phased out"; } - // check for rules - if (attach.isAura() && !canBeEnchantedBy(attach)) { - return false; + if (attach.isAura()) { + String msg = cantBeEnchantedByMsg(attach); + if (msg != null) { + return msg; + } } - if (attach.isEquipment() && !canBeEquippedBy(attach, sa)) { - return false; + if (attach.isEquipment()) { + String msg = cantBeEquippedByMsg(attach, sa); + if (msg != null) { + return msg; + } } - if (attach.isFortification() && !canBeFortifiedBy(attach)) { - return false; + if (attach.isFortification()) { + String msg = cantBeFortifiedByMsg(attach); + if (msg != null) { + return msg; + } } - // check for can't attach static - if (StaticAbilityCantAttach.cantAttach(this, attach, checkSBA)) { - return false; + StaticAbility stAb = StaticAbilityCantAttach.cantAttach(this, attach, checkSBA); + if (stAb != null) { + return stAb.toString(); } - // true for all - return true; + return null; } - protected boolean canBeEquippedBy(final Card aura, SpellAbility sa) { - /** - * Equip only to Creatures which are cards - */ - return false; - } - - protected boolean canBeFortifiedBy(final Card aura) { + protected String cantBeEquippedByMsg(final Card aura, SpellAbility sa) { /** * Equip only to Lands which are cards */ - return false; + return getName() + " is not a Creature"; } - protected boolean canBeEnchantedBy(final Card aura) { + protected String cantBeFortifiedByMsg(final Card fort) { + /** + * Equip only to Lands which are cards + */ + return getName() + " is not a Land"; + } + + protected String cantBeEnchantedByMsg(final Card aura) { if (!aura.hasKeyword(Keyword.ENCHANT)) { - return false; + return "No Enchant Keyword"; } for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) { - String k = ki.getOriginal(); - String m[] = k.split(":"); - String v = m[1]; - if (!isValid(v.split(","), aura.getController(), aura, null)) { - return false; + if (ki instanceof KeywordWithType kwt) { + String v = kwt.getValidType(); + String desc = kwt.getTypeDescription(); + if (!isValid(v.split(","), aura.getController(), aura, null)) { + return getName() + " is not " + Lang.nounWithAmount(1, desc); + } } } - return true; + return null; } public boolean hasCounters() { @@ -305,9 +324,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { Integer value = counters.get(counterName); return value == null ? 0 : value; } - public final int getCounters(final CounterEnumType counterType) { - return getCounters(CounterType.get(counterType)); - } public void setCounters(final CounterType counterType, final Integer num) { if (num <= 0) { @@ -316,9 +332,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { counters.put(counterType, num); } } - public void setCounters(final CounterEnumType counterType, final Integer num) { - setCounters(CounterType.get(counterType), num); - } abstract public void setCounters(final Map allCounters); @@ -328,10 +341,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { abstract public int subtractCounter(final CounterType counterName, final int n, final Player remover); abstract public void clearCounters(); - public boolean canReceiveCounters(final CounterEnumType type) { - return canReceiveCounters(CounterType.get(type)); - } - public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) { if (n <= 0 || !canReceiveCounters(counterType)) { // As per rule 107.1b @@ -351,18 +360,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { table.put(source, this, counterType, n); } - public final void addCounter(final CounterEnumType counterType, final int n, final Player source, GameEntityCounterTable table) { - addCounter(CounterType.get(counterType), n, source, table); - } - - public int subtractCounter(final CounterEnumType counterName, final int n, final Player remover) { - return subtractCounter(CounterType.get(counterName), n, remover); - } - abstract public void addCounterInternal(final CounterType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map params); - public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map params) { - addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params); - } public Integer getCounterMax(final CounterType counterType) { return null; } diff --git a/forge-game/src/main/java/forge/game/GameLogFormatter.java b/forge-game/src/main/java/forge/game/GameLogFormatter.java index 2a2c60eb227..7adab70f9a2 100644 --- a/forge-game/src/main/java/forge/game/GameLogFormatter.java +++ b/forge-game/src/main/java/forge/game/GameLogFormatter.java @@ -29,25 +29,25 @@ public class GameLogFormatter extends IGameEventVisitor.Base { @Override public GameLogEntry visit(GameEventGameOutcome ev) { // Turn number counted from the starting player - int lastTurn = (int)Math.ceil((float)ev.result.getLastTurnNumber() / 2.0); + int lastTurn = (int)Math.ceil((float)ev.result().getLastTurnNumber() / 2.0); log.add(GameLogEntryType.GAME_OUTCOME, localizer.getMessage("lblTurn") + " " + lastTurn); - for (String outcome : ev.result.getOutcomeStrings()) { + for (String outcome : ev.result().getOutcomeStrings()) { log.add(GameLogEntryType.GAME_OUTCOME, outcome); } - return generateSummary(ev.history); + return generateSummary(ev.history()); } @Override public GameLogEntry visit(GameEventScry ev) { String scryOutcome = ""; - if (ev.toTop > 0 && ev.toBottom > 0) { - scryOutcome = localizer.getMessage("lblLogScryTopBottomLibrary").replace("%s", ev.player.toString()).replace("%top", String.valueOf(ev.toTop)).replace("%bottom", String.valueOf(ev.toBottom)); - } else if (ev.toBottom == 0) { - scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player.toString()).replace("%top", String.valueOf(ev.toTop)); + if (ev.toTop() > 0 && ev.toBottom() > 0) { + scryOutcome = localizer.getMessage("lblLogScryTopBottomLibrary").replace("%s", ev.player().toString()).replace("%top", String.valueOf(ev.toTop())).replace("%bottom", String.valueOf(ev.toBottom())); + } else if (ev.toBottom() == 0) { + scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player().toString()).replace("%top", String.valueOf(ev.toTop())); } else { - scryOutcome = localizer.getMessage("lblLogScryBottomLibrary").replace("%s", ev.player.toString()).replace("%bottom", String.valueOf(ev.toBottom)); + scryOutcome = localizer.getMessage("lblLogScryBottomLibrary").replace("%s", ev.player().toString()).replace("%bottom", String.valueOf(ev.toBottom())); } return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, scryOutcome); @@ -57,12 +57,12 @@ public class GameLogFormatter extends IGameEventVisitor.Base { public GameLogEntry visit(GameEventSurveil ev) { String surveilOutcome = ""; - if (ev.toLibrary > 0 && ev.toGraveyard > 0) { - surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player.toString(), String.valueOf(ev.toLibrary), String.valueOf(ev.toGraveyard)); - } else if (ev.toGraveyard == 0) { - surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player.toString(), String.valueOf(ev.toLibrary)); + if (ev.toLibrary() > 0 && ev.toGraveyard() > 0) { + surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player().toString(), String.valueOf(ev.toLibrary()), String.valueOf(ev.toGraveyard())); + } else if (ev.toGraveyard() == 0) { + surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player().toString(), String.valueOf(ev.toLibrary())); } else { - surveilOutcome = localizer.getMessage("lblLogSurveiledToGraveyard", ev.player.toString(), String.valueOf(ev.toGraveyard)); + surveilOutcome = localizer.getMessage("lblLogSurveiledToGraveyard", ev.player().toString(), String.valueOf(ev.toGraveyard())); } return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, surveilOutcome); @@ -70,26 +70,26 @@ public class GameLogFormatter extends IGameEventVisitor.Base { @Override public GameLogEntry visit(GameEventSpellResolved ev) { - String messageForLog = ev.hasFizzled ? localizer.getMessage("lblLogCardAbilityFizzles", ev.spell.getHostCard().toString()) : ev.spell.getStackDescription(); + String messageForLog = ev.hasFizzled() ? localizer.getMessage("lblLogCardAbilityFizzles", ev.spell().getHostCard().toString()) : ev.spell().getStackDescription(); return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, messageForLog); } @Override public GameLogEntry visit(GameEventSpellAbilityCast event) { - String player = event.sa.getActivatingPlayer().getName(); - String action = event.sa.isSpell() ? localizer.getMessage("lblCast") - : event.sa.isTrigger() ? localizer.getMessage("lblTriggered") + String player = event.sa().getActivatingPlayer().getName(); + String action = event.sa().isSpell() ? localizer.getMessage("lblCast") + : event.sa().isTrigger() ? localizer.getMessage("lblTriggered") : localizer.getMessage("lblActivated"); - String object = event.si.getStackDescription().startsWith("Morph ") + String object = event.si().getStackDescription().startsWith("Morph ") ? localizer.getMessage("lblMorph") - : event.sa.getHostCard().toString(); + : event.sa().getHostCard().toString(); String messageForLog = ""; - if (event.sa.getTargetRestrictions() != null) { + if (event.sa().getTargetRestrictions() != null) { StringBuilder sb = new StringBuilder(); - for (TargetChoices ch : event.sa.getAllTargetChoices()) { + for (TargetChoices ch : event.sa().getAllTargetChoices()) { if (null != ch) { sb.append(ch); } @@ -104,18 +104,18 @@ public class GameLogFormatter extends IGameEventVisitor.Base { @Override public GameLogEntry visit(GameEventCardModeChosen ev) { - if (!ev.log) { + if (!ev.log()) { return null; } String modeChoiceOutcome; - if (ev.random) { - modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName, ev.mode); + if (ev.random()) { + modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName(), ev.mode()); } else { modeChoiceOutcome = localizer.getMessage("lblLogPlayerChosenModeForCard", - ev.player.toString(), ev.mode, ev.cardName); + ev.player().toString(), ev.mode(), ev.cardName()); } - String name = CardTranslation.getTranslatedName(ev.cardName); + String name = CardTranslation.getTranslatedName(ev.cardName()); modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "CARDNAME", name); modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "NICKNAME", Lang.getInstance().getNickName(name)); @@ -124,7 +124,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base { @Override public GameLogEntry visit(GameEventRandomLog ev) { - return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.message); + return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.message()); } private static GameLogEntry generateSummary(final Collection gamesPlayed) { @@ -152,8 +152,8 @@ public class GameLogFormatter extends IGameEventVisitor.Base { @Override public GameLogEntry visit(final GameEventPlayerControl event) { - final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer; - final Player p = event.player; + final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer(); + final Player p = event.player(); final String message; if (newLobbyPlayer == null) { @@ -166,23 +166,23 @@ public class GameLogFormatter extends IGameEventVisitor.Base { @Override public GameLogEntry visit(GameEventTurnPhase ev) { - Player p = ev.playerTurn; - return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc + Lang.getInstance().getPossessedObject(p.getName(), ev.phase.nameForUi)); + Player p = ev.playerTurn(); + return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc() + Lang.getInstance().getPossessedObject(p.getName(), ev.phase().nameForUi)); } @Override public GameLogEntry visit(GameEventCardDamaged event) { String additionalLog = ""; - if (event.type == DamageType.Deathtouch) { + if (event.type() == DamageType.Deathtouch) { additionalLog = localizer.getMessage("lblDeathtouch"); } - if (event.type == DamageType.M1M1Counters) { + if (event.type() == DamageType.M1M1Counters) { additionalLog = localizer.getMessage("lblAsM1M1Counters"); } - if (event.type == DamageType.LoyaltyLoss) { - additionalLog = localizer.getMessage("lblRemovingNLoyaltyCounter", String.valueOf(event.amount)); + if (event.type() == DamageType.LoyaltyLoss) { + additionalLog = localizer.getMessage("lblRemovingNLoyaltyCounter", String.valueOf(event.amount())); } - String message = localizer.getMessage("lblSourceDealsNDamageToDest", event.source.toString(), String.valueOf(event.amount), additionalLog, event.card.toString()); + String message = localizer.getMessage("lblSourceDealsNDamageToDest", event.source().toString(), String.valueOf(event.amount()), additionalLog, event.card().toString()); return new GameLogEntry(GameLogEntryType.DAMAGE, message); } @@ -191,43 +191,43 @@ public class GameLogFormatter extends IGameEventVisitor.Base { */ @Override public GameLogEntry visit(GameEventLandPlayed ev) { - String message = localizer.getMessage("lblLogPlayerPlayedLand", ev.player.toString(), ev.land.toString()); + String message = localizer.getMessage("lblLogPlayerPlayedLand", ev.player().toString(), ev.land().toString()); return new GameLogEntry(GameLogEntryType.LAND, message); } @Override public GameLogEntry visit(GameEventTurnBegan event) { - String message = localizer.getMessage("lblLogTurnNOwnerByPlayer", String.valueOf(event.turnNumber), event.turnOwner.toString()); + String message = localizer.getMessage("lblLogTurnNOwnerByPlayer", String.valueOf(event.turnNumber()), event.turnOwner().toString()); return new GameLogEntry(GameLogEntryType.TURN, message); } @Override public GameLogEntry visit(GameEventPlayerDamaged ev) { - String extra = ev.infect ? localizer.getMessage("lblLogAsPoisonCounters") : ""; - String damageType = ev.combat ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat"); - String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source.toString(), - String.valueOf(ev.amount), damageType, ev.target.toString(), extra); + String extra = ev.infect() ? localizer.getMessage("lblLogAsPoisonCounters") : ""; + String damageType = ev.combat() ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat"); + String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source().toString(), + String.valueOf(ev.amount()), damageType, ev.target().toString(), extra); return new GameLogEntry(GameLogEntryType.DAMAGE, message); } @Override public GameLogEntry visit(GameEventPlayerPoisoned ev) { String message = localizer.getMessage("lblLogPlayerReceivesNPosionCounterFrom", - ev.receiver.toString(), String.valueOf(ev.amount), ev.source.toString()); + ev.receiver().toString(), String.valueOf(ev.amount()), ev.source().toString()); return new GameLogEntry(GameLogEntryType.DAMAGE, message); } @Override public GameLogEntry visit(GameEventPlayerRadiation ev) { String message; - final int change = ev.change; + final int change = ev.change(); String radCtr = CounterEnumType.RAD.getName().toLowerCase() + " " + Localizer.getInstance().getMessage("lblCounter").toLowerCase(); if (change >= 0) message = localizer.getMessage("lblLogPlayerRadiation", - ev.receiver.toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr), - ev.source.toString()); + ev.receiver().toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr), + ev.source().toString()); else message = localizer.getMessage("lblLogPlayerRadRemove", - ev.receiver.toString(), Lang.nounWithNumeralExceptOne(String.valueOf(Math.abs(change)), radCtr)); + ev.receiver().toString(), Lang.nounWithNumeralExceptOne(String.valueOf(Math.abs(change)), radCtr)); return new GameLogEntry(GameLogEntryType.DAMAGE, message); } @@ -239,16 +239,16 @@ public class GameLogFormatter extends IGameEventVisitor.Base { // Append Defending Player/Planeswalker // Not a big fan of the triple nested loop here - for (GameEntity k : ev.attackersMap.keySet()) { - Collection attackers = ev.attackersMap.get(k); + for (GameEntity k : ev.attackersMap().keySet()) { + Collection attackers = ev.attackersMap().get(k); if (attackers == null || attackers.isEmpty()) { continue; } if (sb.length() > 0) sb.append("\n"); - sb.append(localizer.getMessage("lblLogPlayerAssignedAttackerToAttackTarget", ev.player, Lang.joinHomogenous(attackers), k)); + sb.append(localizer.getMessage("lblLogPlayerAssignedAttackerToAttackTarget", ev.player(), Lang.joinHomogenous(attackers), k)); } if (sb.length() == 0) { - sb.append(localizer.getMessage("lblPlayerDidntAttackThisTurn").replace("%s", ev.player.toString())); + sb.append(localizer.getMessage("lblPlayerDidntAttackThisTurn").replace("%s", ev.player().toString())); } return new GameLogEntry(GameLogEntryType.COMBAT, sb.toString()); } @@ -262,7 +262,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base { Collection blockers = null; - for (Entry> kv : ev.blockers.entrySet()) { + for (Entry> kv : ev.blockers().entrySet()) { GameEntity defender = kv.getKey(); MapOfLists attackers = kv.getValue(); if (attackers == null || attackers.isEmpty()) { @@ -298,7 +298,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base { @Override public GameLogEntry visit(GameEventMulligan ev) { - String message = localizer.getMessage("lblPlayerHasMulliganedDownToNCards").replace("%d", String.valueOf(ev.player.getZone(ZoneType.Hand).size())).replace("%s", ev.player.toString()); + String message = localizer.getMessage("lblPlayerHasMulliganedDownToNCards").replace("%d", String.valueOf(ev.player().getZone(ZoneType.Hand).size())).replace("%s", ev.player().toString()); return new GameLogEntry(GameLogEntryType.MULLIGAN, message); } diff --git a/forge-game/src/main/java/forge/game/GameRules.java b/forge-game/src/main/java/forge/game/GameRules.java index f36ccef565c..c38b6c113c9 100644 --- a/forge-game/src/main/java/forge/game/GameRules.java +++ b/forge-game/src/main/java/forge/game/GameRules.java @@ -15,6 +15,7 @@ public class GameRules { private boolean AISideboardingEnabled = false; private boolean sideboardForAI = false; private final Set appliedVariants = EnumSet.noneOf(GameType.class); + private int simTimeout = 120; // it's a preference, not rule... but I could hardly find a better place for it private boolean useGrayText; @@ -124,4 +125,12 @@ public class GameRules { public void setWarnAboutAICards(final boolean warnAboutAICards) { this.warnAboutAICards = warnAboutAICards; } + + public int getSimTimeout() { + return this.simTimeout; + } + + public void setSimTimeout(final int duration) { + this.simTimeout = duration; + } } diff --git a/forge-game/src/main/java/forge/game/GameView.java b/forge-game/src/main/java/forge/game/GameView.java index 2c5540db3dd..db597afa820 100644 --- a/forge-game/src/main/java/forge/game/GameView.java +++ b/forge-game/src/main/java/forge/game/GameView.java @@ -215,6 +215,7 @@ public class GameView extends TrackableObject { } public void setDependencies(Table> dependencies) { if (dependencies.isEmpty()) { + set(TrackableProperty.Dependencies, ""); return; } StringBuilder sb = new StringBuilder(); diff --git a/forge-game/src/main/java/forge/game/Match.java b/forge-game/src/main/java/forge/game/Match.java index 2c5b7e3605e..90759e6274c 100644 --- a/forge-game/src/main/java/forge/game/Match.java +++ b/forge-game/src/main/java/forge/game/Match.java @@ -23,6 +23,7 @@ import forge.item.PaperCard; import forge.util.Localizer; import forge.util.MyRandom; import forge.util.collect.FCollectionView; +import org.apache.commons.lang3.tuple.Pair; import java.util.*; import java.util.Map.Entry; @@ -224,6 +225,7 @@ public class Match { // friendliness Map>> rAICards = new HashMap<>(); Multimap removedAnteCards = ArrayListMultimap.create(); + Map> unsupported = new HashMap<>(); final FCollectionView players = game.getPlayers(); final List playersConditions = game.getMatch().getPlayers(); @@ -288,22 +290,32 @@ public class Match { } } - Deck myDeck = psc.getDeck(); - player.setDraftNotes(myDeck.getDraftNotes()); + Deck toCheck = psc.getDeck(); + if (toCheck == null) { + try { + System.err.println(psc.getPlayer().getName() + " Deck is NULL..."); + int val = rules.getGameType().getDeckFormat().getMainRange().getMinimum(); + toCheck = new Deck("NULL"); + if (val > 0) + toCheck.getMain().add("Wastes", val); + } catch (Exception ignored) {} + } + Pair> myDeck = toCheck.getValid(); + player.setDraftNotes(myDeck.getLeft().getDraftNotes()); Set myRemovedAnteCards = null; if (!rules.useAnte()) { - myRemovedAnteCards = getRemovedAnteCards(myDeck); + myRemovedAnteCards = getRemovedAnteCards(myDeck.getLeft()); for (PaperCard cp: myRemovedAnteCards) { - for (Entry ds : myDeck) { + for (Entry ds : myDeck.getLeft()) { ds.getValue().removeAll(cp); } } } - preparePlayerZone(player, ZoneType.Library, myDeck.getMain(), psc.useRandomFoil()); - if (myDeck.has(DeckSection.Sideboard)) { - preparePlayerZone(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil()); + preparePlayerZone(player, ZoneType.Library, myDeck.getLeft().getMain(), psc.useRandomFoil()); + if (myDeck.getLeft().has(DeckSection.Sideboard)) { + preparePlayerZone(player, ZoneType.Sideboard, myDeck.getLeft().get(DeckSection.Sideboard), psc.useRandomFoil()); // Assign Companion Card companion = player.assignCompanion(game, person); @@ -322,7 +334,7 @@ public class Match { player.shuffle(null); if (isFirstGame) { - Map> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck); + Map> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck.getLeft()); if (cardsComplained != null && !cardsComplained.isEmpty()) { rAICards.put(player, cardsComplained); } @@ -337,6 +349,7 @@ public class Match { if (myRemovedAnteCards != null && !myRemovedAnteCards.isEmpty()) { removedAnteCards.putAll(player, myRemovedAnteCards); } + unsupported.put(player, myDeck.getRight()); } final Localizer localizer = Localizer.getInstance(); @@ -347,6 +360,10 @@ public class Match { if (!removedAnteCards.isEmpty()) { game.getAction().revealAnte(localizer.getMessage("lblAnteCardsRemoved"), removedAnteCards); } + + if (!unsupported.isEmpty()) { + game.getAction().revealUnsupported(unsupported); + } } private void executeAnte(Game lastGame) { diff --git a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java index 4b452a7620c..f0c89aa25d2 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java @@ -239,6 +239,10 @@ public final class AbilityFactory { spellAbility.putParam("PrecostDesc", "Exhaust — "); } + if (mapParams.containsKey("Named")) { + spellAbility.setName(mapParams.get("Named")); + } + // ********************************************* // set universal properties of the SpellAbility @@ -359,9 +363,6 @@ public final class AbilityFactory { if (mapParams.containsKey("TargetUnique")) { abTgt.setUniqueTargets(true); } - if (mapParams.containsKey("TargetsFromSingleZone")) { - abTgt.setSingleZone(true); - } if (mapParams.containsKey("TargetsWithoutSameCreatureType")) { abTgt.setWithoutSameCreatureType(true); } @@ -383,6 +384,9 @@ public final class AbilityFactory { if (mapParams.containsKey("TargetsWithDifferentCMC")) { abTgt.setDifferentCMC(true); } + if (mapParams.containsKey("TargetsWithDifferentNames")) { + abTgt.setDifferentNames(true); + } if (mapParams.containsKey("TargetsWithEqualToughness")) { abTgt.setEqualToughness(true); } 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 94d1d10f119..2f8d310fcc7 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -43,7 +43,6 @@ import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; - public class AbilityUtils { private final static ImmutableList cmpList = ImmutableList.of("LT", "LE", "EQ", "GE", "GT", "NE"); @@ -523,6 +522,8 @@ public class AbilityUtils { } } else if (calcX[0].equals("OriginalHost")) { val = xCount(ability.getOriginalHost(), calcX[1], ability); + } else if (calcX[0].equals("DungeonsCompleted")) { + val = handlePaid(player.getCompletedDungeons(), calcX[1], card, ability); } else if (calcX[0].startsWith("ExiledWith")) { val = handlePaid(card.getExiledCards(), calcX[1], card, ability); } else if (calcX[0].startsWith("Convoked")) { @@ -1870,6 +1871,14 @@ public class AbilityUtils { } return doXMath(v, expr, c, ctb); } + + // Count$FromNamedAbility[abilityName].. + if (sq[0].startsWith("FromNamedAbility")) { + String abilityNamed = sq[0].substring(16); + SpellAbility trigSA = sa.getHostCard().getCastSA(); + boolean fromNamedAbility = trigSA != null && trigSA.getName().equals(abilityNamed); + return doXMath(calculateAmount(c, sq[fromNamedAbility ? 1 : 2], ctb), expr, c, ctb); + } } else { // fallback if ctb isn't a spellability if (sq[0].startsWith("LastStateBattlefield")) { @@ -2534,34 +2543,13 @@ public class AbilityUtils { return doXMath(CardLists.getValidCardCount(game.getLeftGraveyardThisTurn(), validFilter, player, c, ctb), expr, c, ctb); } - // Count$UnlockedDoors - if (sq[0].startsWith("UnlockedDoors")) { - final String[] workingCopy = l[0].split(" ", 2); - final String validFilter = workingCopy[1]; - - int unlocked = 0; - for (Card doorCard : CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), validFilter, player, c, ctb)) { - unlocked += doorCard.getUnlockedRooms().size(); - } - - return doXMath(unlocked, expr, c, ctb); + if (sq[0].equals("UnlockedDoors")) { + return doXMath(player.getUnlockedDoors().size(), expr, c, ctb); } - // Count$DistinctUnlockedDoors // Counts the distinct names of unlocked doors. Used for the "Promising Stairs" - if (sq[0].startsWith("DistinctUnlockedDoors")) { - final String[] workingCopy = l[0].split(" ", 2); - final String validFilter = workingCopy[1]; - - Set viewedNames = new HashSet<>(); - for (Card doorCard : CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), validFilter, player, c, ctb)) { - for(CardStateName stateName : doorCard.getUnlockedRooms()) { - viewedNames.add(doorCard.getState(stateName).getName()); - } - } - int distinctUnlocked = viewedNames.size(); - - return doXMath(distinctUnlocked, expr, c, ctb); + if (sq[0].equals("DistinctUnlockedDoors")) { + return doXMath(Sets.newHashSet(player.getUnlockedDoors()).size(), expr, c, ctb); } // Manapool @@ -2881,21 +2869,6 @@ public class AbilityUtils { return max; } - if (sq[0].startsWith("DifferentCardNames_")) { - final List crdname = Lists.newArrayList(); - final String restriction = l[0].substring(19); - CardCollection list = CardLists.getValidCards(game.getCardsInGame(), restriction, player, c, ctb); - // TODO rewrite with sharesName to respect Spy Kit - for (final Card card : list) { - String name = card.getName(); - // CR 201.2b Those objects have different names only if each of them has at least one name and no two objects in that group have a name in common - if (!crdname.contains(name) && !name.isEmpty()) { - crdname.add(name); - } - } - return doXMath(crdname.size(), expr, c, ctb); - } - if (sq[0].startsWith("MostProminentCreatureType")) { String restriction = l[0].split(" ")[1]; CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb); @@ -2910,13 +2883,6 @@ public class AbilityUtils { } // TODO move below to handlePaid - if (sq[0].startsWith("SumPower")) { - final String[] restrictions = l[0].split("_"); - int sumPower = game.getCardsIn(ZoneType.Battlefield).stream() - .filter(CardPredicates.restriction(restrictions[1], player, c, ctb)) - .mapToInt(Card::getNetPower).sum(); - return doXMath(sumPower, expr, c, ctb); - } if (sq[0].startsWith("DifferentPower_")) { final String restriction = l[0].substring(15); final int uniquePowers = (int) game.getCardsIn(ZoneType.Battlefield).stream() @@ -3436,6 +3402,7 @@ public class AbilityUtils { } public static int playerXProperty(final Player player, final String s, final Card source, CardTraitBase ctb) { + final String[] l = s.split("/"); final String m = CardFactoryUtil.extractOperators(s); @@ -3622,46 +3589,10 @@ public class AbilityUtils { return doXMath(player.hasBeenDealtCombatDamageSinceLastTurn() ? 1 : 0, m, source, ctb); } - if (value.equals("DungeonsCompleted")) { - return doXMath(player.getCompletedDungeons().size(), m, source, ctb); - } - if (value.equals("RingTemptedYou")) { return doXMath(player.getNumRingTemptedYou(), m, source, ctb); } - if (value.startsWith("DungeonCompletedNamed")) { - String [] full = value.split("_"); - String name = full[1]; - int completed = 0; - List dungeons = player.getCompletedDungeons(); - for (Card c : dungeons) { - if (c.getName().equals(name)) { - ++completed; - } - } - return doXMath(completed, m, source, ctb); - } - if (value.equals("DifferentlyNamedDungeonsCompleted")) { - int amount = 0; - List dungeons = player.getCompletedDungeons(); - for (int i = 0; i < dungeons.size(); ++i) { - Card d1 = dungeons.get(i); - boolean hasSameName = false; - for (int j = i - 1; j >= 0; --j) { - Card d2 = dungeons.get(j); - if (d1.getName().equals(d2.getName())) { - hasSameName = true; - break; - } - } - if (!hasSameName) { - ++amount; - } - } - return doXMath(amount, m, source, ctb); - } - if (value.equals("AttractionsVisitedThisTurn")) { return doXMath(player.getAttractionsVisitedThisTurn(), m, source, ctb); } @@ -3740,10 +3671,6 @@ public class AbilityUtils { return CardLists.getTotalPower(paidList, ctb); } - if (string.startsWith("SumToughness")) { - return Aggregates.sum(paidList, Card::getNetToughness); - } - if (string.startsWith("GreatestCMC")) { return Aggregates.max(paidList, Card::getCMC); } @@ -3752,6 +3679,10 @@ public class AbilityUtils { return CardUtil.getColorsFromCards(paidList).countColors(); } + if (string.startsWith("DifferentCardNames")) { + return doXMath(CardLists.getDifferentNamesCount(paidList), CardFactoryUtil.extractOperators(string), source, ctb); + } + if (string.equals("DifferentColorPair")) { final Set diffPair = new HashSet<>(); for (final Card card : paidList) { 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 96bc1c9c6e1..f9e8358fa23 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -1069,7 +1069,7 @@ public abstract class SpellAbilityEffect { // if ability was granted use that source so they can be kept apart later if (cause.isCopiedTrait()) { exilingSource = cause.getOriginalHost(); - } else if (cause.getKeyword() != null && cause.getKeyword().getStatic() != null) { + } else if (!cause.isSpell() && cause.getKeyword() != null && cause.getKeyword().getStatic() != null) { exilingSource = cause.getKeyword().getStatic().getOriginalHost(); } movedCard.setExiledWith(exilingSource); diff --git a/forge-game/src/main/java/forge/game/ability/effects/AirbendEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AirbendEffect.java index a32995cd92d..bfb9f8b85d9 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AirbendEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AirbendEffect.java @@ -20,14 +20,14 @@ public class AirbendEffect extends SpellAbilityEffect { @Override protected String getStackDescription(SpellAbility sa) { final StringBuilder sb = new StringBuilder("Airbend "); - + Iterable tgts; if (sa.usesTargeting()) { tgts = getCardsfromTargets(sa); } else { // otherwise add self to list and go from there tgts = sa.knownDetermineDefined(sa.getParam("Defined")); } - + sb.append(sa.getParamOrDefault("DefinedDesc", Lang.joinHomogenous(tgts))); sb.append("."); if (Iterables.size(tgts) > 1) { @@ -46,7 +46,7 @@ public class AirbendEffect extends SpellAbilityEffect { final Player pl = sa.getActivatingPlayer(); final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa); - + for (Card c : getTargetCards(sa)) { final Card gameCard = game.getCardState(c, null); // gameCard is LKI in that case, the card is not in game anymore @@ -55,7 +55,7 @@ public class AirbendEffect extends SpellAbilityEffect { if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) { continue; } - + if (!gameCard.canExiledBy(sa, true)) { continue; } @@ -86,7 +86,7 @@ public class AirbendEffect extends SpellAbilityEffect { } triggerList.triggerChangesZoneAll(game, sa); handleExiledWith(triggerList.allCards(), sa); - + pl.triggerElementalBend(TriggerType.Airbend); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/AlterAttributeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AlterAttributeEffect.java index 79ad1993dc2..14ae5d11caf 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AlterAttributeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AlterAttributeEffect.java @@ -46,6 +46,9 @@ public class AlterAttributeEffect extends SpellAbilityEffect { boolean altered = false; switch (attr.trim()) { + case "Harnessed": + altered = gameCard.setHarnessed(activate); + break; case "Plotted": altered = gameCard.setPlotted(activate); diff --git a/forge-game/src/main/java/forge/game/ability/effects/AmassEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AmassEffect.java index 848e5fb87ca..1eafcfab5b0 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AmassEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AmassEffect.java @@ -17,7 +17,6 @@ import forge.game.card.CardLists; import forge.game.card.CardPredicates; import forge.game.card.CardZoneTable; import forge.game.card.CounterEnumType; -import forge.game.card.CounterType; import forge.game.card.token.TokenInfo; import forge.game.event.GameEventCombatChanged; import forge.game.event.GameEventTokenCreated; @@ -86,7 +85,7 @@ public class AmassEffect extends TokenEffectBase { } Map params = Maps.newHashMap(); - params.put("CounterType", CounterType.get(CounterEnumType.P1P1)); + params.put("CounterType", CounterEnumType.P1P1); params.put("Amount", amount); Card tgt = activator.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblChooseAnArmy"), false, params); diff --git a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java index 667257cb0bc..efdfb6b197e 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AnimateEffectBase.java @@ -90,6 +90,16 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect { c.addPerpetual(p); p.applyEffect(c); } + if (sa.hasParam("ManaCost")) { + final ManaCost manaCost = new ManaCost(new ManaCostParser(sa.getParam("ManaCost"))); + if (perpetual) { + PerpetualManaCost p = new PerpetualManaCost(timestamp, manaCost); + c.addPerpetual(p); + p.applyEffect(c); + } else { + c.addChangedManaCost(manaCost, timestamp, (long) 0); + } + } if (!addType.isEmpty() || !removeType.isEmpty() || addAllCreatureTypes || !remove.isEmpty()) { if (perpetual) { @@ -128,7 +138,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect { if (perpetual) { c.addPerpetual(new PerpetualColors(timestamp, colors, overwrite)); } - c.addColor(colors, !overwrite, timestamp, 0, false); + c.addColor(colors, !overwrite, timestamp, null); } if (sa.hasParam("LeaveBattlefield")) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeTextEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeTextEffect.java index b425d41a6f8..74308bc66c7 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeTextEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeTextEffect.java @@ -34,7 +34,7 @@ public class ChangeTextEffect extends SpellAbilityEffect { final String[] changedColorWordsArray = sa.getParam("ChangeColorWord").split(" "); if (changedColorWordsArray[0].equals("Choose")) { originalColor = sa.getActivatingPlayer().getController().chooseColor( - Localizer.getInstance().getMessage("lblChooseColorReplace"), sa, ColorSet.ALL_COLORS); + Localizer.getInstance().getMessage("lblChooseColorReplace"), sa, ColorSet.WUBRG); changedColorWordOriginal = TextUtil.capitalize(MagicColor.toLongString(originalColor)); } else { changedColorWordOriginal = changedColorWordsArray[0]; @@ -44,7 +44,7 @@ public class ChangeTextEffect extends SpellAbilityEffect { if (changedColorWordsArray[1].equals("Choose")) { final ColorSet possibleNewColors; if (originalColor == 0) { // no original color (ie. any or absent) - possibleNewColors = ColorSet.ALL_COLORS; + possibleNewColors = ColorSet.WUBRG; } else { // may choose any except original color possibleNewColors = ColorSet.fromMask(originalColor).inverse(); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java index 530e944e6c1..885b6ab442d 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java @@ -31,6 +31,7 @@ import org.apache.commons.lang3.tuple.Pair; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class ChangeZoneEffect extends SpellAbilityEffect { @@ -103,6 +104,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } final String destination = sa.getParam("Destination"); + final int num = sa.hasParam("ChangeNum") ? AbilityUtils.calculateAmount(host, sa.getParam("ChangeNum"), sa) : 1; String type = "card"; boolean defined = false; if (sa.hasParam("ChangeTypeDesc")) { @@ -117,12 +119,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect { type = Lang.joinHomogenous(tgts); defined = true; } else if (sa.hasParam("ChangeType") && !sa.getParam("ChangeType").equals("Card")) { - final String ct = sa.getParam("ChangeType"); - type = CardType.CoreType.isValidEnum(ct) ? ct.toLowerCase() : ct; + List typeList = Arrays.stream(sa.getParam("ChangeType").split(",")).map(ct -> CardType.isACardType(ct) ? ct.toLowerCase() : ct).collect(Collectors.toList()); + type = Lang.joinHomogenous(typeList, null, num == 1 ? "or" : "and/or"); } final String cardTag = type.contains("card") ? "" : " card"; - final int num = sa.hasParam("ChangeNum") ? AbilityUtils.calculateAmount(host, sa.getParam("ChangeNum"), sa) : 1; boolean tapped = sa.hasParam("Tapped"); boolean attacking = sa.hasParam("Attacking"); if (sa.isNinjutsu()) { @@ -152,6 +153,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } else { sb.append(" for "); } + if (num != 1) { + sb.append(" up to "); + } sb.append(Lang.nounWithNumeralExceptOne(num, type + cardTag)).append(", "); if (!sa.hasParam("NoReveal") && ZoneType.smartValueOf(destination) != null && ZoneType.smartValueOf(destination).isHidden()) { if (choosers.size() == 1) { @@ -928,7 +932,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { List origin = Lists.newArrayList(); if (sa.hasParam("Origin")) { - origin = ZoneType.listValueOf(sa.getParam("Origin")); + origin.addAll(ZoneType.listValueOf(sa.getParam("Origin"))); } ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); @@ -969,12 +973,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect { String prompt; if (sa.hasParam("OptionalPrompt")) { prompt = sa.getParam("OptionalPrompt"); + } else if (defined) { + prompt = Localizer.getInstance().getMessage("lblPutThatCardFromPlayerOriginToDestination", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase(), destination.getTranslatedName().toLowerCase()); } else { - if (defined) { - prompt = Localizer.getInstance().getMessage("lblPutThatCardFromPlayerOriginToDestination", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase(), destination.getTranslatedName().toLowerCase()); - } else { - prompt = Localizer.getInstance().getMessage("lblSearchPlayerZoneConfirm", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase()); - } + prompt = Localizer.getInstance().getMessage("lblSearchPlayerZoneConfirm", "{player's}", Lang.joinHomogenous(origin, ZoneType::getTranslatedName).toLowerCase()); } String message = MessageUtil.formatMessage(prompt , decider, player); if (!decider.getController().confirmAction(sa, PlayerActionConfirmMode.ChangeZoneGeneral, message, null)) { @@ -1101,7 +1103,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { if (changeType.startsWith("EACH")) { String[] eachTypes = changeType.substring(5).split(" & "); for (String thisType : eachTypes) { - for (int i = 0; i < changeNum && destination != null; i++) { + for (int i = 0; i < changeNum; i++) { CardCollection thisList = (CardCollection) AbilityUtils.filterListByType(fetchList, thisType, sa); if (!chosenCards.isEmpty()) { thisList.removeAll(chosenCards); @@ -1136,7 +1138,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { // maybe prompt the user if they selected fewer than the maximum possible? } else { // one at a time - for (int i = 0; i < changeNum && destination != null; i++) { + for (int i = 0; i < changeNum; i++) { if (sa.hasParam("DifferentNames")) { for (Card c : chosenCards) { fetchList = CardLists.filter(fetchList, CardPredicates.sharesNameWith(c).negate()); @@ -1250,6 +1252,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect { player.removeController(controlTimestamp); } + if (sa.hasParam("Exactly") && chosenCards.size() < changeNum) { + continue; + } + HiddenOriginChoices choices = new HiddenOriginChoices(); choices.searchedLibrary = searchedLibrary; choices.shuffleMandatory = shuffleMandatory; @@ -1279,7 +1285,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { Player decider = ObjectUtils.firstNonNull(chooser, player); for (final Card c : chosenCards) { - Card movedCard = null; + Card movedCard; final Zone originZone = game.getZoneOf(c); Map moveParams = AbilityKey.newMap(); moveParams.put(AbilityKey.FoundSearchingLibrary, searchedLibrary); @@ -1411,6 +1417,9 @@ public class ChangeZoneEffect extends SpellAbilityEffect { movedCard.addMayLookFaceDownExile(sa.getActivatingPlayer()); } } + else if (destination == null) { + movedCard = c; + } else { movedCard = game.getAction().moveTo(destination, c, 0, sa, moveParams); } @@ -1476,7 +1485,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } if (ZoneType.Exile.equals(destination) && sa.hasParam("WithCountersType")) { CounterType cType = CounterType.getType(sa.getParam("WithCountersType")); - int cAmount = AbilityUtils.calculateAmount(sa.getOriginalHost(), sa.getParamOrDefault("WithCountersAmount", "1"), sa); + int cAmount = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("WithCountersAmount", "1"), sa); GameEntityCounterTable table = new GameEntityCounterTable(); movedCard.addCounter(cType, cAmount, player, table); table.replaceCounterEffect(game, sa, true); diff --git a/forge-game/src/main/java/forge/game/ability/effects/CopyPermanentEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CopyPermanentEffect.java index 7345e824bb1..771909901ab 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CopyPermanentEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CopyPermanentEffect.java @@ -287,22 +287,17 @@ public class CopyPermanentEffect extends TokenEffectBase { int id = newOwner == null ? 0 : newOwner.getGame().nextCardId(); // need to create a physical card first, i need the original card faces copy = CardFactory.getCard(original.getPaperCard(), newOwner, id, host.getGame()); + + copy.setStates(CardFactory.getCloneStates(original, copy, sa)); + // force update the now set State if (original.isTransformable()) { + copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true); // 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(); - } - } - - copy.setStates(CardFactory.getCloneStates(original, copy, sa)); - // force update the now set State - if (original.isTransformable()) { - copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true); } else { copy.setState(copy.getCurrentStateName(), true, true); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java index fd401571072..41b4c840054 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersRemoveEffect.java @@ -102,24 +102,29 @@ public class CountersRemoveEffect extends SpellAbilityEffect { int totalRemoved = 0; CardCollectionView srcCards; - if (sa.hasParam("Choices")) { ZoneType choiceZone = sa.hasParam("ChoiceZone") ? ZoneType.smartValueOf(sa.getParam("ChoiceZone")) : ZoneType.Battlefield; - - CardCollection choices = CardLists.getValidCards(game.getCardsIn(choiceZone), sa.getParam("Choices"), + srcCards = CardLists.getValidCards(game.getCardsIn(choiceZone), sa.getParam("Choices"), activator, source, sa); + } else { + srcCards = getTargetCards(sa); + } + if (sa.isReplacementAbility()) { + srcCards = new CardCollection(srcCards).filter(c -> !c.isInPlay() || sa.getLastStateBattlefield().contains(c)); + } + if (sa.hasParam("Choices")) { int min = 1; int max = 1; if (sa.hasParam("ChoiceOptional")) { min = 0; - max = choices.size(); + max = srcCards.size(); } if (sa.hasParam("ChoiceNum")) { min = max = AbilityUtils.calculateAmount(source, sa.getParam("ChoiceNum"), sa); } - if (choices.size() < min) { + if (srcCards.size() < min) { return; } @@ -128,13 +133,12 @@ public class CountersRemoveEffect extends SpellAbilityEffect { title = title.replace(" ", " "); Map params = Maps.newHashMap(); params.put("CounterType", counterType); - srcCards = pc.chooseCardsForEffect(choices, sa, title, min, max, min == 0, params); + srcCards = pc.chooseCardsForEffect(srcCards, sa, title, min, max, min == 0, params); } else { for (final Player tgtPlayer : getTargetPlayers(sa)) { if (!tgtPlayer.isInGame()) { continue; } - // Removing energy if (type.equals("All")) { for (Map.Entry e : Lists.newArrayList(tgtPlayer.getCounters().entrySet())) { totalRemoved += tgtPlayer.subtractCounter(e.getKey(), e.getValue(), activator); @@ -150,8 +154,6 @@ public class CountersRemoveEffect extends SpellAbilityEffect { } } } - - srcCards = getTargetCards(sa); } for (final Card tgtCard : srcCards) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/DamageResolveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DamageResolveEffect.java index c96a27937fc..e4e85049f56 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DamageResolveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DamageResolveEffect.java @@ -18,6 +18,10 @@ public class DamageResolveEffect extends SpellAbilityEffect { @Override public void resolve(SpellAbility sa) { CardDamageMap damageMap = sa.getDamageMap(); + if (damageMap == null) { + // this can happen if damagesource was missing + return; + } CardDamageMap preventMap = sa.getPreventMap(); GameEntityCounterTable counterTable = sa.getCounterTable(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java index 8c2ff61c0e8..e1a707bd1d8 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java @@ -73,6 +73,11 @@ import java.util.*; } Card made = game.getAction().moveTo(zone, c, sa, moveParams); + if (zone.equals(ZoneType.Battlefield)) { + if (sa.hasParam("Tapped")) { + made.setTapped(true); + } + } if (zone.equals(ZoneType.Exile)) { handleExiledWith(made, sa); if (sa.hasParam("ExileFaceDown")) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/EarthbendEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EarthbendEffect.java index 5e58004a14c..723bf4522e9 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/EarthbendEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/EarthbendEffect.java @@ -47,7 +47,7 @@ public class EarthbendEffect extends SpellAbilityEffect { final Game game = source.getGame(); final Player pl = sa.getActivatingPlayer(); int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("Num", "1"), sa); - + long ts = game.getNextTimestamp(); String desc = "When it dies or is exiled, return it to the battlefield tapped."; @@ -59,17 +59,17 @@ public class EarthbendEffect extends SpellAbilityEffect { c.addNewPT(0, 0, ts, 0); c.addChangedCardTypes(Arrays.asList("Creature"), null, false, EnumSet.noneOf(RemoveType.class), ts, 0, true, false); c.addChangedCardKeywords(Arrays.asList("Haste"), null, false, ts, null); - + GameEntityCounterTable table = new GameEntityCounterTable(); c.addCounter(CounterEnumType.P1P1, num, pl, table); table.replaceCounterEffect(game, sa, true); - + buildTrigger(sa, c, sbTrigA, "Graveyard"); buildTrigger(sa, c, sbTrigB, "Exile"); } pl.triggerElementalBend(TriggerType.Earthbend); } - + protected void buildTrigger(SpellAbility sa, Card c, String sbTrig, String zone) { final Card source = sa.getHostCard(); final Game game = source.getGame(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java index a7ca54bd0e2..954c833b7ab 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java @@ -269,22 +269,22 @@ public class EffectEffect extends SpellAbilityEffect { } } - // Set Chosen Color(s) if (hostCard.hasChosenColor()) { eff.setChosenColors(Lists.newArrayList(hostCard.getChosenColors())); } - // Set Chosen Cards if (hostCard.hasChosenCard()) { eff.setChosenCards(hostCard.getChosenCards()); } - // Set Chosen Player if (hostCard.hasChosenPlayer()) { eff.setChosenPlayer(hostCard.getChosenPlayer()); } - // Set Chosen Type + if (hostCard.getChosenDirection() != null) { + eff.setChosenDirection(hostCard.getChosenDirection()); + } + if (hostCard.hasChosenType()) { eff.setChosenType(hostCard.getChosenType()); } @@ -292,12 +292,10 @@ public class EffectEffect extends SpellAbilityEffect { eff.setChosenType2(hostCard.getChosenType2()); } - // Set Chosen name if (hostCard.hasNamedCard()) { eff.setNamedCards(Lists.newArrayList(hostCard.getNamedCards())); } - // chosen number if (sa.hasParam("SetChosenNumber")) { eff.setChosenNumber(AbilityUtils.calculateAmount(hostCard, sa.getParam("SetChosenNumber"), sa)); } else if (hostCard.hasChosenNumber()) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ImmediateTriggerEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ImmediateTriggerEffect.java index 49e8af3f38d..5f640ddaa82 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ImmediateTriggerEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ImmediateTriggerEffect.java @@ -34,6 +34,9 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect { public void resolve(SpellAbility sa) { final Card host = sa.getHostCard(); final Game game = host.getGame(); + + // CR 603.12a if the trigger event or events occur multiple times during the resolution of the spell or ability that created it, + // the reflexive triggered ability will trigger once for each of those times int amt = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("TriggerAmount", "1"), sa); if (amt <= 0) { return; diff --git a/forge-game/src/main/java/forge/game/ability/effects/LifeExchangeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/LifeExchangeEffect.java index c179f83b10d..3c4bcfd5c08 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/LifeExchangeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/LifeExchangeEffect.java @@ -54,33 +54,29 @@ public class LifeExchangeEffect extends SpellAbilityEffect { final int life1 = p1.getLife(); final int life2 = p2.getLife(); + final int diff = Math.abs(life1 - life2); - if (sa.hasParam("RememberDifference")) { - final int diff = life1 - life2; - source.addRemembered(diff); + if (life2 > life1) { + // swap players + Player tmp = p2; + p2 = p1; + p1 = tmp; } - - final Map lossMap = Maps.newHashMap(); - if ((life1 > life2) && p1.canLoseLife() && p2.canGainLife()) { - final int diff = life1 - life2; + if (diff > 0 && p1.canLoseLife() && p2.canGainLife()) { final int lost = p1.loseLife(diff, false, false); p2.gainLife(diff, source, sa); if (lost > 0) { + final Map lossMap = Maps.newHashMap(); lossMap.put(p1, lost); + final Map runParams = AbilityKey.mapFromPIMap(lossMap); + source.getGame().getTriggerHandler().runTrigger(TriggerType.LifeLostAll, runParams, false); + if (sa.hasParam("RememberOwnLoss") && p1.equals(sa.getActivatingPlayer())) { + source.addRemembered(lost); + } } - } else if ((life2 > life1) && p2.canLoseLife() && p1.canGainLife()) { - final int diff = life2 - life1; - final int lost = p2.loseLife(diff, false, false); - p1.gainLife(diff, source, sa); - if (lost > 0) { - lossMap.put(p2, lost); - } - } else { - // they are equal or can't be exchanged, so nothing to do } - if (!lossMap.isEmpty()) { // Run triggers if any player actually lost life - final Map runParams = AbilityKey.mapFromPIMap(lossMap); - source.getGame().getTriggerHandler().runTrigger(TriggerType.LifeLostAll, runParams, false); + if (sa.hasParam("RememberDifference")) { + source.addRemembered(p1.getLife() - p2.getLife()); } } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java index 49bff555a0f..b21b252e4cb 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ManaEffect.java @@ -156,7 +156,7 @@ public class ManaEffect extends SpellAbilityEffect { for (int nChar = 0; nChar < colorsNeeded.length(); nChar++) { mask |= MagicColor.fromName(colorsNeeded.charAt(nChar)); } - colorMenu = mask == 0 ? ColorSet.ALL_COLORS : ColorSet.fromMask(mask); + colorMenu = mask == 0 ? ColorSet.WUBRG : ColorSet.fromMask(mask); byte val = chooser.getController().chooseColor(Localizer.getInstance().getMessage("lblSelectManaProduce"), sa, colorMenu); if (0 == val) { throw new RuntimeException("ManaEffect::resolve() /*any mana*/ - " + p + " color mana choice is empty for " + card.getName()); @@ -271,10 +271,11 @@ public class ManaEffect extends SpellAbilityEffect { producedMana.append(abMana.produceMana(mana, p, sa)); } - abMana.tapsForMana(sa.getRootAbility(), producedMana.toString()); - // Only clear express choice after mana has been produced abMana.clearExpressChoice(); + + abMana.tapsForMana(sa.getRootAbility(), producedMana.toString()); + if (sa.isKeyword(Keyword.FIREBENDING)) { activator.triggerElementalBend(TriggerType.Firebend); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/MutateEffect.java b/forge-game/src/main/java/forge/game/ability/effects/MutateEffect.java index 4cf4203da68..3500663e6cf 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/MutateEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/MutateEffect.java @@ -80,7 +80,7 @@ public class MutateEffect extends SpellAbilityEffect { game.getTriggerHandler().clearActiveTriggers(target, null); game.getTriggerHandler().registerActiveTrigger(target, false); - game.getAction().moveTo(p.getZone(ZoneType.Merged), host, sa, AbilityKey.newMap()); + game.getAction().moveTo(p.getZone(ZoneType.Merged), host, sa); host.setTapped(target.isTapped()); host.setFlipped(target.isFlipped()); diff --git a/forge-game/src/main/java/forge/game/ability/effects/PermanentCreatureEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PermanentCreatureEffect.java index 220db684055..af40471d554 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PermanentCreatureEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PermanentCreatureEffect.java @@ -15,8 +15,9 @@ public class PermanentCreatureEffect extends PermanentEffect { public String getStackDescription(final SpellAbility sa) { final CardState source = sa.getCardState(); final StringBuilder sb = new StringBuilder(); - sb.append(CardTranslation.getTranslatedName(source.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" ").append(source.getBasePowerString()); - sb.append(" / ").append(source.getBaseToughnessString()); + sb.append(CardTranslation.getTranslatedName(source.getName())).append(" - ").append(Localizer.getInstance().getMessage("lblCreature")).append(" "); + sb.append(sa.getParamOrDefault("SetPower", source.getBasePowerString())); + sb.append(" / ").append(sa.getParamOrDefault("SetToughness", source.getBaseToughnessString())); return sb.toString(); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java index 5bc43048acc..514b5c2bd16 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java @@ -428,6 +428,10 @@ public class PlayEffect extends SpellAbilityEffect { tgtSA.getTargetRestrictions().setMandatory(true); } + if (sa.hasParam("Named")) { + tgtSA.setName(sa.getName()); + } + // can't be done later if (sa.hasParam("ReplaceGraveyard")) { if (!sa.hasParam("ReplaceGraveyardValid") 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 278080894a4..bbcab86846e 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 @@ -124,8 +124,8 @@ public class ProtectEffect extends SpellAbilityEffect { } } else if (sa.getParam("Gains").startsWith("Defined")) { CardCollection def = AbilityUtils.getDefinedCards(host, sa.getParam("Gains").substring(8), sa); - for (final Byte color : def.get(0).getColor()) { - gains.add(MagicColor.toLongString(color)); + for (final MagicColor.Color color : def.get(0).getColor()) { + gains.add(color.getName()); } } else { gains.addAll(choices); diff --git a/forge-game/src/main/java/forge/game/ability/effects/PumpEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PumpEffect.java index 1369f2bd4f5..86d108b9225 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PumpEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PumpEffect.java @@ -17,7 +17,6 @@ import forge.game.GameEntity; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; -import forge.game.card.CardCollection; import forge.game.card.CardFactoryUtil; import forge.game.card.CardUtil; import forge.game.card.perpetual.PerpetualKeywords; @@ -282,6 +281,17 @@ public class PumpEffect extends SpellAbilityEffect { List tgtCards = getCardsfromTargets(sa); List tgtPlayers = getTargetPlayers(sa); + if (sa.hasParam("Optional")) { + final String targets = Lang.joinHomogenous(tgtCards); + final String message = sa.hasParam("OptionQuestion") + ? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets) + : Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets); + + if (!activator.getController().confirmAction(sa, null, message, null)) { + return; + } + } + List keywords = Lists.newArrayList(); if (sa.hasParam("KW")) { keywords.addAll(Arrays.asList(sa.getParam("KW").split(" & "))); @@ -307,8 +317,6 @@ public class PumpEffect extends SpellAbilityEffect { keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, host, sa); } - final CardCollection untargetedCards = CardUtil.getRadiance(sa); - if (sa.hasParam("DefinedKW")) { String defined = sa.getParam("DefinedKW"); if (defined.equals("ChosenType")) { @@ -394,17 +402,6 @@ public class PumpEffect extends SpellAbilityEffect { keywords = choice; } - if (sa.hasParam("Optional")) { - final String targets = Lang.joinHomogenous(tgtCards); - final String message = sa.hasParam("OptionQuestion") - ? TextUtil.fastReplace(sa.getParam("OptionQuestion"), "TARGETS", targets) - : Localizer.getInstance().getMessage("lblApplyPumpToTarget", targets); - - if (!activator.getController().confirmAction(sa, null, message, null)) { - return; - } - } - if (sa.hasParam("RememberObjects")) { host.addRemembered(AbilityUtils.getDefinedObjects(host, sa.getParam("RememberObjects"), sa)); } @@ -494,7 +491,7 @@ public class PumpEffect extends SpellAbilityEffect { registerDelayedTrigger(sa, sa.getParam("AtEOT"), tgtCards); } - for (final Card tgtC : untargetedCards) { + for (final Card tgtC : CardUtil.getRadiance(sa)) { // only pump things in PumpZone if (!tgtC.isInZones(pumpZones)) { continue; diff --git a/forge-game/src/main/java/forge/game/ability/effects/ReplaceManaEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ReplaceManaEffect.java index 70ff51e0829..f754eedb5ad 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ReplaceManaEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ReplaceManaEffect.java @@ -34,14 +34,14 @@ public class ReplaceManaEffect extends SpellAbilityEffect { // replace type and amount replaced = sa.getParam("ReplaceMana"); if ("Any".equals(replaced)) { - byte rs = player.getController().chooseColor("Choose a color", sa, ColorSet.ALL_COLORS); + byte rs = player.getController().chooseColor("Choose a color", sa, ColorSet.WUBRG); replaced = MagicColor.toShortString(rs); } } else if (sa.hasParam("ReplaceType")) { // replace color and colorless String color = sa.getParam("ReplaceType"); if ("Any".equals(color)) { - byte rs = player.getController().chooseColor("Choose a color", sa, ColorSet.ALL_COLORS); + byte rs = player.getController().chooseColor("Choose a color", sa, ColorSet.WUBRG); color = MagicColor.toShortString(rs); } else { // convert in case Color Word used diff --git a/forge-game/src/main/java/forge/game/ability/effects/RestartGameEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RestartGameEffect.java index 32bb7126c0e..92fbc180d36 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RestartGameEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RestartGameEffect.java @@ -75,9 +75,8 @@ public class RestartGameEffect extends SpellAbilityEffect { p.clearController(); CardCollection newLibrary = new CardCollection(p.getCardsIn(restartZones, false)); - List filteredCards = null; if (leaveZone != null) { - filteredCards = CardLists.getValidCards(p.getCardsIn(leaveZone), leaveRestriction, p, sa.getHostCard(), sa); + List filteredCards = CardLists.getValidCards(p.getCardsIn(leaveZone), leaveRestriction, p, sa.getHostCard(), sa); newLibrary.addAll(filteredCards); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java index 44c58aac69c..b04882ef9ea 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java @@ -369,8 +369,8 @@ public class RollDiceEffect extends SpellAbilityEffect { List canIncrementDice = new ArrayList<>(); for (Card c : xenosquirrels) { // Xenosquirrels must have a P1P1 counter on it to remove in order to modify - Integer P1P1Counters = c.getCounters().get(CounterType.get(CounterEnumType.P1P1)); - if (P1P1Counters != null && P1P1Counters > 0 && c.canRemoveCounters(CounterType.get(CounterEnumType.P1P1))) { + Integer P1P1Counters = c.getCounters().get(CounterEnumType.P1P1); + if (P1P1Counters != null && P1P1Counters > 0 && c.canRemoveCounters(CounterEnumType.P1P1)) { canIncrementDice.add(c); } } @@ -399,6 +399,7 @@ public class RollDiceEffect extends SpellAbilityEffect { * @param repParams replacement effect parameters * @return list of final roll results after applying ignores and replacements, sorted in ascending order */ + @SuppressWarnings("unchecked") private static List rollAction(int amount, int sides, int ignore, List rollsResult, List ignored, Map ignoreChosenMap, Set dicePTExchanges, Player player, Map repParams) { repParams.put(AbilityKey.Sides, sides); @@ -416,6 +417,8 @@ public class RollDiceEffect extends SpellAbilityEffect { ignoreChosenMap = (Map) repParams.get(AbilityKey.IgnoreChosen); break; } + default: + break; } List naturalRolls = (rollsResult == null ? new ArrayList<>() : rollsResult); diff --git a/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java index 313261f7459..4fe536aae8f 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java @@ -92,12 +92,11 @@ public class SacrificeEffect extends SpellAbilityEffect { CardZoneTable zoneMovements = AbilityKey.addCardZoneTableParams(params, sa); if (valid.equals("Self") && game.getZoneOf(host) != null) { - if (host.getController().equals(activator) && game.getZoneOf(host).is(ZoneType.Battlefield)) { - if (!optional || activator.getController().confirmAction(sa, null, - Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", host.getName()), null)) { - if (game.getAction().sacrifice(new CardCollection(host), sa, true, params) != null && remSacrificed) { - host.addRemembered(host); - } + if (host.getController().equals(activator) && game.getZoneOf(host).is(ZoneType.Battlefield) && + (!optional || activator.getController().confirmAction(sa, null, + Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", host.getName()), null))) { + if (game.getAction().sacrifice(new CardCollection(host), sa, true, params) != null && remSacrificed) { + host.addRemembered(host); } } } else { diff --git a/forge-game/src/main/java/forge/game/ability/effects/TimeTravelEffect.java b/forge-game/src/main/java/forge/game/ability/effects/TimeTravelEffect.java index 449cfb758b1..69fc1f8a470 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/TimeTravelEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/TimeTravelEffect.java @@ -9,8 +9,10 @@ import forge.game.GameEntityCounterTable; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; +import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.card.CardPredicates; +import forge.game.card.CounterType; import forge.game.card.CounterEnumType; import forge.game.player.Player; import forge.game.player.PlayerController; @@ -19,7 +21,6 @@ import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.CardTranslation; import forge.util.Localizer; -import forge.util.collect.FCollection; public class TimeTravelEffect extends SpellAbilityEffect { @@ -37,13 +38,11 @@ public class TimeTravelEffect extends SpellAbilityEffect { PlayerController pc = activator.getController(); - final CounterEnumType counterType = CounterEnumType.TIME; + final CounterType counterType = CounterEnumType.TIME; for (int i = 0; i < num; i++) { - FCollection list = new FCollection<>(); - // card you own that is suspended - list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend())); + CardCollection list = CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend()); // permanent you control with time counter list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.hasCounter(counterType))); 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 ac7d38b63f6..b9cc50ede98 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -203,6 +203,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr private boolean unearthed; private boolean ringbearer; private boolean monstrous; + private boolean harnessed; private boolean renowned; private boolean solved; private boolean tributed; @@ -256,7 +257,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr private long worldTimestamp = -1; private long bestowTimestamp = -1; - private long transformedTimestamp = 0; + private long transformedTimestamp = -1; private long prototypeTimestamp = -1; private long mutatedTimestamp = -1; private int timesMutated = 0; @@ -424,8 +425,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr public long getPrototypeTimestamp() { return prototypeTimestamp; } public long getTransformedTimestamp() { return transformedTimestamp; } - public void incrementTransformedTimestamp() { this.transformedTimestamp++; } - public void undoIncrementTransformedTimestamp() { this.transformedTimestamp--; } + public void setTransformedTimestamp(long ts) { this.transformedTimestamp = ts; } // The following methods are used to selectively update certain view components (text, // P/T, card types) in order to avoid card flickering due to aggressive full update @@ -449,7 +449,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr public final void updateColorForView() { currentState.getView().updateColors(this); - currentState.getView().updateHasChangeColors(!Iterables.isEmpty(getChangedCardColors())); + currentState.getView().updateHasChangeColors(hasChangedCardColors()); } public void updateAttackingForView() { @@ -695,7 +695,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr final Map runParams = AbilityKey.mapFromCard(this); getGame().getTriggerHandler().runTrigger(TriggerType.Transformed, runParams, false); } - incrementTransformedTimestamp(); + setTransformedTimestamp(ts); return retResult; } else if (mode.equals("Flip")) { @@ -965,7 +965,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr String name = state.getName(); for (CardChangedName change : this.changedCardNames.values()) { if (change.isOverwrite()) { - name = change.getNewName(); + name = change.newName(); } } return alt ? StaticData.instance().getCommonCards().getName(name, true) : name; @@ -980,7 +980,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr for (CardChangedName change : this.changedCardNames.values()) { if (change.isOverwrite()) { result = false; - } else if (change.isAddNonLegendaryCreatureNames()) { + } else if (change.addNonLegendaryCreatureNames()) { result = true; } } @@ -1013,6 +1013,12 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr currentState.getView().updateName(currentState); } + private record CardChangedName(String newName, boolean addNonLegendaryCreatureNames) { + public boolean isOverwrite() { + return newName != null; + } + } + public void setGamePieceType(GamePieceType gamePieceType) { this.gamePieceType = gamePieceType; this.view.updateGamePieceType(this); @@ -1069,7 +1075,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } public final boolean isDoubleFaced() { - return isTransformable() || isMeldable() || isModal(); + return isTransformable() || isMeldable(); } public final boolean isFlipCard() { @@ -1131,7 +1137,10 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } public final boolean isTransformed() { - return getTransformedTimestamp() != 0; + if (isMeldable() || hasMergedCard()) { + return false; + } + return this.isTransformable() && isBackSide(); } public final boolean isFlipped() { @@ -2262,7 +2271,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr public final ColorSet getMarkedColors() { if (markedColor == null) { - return ColorSet.getNullColor(); + return ColorSet.C; } return markedColor; } @@ -2450,22 +2459,11 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } else if (keyword.startsWith("DeckLimit")) { final String[] k = keyword.split(":"); sbLong.append(k[2]).append("\r\n"); - } else if (keyword.startsWith("Enchant")) { - String m[] = keyword.split(":"); - String desc; - if (m.length > 2) { - desc = m[2]; - } else { - desc = m[1]; - if (CardType.isACardType(desc) || "Permanent".equals(desc) || "Player".equals(desc) || "Opponent".equals(desc)) { - desc = desc.toLowerCase(); - } - } + } else if (keyword.startsWith("Enchant") && inst instanceof KeywordWithType kwt) { + String desc = kwt.getTypeDescription(); sbLong.append("Enchant ").append(desc).append("\r\n"); - } else if (keyword.startsWith("Ripple")) { - sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n"); } else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph") - || keyword.startsWith("Disguise") + || keyword.startsWith("Disguise") || keyword.startsWith("Reflect") || keyword.startsWith("Escape") || keyword.startsWith("Foretell:") || keyword.startsWith("Madness:")|| keyword.startsWith("Recover") || keyword.startsWith("Reconfigure") || keyword.startsWith("Squad") @@ -2504,17 +2502,15 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr sbLong.append("."); } if (k.length > 3) { - sbLong.append(". " + k[3]); + sbLong.append(". ").append(k[3]); } } sbLong.append(" (").append(inst.getReminderText()).append(")"); sbLong.append("\r\n"); + } else if (keyword.equals("Mayhem")) { + sbLong.append(" (").append(inst.getReminderText()).append(")"); + sbLong.append("\r\n"); } - } else if (keyword.startsWith("Reflect")) { - final String[] k = keyword.split(":"); - sbLong.append(k[0]).append(" ").append(ManaCostParser.parse(k[1])); - sbLong.append(" (").append(inst.getReminderText()).append(")"); - sbLong.append("\r\n"); } else if (keyword.startsWith("Echo")) { sbLong.append("Echo "); final String[] upkeepCostParams = keyword.split(":"); @@ -2608,6 +2604,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr || keyword.equals("Battle cry") || keyword.equals("Devoid") || keyword.equals("Riot") || keyword.equals("Daybound") || keyword.equals("Nightbound") || keyword.equals("Friends forever") || keyword.equals("Choose a Background") + || keyword.equals("Partner - Father & Son") || keyword.equals("Partner - Survivors") || keyword.equals("Space sculptor") || keyword.equals("Doctor's companion") || keyword.equals("Start your engines")) { sbLong.append(keyword).append(" (").append(inst.getReminderText()).append(")"); @@ -2653,7 +2650,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr || keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing:") || keyword.startsWith("Afterlife") || keyword.startsWith("Hideaway") || keyword.startsWith("Toxic") || keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage") - || keyword.startsWith("Renown") || keyword.startsWith("Annihilator")) { + || keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Ripple")) { final String[] k = keyword.split(":"); sbLong.append(k[0]).append(" ").append(k[1]).append(" (").append(inst.getReminderText()).append(")"); } else if (keyword.startsWith("Crew")) { @@ -2969,6 +2966,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr if (monstrous) { sb.append("Monstrous\r\n"); } + if (harnessed) { + sb.append("Harnessed\r\n"); + } if (renowned) { sb.append("Renowned\r\n"); } @@ -3561,7 +3561,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr if (!getStaticAbilities().isEmpty()) { return false; } - if (!getReplacementEffects().isEmpty()) { + if (!getReplacementEffects().isEmpty() + && (getReplacementEffects().size() > 1 || !isSaga() || hasKeyword(Keyword.READ_AHEAD))) { return false; } if (!getTriggers().isEmpty()) { @@ -3794,7 +3795,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr public final void addLeavesPlayCommand(final GameCommand c) { leavePlayCommandList.add(c); } - + public void addStaticCommandList(Object[] objects) { staticCommandList.add(objects); } @@ -4296,18 +4297,10 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } public boolean clearChangedCardColors() { - boolean changed = false; + boolean changed = hasChangedCardColors(); - if (!changedCardColorsByText.isEmpty()) - changed = true; changedCardColorsByText.clear(); - - if (!changedCardTypesCharacterDefining.isEmpty()) - changed = true; changedCardTypesCharacterDefining.clear(); - - if (!changedCardColors.isEmpty()) - changed = true; changedCardColors.clear(); return changed; @@ -4393,17 +4386,19 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } } - public Iterable getChangedCardColors() { - return Iterables.concat(changedCardColorsByText.values(), changedCardColorsCharacterDefining.values(), changedCardColors.values()); + public boolean hasChangedCardColors() { + return !changedCardColorsByText.isEmpty() || !changedCardColorsCharacterDefining.isEmpty() || !changedCardColors.isEmpty(); } - public void addColorByText(final ColorSet color, final long timestamp, final long staticId) { - changedCardColorsByText.put(timestamp, staticId, new CardColor(color, false)); + public void addColorByText(final ColorSet color, final long timestamp, final StaticAbility stAb) { + changedCardColorsByText.put(timestamp, (long)stAb.getId(), new CardColor(color, false)); updateColorForView(); } - public final void addColor(final ColorSet color, final boolean addToColors, final long timestamp, final long staticId, final boolean cda) { - (cda ? changedCardColorsCharacterDefining : changedCardColors).put(timestamp, staticId, new CardColor(color, addToColors)); + public final void addColor(final ColorSet color, final boolean addToColors, final long timestamp, final StaticAbility stAb) { + (stAb != null && stAb.isCharacteristicDefining() ? changedCardColorsCharacterDefining : changedCardColors).put( + timestamp, stAb != null ? stAb.getId() : (long)0, new CardColor(color, addToColors) + ); updateColorForView(); } @@ -4430,16 +4425,20 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } public final ColorSet getColor(CardState state) { byte colors = state.getColor(); - for (final CardColor cc : getChangedCardColors()) { - if (cc.isAdditional()) { - colors |= cc.getColorMask(); + for (final CardColor cc : Iterables.concat(changedCardColorsByText.values(), changedCardColorsCharacterDefining.values(), changedCardColors.values())) { + if (cc.additional()) { + colors |= cc.color().getColor(); } else { - colors = cc.getColorMask(); + colors = cc.color().getColor(); } } return ColorSet.fromMask(colors); } + private record CardColor(ColorSet color, boolean additional) { + + } + public final int getCurrentLoyalty() { return getCounters(CounterEnumType.LOYALTY); } @@ -4809,7 +4808,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr public void addDraftAction(String s) { draftActions.add(s); } - + private int intensity = 0; public final void addIntensity(final int n) { intensity += n; @@ -6441,10 +6440,10 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr DamageType damageType = DamageType.Normal; if (isPlaneswalker()) { // 120.3c - subtractCounter(CounterType.get(CounterEnumType.LOYALTY), damageIn, null, true); + subtractCounter(CounterEnumType.LOYALTY, damageIn, null, true); } if (isBattle()) { - subtractCounter(CounterType.get(CounterEnumType.DEFENSE), damageIn, null, true); + subtractCounter(CounterEnumType.DEFENSE, damageIn, null, true); } if (isCreature()) { if (source.isWitherDamage()) { // 120.3d @@ -6692,6 +6691,14 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr setRingBearer(false); } + public final boolean isHarnessed() { + return harnessed; + } + public final boolean setHarnessed(final boolean harnessed0) { + harnessed = harnessed0; + return true; + } + public final boolean isMonstrous() { return monstrous; } @@ -6849,6 +6856,10 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr return exiledSA.isKeyword(Keyword.WARP); } + public boolean isWebSlinged() { + return getCastSA() != null && getCastSA().isAlternativeCost(AlternativeCost.WebSlinging); + } + public boolean isSpecialized() { return specialized; } @@ -7144,7 +7155,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr return false; } - if (StaticAbilityCantTarget.cantTarget(this, sa)) { + if (StaticAbilityCantTarget.cantTarget(this, sa) != null) { return false; } @@ -7156,51 +7167,62 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr } @Override - protected final boolean canBeEnchantedBy(final Card aura) { + protected final String cantBeEnchantedByMsg(final Card aura) { if (!aura.hasKeyword(Keyword.ENCHANT)) { - return false; + return "No Enchant Keyword"; } for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) { - String k = ki.getOriginal(); - String m[] = k.split(":"); - String v = m[1]; - if (!isValid(v.split(","), aura.getController(), aura, null)) { - return false; - } - if (!v.contains("inZone") && !isInPlay()) { - return false; + if (ki instanceof KeywordWithType kwt) { + String v = kwt.getValidType(); + String desc = kwt.getTypeDescription(); + if (!isValid(v.split(","), aura.getController(), aura, null) || (!v.contains("inZone") && !isInPlay())) { + return getName() + " is not " + Lang.nounWithAmount(1, desc); + } } } - return true; + return null; } + @Override - protected final boolean canBeEquippedBy(final Card equip, SpellAbility sa) { + protected String cantBeEquippedByMsg(final Card equip, SpellAbility sa) { if (!isInPlay()) { - return false; + return getName() + " is not in play"; } if (sa != null && sa.isEquip()) { - return isValid(sa.getTargetRestrictions().getValidTgts(), sa.getActivatingPlayer(), equip, sa); + if (!isValid(sa.getTargetRestrictions().getValidTgts(), sa.getActivatingPlayer(), equip, sa)) { + Equip eq = (Equip) sa.getKeyword(); + return getName() + " is not " + Lang.nounWithAmount(1, eq.getValidDescription()); + } + return null; } - return isCreature(); + if (!isCreature()) { + return getName() + " is not a creature"; + } + return null; } @Override - protected boolean canBeFortifiedBy(final Card fort) { - return isLand() && isInPlay() && !fort.isLand(); + protected String cantBeFortifiedByMsg(final Card fort) { + if (!isLand()) { + return getName() + " is not a Land"; + } + if (!isInPlay()) { + return getName() + " is not in play"; + } + if (fort.isLand()) { + return fort.getName() + " is a Land"; + } + + return null; } - /* (non-Javadoc) - * @see forge.game.GameEntity#canBeAttached(forge.game.card.Card, boolean) - */ @Override - public boolean canBeAttached(Card attach, SpellAbility sa, boolean checkSBA) { - // phase check there + public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) { if (isPhasedOut() && !attach.isPhasedOut()) { - return false; + return getName() + " is phased out"; } - - return super.canBeAttached(attach, sa, checkSBA); + return super.cantBeAttachedMsg(attach, sa, checkSBA); } public final boolean canBeSacrificedBy(final SpellAbility source, final boolean effect) { @@ -7625,9 +7647,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr if (sa.isBestow()) { animateBestow(); } - if (sa.isDisturb() || sa.hasParam("CastTransformed")) { - incrementTransformedTimestamp(); - } if (sa.hasParam("Prototype") && prototypeTimestamp == -1) { long next = game.getNextTimestamp(); addCloneState(CardFactory.getCloneStates(this, this, sa), next); @@ -7837,8 +7856,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr return currentState.getUntranslatedType(); } @Override - public String getUntranslatedOracle() { - return currentState.getUntranslatedOracle(); + public String getTranslatedName() { + return CardTranslation.getTranslatedName(this); } @Override diff --git a/forge-game/src/main/java/forge/game/card/CardChangedName.java b/forge-game/src/main/java/forge/game/card/CardChangedName.java deleted file mode 100644 index 63dcd82ac8f..00000000000 --- a/forge-game/src/main/java/forge/game/card/CardChangedName.java +++ /dev/null @@ -1,24 +0,0 @@ -package forge.game.card; - -public class CardChangedName { - - protected String newName; - protected boolean addNonLegendaryCreatureNames = false; - - public CardChangedName(String newName, boolean addNonLegendaryCreatureNames) { - this.newName = newName; - this.addNonLegendaryCreatureNames = addNonLegendaryCreatureNames; - } - - public String getNewName() { - return newName; - } - - public boolean isOverwrite() { - return newName != null; - } - - public boolean isAddNonLegendaryCreatureNames() { - return addNonLegendaryCreatureNames; - } -} diff --git a/forge-game/src/main/java/forge/game/card/CardColor.java b/forge-game/src/main/java/forge/game/card/CardColor.java deleted file mode 100644 index c37460a4128..00000000000 --- a/forge-game/src/main/java/forge/game/card/CardColor.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Forge: Play Magic: the Gathering. - * Copyright (C) 2011 Forge Team - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package forge.game.card; - -import forge.card.ColorSet; - -/** - *

- * Card_Color class. - *

- * - * @author Forge - * @version $Id$ - */ -public class CardColor { - private final byte colorMask; - public final byte getColorMask() { - return colorMask; - } - - private final boolean additional; - public final boolean isAdditional() { - return this.additional; - } - - CardColor(final ColorSet colors, final boolean addToColors) { - this.colorMask = colors.getColor(); - this.additional = addToColors; - } -} diff --git a/forge-game/src/main/java/forge/game/card/CardCopyService.java b/forge-game/src/main/java/forge/game/card/CardCopyService.java index 3cd81c703ce..ebf6c3a6009 100644 --- a/forge-game/src/main/java/forge/game/card/CardCopyService.java +++ b/forge-game/src/main/java/forge/game/card/CardCopyService.java @@ -131,9 +131,7 @@ public class CardCopyService { c.setState(in.getCurrentStateName(), false); c.setRules(in.getRules()); - if (in.isTransformed()) { - c.incrementTransformedTimestamp(); - } + c.setBackSide(in.isBackSide()); return c; } @@ -168,9 +166,6 @@ public class CardCopyService { // The characteristics of its front and back face are determined by the copiable values of the same face of the spell it is a copy of, as modified by any other copy effects. // If the spell it is a copy of has its back face up, the copy is created with its back face up. The token that’s put onto the battlefield as that spell resolves is a transforming token. to.setBackSide(copyFrom.isBackSide()); - if (copyFrom.isTransformed()) { - to.incrementTransformedTimestamp(); - } } else if (fromIsTransformedCard) { copyState(copyFrom, copyFrom.getCurrentStateName(), to, CardStateName.Original); } else { @@ -274,9 +269,6 @@ public class CardCopyService { } newCopy.setFlipped(copyFrom.isFlipped()); newCopy.setBackSide(copyFrom.isBackSide()); - if (copyFrom.isTransformed()) { - newCopy.incrementTransformedTimestamp(); - } if (newCopy.hasAlternateState()) { newCopy.setState(copyFrom.getCurrentStateName(), false, true); } 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 b2966cbe849..b800d8e0a25 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -87,22 +87,16 @@ public class CardFactory { // need to create a physical card first, i need the original card faces final Card copy = getCard(original.getPaperCard(), controller, id, game); + copy.setStates(getCloneStates(original, copy, sourceSA)); + // force update the now set State if (original.isTransformable()) { + copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true); // 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(); - } - } - - copy.setStates(getCloneStates(original, copy, sourceSA)); - // force update the now set State - if (original.isTransformable()) { - copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true); } else { copy.setState(copy.getCurrentStateName(), true, true); } @@ -376,22 +370,28 @@ public class CardFactory { } } - // Build English oracle and translated oracle mapping + // Negative card Id's are for view purposes only if (c.getId() >= 0) { + // Build English oracle and translated oracle mapping CardTranslation.buildOracleMapping(face.getName(), face.getOracleText(), variantName); } - // Name first so Senty has the Card name + // Set name for Sentry reports to be identifiable c.setName(face.getName()); - for (Entry v : face.getVariables()) c.setSVar(v.getKey(), v.getValue()); + if (c.getId() >= 0) { // Set Triggers & Abilities if not for view + for (Entry v : face.getVariables()) + c.setSVar(v.getKey(), v.getValue()); + for (String r : face.getReplacements()) + c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true, c.getCurrentState())); + for (String s : face.getStaticAbilities()) + c.addStaticAbility(s); + for (String t : face.getTriggers()) + c.addTrigger(TriggerHandler.parseTrigger(t, c, true, c.getCurrentState())); - for (String r : face.getReplacements()) c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true, c.getCurrentState())); - for (String s : face.getStaticAbilities()) c.addStaticAbility(s); - for (String t : face.getTriggers()) c.addTrigger(TriggerHandler.parseTrigger(t, c, true, c.getCurrentState())); - - // keywords not before variables - c.addIntrinsicKeywords(face.getKeywords(), false); + // keywords not before variables + c.addIntrinsicKeywords(face.getKeywords(), false); + } if (face.getDraftActions() != null) { face.getDraftActions().forEach(c::addDraftAction); } @@ -420,7 +420,8 @@ public class CardFactory { c.setAttractionLights(face.getAttractionLights()); - CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities()); + if (c.getId() > 0) // Set FactoryAbilities if not for view + CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities()); } public static void copySpellAbility(SpellAbility from, SpellAbility to, final Card host, final Player p, final boolean lki, final boolean keepTextChanges) { @@ -465,29 +466,29 @@ public class CardFactory { return new WrappedAbility(sa.getTrigger(), sa.getWrappedAbility().copy(newHost, controller, false), sa.getDecider()); } - public static CardCloneStates getCloneStates(final Card in, final Card out, final CardTraitBase sa) { - final Card host = sa.getHostCard(); + public static CardCloneStates getCloneStates(final Card in, final Card out, final CardTraitBase cause) { + final Card host = cause.getHostCard(); final Map origSVars = host.getSVars(); final List types = Lists.newArrayList(); final List keywords = Lists.newArrayList(); boolean KWifNew = false; final List removeKeywords = Lists.newArrayList(); List creatureTypes = null; - final CardCloneStates result = new CardCloneStates(in, sa); + final CardCloneStates result = new CardCloneStates(in, cause); - final String newName = sa.getParam("NewName"); + final String newName = cause.getParam("NewName"); ColorSet colors = null; - if (sa.hasParam("AddTypes")) { - types.addAll(Arrays.asList(sa.getParam("AddTypes").split(" & "))); + if (cause.hasParam("AddTypes")) { + types.addAll(Arrays.asList(cause.getParam("AddTypes").split(" & "))); } - if (sa.hasParam("SetCreatureTypes")) { - creatureTypes = ImmutableList.copyOf(sa.getParam("SetCreatureTypes").split(" ")); + if (cause.hasParam("SetCreatureTypes")) { + creatureTypes = ImmutableList.copyOf(cause.getParam("SetCreatureTypes").split(" ")); } - if (sa.hasParam("AddKeywords")) { - String kwString = sa.getParam("AddKeywords"); + if (cause.hasParam("AddKeywords")) { + String kwString = cause.getParam("AddKeywords"); if (kwString.startsWith("IfNew ")) { KWifNew = true; kwString = kwString.substring(6); @@ -495,21 +496,21 @@ public class CardFactory { keywords.addAll(Arrays.asList(kwString.split(" & "))); } - if (sa.hasParam("RemoveKeywords")) { - removeKeywords.addAll(Arrays.asList(sa.getParam("RemoveKeywords").split(" & "))); + if (cause.hasParam("RemoveKeywords")) { + removeKeywords.addAll(Arrays.asList(cause.getParam("RemoveKeywords").split(" & "))); } - if (sa.hasParam("AddColors")) { - colors = ColorSet.fromNames(sa.getParam("AddColors").split(",")); + if (cause.hasParam("AddColors")) { + colors = ColorSet.fromNames(cause.getParam("AddColors").split(",")); } - if (sa.hasParam("SetColor")) { - colors = ColorSet.fromNames(sa.getParam("SetColor").split(",")); + if (cause.hasParam("SetColor")) { + colors = ColorSet.fromNames(cause.getParam("SetColor").split(",")); } - if (sa.hasParam("SetColorByManaCost")) { - if (sa.hasParam("SetManaCost")) { - colors = ColorSet.fromManaCost(new ManaCost(new ManaCostParser(sa.getParam("SetManaCost")))); + if (cause.hasParam("SetColorByManaCost")) { + if (cause.hasParam("SetManaCost")) { + colors = ColorSet.fromManaCost(new ManaCost(new ManaCostParser(cause.getParam("SetManaCost")))); } else { colors = ColorSet.fromManaCost(host.getManaCost()); } @@ -521,56 +522,55 @@ public class CardFactory { // if something is cloning a facedown card, it only clones the // facedown state into original final CardState ret = new CardState(out, CardStateName.Original); - ret.copyFrom(in.getFaceDownState(), false, sa); + ret.copyFrom(in.getFaceDownState(), false, cause); result.put(CardStateName.Original, ret); } else if (in.isFlipCard()) { // if something is cloning a flip card, copy both original and // flipped state final CardState ret1 = new CardState(out, CardStateName.Original); - ret1.copyFrom(in.getState(CardStateName.Original), false, sa); + ret1.copyFrom(in.getState(CardStateName.Original), false, cause); result.put(CardStateName.Original, ret1); final CardState ret2 = new CardState(out, CardStateName.Flipped); - ret2.copyFrom(in.getState(CardStateName.Flipped), false, sa); + ret2.copyFrom(in.getState(CardStateName.Flipped), false, cause); result.put(CardStateName.Flipped, ret2); } else if (in.hasState(CardStateName.Secondary)) { final CardState ret1 = new CardState(out, CardStateName.Original); - ret1.copyFrom(in.getState(CardStateName.Original), false, sa); + ret1.copyFrom(in.getState(CardStateName.Original), false, cause); result.put(CardStateName.Original, ret1); final CardState ret2 = new CardState(out, CardStateName.Secondary); - ret2.copyFrom(in.getState(CardStateName.Secondary), false, sa); + ret2.copyFrom(in.getState(CardStateName.Secondary), false, cause); result.put(CardStateName.Secondary, ret2); - } else if (in.isTransformable() && sa instanceof SpellAbility && ( - ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi()) || - ApiType.CopySpellAbility.equals(((SpellAbility)sa).getApi()) || - ApiType.ReplaceToken.equals(((SpellAbility)sa).getApi()) - )) { + } else if (in.isTransformable() && cause instanceof SpellAbility sa && ( + ApiType.CopyPermanent.equals(sa.getApi()) || + ApiType.CopySpellAbility.equals(sa.getApi()) || + ApiType.ReplaceToken.equals(sa.getApi()))) { // CopyPermanent can copy token final CardState ret1 = new CardState(out, CardStateName.Original); - ret1.copyFrom(in.getState(CardStateName.Original), false, sa); + ret1.copyFrom(in.getState(CardStateName.Original), false, cause); result.put(CardStateName.Original, ret1); final CardState ret2 = new CardState(out, CardStateName.Backside); - ret2.copyFrom(in.getState(CardStateName.Backside), false, sa); + ret2.copyFrom(in.getState(CardStateName.Backside), false, cause); result.put(CardStateName.Backside, ret2); } else if (in.isSplitCard()) { // for split cards, copy all three states final CardState ret1 = new CardState(out, CardStateName.Original); - ret1.copyFrom(in.getState(CardStateName.Original), false, sa); + ret1.copyFrom(in.getState(CardStateName.Original), false, cause); result.put(CardStateName.Original, ret1); final CardState ret2 = new CardState(out, CardStateName.LeftSplit); - ret2.copyFrom(in.getState(CardStateName.LeftSplit), false, sa); + ret2.copyFrom(in.getState(CardStateName.LeftSplit), false, cause); result.put(CardStateName.LeftSplit, ret2); final CardState ret3 = new CardState(out, CardStateName.RightSplit); - ret3.copyFrom(in.getState(CardStateName.RightSplit), false, sa); + ret3.copyFrom(in.getState(CardStateName.RightSplit), false, cause); result.put(CardStateName.RightSplit, ret3); } else { // in all other cases just copy the current state to original final CardState ret = new CardState(out, CardStateName.Original); - ret.copyFrom(in.getState(in.getCurrentStateName()), false, sa); + ret.copyFrom(in.getState(in.getCurrentStateName()), false, cause); result.put(CardStateName.Original, ret); } @@ -580,32 +580,32 @@ public class CardFactory { final CardState state = e.getValue(); // has Embalm Condition for extra changes of Vizier of Many Faces - if (sa.hasParam("Embalm") && !out.isEmbalmed()) { + if (cause.hasParam("Embalm") && !out.isEmbalmed()) { continue; } // update the names for the states - if (sa.hasParam("KeepName")) { + if (cause.hasParam("KeepName")) { state.setName(originalState.getName()); } else if (newName != null) { // convert NICKNAME descriptions? state.setName(newName); } - if (sa.hasParam("AddColors")) { + if (cause.hasParam("AddColors")) { state.addColor(colors.getColor()); } - if (sa.hasParam("SetColor") || sa.hasParam("SetColorByManaCost")) { + if (cause.hasParam("SetColor") || cause.hasParam("SetColorByManaCost")) { state.setColor(colors.getColor()); } - if (sa.hasParam("NonLegendary")) { + if (cause.hasParam("NonLegendary")) { state.removeType(CardType.Supertype.Legendary); } - if (sa.hasParam("RemoveCardTypes")) { - state.removeCardTypes(sa.hasParam("RemoveSubTypes")); + if (cause.hasParam("RemoveCardTypes")) { + state.removeCardTypes(cause.hasParam("RemoveSubTypes")); } state.addType(types); @@ -637,31 +637,31 @@ public class CardFactory { // CR 208.3 A noncreature object not on the battlefield has power or toughness only if it has a power and toughness printed on it. // currently only LKI can be trusted? - if ((sa.hasParam("SetPower") || sa.hasParam("SetToughness")) && + if ((cause.hasParam("SetPower") || cause.hasParam("SetToughness")) && (state.getType().isCreature() || (originalState != null && in.getOriginalState(originalState.getStateName()).getBasePowerString() != null))) { - if (sa.hasParam("SetPower")) { - state.setBasePower(AbilityUtils.calculateAmount(host, sa.getParam("SetPower"), sa)); + if (cause.hasParam("SetPower")) { + state.setBasePower(AbilityUtils.calculateAmount(host, cause.getParam("SetPower"), cause)); } - if (sa.hasParam("SetToughness")) { - state.setBaseToughness(AbilityUtils.calculateAmount(host, sa.getParam("SetToughness"), sa)); + if (cause.hasParam("SetToughness")) { + state.setBaseToughness(AbilityUtils.calculateAmount(host, cause.getParam("SetToughness"), cause)); } } - if (state.getType().isPlaneswalker() && sa.hasParam("SetLoyalty")) { - state.setBaseLoyalty(String.valueOf(AbilityUtils.calculateAmount(host, sa.getParam("SetLoyalty"), sa))); + if (state.getType().isPlaneswalker() && cause.hasParam("SetLoyalty")) { + state.setBaseLoyalty(String.valueOf(AbilityUtils.calculateAmount(host, cause.getParam("SetLoyalty"), cause))); } - if (sa.hasParam("RemoveCost")) { + if (cause.hasParam("RemoveCost")) { state.setManaCost(ManaCost.NO_COST); } - if (sa.hasParam("SetManaCost")) { - state.setManaCost(new ManaCost(new ManaCostParser(sa.getParam("SetManaCost")))); + if (cause.hasParam("SetManaCost")) { + state.setManaCost(new ManaCost(new ManaCostParser(cause.getParam("SetManaCost")))); } // SVars to add to clone - if (sa.hasParam("AddSVars") || sa.hasParam("GainTextSVars")) { - final String str = sa.getParamOrDefault("GainTextSVars", sa.getParam("AddSVars")); + if (cause.hasParam("AddSVars") || cause.hasParam("GainTextSVars")) { + final String str = cause.getParamOrDefault("GainTextSVars", cause.getParam("AddSVars")); for (final String s : str.split(",")) { if (origSVars.containsKey(s)) { final String actualsVar = origSVars.get(s); @@ -671,8 +671,8 @@ public class CardFactory { } // triggers to add to clone - if (sa.hasParam("AddTriggers")) { - for (final String s : sa.getParam("AddTriggers").split(",")) { + if (cause.hasParam("AddTriggers")) { + for (final String s : cause.getParam("AddTriggers").split(",")) { if (origSVars.containsKey(s)) { final String actualTrigger = origSVars.get(s); final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, out, true, state); @@ -682,8 +682,8 @@ public class CardFactory { } // abilities to add to clone - if (sa.hasParam("AddAbilities") || sa.hasParam("GainTextAbilities")) { - final String str = sa.getParamOrDefault("GainTextAbilities", sa.getParam("AddAbilities")); + if (cause.hasParam("AddAbilities") || cause.hasParam("GainTextAbilities")) { + final String str = cause.getParamOrDefault("GainTextAbilities", cause.getParam("AddAbilities")); for (final String s : str.split(",")) { if (origSVars.containsKey(s)) { final String actualAbility = origSVars.get(s); @@ -695,18 +695,18 @@ public class CardFactory { } // static abilities to add to clone - if (sa.hasParam("AddStaticAbilities")) { - final String str = sa.getParam("AddStaticAbilities"); + if (cause.hasParam("AddStaticAbilities")) { + final String str = cause.getParam("AddStaticAbilities"); for (final String s : str.split(",")) { if (origSVars.containsKey(s)) { final String actualStatic = origSVars.get(s); - state.addStaticAbility(StaticAbility.create(actualStatic, out, sa.getCardState(), true)); + state.addStaticAbility(StaticAbility.create(actualStatic, out, cause.getCardState(), true)); } } } - if (sa.hasParam("GainThisAbility") && sa instanceof SpellAbility) { - SpellAbility root = ((SpellAbility) sa).getRootAbility(); + if (cause.hasParam("GainThisAbility") && cause instanceof SpellAbility sa) { + SpellAbility root = sa.getRootAbility(); // Aurora Shifter if (root.isTrigger() && root.getTrigger().getSpawningAbility() != null) { @@ -723,35 +723,35 @@ public class CardFactory { } // Special Rules for Embalm and Eternalize - if (sa.isEmbalm() && sa.isIntrinsic()) { + if (cause.isEmbalm() && cause.isIntrinsic()) { String name = "embalm_" + TextUtil.fastReplace( TextUtil.fastReplace(host.getName(), ",", ""), " ", "_").toLowerCase(); state.setImageKey(StaticData.instance().getOtherImageKey(name, host.getSetCode())); } - if (sa.isEternalize() && sa.isIntrinsic()) { + if (cause.isEternalize() && cause.isIntrinsic()) { String name = "eternalize_" + TextUtil.fastReplace( TextUtil.fastReplace(host.getName(), ",", ""), " ", "_").toLowerCase(); state.setImageKey(StaticData.instance().getOtherImageKey(name, host.getSetCode())); } - if (sa.isKeyword(Keyword.OFFSPRING) && sa.isIntrinsic()) { + if (cause.isKeyword(Keyword.OFFSPRING) && cause.isIntrinsic()) { String name = "offspring_" + TextUtil.fastReplace( TextUtil.fastReplace(host.getName(), ",", ""), " ", "_").toLowerCase(); state.setImageKey(StaticData.instance().getOtherImageKey(name, host.getSetCode())); } - if (sa.isKeyword(Keyword.SQUAD) && sa.isIntrinsic()) { + if (cause.isKeyword(Keyword.SQUAD) && cause.isIntrinsic()) { String name = "squad_" + TextUtil.fastReplace( TextUtil.fastReplace(host.getName(), ",", ""), " ", "_").toLowerCase(); state.setImageKey(StaticData.instance().getOtherImageKey(name, host.getSetCode())); } - if (sa.hasParam("GainTextOf") && originalState != null) { + if (cause.hasParam("GainTextOf") && originalState != null) { state.setSetCode(originalState.getSetCode()); state.setRarity(originalState.getRarity()); state.setImageKey(originalState.getImageKey()); @@ -763,27 +763,27 @@ public class CardFactory { continue; } - if (sa.hasParam("SetPower") && sta.hasParam("SetPower")) + if (cause.hasParam("SetPower") && sta.hasParam("SetPower")) state.removeStaticAbility(sta); - if (sa.hasParam("SetToughness") && sta.hasParam("SetToughness")) + if (cause.hasParam("SetToughness") && sta.hasParam("SetToughness")) state.removeStaticAbility(sta); // currently only Changeling and similar should be affected by that // other cards using AddType$ ChosenType should not - if (sa.hasParam("SetCreatureTypes") && sta.hasParam("AddAllCreatureTypes")) { + if (cause.hasParam("SetCreatureTypes") && sta.hasParam("AddAllCreatureTypes")) { state.removeStaticAbility(sta); } - if ((sa.hasParam("SetColor") || sa.hasParam("SetColorByManaCost")) && sta.hasParam("SetColor")) { + if ((cause.hasParam("SetColor") || cause.hasParam("SetColorByManaCost")) && sta.hasParam("SetColor")) { state.removeStaticAbility(sta); } } // remove some keywords - if (sa.hasParam("SetCreatureTypes")) { + if (cause.hasParam("SetCreatureTypes")) { state.removeIntrinsicKeyword(Keyword.CHANGELING); } - if (sa.hasParam("SetColor") || sa.hasParam("SetColorByManaCost")) { + if (cause.hasParam("SetColor") || cause.hasParam("SetColorByManaCost")) { state.removeIntrinsicKeyword(Keyword.DEVOID); } } 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 c817e5d8c74..f5a3c133177 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -2238,7 +2238,7 @@ public class CardFactoryUtil { final String actualRep = "Event$ Draw | ActiveZones$ Graveyard | ValidPlayer$ You | " + "Secondary$ True | Optional$ True | CheckSVar$ " + "DredgeCheckLib | SVarCompare$ GE" + dredgeAmount - + " | AICheckDredge$ True | Description$ CARDNAME - Dredge " + dredgeAmount; + + " | Description$ CARDNAME - Dredge " + dredgeAmount; final String abString = "DB$ Mill | Defined$ You | NumCards$ " + dredgeAmount; @@ -2568,7 +2568,7 @@ public class CardFactoryUtil { } else if (keyword.equals("Sunburst")) { // Rule 702.43a If this object is entering the battlefield as a creature, // ignoring any type-changing effects that would affect it - CounterType t = CounterType.get(host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE); + CounterType t = host.isCreature() ? CounterEnumType.P1P1 : CounterEnumType.CHARGE; StringBuilder sb = new StringBuilder("etbCounter:"); sb.append(t).append(":Sunburst:no Condition:"); @@ -2776,10 +2776,9 @@ public class CardFactoryUtil { final String cost = params[1]; final StringBuilder sbAttach = new StringBuilder(); - sbAttach.append("SP$ Attach | Cost$ "); + sbAttach.append("SP$ Attach | ValidTgts$ Creature | Cost$ "); sbAttach.append(cost); sbAttach.append(" | AILogic$ ").append(params.length > 2 ? params[2] : "Pump"); - sbAttach.append(" | Bestow$ True | ValidTgts$ Creature"); final SpellAbility sa = AbilityFactory.getAbility(sbAttach.toString(), card); final StringBuilder sbDesc = new StringBuilder(); @@ -4113,7 +4112,7 @@ public class CardFactoryUtil { sbValid.append("| ").append(param).append(k[1]); } - String effect = "Mode$ CantTarget | ValidCard$ Card.Self | Secondary$ True" + String effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True" + sbValid.toString() + " | Activator$ Opponent | Description$ " + sbDesc.toString() + " (" + inst.getReminderText() + ")"; inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); @@ -4154,7 +4153,7 @@ public class CardFactoryUtil { inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); // Target - effect = "Mode$ CantTarget | Protection$ True | ValidCard$ Card.Self | Secondary$ True "; + effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True "; if (!valid.isEmpty()) { effect += "| ValidSource$ " + valid; } @@ -4162,7 +4161,7 @@ public class CardFactoryUtil { inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); // Attach - effect = "Mode$ CantAttach | Protection$ True | Target$ Card.Self | Secondary$ True "; + effect = "Mode$ CantAttach | Target$ Card.Self | Secondary$ True "; if (!valid.isEmpty()) { effect += "| ValidCard$ " + valid; } @@ -4183,7 +4182,7 @@ public class CardFactoryUtil { " | Description$ Chapter abilities of this Saga can't trigger the turn it entered the battlefield unless it has exactly the number of lore counters on it specified in the chapter symbol of that ability."; inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Shroud")) { - String effect = "Mode$ CantTarget | ValidCard$ Card.Self | Secondary$ True" + String effect = "Mode$ CantTarget | ValidTarget$ Card.Self | Secondary$ True" + " | Description$ Shroud (" + inst.getReminderText() + ")"; inst.addStaticAbility(StaticAbility.create(effect, state.getCard(), state, intrinsic)); } else if (keyword.equals("Skulk")) { diff --git a/forge-game/src/main/java/forge/game/card/CardLists.java b/forge-game/src/main/java/forge/game/card/CardLists.java index 279499db015..8f91260b10d 100644 --- a/forge-game/src/main/java/forge/game/card/CardLists.java +++ b/forge-game/src/main/java/forge/game/card/CardLists.java @@ -26,12 +26,17 @@ import forge.game.spellability.TargetRestrictions; import forge.game.staticability.StaticAbilityTapPowerValue; import forge.util.IterableUtil; import forge.util.MyRandom; +import forge.util.StreamUtil; import forge.util.collect.FCollectionView; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.function.Predicate; +import java.util.stream.Collector; +import java.util.stream.Collectors; /** *

@@ -480,4 +485,26 @@ public class CardLists { // (b) including the last element return isSubsetSum(numList, sum) || isSubsetSum(numList, sum - last); } + + public static int getDifferentNamesCount(Iterable cardList) { + // first part the ones with SpyKit, and already collect them via + Map> parted = StreamUtil.stream(cardList).collect(Collectors + .partitioningBy(Card::hasNonLegendaryCreatureNames, Collector.of(ArrayList::new, (list, c) -> { + if (!c.hasNoName() && list.stream().noneMatch(c2 -> c.sharesNameWith(c2))) { + list.add(c); + } + }, (l1, l2) -> { + l1.addAll(l2); + return l1; + }))); + List preList = parted.get(Boolean.FALSE); + + // then try to apply the SpyKit ones + for (Card c : parted.get(Boolean.TRUE)) { + if (preList.stream().noneMatch(c2 -> c.sharesNameWith(c2))) { + preList.add(c); + } + } + return preList.size(); + } } diff --git a/forge-game/src/main/java/forge/game/card/CardPredicates.java b/forge-game/src/main/java/forge/game/card/CardPredicates.java index c030da0359d..dc74aedc746 100644 --- a/forge-game/src/main/java/forge/game/card/CardPredicates.java +++ b/forge-game/src/main/java/forge/game/card/CardPredicates.java @@ -213,16 +213,10 @@ public final class CardPredicates { public static Predicate hasCounter(final CounterType type) { return hasCounter(type, 1); } - public static Predicate hasCounter(final CounterEnumType type) { - return hasCounter(type, 1); - } public static Predicate hasCounter(final CounterType type, final int n) { return c -> c.getCounters(type) >= n; } - public static Predicate hasCounter(final CounterEnumType type, final int n) { - return hasCounter(CounterType.get(type), n); - } public static Predicate hasLessCounter(final CounterType type, final int n) { return c -> { @@ -230,16 +224,10 @@ public final class CardPredicates { return x > 0 && x <= n; }; } - public static Predicate hasLessCounter(final CounterEnumType type, final int n) { - return hasLessCounter(CounterType.get(type), n); - } public static Predicate canReceiveCounters(final CounterType counter) { return c -> c.canReceiveCounters(counter); } - public static Predicate canReceiveCounters(final CounterEnumType counter) { - return canReceiveCounters(CounterType.get(counter)); - } public static Predicate hasGreaterPowerThan(final int minPower) { return c -> c.getNetPower() > minPower; @@ -248,9 +236,6 @@ public final class CardPredicates { public static Comparator compareByCounterType(final CounterType type) { return Comparator.comparingInt(arg0 -> arg0.getCounters(type)); } - public static Comparator compareByCounterType(final CounterEnumType type) { - return compareByCounterType(CounterType.get(type)); - } public static Predicate hasSVar(final String name) { return c -> c.hasSVar(name); diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index c0433236286..e27a4f8fed7 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -66,12 +66,6 @@ public class CardProperty { if (!card.sharesNameWith(name)) { return false; } - } else if (property.startsWith("notnamed")) { - String name = TextUtil.fastReplace(property.substring(8), ";", ","); // workaround for card name with "," - name = TextUtil.fastReplace(name, "_", " "); - if (card.sharesNameWith(name)) { - return false; - } } else if (property.equals("NamedCard")) { boolean found = false; for (String name : source.getNamedCards()) { @@ -1244,7 +1238,8 @@ public class CardProperty { if (property.contains("ControlledBy")) { FCollectionView p = AbilityUtils.getDefinedPlayers(source, property.split("ControlledBy")[1], spellAbility); cards = CardLists.filterControlledBy(cards, p); - if (!cards.contains(card)) { + // Kraven the Hunter LTB trigger + if (!card.isLKI() && !cards.contains(card)) { return false; } } @@ -1563,8 +1558,6 @@ public class CardProperty { return false; } } - } else if (property.startsWith("notattacking")) { - return null == combat || !combat.isAttacking(card); } else if (property.startsWith("enlistedThisCombat")) { if (card.getEnlistedThisCombat() == false) return false; } else if (property.startsWith("attackedThisCombat")) { @@ -1618,8 +1611,6 @@ public class CardProperty { if (Collections.disjoint(combat.getAttackersBlockedBy(source), combat.getAttackersBlockedBy(card))) { return false; } - } else if (property.startsWith("notblocking")) { - return null == combat || !combat.isBlocking(card); } // Nex predicates refer to past combat and don't need a reference to actual combat else if (property.equals("blocked")) { @@ -1819,6 +1810,10 @@ public class CardProperty { if (!card.isWarped()) { return false; } + } else if (property.equals("webSlinged")) { + if (!card.isWebSlinged()) { + return false; + } } else if (property.equals("CrewedThisTurn")) { if (!hasTimestampMatch(card, source.getCrewedByThisTurn())) return false; } else if (property.equals("CrewedBySourceThisTurn")) { @@ -1827,6 +1822,10 @@ public class CardProperty { if (card.getDevouredCards().isEmpty()) { return false; } + } else if (property.equals("harnessed")) { + if (!card.isHarnessed()) { + return false; + } } else if (property.equals("IsMonstrous")) { if (!card.isMonstrous()) { return false; @@ -2064,16 +2063,6 @@ public class CardProperty { } else { return false; } - } else if (property.startsWith("NotTriggered")) { - final String key = property.substring("NotTriggered".length()); - if (spellAbility instanceof SpellAbility) { - SpellAbility sa = (SpellAbility) spellAbility; - if (card.equals(sa.getRootAbility().getTriggeringObject(AbilityKey.fromString(key)))) { - return false; - } - } else { - return false; - } } else if (property.startsWith("NotDefined")) { final String key = property.substring("NotDefined".length()); if (AbilityUtils.getDefinedCards(source, key, spellAbility).contains(card)) { diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index d0bb304503b..289520ecf28 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -32,6 +32,7 @@ import forge.game.card.CardView.CardStateView; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordCollection; import forge.game.keyword.KeywordInterface; +import forge.game.keyword.KeywordWithType; import forge.game.player.Player; import forge.game.replacement.ReplacementEffect; import forge.game.spellability.LandAbility; @@ -40,6 +41,7 @@ import forge.game.spellability.SpellAbilityPredicates; import forge.game.spellability.SpellPermanent; import forge.game.staticability.StaticAbility; import forge.game.trigger.Trigger; +import forge.util.CardTranslation; import forge.util.ITranslatable; import forge.util.IterableUtil; import forge.util.collect.FCollection; @@ -367,7 +369,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { public final FCollectionView getManaAbilities() { FCollection newCol = new FCollection<>(); updateSpellAbilities(newCol, true); - // stream().toList() causes crash on Android, use Collectors.toList() + // stream().toList() causes crash on Android 8-13, use Collectors.toList() newCol.addAll(abilities.stream().filter(SpellAbility::isManaAbility).collect(Collectors.toList())); card.updateSpellAbilities(newCol, this, true); return newCol; @@ -375,7 +377,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { public final FCollectionView getNonManaAbilities() { FCollection newCol = new FCollection<>(); updateSpellAbilities(newCol, false); - // stream().toList() causes crash on Android, use Collectors.toList() + // stream().toList() causes crash on Android 8-13, use Collectors.toList() newCol.addAll(abilities.stream().filter(Predicate.not(SpellAbility::isManaAbility)).collect(Collectors.toList())); card.updateSpellAbilities(newCol, this, false); return newCol; @@ -390,7 +392,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { if (null != mana) { leftAbilities = leftAbilities.stream() .filter(mana ? SpellAbility::isManaAbility : Predicate.not(SpellAbility::isManaAbility)) - // stream().toList() causes crash on Android, use Collectors.toList() + // stream().toList() causes crash on Android 8-13, use Collectors.toList() .collect(Collectors.toList()); } newCol.addAll(leftAbilities); @@ -402,7 +404,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { if (null != mana) { rightAbilities = rightAbilities.stream() .filter(mana ? SpellAbility::isManaAbility : Predicate.not(SpellAbility::isManaAbility)) - // stream().toList() causes crash on Android, use Collectors.toList() + // stream().toList() causes crash on Android 8-13, use Collectors.toList() .collect(Collectors.toList()); } newCol.addAll(rightAbilities); @@ -468,6 +470,9 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { return Iterables.getFirst(getIntrinsicSpellAbilities(), null); } public final SpellAbility getFirstSpellAbility() { + if (this.card.getCastSA() != null) { + return this.card.getCastSA(); + } return Iterables.getFirst(getNonManaAbilities(), null); } @@ -497,15 +502,8 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { String desc = ""; String extra = ""; for (KeywordInterface ki : this.getCachedKeyword(Keyword.ENCHANT)) { - String o = ki.getOriginal(); - String m[] = o.split(":"); - if (m.length > 2) { - desc = m[2]; - } else { - desc = m[1]; - if (CardType.isACardType(desc) || "Permanent".equals(desc) || "Player".equals(desc) || "Opponent".equals(desc)) { - desc = desc.toLowerCase(); - } + if (ki instanceof KeywordWithType kwt) { + desc = kwt.getTypeDescription(); } break; } @@ -605,18 +603,18 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { result.add(loyaltyRep); } if (type.isBattle()) { - // TODO This is currently breaking for Battle/Defense - // Going to script the cards to work but ideally it would happen here if (defenseRep == null) { defenseRep = CardFactoryUtil.makeEtbCounter("etbCounter:DEFENSE:" + this.baseDefense, this, true); } result.add(defenseRep); - - // TODO add Siege "Choose a player to protect it" } + + card.updateReplacementEffects(result, this); + + // below are global rules if (type.hasSubtype("Saga") && !hasKeyword(Keyword.READ_AHEAD)) { if (sagaRep == null) { - sagaRep = CardFactoryUtil.makeEtbCounter("etbCounter:LORE:1", this, true); + sagaRep = CardFactoryUtil.makeEtbCounter("etbCounter:LORE:1", this, false); } result.add(sagaRep); } @@ -633,7 +631,6 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { result.add(omenRep); } - card.updateReplacementEffects(result, this); return result; } public boolean addReplacementEffect(final ReplacementEffect replacementEffect) { @@ -944,7 +941,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable { } @Override - public String getUntranslatedOracle() { - return getOracleText(); + public String getTranslatedName() { + return CardTranslation.getTranslatedName(this); } } diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index 674477c3795..773b82204aa 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -60,13 +60,12 @@ public class CardView extends GameEntityView { } public static TrackableCollection getCollection(Iterable cards) { - if (cards == null) { - return null; - } TrackableCollection collection = new TrackableCollection<>(); - for (Card c : cards) { - if (c.getRenderForUI()) { //only add cards that match their card for UI - collection.add(c.getView()); + if (cards != null) { + for (Card c : cards) { + if (c != null && c.getRenderForUI()) { //only add cards that match their card for UI + collection.add(c.getView()); + } } } return collection; @@ -1096,7 +1095,7 @@ public class CardView extends GameEntityView { if (c.getGame() != null) { if (c.hasPerpetual()) currentStateView.updateColors(c); else currentStateView.updateColors(currentState); - currentStateView.updateHasChangeColors(!Iterables.isEmpty(c.getChangedCardColors())); + currentStateView.updateHasChangeColors(c.hasChangedCardColors()); } } else { currentStateView.updateLoyalty(currentState); @@ -1842,8 +1841,8 @@ public class CardView extends GameEntityView { } @Override - public String getUntranslatedOracle() { - return getOracleText(); + public String getTranslatedName() { + return CardTranslation.getTranslatedName(this); } } diff --git a/forge-game/src/main/java/forge/game/card/CounterEnumType.java b/forge-game/src/main/java/forge/game/card/CounterEnumType.java index 2beca786f9f..01b3751f5a5 100644 --- a/forge-game/src/main/java/forge/game/card/CounterEnumType.java +++ b/forge-game/src/main/java/forge/game/card/CounterEnumType.java @@ -28,7 +28,7 @@ import java.util.Locale; * @author Clemens Koza * @version V0.0 17.02.2010 */ -public enum CounterEnumType { +public enum CounterEnumType implements CounterType { M1M1("-1/-1", "-1/-1", 255, 110, 106), P1P1("+1/+1", "+1/+1", 96, 226, 23), @@ -167,6 +167,8 @@ public enum CounterEnumType { FILIBUSTER("FLBTR", 255, 179, 119), + FILM("FILM", 255, 255, 255), + FINALITY("FINAL", 255, 255, 255), FIRE("FIRE", 240, 30, 35), @@ -555,4 +557,14 @@ public enum CounterEnumType { public static final ImmutableList values = ImmutableList.copyOf(values()); + @Override + public boolean is(CounterEnumType eType) { + return this == eType; + } + + @Override + public boolean isKeywordCounter() { + return false; + } + } diff --git a/forge-game/src/main/java/forge/game/card/CounterKeywordType.java b/forge-game/src/main/java/forge/game/card/CounterKeywordType.java new file mode 100644 index 00000000000..6ceea9a0043 --- /dev/null +++ b/forge-game/src/main/java/forge/game/card/CounterKeywordType.java @@ -0,0 +1,74 @@ +package forge.game.card; + +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; + +public record CounterKeywordType(String keyword) implements CounterType { + + // Rule 122.1b + static ImmutableList keywordCounter = ImmutableList.of( + "Flying", "First Strike", "Double Strike", "Deathtouch", "Decayed", "Exalted", "Haste", "Hexproof", + "Indestructible", "Lifelink", "Menace", "Reach", "Shadow", "Trample", "Vigilance"); + private static Map sMap = Maps.newHashMap(); + + + public static CounterKeywordType get(String s) { + if (!sMap.containsKey(s)) { + sMap.put(s, new CounterKeywordType(s)); + } + return sMap.get(s); + } + + @Override + public String toString() { + return keyword; + } + + public String getName() { + return getKeywordDescription(); + } + + public String getCounterOnCardDisplayName() { + return getKeywordDescription(); + } + + private String getKeywordDescription() { + if (keyword.startsWith("Hexproof:")) { + final String[] k = keyword.split(":"); + return "Hexproof from " + k[2]; + } + if (keyword.startsWith("Trample:")) { + return "Trample over Planeswalkers"; + } + return keyword; + } + + public boolean is(CounterEnumType eType) { + return false; + } + + public boolean isKeywordCounter() { + if (keyword.startsWith("Hexproof:")) { + return true; + } + if (keyword.startsWith("Trample:")) { + return true; + } + return keywordCounter.contains(keyword); + } + + + public int getRed() { + return 255; + } + + public int getGreen() { + return 255; + } + + public int getBlue() { + return 255; + } +} diff --git a/forge-game/src/main/java/forge/game/card/CounterType.java b/forge-game/src/main/java/forge/game/card/CounterType.java index 6a5007c2927..75864f47f2a 100644 --- a/forge-game/src/main/java/forge/game/card/CounterType.java +++ b/forge-game/src/main/java/forge/game/card/CounterType.java @@ -1,141 +1,31 @@ package forge.game.card; -import com.google.common.collect.ComparisonChain; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Maps; -import com.google.common.collect.Ordering; -import org.apache.commons.lang3.builder.EqualsBuilder; - import java.io.Serializable; -import java.util.Map; -import java.util.Objects; -public class CounterType implements Comparable, Serializable { - private static final long serialVersionUID = -7575835723159144478L; - - private CounterEnumType eVal = null; - private String sVal = null; - - // Rule 122.1b - static ImmutableList keywordCounter = ImmutableList.of( - "Flying", "First Strike", "Double Strike", "Deathtouch", "Decayed", "Exalted", "Haste", "Hexproof", - "Indestructible", "Lifelink", "Menace", "Reach", "Shadow", "Trample", "Vigilance"); - - private static Map eMap = Maps.newEnumMap(CounterEnumType.class); - private static Map sMap = Maps.newHashMap(); - - private CounterType(CounterEnumType e, String s) { - this.eVal = e; - this.sVal = s; - } - - public static CounterType get(CounterEnumType e) { - if (!eMap.containsKey(e)) { - eMap.put(e, new CounterType(e, null)); - } - return eMap.get(e); - } - - public static CounterType get(String s) { - if (!sMap.containsKey(s)) { - sMap.put(s, new CounterType(null, s)); - } - return sMap.get(s); - } +public interface CounterType extends Serializable { public static CounterType getType(String name) { if ("Any".equalsIgnoreCase(name)) { return null; } try { - return get(CounterEnumType.getType(name)); + return CounterEnumType.getType(name); } catch (final IllegalArgumentException ex) { - return get(name); + return CounterKeywordType.get(name); } } + public String getName(); - @Override - public int hashCode() { - return Objects.hash(eVal, sVal); - } + public String getCounterOnCardDisplayName(); - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (obj.getClass() != getClass()) { - return false; - } - CounterType rhs = (CounterType) obj; - return new EqualsBuilder() - .append(eVal, rhs.eVal) - .append(sVal, rhs.sVal) - .isEquals(); - } + public boolean is(CounterEnumType eType); - @Override - public String toString() { - return eVal != null ? eVal.toString() : sVal; - } + public boolean isKeywordCounter(); - public String getName() { - return eVal != null ? eVal.getName() : getKeywordDescription(); - } + public int getRed(); - public String getCounterOnCardDisplayName() { - return eVal != null ? eVal.getCounterOnCardDisplayName() : getKeywordDescription(); - } + public int getGreen(); - private String getKeywordDescription() { - if (sVal.startsWith("Hexproof:")) { - final String[] k = sVal.split(":"); - return "Hexproof from " + k[2]; - } - if (sVal.startsWith("Trample:")) { - return "Trample over Planeswalkers"; - } - return sVal; - } - - @Override - public int compareTo(CounterType o) { - return ComparisonChain.start() - .compare(eVal, o.eVal, Ordering.natural().nullsLast()) - .compare(sVal, o.sVal, Ordering.natural().nullsLast()) - .result(); - } - - public boolean is(CounterEnumType eType) { - return eVal == eType; - } - - public boolean isKeywordCounter() { - if (eVal != null) { - return false; - } - if (sVal.startsWith("Hexproof:")) { - return true; - } - if (sVal.startsWith("Trample:")) { - return true; - } - return keywordCounter.contains(sVal); - } - - public int getRed() { - return eVal != null ? eVal.getRed() : 255; - } - - public int getGreen() { - return eVal != null ? eVal.getGreen() : 255; - } - - public int getBlue() { - return eVal != null ? eVal.getBlue() : 255; - } + public int getBlue(); } diff --git a/forge-game/src/main/java/forge/game/card/perpetual/PerpetualColors.java b/forge-game/src/main/java/forge/game/card/perpetual/PerpetualColors.java index ecdf4f287cb..20c999c65c5 100644 --- a/forge-game/src/main/java/forge/game/card/perpetual/PerpetualColors.java +++ b/forge-game/src/main/java/forge/game/card/perpetual/PerpetualColors.java @@ -12,7 +12,7 @@ public record PerpetualColors(long timestamp, ColorSet colors, boolean overwrite @Override public void applyEffect(Card c) { - c.addColor(colors, !overwrite, timestamp, (long) 0, false); + c.addColor(colors, !overwrite, timestamp, null); } } diff --git a/forge-game/src/main/java/forge/game/card/perpetual/PerpetualIncorporate.java b/forge-game/src/main/java/forge/game/card/perpetual/PerpetualIncorporate.java index 0b647fe1f5d..386b7c2c588 100644 --- a/forge-game/src/main/java/forge/game/card/perpetual/PerpetualIncorporate.java +++ b/forge-game/src/main/java/forge/game/card/perpetual/PerpetualIncorporate.java @@ -17,7 +17,7 @@ public record PerpetualIncorporate(long timestamp, ManaCost incorporate) impleme ColorSet colors = ColorSet.fromMask(incorporate.getColorProfile()); final ManaCost newCost = ManaCost.combine(c.getManaCost(), incorporate); c.addChangedManaCost(newCost, timestamp, (long) 0); - c.addColor(colors, true, timestamp, (long) 0, false); + c.addColor(colors, true, timestamp, null); c.updateManaCostForView(); if (c.getFirstSpellAbility() != null) { diff --git a/forge-game/src/main/java/forge/game/card/perpetual/PerpetualManaCost.java b/forge-game/src/main/java/forge/game/card/perpetual/PerpetualManaCost.java new file mode 100644 index 00000000000..0c8cffa4673 --- /dev/null +++ b/forge-game/src/main/java/forge/game/card/perpetual/PerpetualManaCost.java @@ -0,0 +1,26 @@ +package forge.game.card.perpetual; + +import forge.card.mana.ManaCost; +import forge.game.card.Card; +import forge.game.cost.Cost; + +public record PerpetualManaCost(long timestamp, ManaCost manaCost) implements PerpetualInterface { + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public void applyEffect(Card c) { + c.addChangedManaCost(manaCost, timestamp, (long) 0); + + c.updateManaCostForView(); + + if (c.getFirstSpellAbility() != null) { + Cost cost = c.getFirstSpellAbility().getPayCosts().copyWithDefinedMana(manaCost); + c.getFirstSpellAbility().setPayCosts(cost); + } + } + +} diff --git a/forge-game/src/main/java/forge/game/combat/AttackRequirement.java b/forge-game/src/main/java/forge/game/combat/AttackRequirement.java index b15ed8cd1ae..678420d49c8 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackRequirement.java +++ b/forge-game/src/main/java/forge/game/combat/AttackRequirement.java @@ -1,6 +1,5 @@ package forge.game.combat; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -39,10 +38,12 @@ public class AttackRequirement { //MustAttack static check final List mustAttack = StaticAbilityMustAttack.entitiesMustAttack(attacker); - nAttackAnything += Collections.frequency(mustAttack, attacker); for (GameEntity e : mustAttack) { - if (e.equals(attacker)) continue; - defenderSpecific.add(e); + if (e.equals(attacker)) { + nAttackAnything++; + } else { + defenderSpecific.add(e); + } } for (final GameEntity defender : possibleDefenders) { 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 351d051fda4..16902d805e1 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -225,7 +225,7 @@ public class CombatUtil { if (!ge.equals(defender) && 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) && !ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) { + if (!attacker.isGoadedBy((Player) ge) && canAttack(attacker, ge)) { return false; } } @@ -233,17 +233,6 @@ public class CombatUtil { } } - // Quasi-goad logic for "Kardur, Doomscourge" etc. that isn't goad but behaves the same - if (defender != null && defender.hasKeyword("Creatures your opponents control attack a player other than you if able.")) { - for (GameEntity ge : getAllPossibleDefenders(attacker.getController())) { - if (!ge.equals(defender) && ge instanceof Player) { - if (!ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) { - return false; - } - } - } - } - // CantAttack static abilities if (StaticAbilityCantAttackBlock.cantAttack(attacker, defender)) { return false; 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 9c0e9070415..8cf784c6e7e 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -237,17 +237,17 @@ public class Cost implements Serializable { CostPartMana parsedMana = null; for (String part : parts) { if (part.startsWith("XMin")) { - xMin = (part); + xMin = part; } else if ("Mandatory".equals(part)) { this.isMandatory = true; } else { CostPart cp = parseCostPart(part, tapCost, untapCost); if (null != cp) - if (cp instanceof CostPartMana) { - parsedMana = (CostPartMana) cp; + if (cp instanceof CostPartMana p) { + parsedMana = p; } else { - if (cp instanceof CostPartWithList) { - ((CostPartWithList)cp).setIntrinsic(intrinsic); + if (cp instanceof CostPartWithList p) { + p.setIntrinsic(intrinsic); } this.costParts.add(cp); } @@ -621,8 +621,11 @@ public class Cost implements Serializable { } public final Cost copyWithDefinedMana(String manaCost) { + return copyWithDefinedMana(new ManaCost(new ManaCostParser(manaCost))); + } + public final Cost copyWithDefinedMana(ManaCost manaCost) { Cost toRet = copyWithNoMana(); - toRet.costParts.add(new CostPartMana(new ManaCost(new ManaCostParser(manaCost)), null)); + toRet.costParts.add(new CostPartMana(manaCost, null)); toRet.cacheTapCost(); return toRet; } @@ -994,9 +997,9 @@ public class Cost implements Serializable { Integer counters = otherAmount - part.convertAmount(); // the cost can turn positive if multiple Carth raise it if (counters < 0) { - costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription())); + costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterEnumType.LOYALTY, part.getType(), part.getTypeDescription())); } else { - costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription(), Lists.newArrayList(ZoneType.Battlefield) , false)); + costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterEnumType.LOYALTY, part.getType(), part.getTypeDescription(), Lists.newArrayList(ZoneType.Battlefield) , false)); } } else { continue; diff --git a/forge-game/src/main/java/forge/game/cost/CostDiscard.java b/forge-game/src/main/java/forge/game/cost/CostDiscard.java index e89a8213419..755c341ed14 100644 --- a/forge-game/src/main/java/forge/game/cost/CostDiscard.java +++ b/forge-game/src/main/java/forge/game/cost/CostDiscard.java @@ -18,7 +18,6 @@ package forge.game.cost; import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import forge.game.ability.AbilityKey; import forge.game.card.*; import forge.game.player.Player; @@ -29,7 +28,6 @@ import forge.util.TextUtil; import java.util.List; import java.util.Map; -import java.util.Set; /** * The Class CostDiscard. @@ -63,11 +61,20 @@ public class CostDiscard extends CostPartWithList { public Integer getMaxAmountX(SpellAbility ability, Player payer, final boolean effect) { final Card source = ability.getHostCard(); String type = this.getType(); + + boolean differentNames = false; + if (type.contains("+WithDifferentNames")) { + type = type.replace("+WithDifferentNames", ""); + differentNames = true; + } CardCollectionView handList = payer.canDiscardBy(ability, effect) ? payer.getCardsIn(ZoneType.Hand) : CardCollection.EMPTY; if (!type.equals("Random")) { handList = CardLists.getValidCards(handList, type.split(";"), payer, source, ability); } + if (differentNames) { + return CardLists.getDifferentNamesCount(handList); + } return handList.size(); } @@ -92,7 +99,7 @@ public class CostDiscard extends CostPartWithList { else if (this.getType().equals("LastDrawn")) { sb.append("the last card you drew this turn"); } - else if (this.getType().equals("DifferentNames")) { + else if (this.getType().contains("+WithDifferentNames")) { sb.append(Cost.convertAmountTypeToWords(i, this.getAmount(), "Card")).append(" with different names"); } else { @@ -145,21 +152,17 @@ public class CostDiscard extends CostPartWithList { final Card c = payer.getLastDrawnCard(); return handList.contains(c); } - else if (type.equals("DifferentNames")) { - Set cardNames = Sets.newHashSet(); - for (Card c : handList) { - if (!c.hasNoName()) { - cardNames.add(c.getName()); - } - } - return cardNames.size() >= amount; - } else { boolean sameName = false; + boolean differentNames = false; if (type.contains("+WithSameName")) { sameName = true; type = TextUtil.fastReplace(type, "+WithSameName", ""); } + if (type.contains("+WithDifferentNames")) { + type = type.replace("+WithDifferentNames", ""); + differentNames = true; + } if (type.contains("ChosenColor") && !source.hasChosenColor()) { //color hasn't been chosen yet, so skip getValidCards } else if (!type.equals("Random") && !type.contains("X")) { @@ -173,6 +176,10 @@ public class CostDiscard extends CostPartWithList { } } return false; + } else if (differentNames) { + if (CardLists.getDifferentNamesCount(handList) < amount) { + return false; + } } int adjustment = 0; if (source.isInZone(ZoneType.Hand) && payer.equals(source.getOwner())) { diff --git a/forge-game/src/main/java/forge/game/cost/CostSacrifice.java b/forge-game/src/main/java/forge/game/cost/CostSacrifice.java index a99c3da8b2d..357235f88bc 100644 --- a/forge-game/src/main/java/forge/game/cost/CostSacrifice.java +++ b/forge-game/src/main/java/forge/game/cost/CostSacrifice.java @@ -17,7 +17,6 @@ */ package forge.game.cost; -import com.google.common.collect.Sets; import forge.card.CardType; import forge.game.Game; import forge.game.ability.AbilityKey; @@ -31,7 +30,6 @@ import forge.game.zone.ZoneType; import forge.util.Lang; import java.util.Map; -import java.util.Set; /** * The Class CostSacrifice. @@ -74,16 +72,7 @@ public class CostSacrifice extends CostPartWithList { } typeList = CardLists.filter(typeList, CardPredicates.canBeSacrificedBy(ability, effect)); if (differentNames) { - // TODO rewrite with sharesName to respect Spy Kit - final Set crdname = Sets.newHashSet(); - for (final Card card : typeList) { - String name = card.getName(); - // CR 201.2b Those objects have different names only if each of them has at least one name and no two objects in that group have a name in common - if (!card.hasNoName()) { - crdname.add(name); - } - } - return crdname.size(); + return CardLists.getDifferentNamesCount(typeList); } return typeList.size(); } diff --git a/forge-game/src/main/java/forge/game/cost/CostUntap.java b/forge-game/src/main/java/forge/game/cost/CostUntap.java index 29420173dff..effcc619523 100644 --- a/forge-game/src/main/java/forge/game/cost/CostUntap.java +++ b/forge-game/src/main/java/forge/game/cost/CostUntap.java @@ -22,7 +22,6 @@ import forge.game.ability.AbilityKey; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CounterEnumType; -import forge.game.card.CounterType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.trigger.TriggerType; @@ -81,7 +80,7 @@ public class CostUntap extends CostPart { public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) { final Card source = ability.getHostCard(); return source.isTapped() && !source.isAbilitySick() && - (source.getCounters(CounterEnumType.STUN) == 0 || source.canRemoveCounters(CounterType.get(CounterEnumType.STUN))); + (source.getCounters(CounterEnumType.STUN) == 0 || source.canRemoveCounters(CounterEnumType.STUN)); } @Override diff --git a/forge-game/src/main/java/forge/game/cost/CostUntapType.java b/forge-game/src/main/java/forge/game/cost/CostUntapType.java index 6f2008a2992..50e95bc2e71 100644 --- a/forge-game/src/main/java/forge/game/cost/CostUntapType.java +++ b/forge-game/src/main/java/forge/game/cost/CostUntapType.java @@ -86,7 +86,7 @@ public class CostUntapType extends CostPartWithList { if (!canUntapSource) { typeList.remove(source); } - typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN))); + typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN)); final int amount = this.getAbilityAmount(ability); return (typeList.size() != 0) && (typeList.size() >= amount); diff --git a/forge-game/src/main/java/forge/game/event/Event.java b/forge-game/src/main/java/forge/game/event/Event.java index 401c49b0803..02e835658e0 100644 --- a/forge-game/src/main/java/forge/game/event/Event.java +++ b/forge-game/src/main/java/forge/game/event/Event.java @@ -1,5 +1,5 @@ package forge.game.event; -public abstract class Event { +public interface Event { } diff --git a/forge-game/src/main/java/forge/game/event/EventValueChangeType.java b/forge-game/src/main/java/forge/game/event/EventValueChangeType.java index 8b35954a2cc..54797cc68f0 100644 --- a/forge-game/src/main/java/forge/game/event/EventValueChangeType.java +++ b/forge-game/src/main/java/forge/game/event/EventValueChangeType.java @@ -1,9 +1,5 @@ package forge.game.event; -/** - * TODO: Write javadoc for this type. - * - */ public enum EventValueChangeType { Added, Removed, diff --git a/forge-game/src/main/java/forge/game/event/GameEvent.java b/forge-game/src/main/java/forge/game/event/GameEvent.java index 77a7e268f04..8b08775ba03 100644 --- a/forge-game/src/main/java/forge/game/event/GameEvent.java +++ b/forge-game/src/main/java/forge/game/event/GameEvent.java @@ -1,6 +1,6 @@ package forge.game.event; -public abstract class GameEvent extends Event { +public interface GameEvent extends Event { public abstract T visit(IGameEventVisitor visitor); } diff --git a/forge-game/src/main/java/forge/game/event/GameEventAnteCardsSelected.java b/forge-game/src/main/java/forge/game/event/GameEventAnteCardsSelected.java index 1b9c06ece6e..01dd5d7fa87 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventAnteCardsSelected.java +++ b/forge-game/src/main/java/forge/game/event/GameEventAnteCardsSelected.java @@ -5,11 +5,7 @@ import com.google.common.collect.Multimap; import forge.game.card.Card; import forge.game.player.Player; -public class GameEventAnteCardsSelected extends GameEvent { - public final Multimap cards; - public GameEventAnteCardsSelected(Multimap list) { - cards = list; - } +public record GameEventAnteCardsSelected(Multimap cards) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventAttackersDeclared.java b/forge-game/src/main/java/forge/game/event/GameEventAttackersDeclared.java index 09ef47c0a43..12b9531bf62 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventAttackersDeclared.java +++ b/forge-game/src/main/java/forge/game/event/GameEventAttackersDeclared.java @@ -6,27 +6,21 @@ import forge.game.GameEntity; import forge.game.card.Card; import forge.game.player.Player; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventAttackersDeclared extends GameEvent { - - public final Player player; - public final Multimap attackersMap; - - public GameEventAttackersDeclared(Player playerTurn, Multimap attackersMap) { - this.player = playerTurn; - this.attackersMap = attackersMap; - } +public record GameEventAttackersDeclared(Player player, Multimap attackersMap) implements GameEvent { /* (non-Javadoc) * @see forge.game.event.GameEvent#visit(forge.game.event.IGameEventVisitor) */ @Override public T visit(IGameEventVisitor visitor) { - // TODO Auto-generated method stub return visitor.visit(this); } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + player + " declared attackers: " + attackersMap; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventBlockersDeclared.java b/forge-game/src/main/java/forge/game/event/GameEventBlockersDeclared.java index 810c727b87c..9ec99ead4e3 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventBlockersDeclared.java +++ b/forge-game/src/main/java/forge/game/event/GameEventBlockersDeclared.java @@ -12,23 +12,10 @@ import forge.util.Lang; import forge.util.TextUtil; import forge.util.maps.MapOfLists; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventBlockersDeclared extends GameEvent { - - public final Map> blockers; - public final Player defendingPlayer; - - public GameEventBlockersDeclared(Player who, Map> blockers) { - this.blockers = blockers; - defendingPlayer = who; - } +public record GameEventBlockersDeclared(Player defendingPlayer, Map> blockers) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { - // TODO Auto-generated method stub return visitor.visit(this); } diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardAttachment.java b/forge-game/src/main/java/forge/game/event/GameEventCardAttachment.java index 1653c15777e..c6acf10358b 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardAttachment.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardAttachment.java @@ -3,20 +3,18 @@ package forge.game.event; import forge.game.GameEntity; import forge.game.card.Card; -public class GameEventCardAttachment extends GameEvent { - - public final Card equipment; - public final GameEntity newTarget; // can enchant player, I'm ssaving a class to enchants - it could be incorrect. - public final GameEntity oldEntiy; - - public GameEventCardAttachment(Card attachment, GameEntity formerEntity, GameEntity newEntity) { - this.equipment = attachment; - this.newTarget = newEntity; - this.oldEntiy = formerEntity; - } +public record GameEventCardAttachment(Card equipment, GameEntity oldEntity, GameEntity newTarget) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return newTarget == null ? "Detached " + equipment + " from " + oldEntity : "Attached " + equipment + (oldEntity == null ? "" : " from " + oldEntity) + " to " + newTarget; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardChangeZone.java b/forge-game/src/main/java/forge/game/event/GameEventCardChangeZone.java index fce42173208..8a105185a45 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardChangeZone.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardChangeZone.java @@ -4,17 +4,7 @@ import forge.game.card.Card; import forge.game.zone.Zone; import forge.util.TextUtil; -public class GameEventCardChangeZone extends GameEvent { - - public final Card card; - public final Zone from; - public final Zone to; - - public GameEventCardChangeZone(Card c, Zone zoneFrom, Zone zoneTo) { - card = c; - from = zoneFrom; - to = zoneTo; - } +public record GameEventCardChangeZone(Card card, Zone from, Zone to) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardCounters.java b/forge-game/src/main/java/forge/game/event/GameEventCardCounters.java index 31c09db1b7e..32a1de9e38b 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardCounters.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardCounters.java @@ -3,21 +3,17 @@ package forge.game.event; import forge.game.card.Card; import forge.game.card.CounterType; -public class GameEventCardCounters extends GameEvent { - public final Card card; - public final CounterType type; - public final int oldValue; - public final int newValue; - - public GameEventCardCounters(Card card, CounterType counterType, int old, int newValue) { - this.card = card; - type = counterType; - this.oldValue = old; - this.newValue = newValue; - } - +public record GameEventCardCounters(Card card, CounterType type, int oldValue, int newValue) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + card + " " + type + " counters: " + oldValue + " -> " + newValue; + } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardDamaged.java b/forge-game/src/main/java/forge/game/event/GameEventCardDamaged.java index 69a4c156205..f91fe98a910 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardDamaged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardDamaged.java @@ -2,7 +2,7 @@ package forge.game.event; import forge.game.card.Card; -public class GameEventCardDamaged extends GameEvent { +public record GameEventCardDamaged(Card card, Card source, int amount, DamageType type) implements GameEvent { public enum DamageType { Normal, @@ -11,21 +11,16 @@ public class GameEventCardDamaged extends GameEvent { LoyaltyLoss } - public final Card card; - public final Card source; - public final int amount; - public final DamageType type; - - public GameEventCardDamaged(Card card, Card src, int damageToAdd, DamageType damageType) { - this.card = card; - source = src; - amount = damageToAdd; - type = damageType; - } - @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + source + " dealt " + amount + " " + type + " damage to " + card; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardDestroyed.java b/forge-game/src/main/java/forge/game/event/GameEventCardDestroyed.java index bfb0d0590cf..05074aad3c9 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardDestroyed.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardDestroyed.java @@ -1,10 +1,17 @@ package forge.game.event; -public class GameEventCardDestroyed extends GameEvent { +public record GameEventCardDestroyed() implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Card destroyed"; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardForetold.java b/forge-game/src/main/java/forge/game/event/GameEventCardForetold.java index b7003036045..891b6438f48 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardForetold.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardForetold.java @@ -2,12 +2,7 @@ package forge.game.event; import forge.game.player.Player; -public class GameEventCardForetold extends GameEvent { - public final Player activatingPlayer; - - public GameEventCardForetold(Player player) { - activatingPlayer = player; - } +public record GameEventCardForetold(Player activatingPlayer) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardModeChosen.java b/forge-game/src/main/java/forge/game/event/GameEventCardModeChosen.java index 865cc2cfa4e..a289bb916eb 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardModeChosen.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardModeChosen.java @@ -2,21 +2,7 @@ package forge.game.event; import forge.game.player.Player; -public class GameEventCardModeChosen extends GameEvent { - - public final Player player; - public final String cardName; - public final String mode; - public final boolean log; - public final boolean random; - - public GameEventCardModeChosen(Player player, String cardName, String mode, boolean log, boolean random) { - this.player = player; - this.cardName = cardName; - this.mode = mode; - this.log = log; - this.random = random; - } +public record GameEventCardModeChosen(Player player, String cardName, String mode, boolean log, boolean random) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardPhased.java b/forge-game/src/main/java/forge/game/event/GameEventCardPhased.java index 132aae4bce7..a7038d21939 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardPhased.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardPhased.java @@ -2,19 +2,7 @@ package forge.game.event; import forge.game.card.Card; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventCardPhased extends GameEvent { - - public final Card card; - public final boolean phaseState; - - public GameEventCardPhased(Card card, boolean state) { - this.card = card; - phaseState = state; - } +public record GameEventCardPhased(Card card, boolean phaseState) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardPlotted.java b/forge-game/src/main/java/forge/game/event/GameEventCardPlotted.java index 785a4d58be7..e789fb522d5 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardPlotted.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardPlotted.java @@ -3,16 +3,7 @@ package forge.game.event; import forge.game.card.Card; import forge.game.player.Player; -public class GameEventCardPlotted extends GameEvent { - - public final Card card; - - public final Player activatingPlayer; - - public GameEventCardPlotted(Card card, Player player) { - this.card = card; - activatingPlayer = player; - } +public record GameEventCardPlotted(Card card, Player activatingPlayer) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardRegenerated.java b/forge-game/src/main/java/forge/game/event/GameEventCardRegenerated.java index a348a2ad77e..147bfb088e7 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardRegenerated.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardRegenerated.java @@ -5,11 +5,9 @@ import forge.game.card.Card; import java.util.Arrays; import java.util.Collection; -public class GameEventCardRegenerated extends GameEvent { - - public final Collection cards; +public record GameEventCardRegenerated(Collection cards) implements GameEvent { public GameEventCardRegenerated(Card affected) { - cards = Arrays.asList(affected); + this(Arrays.asList(affected)); } @Override diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardSacrificed.java b/forge-game/src/main/java/forge/game/event/GameEventCardSacrificed.java index 105d9d1724c..bda3c86f631 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardSacrificed.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardSacrificed.java @@ -1,9 +1,19 @@ package forge.game.event; -public class GameEventCardSacrificed extends GameEvent { +import forge.game.card.Card; + +public record GameEventCardSacrificed(Card card) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + card.getController() + " sacrificed " + card; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardStatsChanged.java b/forge-game/src/main/java/forge/game/event/GameEventCardStatsChanged.java index f58f4c85e11..83ed5146ebb 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardStatsChanged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardStatsChanged.java @@ -11,23 +11,20 @@ import forge.game.card.Card; /** * This means card's characteristics have changed on server, clients must re-request them */ -public class GameEventCardStatsChanged extends GameEvent { +public record GameEventCardStatsChanged(Collection cards, boolean transform) implements GameEvent { - public final Collection cards; - public boolean transform = false; public GameEventCardStatsChanged(Card affected) { this(affected, false); } public GameEventCardStatsChanged(Card affected, boolean isTransform) { - cards = Arrays.asList(affected); + this(Arrays.asList(affected), false); //the transform should only fire once so the flip effect sound will trigger once every transformation... // disable for now - transform = false; } public GameEventCardStatsChanged(Collection affected) { - cards = affected; + this(affected, false); } /* (non-Javadoc) @@ -35,7 +32,6 @@ public class GameEventCardStatsChanged extends GameEvent { */ @Override public T visit(IGameEventVisitor visitor) { - // TODO Auto-generated method stub return visitor.visit(this); } diff --git a/forge-game/src/main/java/forge/game/event/GameEventCardTapped.java b/forge-game/src/main/java/forge/game/event/GameEventCardTapped.java index 993e1cda1db..bfd538e0951 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCardTapped.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCardTapped.java @@ -2,17 +2,18 @@ package forge.game.event; import forge.game.card.Card; -public class GameEventCardTapped extends GameEvent { - public final boolean tapped; - public final Card card; - - public GameEventCardTapped(final Card card, final boolean tapped) { - this.tapped = tapped; - this.card = card; - } +public record GameEventCardTapped(Card card, boolean tapped) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + card.getController() + (tapped ? " tapped " : " untapped ") + card; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventCombatChanged.java b/forge-game/src/main/java/forge/game/event/GameEventCombatChanged.java index e600e56a66c..3bcd3e92d00 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCombatChanged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCombatChanged.java @@ -1,13 +1,17 @@ package forge.game.event; -public class GameEventCombatChanged extends GameEvent { - - public GameEventCombatChanged() { - } +public record GameEventCombatChanged() implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Combat changed"; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventCombatEnded.java b/forge-game/src/main/java/forge/game/event/GameEventCombatEnded.java index db7b42e3de4..fc4dbd8ebd6 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCombatEnded.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCombatEnded.java @@ -4,19 +4,18 @@ import java.util.List; import forge.game.card.Card; -public class GameEventCombatEnded extends GameEvent { - - public final List attackers; - public final List blockers; - - public GameEventCombatEnded(List attackers, List blockers) { - this.attackers = attackers; - this.blockers = blockers; - } +public record GameEventCombatEnded(List attackers, List blockers) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Combat ended. Attackers: " + attackers + " Blockers: " + blockers; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventCombatUpdate.java b/forge-game/src/main/java/forge/game/event/GameEventCombatUpdate.java index 50989e9d7eb..73f68ad4352 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventCombatUpdate.java +++ b/forge-game/src/main/java/forge/game/event/GameEventCombatUpdate.java @@ -4,15 +4,7 @@ import java.util.List; import forge.game.card.Card; -public class GameEventCombatUpdate extends GameEvent { - - public final List attackers; - public final List blockers; - - public GameEventCombatUpdate(List attackers, List blockers) { - this.attackers = attackers; - this.blockers = blockers; - } +public record GameEventCombatUpdate(List attackers, List blockers) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventDayTimeChanged.java b/forge-game/src/main/java/forge/game/event/GameEventDayTimeChanged.java index e4c67e4cb2f..d41c8b9b046 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventDayTimeChanged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventDayTimeChanged.java @@ -1,11 +1,6 @@ package forge.game.event; -public class GameEventDayTimeChanged extends GameEvent { - public final boolean daytime; - - public GameEventDayTimeChanged(final boolean daytime) { - this.daytime = daytime; - } +public record GameEventDayTimeChanged(boolean daytime) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventDoorChanged.java b/forge-game/src/main/java/forge/game/event/GameEventDoorChanged.java index 5f04cf5a7de..bf846625b1f 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventDoorChanged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventDoorChanged.java @@ -3,21 +3,9 @@ package forge.game.event; import forge.card.CardStateName; import forge.game.card.Card; import forge.game.player.Player; -import forge.util.CardTranslation; import forge.util.Lang; -public class GameEventDoorChanged extends GameEvent { - public final Player activatingPlayer; - public final Card card; - public final CardStateName state; - public boolean unlock; - - public GameEventDoorChanged(Player player, Card c, CardStateName state, boolean unlock) { - activatingPlayer = player; - card = c; - this.state = state; - this.unlock = unlock; - } +public record GameEventDoorChanged(Player activatingPlayer, Card card, CardStateName state, boolean unlock) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { @@ -26,7 +14,7 @@ public class GameEventDoorChanged extends GameEvent { @Override public String toString() { - String doorName = CardTranslation.getTranslatedName(card.getState(state)); + String doorName = card.getState(state).getTranslatedName(); StringBuilder sb = new StringBuilder(); sb.append(activatingPlayer); diff --git a/forge-game/src/main/java/forge/game/event/GameEventFlipCoin.java b/forge-game/src/main/java/forge/game/event/GameEventFlipCoin.java index a28dc4ed16e..ebed52d2424 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventFlipCoin.java +++ b/forge-game/src/main/java/forge/game/event/GameEventFlipCoin.java @@ -1,9 +1,17 @@ package forge.game.event; -public class GameEventFlipCoin extends GameEvent { +public record GameEventFlipCoin() implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Flipped coin"; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventGameFinished.java b/forge-game/src/main/java/forge/game/event/GameEventGameFinished.java index 4b600634104..7e0078d07de 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventGameFinished.java +++ b/forge-game/src/main/java/forge/game/event/GameEventGameFinished.java @@ -1,8 +1,17 @@ package forge.game.event; -public class GameEventGameFinished extends GameEvent { +public record GameEventGameFinished() implements GameEvent { + @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Game finished"; + } } // need this class to launch after log was built via previous event \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/event/GameEventGameOutcome.java b/forge-game/src/main/java/forge/game/event/GameEventGameOutcome.java index 4a675c05881..3351dffb1f8 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventGameOutcome.java +++ b/forge-game/src/main/java/forge/game/event/GameEventGameOutcome.java @@ -4,17 +4,18 @@ import java.util.Collection; import forge.game.GameOutcome; -public class GameEventGameOutcome extends GameEvent { - public final GameOutcome result; - public final Collection history; - - public GameEventGameOutcome(GameOutcome lastOne, Collection history) { - this.result = lastOne; - this.history = history; - } +public record GameEventGameOutcome(GameOutcome result, Collection history) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Game Outcome: " + result.getOutcomeStrings(); + } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/event/GameEventGameRestarted.java b/forge-game/src/main/java/forge/game/event/GameEventGameRestarted.java index e997831a714..0ad85a4e068 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventGameRestarted.java +++ b/forge-game/src/main/java/forge/game/event/GameEventGameRestarted.java @@ -2,17 +2,7 @@ package forge.game.event; import forge.game.player.Player; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventGameRestarted extends GameEvent { - - public final Player whoRestarted; - - public GameEventGameRestarted(Player playerTurn) { - whoRestarted = playerTurn; - } +public record GameEventGameRestarted(Player whoRestarted) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventGameStarted.java b/forge-game/src/main/java/forge/game/event/GameEventGameStarted.java index 550a439d5e5..3bf5f55b436 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventGameStarted.java +++ b/forge-game/src/main/java/forge/game/event/GameEventGameStarted.java @@ -5,22 +5,7 @@ import forge.game.player.Player; import forge.util.Lang; import forge.util.TextUtil; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventGameStarted extends GameEvent { - - public final Player firstTurn; - public final Iterable players; - public final GameType gameType; - - public GameEventGameStarted(GameType type, Player firstTurn, Iterable players) { - super(); - this.gameType = type; - this.firstTurn = firstTurn; - this.players = players; - } +public record GameEventGameStarted(GameType gameType, Player firstTurn, Iterable players) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventLandPlayed.java b/forge-game/src/main/java/forge/game/event/GameEventLandPlayed.java index 595bf9783c9..6a6224239a2 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventLandPlayed.java +++ b/forge-game/src/main/java/forge/game/event/GameEventLandPlayed.java @@ -3,19 +3,17 @@ package forge.game.event; import forge.game.card.Card; import forge.game.player.Player; -public class GameEventLandPlayed extends GameEvent { - - public final Player player; - public final Card land; - - public GameEventLandPlayed(Player player, Card land) { - this.player = player; - this.land = land; - - } - +public record GameEventLandPlayed(Player player, Card land) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + player + " played " + land; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventManaBurn.java b/forge-game/src/main/java/forge/game/event/GameEventManaBurn.java index 5b9a06367f1..db8e4790316 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventManaBurn.java +++ b/forge-game/src/main/java/forge/game/event/GameEventManaBurn.java @@ -3,22 +3,7 @@ package forge.game.event; import forge.game.player.Player; // This special event denotes loss of mana due to phase end -public class GameEventManaBurn extends GameEvent { - - public final Player player; - public final boolean causedLifeLoss; - public final int amount; - - /** - * TODO: Write javadoc for Constructor. - * @param dealDamage - * @param burn - */ - public GameEventManaBurn(Player who, int burn, boolean dealDamage) { - player = who; - amount = burn; - causedLifeLoss = dealDamage; - } +public record GameEventManaBurn(Player player, boolean causedLifeLoss, int amount) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventManaPool.java b/forge-game/src/main/java/forge/game/event/GameEventManaPool.java index 473e25f6191..6602376dfec 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventManaPool.java +++ b/forge-game/src/main/java/forge/game/event/GameEventManaPool.java @@ -4,20 +4,7 @@ import forge.game.mana.Mana; import forge.game.player.Player; import forge.util.Lang; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventManaPool extends GameEvent { - public final Player player; - public final EventValueChangeType mode; - public final Mana mana; - - public GameEventManaPool(Player owner, EventValueChangeType changeMode, Mana mana) { - this.mana = mana; - player = owner; - mode = changeMode; - } +public record GameEventManaPool(Player player, EventValueChangeType mode, Mana mana) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventMulligan.java b/forge-game/src/main/java/forge/game/event/GameEventMulligan.java index 30dcd25391b..34c763253d7 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventMulligan.java +++ b/forge-game/src/main/java/forge/game/event/GameEventMulligan.java @@ -2,19 +2,18 @@ package forge.game.event; import forge.game.player.Player; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventMulligan extends GameEvent { - - public final Player player; - public GameEventMulligan(Player p) { - player = p; - } +public record GameEventMulligan(Player player) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + player + " mulligans"; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventPlayerControl.java b/forge-game/src/main/java/forge/game/event/GameEventPlayerControl.java index b0fc48c9406..e3c28ca37d3 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventPlayerControl.java +++ b/forge-game/src/main/java/forge/game/event/GameEventPlayerControl.java @@ -4,23 +4,18 @@ import forge.LobbyPlayer; import forge.game.player.Player; import forge.game.player.PlayerController; -public class GameEventPlayerControl extends GameEvent { - public final Player player; - public final LobbyPlayer oldLobbyPlayer; - public final PlayerController oldController; - public final LobbyPlayer newLobbyPlayer; - public final PlayerController newController; - - public GameEventPlayerControl(final Player p, final LobbyPlayer oldLobbyPlayer, final PlayerController oldController, final LobbyPlayer newLobbyPlayer, final PlayerController newController) { - this.player = p; - this.oldLobbyPlayer = oldLobbyPlayer; - this.oldController = oldController; - this.newLobbyPlayer = newLobbyPlayer; - this.newController = newController; - } +public record GameEventPlayerControl(Player player, LobbyPlayer oldLobbyPlayer, PlayerController oldController, LobbyPlayer newLobbyPlayer, PlayerController newController) implements GameEvent { @Override public T visit(final IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + player + " controlled by " + player.getControllingPlayer(); + } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/event/GameEventPlayerCounters.java b/forge-game/src/main/java/forge/game/event/GameEventPlayerCounters.java index d595012e164..e79b1e1a6d8 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventPlayerCounters.java +++ b/forge-game/src/main/java/forge/game/event/GameEventPlayerCounters.java @@ -3,21 +3,18 @@ package forge.game.event; import forge.game.card.CounterType; import forge.game.player.Player; -public class GameEventPlayerCounters extends GameEvent { - public final Player receiver; - public final CounterType type; - public final int oldValue; - public final int amount; +public record GameEventPlayerCounters(Player receiver, CounterType type, int oldValue, int amount) implements GameEvent { - public GameEventPlayerCounters(Player recv, CounterType t, int old, int num) { - receiver = recv; - type = t; - oldValue = old; - amount = num; - } + @Override + public T visit(IGameEventVisitor visitor) { + return visitor.visit(this); + } - @Override - public T visit(IGameEventVisitor visitor) { - return visitor.visit(this); - } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + receiver + " got " + oldValue + " plus " + amount + " " + type; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventPlayerDamaged.java b/forge-game/src/main/java/forge/game/event/GameEventPlayerDamaged.java index 62197fecd5c..499a2cd924f 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventPlayerDamaged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventPlayerDamaged.java @@ -3,33 +3,7 @@ package forge.game.event; import forge.game.card.Card; import forge.game.player.Player; - -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventPlayerDamaged extends GameEvent { - public final Player target; - public final Card source; - public final int amount; - final public boolean infect; - public final boolean combat; - - /** - * TODO: Write javadoc for Constructor. - * @param player - * @param source - * @param amount - * @param isCombat - * @param infect - */ - public GameEventPlayerDamaged(Player player, Card source, int amount, boolean isCombat, boolean infect) { - target = player; - this.source = source; - this.amount = amount; - combat = isCombat; - this.infect = infect; - } +public record GameEventPlayerDamaged(Player target, Card source, int amount, boolean combat, boolean infect) implements GameEvent { /* (non-Javadoc) * @see forge.game.event.GameEvent#visit(forge.game.event.IGameEventVisitor) @@ -39,4 +13,11 @@ public class GameEventPlayerDamaged extends GameEvent { return visitor.visit(this); } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + target + " took " + amount + (infect ? " infect" : combat ? " combat" : "") + " damage from " + source; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventPlayerLivesChanged.java b/forge-game/src/main/java/forge/game/event/GameEventPlayerLivesChanged.java index c84ab7edea5..cb3d8686618 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventPlayerLivesChanged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventPlayerLivesChanged.java @@ -4,16 +4,7 @@ import forge.game.player.Player; import forge.util.Lang; import forge.util.TextUtil; -public class GameEventPlayerLivesChanged extends GameEvent { - public final Player player; - public final int oldLives; - public final int newLives; - - public GameEventPlayerLivesChanged(Player who, int oldValue, int newValue) { - player = who; - oldLives = oldValue; - newLives = newValue; - } +public record GameEventPlayerLivesChanged(Player player, int oldLives, int newLives) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventPlayerPoisoned.java b/forge-game/src/main/java/forge/game/event/GameEventPlayerPoisoned.java index 7e4c24efcad..e53d08b0e9f 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventPlayerPoisoned.java +++ b/forge-game/src/main/java/forge/game/event/GameEventPlayerPoisoned.java @@ -6,18 +6,7 @@ import forge.game.player.Player; * * */ -public class GameEventPlayerPoisoned extends GameEvent { - public final Player receiver; - public final Player source; - public final int oldValue; - public final int amount; - - public GameEventPlayerPoisoned(Player recv, Player src, int old, int num) { - receiver = recv; - source = src; - oldValue = old; - amount = num; - } +public record GameEventPlayerPoisoned(Player receiver, Player source, int oldValue, int amount) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventPlayerPriority.java b/forge-game/src/main/java/forge/game/event/GameEventPlayerPriority.java index 0a748996269..bfc9e87c3cc 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventPlayerPriority.java +++ b/forge-game/src/main/java/forge/game/event/GameEventPlayerPriority.java @@ -4,21 +4,7 @@ import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.util.TextUtil; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventPlayerPriority extends GameEvent { - - public final Player turn; - public final PhaseType phase; - public final Player priority; - - public GameEventPlayerPriority(Player playerTurn, PhaseType phase, Player priorityPlayer) { - turn = playerTurn; - this.phase = phase; - priority = priorityPlayer; - } +public record GameEventPlayerPriority(Player turn, PhaseType phase, Player priority) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventPlayerRadiation.java b/forge-game/src/main/java/forge/game/event/GameEventPlayerRadiation.java index b9214b15b63..af7347dab51 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventPlayerRadiation.java +++ b/forge-game/src/main/java/forge/game/event/GameEventPlayerRadiation.java @@ -2,16 +2,7 @@ package forge.game.event; import forge.game.player.Player; -public class GameEventPlayerRadiation extends GameEvent { - public final Player receiver; - public final Player source; - public final int change; - - public GameEventPlayerRadiation(Player recv, Player src, int chng) { - receiver = recv; - source = src; - change = chng; - } +public record GameEventPlayerRadiation(Player receiver, Player source, int change) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventPlayerShardsChanged.java b/forge-game/src/main/java/forge/game/event/GameEventPlayerShardsChanged.java index 4ff56fc6bae..6edd2c33bbc 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventPlayerShardsChanged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventPlayerShardsChanged.java @@ -4,16 +4,7 @@ import forge.game.player.Player; import forge.util.Lang; import forge.util.TextUtil; -public class GameEventPlayerShardsChanged extends GameEvent { - public final Player player; - public final int oldShards; - public final int newShards; - - public GameEventPlayerShardsChanged(Player who, int oldValue, int newValue) { - player = who; - oldShards = oldValue; - newShards = newValue; - } +public record GameEventPlayerShardsChanged(Player player, int oldShards, int newShards) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventPlayerStatsChanged.java b/forge-game/src/main/java/forge/game/event/GameEventPlayerStatsChanged.java index b077517cc24..fcaf4887dbb 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventPlayerStatsChanged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventPlayerStatsChanged.java @@ -10,18 +10,10 @@ import forge.util.TextUtil; /** * This means card's characteristics have changed on server, clients must re-request them */ -public class GameEventPlayerStatsChanged extends GameEvent { +public record GameEventPlayerStatsChanged(Collection players, boolean updateCards) implements GameEvent { - public final Collection players; - public final boolean updateCards; public GameEventPlayerStatsChanged(Player affected, boolean updateCards) { - players = Arrays.asList(affected); - this.updateCards = updateCards; - } - - public GameEventPlayerStatsChanged(Collection affected, boolean updateCards) { - players = affected; - this.updateCards = updateCards; + this(Arrays.asList(affected), updateCards); } /* (non-Javadoc) @@ -29,7 +21,6 @@ public class GameEventPlayerStatsChanged extends GameEvent { */ @Override public T visit(IGameEventVisitor visitor) { - // TODO Auto-generated method stub return visitor.visit(this); } diff --git a/forge-game/src/main/java/forge/game/event/GameEventRandomLog.java b/forge-game/src/main/java/forge/game/event/GameEventRandomLog.java index 3fa778bbd74..4552e1fbf19 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventRandomLog.java +++ b/forge-game/src/main/java/forge/game/event/GameEventRandomLog.java @@ -1,12 +1,6 @@ package forge.game.event; -public class GameEventRandomLog extends GameEvent { - - public final String message; - - public GameEventRandomLog(String message) { - this.message = message; - } +public record GameEventRandomLog(String message) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventRollDie.java b/forge-game/src/main/java/forge/game/event/GameEventRollDie.java index b44674e924e..a3046fb04d8 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventRollDie.java +++ b/forge-game/src/main/java/forge/game/event/GameEventRollDie.java @@ -1,6 +1,6 @@ package forge.game.event; -public class GameEventRollDie extends GameEvent { +public record GameEventRollDie() implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventScry.java b/forge-game/src/main/java/forge/game/event/GameEventScry.java index 624de741bf2..96221a0c6b4 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventScry.java +++ b/forge-game/src/main/java/forge/game/event/GameEventScry.java @@ -2,19 +2,18 @@ package forge.game.event; import forge.game.player.Player; -public class GameEventScry extends GameEvent { - - public final Player player; - public final int toTop, toBottom; - - public GameEventScry(Player player, int toTop, int toBottom) { - this.player = player; - this.toTop = toTop; - this.toBottom = toBottom; - } +public record GameEventScry(Player player, int toTop, int toBottom) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + player + " scried " + toTop + " to top, " + toBottom + " to bottom"; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventShuffle.java b/forge-game/src/main/java/forge/game/event/GameEventShuffle.java index 893d617fe16..96502a89597 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventShuffle.java +++ b/forge-game/src/main/java/forge/game/event/GameEventShuffle.java @@ -4,13 +4,7 @@ import forge.game.player.Player; import forge.util.Lang; import forge.util.TextUtil; -public class GameEventShuffle extends GameEvent { - - public final Player player; - - public GameEventShuffle(Player player) { - this.player = player; - } +public record GameEventShuffle(Player player) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { @@ -22,6 +16,6 @@ public class GameEventShuffle extends GameEvent { */ @Override public String toString() { - return TextUtil.concatWithSpace(player.toString(), Lang.joinVerb(player.getName(), "shuffle"),"his/her/its library"); + return TextUtil.concatWithSpace(player.toString(), Lang.joinVerb(player.getName(), "shuffle"), "their library"); } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventSpeedChanged.java b/forge-game/src/main/java/forge/game/event/GameEventSpeedChanged.java index 5320fef0626..31fb2abf7c9 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventSpeedChanged.java +++ b/forge-game/src/main/java/forge/game/event/GameEventSpeedChanged.java @@ -2,18 +2,7 @@ package forge.game.event; import forge.game.player.Player; -public class GameEventSpeedChanged extends GameEvent { - - public final Player player; - public final int oldValue; - public final int newValue; - - public GameEventSpeedChanged(Player affected, int oldValue, int newValue) { - player = affected; - this.oldValue = oldValue; - this.newValue = newValue; - } - +public record GameEventSpeedChanged(Player player, int oldValue, int newValue) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); diff --git a/forge-game/src/main/java/forge/game/event/GameEventSpellAbilityCast.java b/forge-game/src/main/java/forge/game/event/GameEventSpellAbilityCast.java index a64240e989d..e38c374abde 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventSpellAbilityCast.java +++ b/forge-game/src/main/java/forge/game/event/GameEventSpellAbilityCast.java @@ -3,21 +3,7 @@ package forge.game.event; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventSpellAbilityCast extends GameEvent { - - public final SpellAbility sa; - public final SpellAbilityStackInstance si; - public final int stackIndex; - - public GameEventSpellAbilityCast(SpellAbility sp, SpellAbilityStackInstance si, int stackIndex) { - sa = sp; - this.si = si; - this.stackIndex = stackIndex; - } +public record GameEventSpellAbilityCast(SpellAbility sa, SpellAbilityStackInstance si, int stackIndex) implements GameEvent { /* (non-Javadoc) * @see forge.game.event.GameEvent#visit(forge.game.event.IGameEventVisitor) @@ -27,4 +13,11 @@ public class GameEventSpellAbilityCast extends GameEvent { return visitor.visit(this); } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + sa.getActivatingPlayer() + (sa.isSpell() ? " cast " : sa.isActivatedAbility() ? " activated " : " triggered ") + sa; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventSpellRemovedFromStack.java b/forge-game/src/main/java/forge/game/event/GameEventSpellRemovedFromStack.java index 0725efff997..5a089f6a7d1 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventSpellRemovedFromStack.java +++ b/forge-game/src/main/java/forge/game/event/GameEventSpellRemovedFromStack.java @@ -2,21 +2,18 @@ package forge.game.event; import forge.game.spellability.SpellAbility; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventSpellRemovedFromStack extends GameEvent { - public final SpellAbility sa; - - public GameEventSpellRemovedFromStack(SpellAbility spellAbility) { - sa = spellAbility; - } +public record GameEventSpellRemovedFromStack(SpellAbility sa) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { - // TODO Auto-generated method stub return visitor.visit(this); } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Stack removed " + sa; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventSpellResolved.java b/forge-game/src/main/java/forge/game/event/GameEventSpellResolved.java index af97226fcfb..c54571a26ea 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventSpellResolved.java +++ b/forge-game/src/main/java/forge/game/event/GameEventSpellResolved.java @@ -2,29 +2,18 @@ package forge.game.event; import forge.game.spellability.SpellAbility; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventSpellResolved extends GameEvent { - - public final SpellAbility spell; - public final boolean hasFizzled; - - /** - * TODO: Write javadoc for Constructor. - * @param source - * @param sa - * @param hasFizzled - */ - public GameEventSpellResolved(SpellAbility sa, boolean hasFizzled) { - // TODO Auto-generated constructor stub - this.spell = sa; - this.hasFizzled = hasFizzled; - } +public record GameEventSpellResolved(SpellAbility spell, boolean hasFizzled) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Stack resolved " + spell + (hasFizzled ? " (fizzled)" : ""); + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventSprocketUpdate.java b/forge-game/src/main/java/forge/game/event/GameEventSprocketUpdate.java index 601ee1bafff..690f57422d4 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventSprocketUpdate.java +++ b/forge-game/src/main/java/forge/game/event/GameEventSprocketUpdate.java @@ -2,18 +2,7 @@ package forge.game.event; import forge.game.card.Card; -public class GameEventSprocketUpdate extends GameEvent { - - public final Card contraption; - public final int oldSprocket; - public final int sprocket; - - public GameEventSprocketUpdate(Card contraption, int oldSprocket, int sprocket) { - this.contraption = contraption; - this.oldSprocket = oldSprocket; - this.sprocket = sprocket; - } - +public record GameEventSprocketUpdate(Card contraption, int oldSprocket, int sprocket) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventSubgameEnd.java b/forge-game/src/main/java/forge/game/event/GameEventSubgameEnd.java index 5fcaf2a9256..89a9ed7d99a 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventSubgameEnd.java +++ b/forge-game/src/main/java/forge/game/event/GameEventSubgameEnd.java @@ -2,15 +2,7 @@ package forge.game.event; import forge.game.Game; -public class GameEventSubgameEnd extends GameEvent { - public final Game maingame; - public final String message; - - public GameEventSubgameEnd(Game game, String message0) { - maingame = game; - message = message0; - } - +public record GameEventSubgameEnd(Game maingame, String message) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); diff --git a/forge-game/src/main/java/forge/game/event/GameEventSubgameStart.java b/forge-game/src/main/java/forge/game/event/GameEventSubgameStart.java index 8d9f733a52b..eb13c72f7ca 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventSubgameStart.java +++ b/forge-game/src/main/java/forge/game/event/GameEventSubgameStart.java @@ -2,14 +2,7 @@ package forge.game.event; import forge.game.Game; -public class GameEventSubgameStart extends GameEvent { - public final Game subgame; - public final String message; - - public GameEventSubgameStart(Game subgame0, String message0) { - subgame = subgame0; - message = message0; - } +public record GameEventSubgameStart(Game subgame, String message) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventSurveil.java b/forge-game/src/main/java/forge/game/event/GameEventSurveil.java index 8ef9e8710f9..3e2f11d27f0 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventSurveil.java +++ b/forge-game/src/main/java/forge/game/event/GameEventSurveil.java @@ -2,19 +2,18 @@ package forge.game.event; import forge.game.player.Player; -public class GameEventSurveil extends GameEvent { - - public final Player player; - public final int toLibrary, toGraveyard; - - public GameEventSurveil(Player player, int toLibrary, int toGraveyard) { - this.player = player; - this.toLibrary = toLibrary; - this.toGraveyard = toGraveyard; - } +public record GameEventSurveil(Player player, int toLibrary, int toGraveyard) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "" + player + " surveilled " + toLibrary + " to library, " + toGraveyard + " to graveyard"; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventTokenCreated.java b/forge-game/src/main/java/forge/game/event/GameEventTokenCreated.java index 9cae6830b1f..d72bec6ece7 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventTokenCreated.java +++ b/forge-game/src/main/java/forge/game/event/GameEventTokenCreated.java @@ -1,9 +1,17 @@ package forge.game.event; -public class GameEventTokenCreated extends GameEvent { +public record GameEventTokenCreated() implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Token created"; + } } diff --git a/forge-game/src/main/java/forge/game/event/GameEventTurnBegan.java b/forge-game/src/main/java/forge/game/event/GameEventTurnBegan.java index 198e4c86614..eecb4281f08 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventTurnBegan.java +++ b/forge-game/src/main/java/forge/game/event/GameEventTurnBegan.java @@ -3,16 +3,7 @@ package forge.game.event; import forge.game.player.Player; import forge.util.TextUtil; -public class GameEventTurnBegan extends GameEvent { - - public final Player turnOwner; - public final int turnNumber; - - public GameEventTurnBegan(Player turnOwner, int turnNumber) { - super(); - this.turnOwner = turnOwner; - this.turnNumber = turnNumber; - } +public record GameEventTurnBegan(Player turnOwner, int turnNumber) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventTurnEnded.java b/forge-game/src/main/java/forge/game/event/GameEventTurnEnded.java index 04e3fc22c30..b9bb4ab86a2 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventTurnEnded.java +++ b/forge-game/src/main/java/forge/game/event/GameEventTurnEnded.java @@ -1,9 +1,17 @@ package forge.game.event; -public class GameEventTurnEnded extends GameEvent { +public record GameEventTurnEnded() implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { return visitor.visit(this); } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Turn ended"; + } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/event/GameEventTurnPhase.java b/forge-game/src/main/java/forge/game/event/GameEventTurnPhase.java index e0b99b5ce49..79fd40657fd 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventTurnPhase.java +++ b/forge-game/src/main/java/forge/game/event/GameEventTurnPhase.java @@ -5,20 +5,7 @@ import forge.game.player.Player; import forge.util.Lang; import forge.util.TextUtil; -/** - * TODO: Write javadoc for this type. - * - */ -public class GameEventTurnPhase extends GameEvent { - public final Player playerTurn; - public final PhaseType phase; - public final String phaseDesc; - - public GameEventTurnPhase(Player player, PhaseType ph, String desc) { - playerTurn = player; - phase = ph; - phaseDesc = desc; - } +public record GameEventTurnPhase(Player playerTurn, PhaseType phase, String phaseDesc) implements GameEvent { @Override public T visit(IGameEventVisitor visitor) { diff --git a/forge-game/src/main/java/forge/game/event/GameEventZone.java b/forge-game/src/main/java/forge/game/event/GameEventZone.java index fd9f5bd72f2..706cd2e1690 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventZone.java +++ b/forge-game/src/main/java/forge/game/event/GameEventZone.java @@ -2,31 +2,28 @@ package forge.game.event; import forge.game.card.Card; import forge.game.player.Player; +import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.Lang; import forge.util.TextUtil; -/** - * TODO: Write javadoc for this type. - * +/** + * Represents a game event related to a card or ability entering or leaving a zone. + * Stores information about the affected zone, player, card, and spell ability. + * Used for tracking zone changes such as casting, moving, or activating cards and abilities. */ -public class GameEventZone extends GameEvent { - - public final ZoneType zoneType; - public final Player player; - public final EventValueChangeType mode; - public final Card card; +public record GameEventZone(ZoneType zoneType, Player player, EventValueChangeType mode, Card card, SpellAbility sa) implements GameEvent { public GameEventZone(ZoneType zoneType, Player player, EventValueChangeType added, Card c) { - this.zoneType = zoneType; - this.player = player; - this.mode = added; - this.card = c; + this(zoneType, player, added, c, null); + } + + public GameEventZone(ZoneType zoneType, SpellAbility sa, EventValueChangeType added) { + this(zoneType, sa.getActivatingPlayer(), added, sa.getHostCard(), sa); } @Override public T visit(IGameEventVisitor visitor) { - // TODO Auto-generated method stub return visitor.visit(this); } @@ -36,10 +33,9 @@ public class GameEventZone extends GameEvent { @Override public String toString() { String owners = player == null ? "Game" : Lang.getInstance().getPossesive(player.getName()); - return card == null - ? TextUtil.concatWithSpace(owners, zoneType.toString(), ":", mode.toString()) - : TextUtil.concatWithSpace(owners, zoneType.toString(), ":", mode.toString(), card.toString() - ); + return card == null && sa == null ? + TextUtil.concatWithSpace(owners, zoneType.toString(), ":", mode.toString()) : + TextUtil.concatWithSpace(owners, zoneType.toString(), ":", mode.toString(), "" + (sa == null ? card : sa)); } } diff --git a/forge-game/src/main/java/forge/game/event/IGameEventVisitor.java b/forge-game/src/main/java/forge/game/event/IGameEventVisitor.java index f07f5bb031a..4e55f671d7f 100644 --- a/forge-game/src/main/java/forge/game/event/IGameEventVisitor.java +++ b/forge-game/src/main/java/forge/game/event/IGameEventVisitor.java @@ -1,9 +1,5 @@ package forge.game.event; -/** - * TODO: Write javadoc for this type. - * - */ public interface IGameEventVisitor { T visit(GameEventAnteCardsSelected event); T visit(GameEventAttackersDeclared event); diff --git a/forge-game/src/main/java/forge/game/keyword/Equip.java b/forge-game/src/main/java/forge/game/keyword/Equip.java index 66a5c2ad65d..bf7313943f1 100644 --- a/forge-game/src/main/java/forge/game/keyword/Equip.java +++ b/forge-game/src/main/java/forge/game/keyword/Equip.java @@ -7,6 +7,8 @@ public class Equip extends KeywordWithCost { public Equip() { } + public String getValidDescription() { return type; } + @Override protected void parse(String details) { String[] k = details.split(":"); 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 9853a510179..65b74ab7276 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -1,8 +1,8 @@ package forge.game.keyword; -import forge.StaticData; -import forge.game.card.Card; +import forge.card.CardSplitType; import forge.item.PaperCard; +import org.apache.commons.lang3.tuple.Pair; import java.util.*; @@ -119,7 +119,7 @@ public enum Keyword { LIVING_METAL("Living metal", SimpleKeyword.class, true, "During your turn, this Vehicle is also a creature."), LIVING_WEAPON("Living Weapon", SimpleKeyword.class, true, "When this Equipment enters, create a 0/0 black Phyrexian Germ creature token, then attach this to it."), MADNESS("Madness", KeywordWithCost.class, false, "If you discard this card, discard it into exile. When you do, cast it for its madness cost or put it into your graveyard."), - MAYHEM("Mayhem", KeywordWithCost.class, false, "You may cast this card from your graveyard for %s if you discarded it this turn. Timing rules still apply."), + MAYHEM("Mayhem", Mayhem.class, false, "You may cast this card from your graveyard for %s if you discarded it this turn. Timing rules still apply."), MELEE("Melee", SimpleKeyword.class, false, "Whenever this creature attacks, it gets +1/+1 until end of turn for each opponent you attacked this combat."), MENTOR("Mentor", SimpleKeyword.class, false, "Whenever this creature attacks, put a +1/+1 counter on target attacking creature with lesser power."), MENACE("Menace", SimpleKeyword.class, true, "This creature can't be blocked except by two or more creatures."), @@ -141,6 +141,9 @@ public enum Keyword { OFFSPRING("Offspring", KeywordWithCost.class, false, "You may pay an additional %s as you cast this spell. If you do, when this creature enters, create a 1/1 token copy of it."), OVERLOAD("Overload", KeywordWithCost.class, false, "You may cast this spell for its overload cost. If you do, change its text by replacing all instances of \"target\" with \"each.\""), PARTNER("Partner", Partner.class, true, "You can have two commanders if both have partner."), + PARTNER_SURVIVORS("Partner - Survivors", Partner.class, true, "You can have two commanders if both have this ability."), + PARTNER_FATHER_AND_SON("Partner - Father & Son", Partner.class, true, "You can have two commanders if both have this ability."), + PARTNER_CHARACTER_SELECT("Partner - Character select", Partner.class, true, "You can have two commanders if both have this ability."), PERSIST("Persist", SimpleKeyword.class, false, "When this creature dies, if it had no -1/-1 counters on it, return it to the battlefield under its owner's control with a -1/-1 counter on it."), PHASING("Phasing", SimpleKeyword.class, true, "This phases in or out before you untap during each of your untap steps. While it's phased out, it's treated as though it doesn't exist."), PLOT("Plot", KeywordWithCost.class, false, "You may pay %s and exile this card from your hand. Cast it as a sorcery on a later turn without paying its mana cost. Plot only as a sorcery."), @@ -223,7 +226,7 @@ public enum Keyword { displayName = displayName0; } - public static KeywordInterface getInstance(String k) { + private static Pair getKeywordDetails(String k) { Keyword keyword = Keyword.UNDEFINED; String details = k; // try to get real part @@ -255,15 +258,20 @@ public enum Keyword { keyword = smartValueOf(k); details = ""; } + return Pair.of(keyword, details); + } + + public static KeywordInterface getInstance(String k) { + Pair p = getKeywordDetails(k); KeywordInstance inst; try { - inst = keyword.type.getConstructor().newInstance(); + inst = p.getKey().type.getConstructor().newInstance(); } catch (Exception e) { inst = new UndefinedKeyword(); } - inst.initialize(k, keyword, details); + inst.initialize(k, p.getKey(), p.getValue()); return inst; } @@ -278,36 +286,44 @@ public enum Keyword { return keywords; } + private static Keyword get(String k) { + if (k == null || k.isEmpty()) + return Keyword.UNDEFINED; + + return getKeywordDetails(k).getKey(); + } + private static final Map> cardKeywordSetLookup = new HashMap<>(); public static Set getKeywordSet(PaperCard card) { - String key = card.getName(); - Set keywordSet = cardKeywordSetLookup.get(key); + String name = card.getName(); + Set keywordSet = cardKeywordSetLookup.get(name); if (keywordSet == null) { - keywordSet = new HashSet<>(); - for (KeywordInterface inst : Card.getCardForUi(card).getKeywords()) { - final Keyword keyword = inst.getKeyword(); - if (keyword != Keyword.UNDEFINED) { - keywordSet.add(keyword); + CardSplitType cardSplitType = card.getRules().getSplitType(); + keywordSet = EnumSet.noneOf(Keyword.class); + if (cardSplitType != CardSplitType.None && cardSplitType != CardSplitType.Split) { + if (card.getRules().getOtherPart() != null) { + if (card.getRules().getOtherPart().getKeywords() != null) { + for (String key : card.getRules().getOtherPart().getKeywords()) { + Keyword keyword = get(key); + if (!Keyword.UNDEFINED.equals(keyword)) + keywordSet.add(keyword); + } + } } } - cardKeywordSetLookup.put(card.getName(), keywordSet); + if (card.getRules().getMainPart().getKeywords() != null) { + for (String key : card.getRules().getMainPart().getKeywords()) { + Keyword keyword = get(key); + if (!Keyword.UNDEFINED.equals(keyword)) + keywordSet.add(keyword); + } + } + cardKeywordSetLookup.put(name, keywordSet); } return keywordSet; } - public static Runnable getPreloadTask() { - if (cardKeywordSetLookup.size() < 10000) { //allow preloading even if some but not all cards loaded - return () -> { - final Collection cards = StaticData.instance().getCommonCards().getUniqueCards(); - for (PaperCard card : cards) { - getKeywordSet(card); - } - }; - } - return null; - } - public static Keyword smartValueOf(String value) { for (final Keyword v : Keyword.values()) { if (v.displayName.equalsIgnoreCase(value)) { diff --git a/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java b/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java index a31ace72781..2bf274f1272 100644 --- a/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java +++ b/forge-game/src/main/java/forge/game/keyword/KeywordWithType.java @@ -3,38 +3,50 @@ package forge.game.keyword; import forge.card.CardType; public class KeywordWithType extends KeywordInstance { - protected String type; + protected String type = null; + protected String descType = null; + protected String reminderType = null; + + public String getValidType() { return type; } + public String getTypeDescription() { return descType; } @Override protected void parse(String details) { - if (CardType.isACardType(details)) { - type = details.toLowerCase(); - } else if (details.contains(":")) { + String k[]; + if (details.contains(":")) { switch (getKeyword()) { case AFFINITY: - type = details.split(":")[1]; - // type lists defined by rules should not be changed by TextChange in reminder text - if (type.equalsIgnoreCase("Outlaw")) { - type = "Assassin, Mercenary, Pirate, Rogue, and/or Warlock"; - } else if (type.equalsIgnoreCase("historic permanent")) { - type = "artifact, legendary, and/or Saga permanent"; - } - break; case BANDSWITH: + case ENCHANT: case HEXPROOF: case LANDWALK: - type = details.split(":")[1]; + k = details.split(":"); + type = k[0]; + descType = k[1]; break; default: - type = details.split(":")[0]; + k = details.split(":"); + type = k[1]; + descType = k[0]; } } else { - type = details; + descType = type = details; + } + + if (CardType.isACardType(descType) || "Permanent".equals(descType) || "Player".equals(descType) || "Opponent".equals(descType)) { + descType = descType.toLowerCase(); + } else if (descType.equalsIgnoreCase("Outlaw")) { + reminderType = "Assassin, Mercenary, Pirate, Rogue, and/or Warlock"; + } else if (type.equalsIgnoreCase("historic permanent")) { + reminderType = "artifact, legendary, and/or Saga permanent"; + } + if (reminderType == null) { + reminderType = type; } } @Override protected String formatReminderText(String reminderText) { - return String.format(reminderText, type); + return String.format(reminderText, reminderType); } } diff --git a/forge-game/src/main/java/forge/game/keyword/Mayhem.java b/forge-game/src/main/java/forge/game/keyword/Mayhem.java new file mode 100644 index 00000000000..ec364d649f9 --- /dev/null +++ b/forge-game/src/main/java/forge/game/keyword/Mayhem.java @@ -0,0 +1,25 @@ +package forge.game.keyword; + +import forge.card.mana.ManaCost; +import forge.game.cost.Cost; + +public class Mayhem extends KeywordWithCost { + + + @Override + protected void parse(String details) { + if (!details.isEmpty()) { + super.parse(details); + } else { + this.cost = new Cost(ManaCost.NO_COST, true); + } + } + + @Override + protected String formatReminderText(String reminderText) { + if (this.cost.getTotalMana().isNoCost()) { + return "You may play this card from your graveyard if you discarded it this turn. Timing rules still apply."; + } + return super.formatReminderText(reminderText); + } +} diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 3513eb77976..035ee178385 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -62,6 +62,9 @@ import java.util.*; public class PhaseHandler implements java.io.Serializable { private static final long serialVersionUID = 5207222278370963197L; + // used for debugging phase timing + private final StopWatch sw = new StopWatch(); + // Start turn at 0, since we start even before first untap private PhaseType phase = null; private int turn = 0; @@ -92,6 +95,7 @@ public class PhaseHandler implements java.io.Serializable { private final transient Game game; + public PhaseHandler(final Game game0) { game = game0; } @@ -1003,12 +1007,7 @@ public class PhaseHandler implements java.io.Serializable { private final static boolean DEBUG_PHASES = false; - public void startFirstTurn(Player goesFirst) { - startFirstTurn(goesFirst, null); - } - public void startFirstTurn(Player goesFirst, Runnable startGameHook) { - StopWatch sw = new StopWatch(); - + public void setupFirstTurn(Player goesFirst, Runnable startGameHook) { if (phase != null) { throw new IllegalStateException("Turns already started, call this only once per game"); } @@ -1024,132 +1023,146 @@ public class PhaseHandler implements java.io.Serializable { startGameHook.run(); givePriorityToPlayer = true; } + } + public void startFirstTurn(Player goesFirst) { + startFirstTurn(goesFirst, null); + } + public void startFirstTurn(Player goesFirst, Runnable startGameHook) { + setupFirstTurn(goesFirst, startGameHook); + mainGameLoop(); + } + + public void mainGameLoop() { // MAIN GAME LOOP - while (!game.isGameOver()) { - if (givePriorityToPlayer) { - if (DEBUG_PHASES) { - sw.start(); - } + while (!game.isGameOver() && !(game.getAge() == GameStage.RestartedByKarn)) { + mainLoopStep(); + } + } - game.fireEvent(new GameEventPlayerPriority(playerTurn, phase, getPriorityPlayer())); - List chosenSa = null; - - int loopCount = 0; - do { - if (checkStateBasedEffects()) { - // state-based effects check could lead to game over - return; - } - game.stashGameState(); - - chosenSa = pPlayerPriority.getController().chooseSpellAbilityToPlay(); - - // this needs to come after chosenSa so it sees you conceding on own turn - if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) { - // If the active player has lost, and they have priority, set the next player to have priority - System.out.println("Active player is no longer in the game..."); - pPlayerPriority = game.getNextPlayerAfter(getPriorityPlayer()); - pFirstPriority = pPlayerPriority; - } - - if (chosenSa == null) { - break; // that means 'I pass' - } - if (DEBUG_PHASES) { - System.out.print("... " + pPlayerPriority + " plays " + chosenSa); - } - - boolean rollback = false; - for (SpellAbility sa : chosenSa) { - Card saHost = sa.getHostCard(); - final Zone originZone = saHost.getZone(); - final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard()); - - if (pPlayerPriority.getController().playChosenSpellAbility(sa)) { - // 117.3c If a player has priority when they cast a spell, activate an ability, [play a land] - // that player receives priority afterward. - pFirstPriority = pPlayerPriority; // all opponents have to pass before stack is allowed to resolve - } else if (game.EXPERIMENTAL_RESTORE_SNAPSHOT) { - rollback = true; - } - - saHost = game.getCardState(saHost); - final Zone currentZone = saHost.getZone(); - - // Need to check if Zone did change - if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa.isLandAbility())) { - // currently there can be only one Spell put on the Stack at once, or Land Abilities be played - triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost); - triggerList.triggerChangesZoneAll(game, sa); - } - } - // Don't copy last state if we're in the middle of rolling back a spell... - if (!rollback) { - game.copyLastState(); - } - loopCount++; - } while (loopCount < 999 || !pPlayerPriority.getController().isAI()); - - if (loopCount >= 999 && pPlayerPriority.getController().isAI()) { - System.out.print("AI looped too much with: " + chosenSa); - } - - if (DEBUG_PHASES) { - sw.stop(); - System.out.print("... passed in " + sw.getTime()/1000f + " s\n"); - System.out.println("\t\tStack: " + game.getStack()); - sw.reset(); - } - } - else if (DEBUG_PHASES) { - System.out.print(" >> (no priority given to " + getPriorityPlayer() + ")\n"); + public void mainLoopStep() { + if (givePriorityToPlayer) { + if (DEBUG_PHASES) { + sw.start(); } - // actingPlayer is the player who may act - // the firstAction is the player who gained Priority First in this segment - // of Priority - Player nextPlayer = game.getNextPlayerAfter(getPriorityPlayer()); + game.fireEvent(new GameEventPlayerPriority(playerTurn, phase, getPriorityPlayer())); + List chosenSa = null; - if (game.isGameOver() || nextPlayer == null) { return; } // conceded? + int loopCount = 0; + do { + if (checkStateBasedEffects()) { + // state-based effects check could lead to game over + return; + } + game.stashGameState(); + + chosenSa = pPlayerPriority.getController().chooseSpellAbilityToPlay(); + + // this needs to come after chosenSa so it sees you conceding on own turn + if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) { + // If the active player has lost, and they have priority, set the next player to have priority + System.out.println("Active player is no longer in the game..."); + pPlayerPriority = game.getNextPlayerAfter(getPriorityPlayer()); + pFirstPriority = pPlayerPriority; + } + + if (chosenSa == null) { + break; // that means 'I pass' + } + if (DEBUG_PHASES) { + System.out.print("... " + pPlayerPriority + " plays " + chosenSa); + } + + boolean rollback = false; + for (SpellAbility sa : chosenSa) { + Card saHost = sa.getHostCard(); + final Zone originZone = saHost.getZone(); + final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard()); + + if (pPlayerPriority.getController().playChosenSpellAbility(sa)) { + // 117.3c If a player has priority when they cast a spell, activate an ability, [play a land] + // that player receives priority afterward. + pFirstPriority = pPlayerPriority; // all opponents have to pass before stack is allowed to resolve + } else if (game.EXPERIMENTAL_RESTORE_SNAPSHOT) { + rollback = true; + } + + saHost = game.getCardState(saHost); + final Zone currentZone = saHost.getZone(); + + // Need to check if Zone did change + if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa.isLandAbility())) { + // currently there can be only one Spell put on the Stack at once, or Land Abilities be played + triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost); + triggerList.triggerChangesZoneAll(game, sa); + } + } + // Don't copy last state if we're in the middle of rolling back a spell... + if (!rollback) { + game.copyLastState(); + } + loopCount++; + } while (loopCount < 999 || !pPlayerPriority.getController().isAI()); + + if (loopCount >= 999 && pPlayerPriority.getController().isAI()) { + System.out.print("AI looped too much with: " + chosenSa); + } if (DEBUG_PHASES) { - System.out.println(TextUtil.concatWithSpace(playerTurn.toString(),TextUtil.addSuffix(phase.toString(),":"), pPlayerPriority.toString(),"is active, previous was", nextPlayer.toString())); + sw.stop(); + System.out.print("... passed in " + sw.getTime()/1000f + " s\n"); + System.out.println("\t\tStack: " + game.getStack()); + sw.reset(); } - if (pFirstPriority == nextPlayer) { - if (game.getStack().isEmpty()) { - if (playerTurn.hasLost()) { - setPriority(game.getNextPlayerAfter(playerTurn)); - } else { - setPriority(playerTurn); - } + } + else if (DEBUG_PHASES) { + System.out.print(" >> (no priority given to " + getPriorityPlayer() + ")\n"); + } - // end phase - givePriorityToPlayer = true; - onPhaseEnd(); - advanceToNextPhase(); - onPhaseBegin(); + // actingPlayer is the player who may act + // the firstAction is the player who gained Priority First in this segment + // of Priority + Player nextPlayer = game.getNextPlayerAfter(getPriorityPlayer()); + + if (game.isGameOver() || nextPlayer == null) { return; } // conceded? + + if (DEBUG_PHASES) { + System.out.println(TextUtil.concatWithSpace(playerTurn.toString(),TextUtil.addSuffix(phase.toString(),":"), pPlayerPriority.toString(),"is active, previous was", nextPlayer.toString())); + } + if (pFirstPriority == nextPlayer) { + if (game.getStack().isEmpty()) { + if (playerTurn.hasLost()) { + setPriority(game.getNextPlayerAfter(playerTurn)); + } else { + setPriority(playerTurn); } - else if (!game.getStack().hasSimultaneousStackEntries()) { - game.getStack().resolveStack(); - } - } else { - // pass the priority to other player - pPlayerPriority = nextPlayer; - } - // If ever the karn's ultimate resolved - if (game.getAge() == GameStage.RestartedByKarn) { - setPhase(null); - game.updatePhaseForView(); - game.fireEvent(new GameEventGameRestarted(playerTurn)); - return; + // end phase + givePriorityToPlayer = true; + onPhaseEnd(); + advanceToNextPhase(); + onPhaseBegin(); } + else if (!game.getStack().hasSimultaneousStackEntries()) { + game.getStack().resolveStack(); + } + } else { + // pass the priority to other player + pPlayerPriority = nextPlayer; + } - // update Priority for all players - for (final Player p : game.getPlayers()) { - p.setHasPriority(getPriorityPlayer() == p); - } + // If ever the karn's ultimate resolved + if (game.getAge() == GameStage.RestartedByKarn) { + setPhase(null); + game.updatePhaseForView(); + game.fireEvent(new GameEventGameRestarted(playerTurn)); + return; + } + + // update Priority for all players + for (final Player p : game.getPlayers()) { + p.setHasPriority(getPriorityPlayer() == p); } } diff --git a/forge-game/src/main/java/forge/game/player/AchievementTracker.java b/forge-game/src/main/java/forge/game/player/AchievementTracker.java index a597642ff08..457c75451a0 100644 --- a/forge-game/src/main/java/forge/game/player/AchievementTracker.java +++ b/forge-game/src/main/java/forge/game/player/AchievementTracker.java @@ -27,7 +27,7 @@ public class AchievementTracker { activatedNonPWUltimates.add(card.getName()); } } - if (card.getColor().equals(ColorSet.ALL_COLORS)) { + if (card.getColor().equals(ColorSet.WUBRG)) { challengesCompleted.add("Chromatic"); } } 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 e64dbb1c10f..60525c5a051 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -44,6 +44,7 @@ import forge.game.replacement.ReplacementResult; import forge.game.replacement.ReplacementType; import forge.game.spellability.AbilitySub; +import forge.game.spellability.AlternativeCost; import forge.game.spellability.SpellAbility; import forge.game.staticability.*; import forge.game.trigger.Trigger; @@ -454,7 +455,6 @@ public class Player extends GameEntity implements Comparable { return false; } - // Run any applicable replacement effects. final Map repParams = AbilityKey.mapFromAffected(this); repParams.put(AbilityKey.LifeGained, lifeGain); repParams.put(AbilityKey.SourceSA, sa); @@ -520,7 +520,7 @@ public class Player extends GameEntity implements Comparable { return 0; } int oldLife = life; - // Run applicable replacement effects + final Map repParams = AbilityKey.mapFromAffected(this); repParams.put(AbilityKey.Amount, toLose); repParams.put(AbilityKey.IsDamage, damage); @@ -547,13 +547,12 @@ public class Player extends GameEntity implements Comparable { life -= toLose; view.updateLife(this); if (manaBurn) { - game.fireEvent(new GameEventManaBurn(this, toLose, true)); + game.fireEvent(new GameEventManaBurn(this, true, toLose)); } else { game.fireEvent(new GameEventPlayerLivesChanged(this, oldLife, life)); } boolean firstLost = lifeLostThisTurn == 0; - lifeLostThisTurn += toLose; final Map runParams = AbilityKey.mapFromPlayer(this); @@ -578,9 +577,6 @@ public class Player extends GameEntity implements Comparable { } public final boolean payLife(final int lifePayment, final SpellAbility cause, final boolean effect) { - return payLife(lifePayment, cause, effect, null); - } - public final boolean payLife(final int lifePayment, final SpellAbility cause, final boolean effect, Map params) { // fast check for pay zero life if (lifePayment <= 0) { cause.setPaidLife(0); @@ -600,9 +596,6 @@ public class Player extends GameEntity implements Comparable { if (cause.isReplacementAbility() && effect) { replaceParams.putAll(cause.getReplacingObjects()); } - if (params != null) { - replaceParams.putAll(params); - } switch (getGame().getReplacementHandler().run(ReplacementType.PayLife, replaceParams)) { case Replaced: return true; @@ -944,10 +937,6 @@ public class Player extends GameEntity implements Comparable { getGame().fireEvent(new GameEventPlayerCounters(this, null, 0, 0)); } - public void setCounters(final CounterEnumType counterType, final Integer num, Player source, boolean fireEvents) { - this.setCounters(CounterType.get(counterType), num, source, fireEvents); - } - public void setCounters(final CounterType counterType, final Integer num, Player source, boolean fireEvents) { int old = getCounters(counterType); setCounters(counterType, num); @@ -973,7 +962,7 @@ public class Player extends GameEntity implements Comparable { getGame().fireEvent(new GameEventPlayerCounters(this, null, 0, 0)); // create Radiation Effect for GameState - if (counters.getOrDefault(CounterType.get(CounterEnumType.RAD), 0) > 0) { + if (counters.getOrDefault(CounterEnumType.RAD, 0) > 0) { this.createRadiationEffect(null); } else { this.removeRadiationEffect(); @@ -1089,7 +1078,7 @@ public class Player extends GameEntity implements Comparable { } // CantTarget static abilities - if (StaticAbilityCantTarget.cantTarget(this, sa)) { + if (StaticAbilityCantTarget.cantTarget(this, sa) != null) { return false; } @@ -1131,13 +1120,14 @@ public class Player extends GameEntity implements Comparable { getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave)); } - surveilThisTurn++; final Map runParams = AbilityKey.mapFromPlayer(this); - runParams.put(AbilityKey.FirstTime, surveilThisTurn == 1); + runParams.put(AbilityKey.FirstTime, surveilThisTurn == 0); if (params != null) { runParams.putAll(params); } getGame().getTriggerHandler().runTrigger(TriggerType.Surveil, runParams, false); + + surveilThisTurn++; } public int getSurveilThisTurn() { @@ -1722,7 +1712,8 @@ public class Player extends GameEntity implements Comparable { } final Zone zone = game.getZoneOf(land); - if (zone != null && (zone.is(ZoneType.Battlefield) || (!zone.is(ZoneType.Hand) && !mayPlay))) { + if (zone != null && (zone.is(ZoneType.Battlefield) || (!zone.is(ZoneType.Hand) && !mayPlay + && (landSa == null || !landSa.isAlternativeCost(AlternativeCost.Mayhem))))) { return false; } } @@ -1970,10 +1961,11 @@ public class Player extends GameEntity implements Comparable { CardState speedFront = speedEffect.getState(CardStateName.Original); CardState speedBack = speedEffect.getState(CardStateName.Backside); - speedFront.setImageKey("t:speed"); + + speedFront.setImageKey(StaticData.instance().getOtherImageKey(ImageKeys.SPEED_IMAGE, CardEdition.UNKNOWN_CODE)); speedFront.setName("Start Your Engines!"); - speedBack.setImageKey("t:max_speed"); + speedBack.setImageKey(StaticData.instance().getOtherImageKey(ImageKeys.MAX_SPEED_IMAGE, CardEdition.UNKNOWN_CODE)); speedBack.setName("Max Speed!"); String label = Localizer.getInstance().getMessage("lblSpeed", this.speed); @@ -2321,7 +2313,7 @@ public class Player extends GameEntity implements Comparable { public final void addSacrificedThisTurn(final Card cpy, final SpellAbility source) { // Play the Sacrifice sound - game.fireEvent(new GameEventCardSacrificed()); + game.fireEvent(new GameEventCardSacrificed(cpy)); sacrificedThisTurn.add(cpy); @@ -2349,9 +2341,6 @@ public class Player extends GameEntity implements Comparable { public final void resetSpellCastSinceBegOfYourLastTurn() { spellsCastSinceBeginningOfLastTurn = Lists.newArrayList(); } - public final void setSpellCastSinceBegOfYourLastTurn(List spells) { - spellsCastSinceBeginningOfLastTurn = new ArrayList<>(spells); - } public final void addSpellCastSinceBegOfYourLastTurn(List spells) { spellsCastSinceBeginningOfLastTurn.addAll(spells); } diff --git a/forge-game/src/main/java/forge/game/player/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index c44f84e8470..b6eff922da9 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -287,6 +287,8 @@ public abstract class PlayerController { public abstract void revealAnte(String message, Multimap removedAnteCards); public abstract void revealAISkipCards(String message, Map>> deckCards); + public abstract void revealUnsupported(Map> unsupported); + // These 2 are for AI public CardCollectionView cheatShuffle(CardCollectionView list) { return list; } public Map> complainCardsCantPlayWell(Deck myDeck) { return null; } diff --git a/forge-game/src/main/java/forge/game/player/PlayerFactoryUtil.java b/forge-game/src/main/java/forge/game/player/PlayerFactoryUtil.java index d5d0cc00e1b..34933815924 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/player/PlayerFactoryUtil.java @@ -23,21 +23,21 @@ public class PlayerFactoryUtil { sbValid.append("| ValidSource$ ").append(k[1]); } - String effect = "Mode$ CantTarget | ValidPlayer$ Player.You | Secondary$ True " + String effect = "Mode$ CantTarget | ValidTarget$ Player.You | Secondary$ True " + sbValid.toString() + " | Activator$ Opponent | EffectZone$ Command | Description$ " + sbDesc.toString() + " (" + inst.getReminderText() + ")"; final Card card = player.getKeywordCard(); inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false)); } else if (keyword.equals("Shroud")) { - String effect = "Mode$ CantTarget | ValidPlayer$ Player.You | Secondary$ True " + String effect = "Mode$ CantTarget | ValidTarget$ Player.You | Secondary$ True " + "| EffectZone$ Command | Description$ Shroud (" + inst.getReminderText() + ")"; final Card card = player.getKeywordCard(); inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false)); } else if (keyword.startsWith("Protection")) { String valid = CardFactoryUtil.getProtectionValid(keyword, false); - String effect = "Mode$ CantTarget | Protection$ True | ValidPlayer$ Player.You | EffectZone$ Command | Secondary$ True "; + String effect = "Mode$ CantTarget | ValidTarget$ Player.You | EffectZone$ Command | Secondary$ True "; if (!valid.isEmpty()) { effect += "| ValidSource$ " + valid; } @@ -45,7 +45,7 @@ public class PlayerFactoryUtil { inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false)); // Attach - effect = "Mode$ CantAttach | Protection$ True | Target$ Player.You | EffectZone$ Command | Secondary$ True "; + effect = "Mode$ CantAttach | Target$ Player.You | EffectZone$ Command | Secondary$ True "; if (!valid.isEmpty()) { effect += "| ValidCard$ " + valid; } diff --git a/forge-game/src/main/java/forge/game/player/PlayerPredicates.java b/forge-game/src/main/java/forge/game/player/PlayerPredicates.java index 3bd1fbc3f35..d9535bcfd95 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerPredicates.java +++ b/forge-game/src/main/java/forge/game/player/PlayerPredicates.java @@ -7,7 +7,6 @@ import forge.game.CardTraitBase; import forge.game.GameEntity; import forge.game.card.Card; import forge.game.card.CardLists; -import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; @@ -57,14 +56,6 @@ public final class PlayerPredicates { public static Predicate hasCounter(final CounterType type, final int n) { return p -> p.getCounters(type) >= n; } - - public static Predicate hasCounter(final CounterEnumType type) { - return hasCounter(CounterType.get(type), 1); - } - - public static Predicate hasCounter(final CounterEnumType type, final int n) { - return hasCounter(CounterType.get(type), n); - } public static Predicate hasKeyword(final String keyword) { return p -> p.hasKeyword(keyword); diff --git a/forge-game/src/main/java/forge/game/player/PlayerView.java b/forge-game/src/main/java/forge/game/player/PlayerView.java index d7130dab99a..b13063f585f 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerView.java +++ b/forge-game/src/main/java/forge/game/player/PlayerView.java @@ -12,7 +12,6 @@ import forge.card.mana.ManaAtom; import forge.game.GameEntityView; import forge.game.card.Card; import forge.game.card.CardView; -import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.zone.PlayerZone; import forge.game.zone.ZoneType; @@ -192,9 +191,6 @@ public class PlayerView extends GameEntityView { } return 0; } - public int getCounters(CounterEnumType counterType) { - return getCounters(CounterType.get(counterType)); - } void updateCounters(Player p) { set(TrackableProperty.Counters, p.getCounters()); } diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java index f81dbb8f47d..a1c82f30a5e 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java @@ -242,7 +242,7 @@ public abstract class ReplacementEffect extends TriggerReplacementBase { String desc = AbilityUtils.applyDescriptionTextChangeEffects(getParam("Description"), this); ITranslatable nameSource = getHostName(this); desc = CardTranslation.translateMultipleDescriptionText(desc, nameSource); - String translatedName = CardTranslation.getTranslatedName(nameSource); + String translatedName = nameSource.getTranslatedName(); desc = TextUtil.fastReplace(desc, "CARDNAME", translatedName); desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(translatedName)); if (desc.contains("EFFECTSOURCE")) { diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java index afd07235a24..ac7912a1f44 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java @@ -280,15 +280,8 @@ public class ReplacementHandler { host = game.getCardState(host); } - if (replacementEffect.getOverridingAbility() == null && replacementEffect.hasParam("ReplaceWith")) { - // TODO: the source of replacement effect should be the source of the original effect - effectSA = AbilityFactory.getAbility(host, replacementEffect.getParam("ReplaceWith"), replacementEffect); - //replacementEffect.setOverridingAbility(effectSA); - //effectSA.setTrigger(true); - } else if (replacementEffect.getOverridingAbility() != null) { - effectSA = replacementEffect.getOverridingAbility(); - } - + // TODO: the source of replacement effect should be the source of the original effect + effectSA = replacementEffect.ensureAbility(); if (effectSA != null) { SpellAbility tailend = effectSA; do { 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 ce10ab07de9..fcdc1b77f54 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java @@ -92,7 +92,7 @@ public abstract class AbilityActivated extends SpellAbility implements Cloneable return false; } - if (!(this.getRestrictions().canPlay(c, this))) { + if (!getRestrictions().canPlay(c, this)) { return false; } diff --git a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java index 005fed45f76..fd1b57c2969 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilityManaPart.java @@ -17,7 +17,6 @@ */ package forge.game.spellability; -import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import forge.card.ColorSet; import forge.card.GamePieceType; @@ -49,9 +48,9 @@ import forge.game.zone.ZoneType; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; -import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** *

@@ -658,8 +657,8 @@ public class AbilityManaPart implements java.io.Serializable { } // replace Chosen for Spire colors if (origProduced.contains("ColorID")) { - Iterator colors = Iterators.transform(sa.getHostCard().getMarkedColors().iterator(), MagicColor::toLongString); - origProduced = origProduced.replace("ColorID", getChosenColor(sa, () -> colors)); + String str = sa.getHostCard().getMarkedColors().stream().map(c -> c.getShortName()).collect(Collectors.joining(" ")); + origProduced = origProduced.replace("ColorID", str); } if (origProduced.contains("NotedColors")) { // Should only be used for Paliano, the High City 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 12cc7fb1b68..f404a3bb148 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -174,6 +174,8 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private CardZoneTable changeZoneTable; private Map loseLifeMap; + private String name = ""; + public CardCollection getLastStateBattlefield() { return lastStateBattlefield; } @@ -354,7 +356,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public int totalAmountOfManaGenerated(SpellAbility saPaidFor, boolean multiply) { int result = 0; AbilityManaPart mp = getManaPart(); - if (mp != null && metConditions() && mp.meetsManaRestrictions(saPaidFor)) { + if (mp != null && mp.meetsManaRestrictions(saPaidFor)) { result += amountOfManaGenerated(multiply); } result += subAbility != null ? subAbility.totalAmountOfManaGenerated(saPaidFor, multiply) : 0; @@ -671,6 +673,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public final boolean isMadness() { return isAlternativeCost(AlternativeCost.Madness); } + + public final boolean isMayhem() { + return isAlternativeCost(AlternativeCost.Mayhem); + } public final boolean isMutate() { return isAlternativeCost(AlternativeCost.Mutate); @@ -1115,7 +1121,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit if (node.getHostCard() != null && !desc.isEmpty()) { ITranslatable nameSource = getHostName(node); desc = CardTranslation.translateMultipleDescriptionText(desc, nameSource); - String translatedName = CardTranslation.getTranslatedName(nameSource); + String translatedName = nameSource.getTranslatedName(); desc = TextUtil.fastReplace(desc, "CARDNAME", translatedName); desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(translatedName)); if (node.getOriginalHost() != null) { @@ -1242,6 +1248,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit clone.mayChooseNewTargets = false; clone.triggeringObjects = AbilityKey.newMap(this.triggeringObjects); + if (!lki) { + clone.replacingObjects = AbilityKey.newMap(); + } clone.setPayCosts(getPayCosts().copy()); if (manaPart != null) { @@ -1500,6 +1509,22 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit } } + if (tr.isDifferentCMC() && entity instanceof Card) { + for (final Card c : getTargets().getTargetCards()) { + if (entity != c && c.getCMC() == (((Card) entity).getCMC())) { + return false; + } + } + } + + if (tr.isDifferentNames() && entity instanceof Card) { + for (final Card c : getTargets().getTargetCards()) { + if (entity != c && c.sharesNameWith(((Card) entity).getName())) { + return false; + } + } + } + if (tr.isSameController() && entity instanceof Card) { Player newController; newController = ((Card) entity).getController(); @@ -2655,4 +2680,12 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void clearOptionalKeywordAmount() { optionalKeywordAmount.clear(); } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } diff --git a/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java b/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java index 0c5f6209adc..5f1713723d5 100644 --- a/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java +++ b/forge-game/src/main/java/forge/game/spellability/TargetRestrictions.java @@ -55,10 +55,10 @@ public class TargetRestrictions { // Additional restrictions that may not fit into Valid private boolean uniqueTargets = false; - private boolean singleZone = false; private boolean forEachPlayer = false; private boolean differentControllers = false; private boolean differentCMC = false; + private boolean differentNames = false; private boolean equalToughness = false; private boolean sameController = false; private boolean withoutSameCreatureType = false; @@ -99,7 +99,6 @@ public class TargetRestrictions { this.tgtZone = target.getZone(); this.saValidTargeting = target.getSAValidTargeting(); this.uniqueTargets = target.isUniqueTargets(); - this.singleZone = target.isSingleZone(); this.forEachPlayer = target.isForEachPlayer(); this.differentControllers = target.isDifferentControllers(); this.differentCMC = target.isDifferentCMC(); @@ -537,12 +536,6 @@ public class TargetRestrictions { public final void setUniqueTargets(final boolean unique) { this.uniqueTargets = unique; } - public final boolean isSingleZone() { - return this.singleZone; - } - public final void setSingleZone(final boolean single) { - this.singleZone = single; - } public boolean isWithoutSameCreatureType() { return withoutSameCreatureType; } @@ -621,6 +614,13 @@ public class TargetRestrictions { this.differentCMC = different; } + public boolean isDifferentNames() { + return differentNames; + } + public void setDifferentNames(boolean different) { + this.differentNames = different; + } + /** * @return the equalToughness */ diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java index 957f10a494f..1d362b75a98 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbility.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbility.java @@ -206,7 +206,7 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone if (hasParam("Description") && !this.isSuppressed()) { ITranslatable nameSource = getHostName(this); String desc = CardTranslation.translateSingleDescriptionText(getParam("Description"), nameSource); - String translatedName = CardTranslation.getTranslatedName(nameSource); + String translatedName = nameSource.getTranslatedName(); desc = TextUtil.fastReplace(desc, "CARDNAME", translatedName); desc = TextUtil.fastReplace(desc, "NICKNAME", Lang.getInstance().getNickName(translatedName)); diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java index 8b2ba2bda27..70704f3ac95 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttach.java @@ -6,7 +6,7 @@ import forge.game.zone.ZoneType; public class StaticAbilityCantAttach { - public static boolean cantAttach(final GameEntity target, final Card card, boolean checkSBA) { + public static StaticAbility cantAttach(final GameEntity target, final Card card, boolean checkSBA) { // CantTarget static abilities for (final Card ca : target.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { for (final StaticAbility stAb : ca.getStaticAbilities()) { @@ -15,11 +15,11 @@ public class StaticAbilityCantAttach { } if (applyCantAttachAbility(stAb, card, target, checkSBA)) { - return true; + return stAb; } } } - return false; + return null; } public static boolean applyCantAttachAbility(final StaticAbility stAb, final Card card, final GameEntity target, boolean checkSBA) { diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantTarget.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantTarget.java index ec214157f55..f96c20095bb 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantTarget.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantTarget.java @@ -39,36 +39,20 @@ public class StaticAbilityCantTarget { static String MODE = "CantTarget"; - public static boolean cantTarget(final Card card, final SpellAbility spellAbility) { - final Game game = card.getGame(); + public static StaticAbility cantTarget(final GameEntity entity, final SpellAbility spellAbility) { + final Game game = entity.getGame(); for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { for (final StaticAbility stAb : ca.getStaticAbilities()) { if (!stAb.checkConditions(StaticAbilityMode.CantTarget)) { continue; } - if (applyCantTargetAbility(stAb, card, spellAbility)) { - return true; + if (applyCantTargetAbility(stAb, entity, spellAbility)) { + return stAb; } } } - return false; - } - - public static boolean cantTarget(final Player player, final SpellAbility spellAbility) { - final Game game = player.getGame(); - for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { - for (final StaticAbility stAb : ca.getStaticAbilities()) { - if (!stAb.checkConditions(StaticAbilityMode.CantTarget)) { - continue; - } - - if (applyCantTargetAbility(stAb, player, spellAbility)) { - return true; - } - } - } - return false; + return null; } /** @@ -82,57 +66,27 @@ public class StaticAbilityCantTarget { * the spell/ability * @return true, if successful */ - public static boolean applyCantTargetAbility(final StaticAbility stAb, final Card card, final SpellAbility spellAbility) { - if (stAb.hasParam("ValidPlayer")) { - return false; - } + public static boolean applyCantTargetAbility(final StaticAbility stAb, final GameEntity entity, final SpellAbility spellAbility) { + if (entity instanceof Card card) { + if (stAb.hasParam("AffectedZone")) { + if (ZoneType.listValueOf(stAb.getParam("AffectedZone")).stream().noneMatch(zt -> card.isInZone(zt))) { + return false; + } + } else if (!card.isInPlay()) { // default zone is battlefield + return false; + } + Set zones = stAb.getActiveZone(); - if (stAb.hasParam("AffectedZone")) { - boolean inZone = false; - for (final ZoneType zt : ZoneType.listValueOf(stAb.getParam("AffectedZone"))) { - if (card.isInZone(zt)) { - inZone = true; - break; + if (zones != null && zones.contains(ZoneType.Stack)) { + // Enthralling Hold: only works if it wasn't already cast + if (card.getGame().getStack().getSpellMatchingHost(spellAbility.getHostCard()) != null) { + return false; } } - - if (!inZone) { - return false; - } - } else { // default zone is battlefield - if (!card.isInPlay()) { - return false; - } - } - Set zones = stAb.getActiveZone(); - - if (zones != null && zones.contains(ZoneType.Stack)) { - // Enthralling Hold: only works if it wasn't already cast - if (card.getGame().getStack().getSpellMatchingHost(spellAbility.getHostCard()) != null) { - return false; - } - } - - if (!stAb.matchesValidParam("ValidCard", card)) { + } else if (stAb.hasParam("AffectedZone")) { return false; } - return common(stAb, card, spellAbility); - } - - public static boolean applyCantTargetAbility(final StaticAbility stAb, final Player player, final SpellAbility spellAbility) { - if (stAb.hasParam("ValidCard") || stAb.hasParam("AffectedZone")) { - return false; - } - - if (!stAb.matchesValidParam("ValidPlayer", player)) { - return false; - } - - return common(stAb, player, spellAbility); - } - - protected static boolean common(final StaticAbility stAb, GameEntity entity, final SpellAbility spellAbility) { final Card source = spellAbility.getHostCard(); final Player activator = spellAbility.getActivatingPlayer(); @@ -140,6 +94,10 @@ public class StaticAbilityCantTarget { return false; } + if (!stAb.matchesValidParam("ValidTarget", entity)) { + return false; + } + if (!stAb.matchesValidParam("ValidSA", spellAbility)) { return false; } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java index 64c755f6810..bd394802db2 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityContinuous.java @@ -170,9 +170,11 @@ public final class StaticAbilityContinuous { addKeywords = Lists.newArrayList(Arrays.asList(params.get("AddKeyword").split(" & "))); final List newKeywords = Lists.newArrayList(); - // update keywords with Chosen parts - final String hostCardUID = Integer.toString(hostCard.getId()); // Protection with "doesn't remove" effect + // Protection with "doesn't remove" effect + final String hostCardUID = Integer.toString(hostCard.getId()); + final String hostCardControllerUID = Integer.toString(hostCard.getController().getId()); + // update keywords with Chosen parts addKeywords.removeIf(input -> { if (!hostCard.hasChosenColor() && input.contains("ChosenColor")) { return true; @@ -205,12 +207,12 @@ public final class StaticAbilityContinuous { if (input.contains("CommanderColorID")) { if (!hostCard.getController().getCommanders().isEmpty()) { if (input.contains("NotCommanderColorID")) { - for (Byte color : hostCard.getController().getNotCommanderColorID()) { - newKeywords.add(input.replace("NotCommanderColorID", MagicColor.toLongString(color))); + for (MagicColor.Color color : hostCard.getController().getNotCommanderColorID()) { + newKeywords.add(input.replace("NotCommanderColorID", color.getName())); } return true; - } else for (Byte color : hostCard.getController().getCommanderColorID()) { - newKeywords.add(input.replace("CommanderColorID", MagicColor.toLongString(color))); + } else for (MagicColor.Color color : hostCard.getController().getCommanderColorID()) { + newKeywords.add(input.replace("CommanderColorID", color.getName())); } return true; } @@ -218,12 +220,9 @@ public final class StaticAbilityContinuous { } // two variants for Red vs. red in keyword if (input.contains("ColorsYouCtrl") || input.contains("colorsYouCtrl")) { - final ColorSet colorsYouCtrl = CardUtil.getColorsFromCards(controller.getCardsIn(ZoneType.Battlefield)); - - for (byte color : colorsYouCtrl) { - final String colorWord = MagicColor.toLongString(color); - String y = input.replaceAll("ColorsYouCtrl", StringUtils.capitalize(colorWord)); - y = y.replaceAll("colorsYouCtrl", colorWord); + for (MagicColor.Color color : CardUtil.getColorsFromCards(controller.getCardsIn(ZoneType.Battlefield))) { + String y = input.replaceAll("ColorsYouCtrl", StringUtils.capitalize(color.getName())); + y = y.replaceAll("colorsYouCtrl", color.getName()); newKeywords.add(y); } return true; @@ -285,6 +284,7 @@ public final class StaticAbilityContinuous { input = input.replaceAll("chosenEvenOdd", hostCard.getChosenEvenOdd().toString().toLowerCase()); } input = input.replace("HostCardUID", hostCardUID); + input = input.replace("HostCardControllerUID", hostCardControllerUID); if (params.containsKey("CalcKeywordN")) { input = input.replace("N", String.valueOf(AbilityUtils.calculateAmount(hostCard, params.get("CalcKeywordN"), stAb))); } @@ -623,7 +623,7 @@ public final class StaticAbilityContinuous { // Mana cost affectedCard.addChangedManaCost(state.getManaCost(), se.getTimestamp(), stAb.getId()); // color - affectedCard.addColorByText(ColorSet.fromMask(state.getColor()), se.getTimestamp(), stAb.getId()); + affectedCard.addColorByText(ColorSet.fromMask(state.getColor()), se.getTimestamp(), stAb); // type affectedCard.addChangedCardTypesByText(new CardType(state.getType()), se.getTimestamp(), stAb.getId()); // abilities @@ -706,8 +706,8 @@ public final class StaticAbilityContinuous { newKeywords.removeIf(input -> { // replace one Keyword with list of keywords if (input.startsWith("Protection") && input.contains("CardColors")) { - for (Byte color : affectedCard.getColor()) { - extraKeywords.add(input.replace("CardColors", MagicColor.toLongString(color))); + for (MagicColor.Color color : affectedCard.getColor()) { + extraKeywords.add(input.replace("CardColors", color.getName())); } return true; } @@ -856,7 +856,7 @@ public final class StaticAbilityContinuous { // add colors if (addColors != null) { - affectedCard.addColor(addColors, !overwriteColors, se.getTimestamp(), stAb.getId(), stAb.isCharacteristicDefining()); + affectedCard.addColor(addColors, !overwriteColors, se.getTimestamp(), stAb); } if (layer == StaticAbilityLayer.RULES) { @@ -921,7 +921,7 @@ public final class StaticAbilityContinuous { addColors = ColorSet.fromNames(hostCard.getChosenColors()); } } else if (colors.equals("All")) { - addColors = ColorSet.ALL_COLORS; + addColors = ColorSet.WUBRG; } else { addColors = ColorSet.fromNames(colors.split(" & ")); } diff --git a/forge-game/src/main/java/forge/game/trigger/Trigger.java b/forge-game/src/main/java/forge/game/trigger/Trigger.java index dbcd5ee48de..21e3ce718ac 100644 --- a/forge-game/src/main/java/forge/game/trigger/Trigger.java +++ b/forge-game/src/main/java/forge/game/trigger/Trigger.java @@ -124,7 +124,7 @@ public abstract class Trigger extends TriggerReplacementBase { String desc = getParam("TriggerDescription"); if (!desc.contains("ABILITY")) { desc = CardTranslation.translateSingleDescriptionText(getParam("TriggerDescription"), nameSource); - String translatedName = CardTranslation.getTranslatedName(nameSource); + String translatedName = nameSource.getTranslatedName(); desc = TextUtil.fastReplace(desc,"CARDNAME", translatedName); desc = TextUtil.fastReplace(desc,"NICKNAME", Lang.getInstance().getNickName(translatedName)); if (desc.contains("ORIGINALHOST") && this.getOriginalHost() != null) { @@ -218,7 +218,7 @@ public abstract class Trigger extends TriggerReplacementBase { result = TextUtil.fastReplace(result, "ABILITY", saDesc); result = CardTranslation.translateMultipleDescriptionText(result, sa.getHostCard()); - String translatedName = CardTranslation.getTranslatedName(sa.getHostCard()); + String translatedName = sa.getHostCard().getTranslatedName(); result = TextUtil.fastReplace(result,"CARDNAME", translatedName); result = TextUtil.fastReplace(result,"NICKNAME", Lang.getInstance().getNickName(translatedName)); } @@ -392,6 +392,10 @@ public abstract class Trigger extends TriggerReplacementBase { } } + if (condition == null) { + return true; + } + if ("LifePaid".equals(condition)) { final SpellAbility trigSA = (SpellAbility) runParams.get(AbilityKey.SpellAbility); if (trigSA != null && trigSA.getAmountLifePaid() <= 0) { @@ -442,7 +446,15 @@ public abstract class Trigger extends TriggerReplacementBase { if (game.getCombat().getAttackersAndDefenders().values().containsAll(attacker.getOpponents())) { return false; } + } else if (condition.startsWith("FromNamedAbility")) { + var rest = condition.substring(16); + final SpellAbility trigSA = (SpellAbility) runParams.get(AbilityKey.Cause); + + if (trigSA != null && !trigSA.getName().equals(rest)) { + return false; + } } + return true; } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerAttackerBlocked.java b/forge-game/src/main/java/forge/game/trigger/TriggerAttackerBlocked.java index 3f6ada026e7..676283cfffd 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerAttackerBlocked.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerAttackerBlocked.java @@ -21,9 +21,12 @@ import java.util.Map; import com.google.common.collect.Iterables; import forge.game.ability.AbilityKey; +import forge.game.ability.AbilityUtils; import forge.game.card.Card; +import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.spellability.SpellAbility; +import forge.util.Expressions; import forge.util.Localizer; /** @@ -61,14 +64,10 @@ public class TriggerAttackerBlocked extends Trigger { } if (hasParam("ValidBlocker")) { - @SuppressWarnings("unchecked") - int count = CardLists.getValidCardCount( - (Iterable) runParams.get(AbilityKey.Blockers), - getParam("ValidBlocker"), - getHostCard().getController(), getHostCard(), this - ); - - if (count == 0) { + String param = getParamOrDefault("ValidBlockerAmount", "GE1"); + int attackers = CardLists.getValidCardCount((CardCollection) runParams.get(AbilityKey.Blockers), getParam("ValidBlocker"), getHostCard().getController(), getHostCard(), this); + int amount = AbilityUtils.calculateAmount(getHostCard(), param.substring(2), this); + if (!Expressions.compare(attackers, param, amount)) { return false; } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java b/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java index e73c4e112ee..5bf497ec3c7 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerChangesZone.java @@ -92,6 +92,14 @@ public class TriggerChangesZone extends Trigger { } } + if (hasParam("ExcludedOrigins")) { + if (ArrayUtils.contains( + getParam("ExcludedOrigins").split(","), runParams.get(AbilityKey.Origin) + )) { + return false; + } + } + if (hasParam("ExcludedDestinations")) { if (ArrayUtils.contains( getParam("ExcludedDestinations").split(","), runParams.get(AbilityKey.Destination) @@ -108,9 +116,8 @@ public class TriggerChangesZone extends Trigger { } } + Card moved = (Card) runParams.get(AbilityKey.Card); if (hasParam("ValidCard")) { - Card moved = (Card) runParams.get(AbilityKey.Card); - // CR 603.10a leaves battlefield or GY look back in time if ("Battlefield".equals(getParam("Origin")) || ("Graveyard".equals(getParam("Origin")) && !"Battlefield".equals(getParam("Destination")))) { @@ -147,8 +154,7 @@ public class TriggerChangesZone extends Trigger { final Card host = hostCard.getGame().getCardState(hostCard); final String comparator = condition.length < 2 ? "GE1" : condition[1]; final int referenceValue = AbilityUtils.calculateAmount(host, comparator.substring(2), this); - final Card triggered = (Card) runParams.get(AbilityKey.Card); - final int actualValue = AbilityUtils.calculateAmount(triggered, condition[0], this); + final int actualValue = AbilityUtils.calculateAmount(moved, condition[0], this); if (!Expressions.compare(actualValue, comparator.substring(0, 2), referenceValue)) { return false; } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java index 4bd193848f2..013e3a5b60a 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java @@ -302,29 +302,25 @@ public class TriggerHandler { } private boolean runWaitingTrigger(final TriggerWaiting wt) { - final TriggerType mode = wt.getMode(); - final Map runParams = wt.getParams(); - final Player playerAP = game.getPhaseHandler().getPlayerTurn(); if (playerAP == null) { // This should only happen outside of games, so it's safe to abort. return false; } + final TriggerType mode = wt.getMode(); + final Map runParams = wt.getParams(); // Copy triggers here, so things can be modified just in case final List delayedTriggersWorkingCopy = new ArrayList<>(delayedTriggers); - boolean checkStatics = false; - // Static triggers + // Static ones should happen first for (final Trigger t : Lists.newArrayList(activeTriggers)) { if (t.isStatic() && canRunTrigger(t, mode, runParams)) { - int x = 1 + StaticAbilityPanharmonicon.handlePanharmonicon(game, t, runParams); - - for (int i = 0; i < x; ++i) { + int trigAmt = 1 + StaticAbilityPanharmonicon.handlePanharmonicon(game, t, runParams); + for (int i = 0; i < trigAmt; ++i) { runSingleTrigger(t, runParams); } - checkStatics = true; } } @@ -342,36 +338,17 @@ public class TriggerHandler { } } - // AP - checkStatics |= runNonStaticTriggersForPlayer(playerAP, wt, delayedTriggersWorkingCopy); - - // NAPs - for (final Player nap : game.getNonactivePlayers()) { - checkStatics |= runNonStaticTriggersForPlayer(nap, wt, delayedTriggersWorkingCopy); - } - return checkStatics; - } - - public void clearWaitingTriggers() { - waitingTriggers.clear(); - } - - private boolean runNonStaticTriggersForPlayer(final Player player, final TriggerWaiting wt, final List delayedTriggersWorkingCopy) { - final TriggerType mode = wt.getMode(); - final Map runParams = wt.getParams(); final boolean wasCollected = wt.getTriggers() != null; final Iterable triggers = wasCollected ? wt.getTriggers() : activeTriggers; - boolean checkStatics = false; - + // the trigger will be ordered later in MagicStack for (final Trigger t : triggers) { - if (!t.isStatic() && t.getHostCard().getController().equals(player) && (wasCollected || canRunTrigger(t, mode, runParams))) { + if (!t.isStatic() && (wasCollected || canRunTrigger(t, mode, runParams))) { if (wasCollected && !t.checkActivationLimit()) { continue; } - int x = 1 + StaticAbilityPanharmonicon.handlePanharmonicon(game, t, runParams); - - for (int i = 0; i < x; ++i) { + int trigAmt = 1 + StaticAbilityPanharmonicon.handlePanharmonicon(game, t, runParams); + for (int i = 0; i < trigAmt; ++i) { runSingleTrigger(t, runParams, wt.getController(t)); } checkStatics = true; @@ -379,15 +356,19 @@ public class TriggerHandler { } for (final Trigger deltrig : delayedTriggersWorkingCopy) { - if (deltrig.getHostCard().getController().equals(player) && - isTriggerActive(deltrig) && canRunTrigger(deltrig, mode, runParams)) { + if (isTriggerActive(deltrig) && canRunTrigger(deltrig, mode, runParams)) { delayedTriggers.remove(deltrig); runSingleTrigger(deltrig, runParams); } } + return checkStatics; } + public void clearWaitingTriggers() { + waitingTriggers.clear(); + } + private boolean isTriggerActive(final Trigger regtrig) { if (!regtrig.phasesCheck(game)) { return false; // It's not the right phase to go off. @@ -533,11 +514,6 @@ public class TriggerHandler { sa.setActivatingPlayer(p); } - if (regtrig.hasParam("RememberTriggeringCard")) { - Card triggeredCard = ((Card) sa.getTriggeringObject(AbilityKey.Card)); - host.addRemembered(triggeredCard); - } - if (!sa.getActivatingPlayer().isInGame()) { return; } @@ -636,7 +612,7 @@ public class TriggerHandler { List lost = new ArrayList<>(delayedTriggers); for (Trigger t : lost) { // CR 800.4d trigger controller lost game - if (t.getHostCard().getOwner().equals(p)) { + if (p.equals(t.getSpawningAbility().getActivatingPlayer())) { delayedTriggers.remove(t); } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerType.java b/forge-game/src/main/java/forge/game/trigger/TriggerType.java index 4059b28d8d4..241712e5dc4 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerType.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerType.java @@ -139,6 +139,7 @@ public enum TriggerType { SpellCast(TriggerSpellAbilityCastOrCopy.class), SpellCastOrCopy(TriggerSpellAbilityCastOrCopy.class), SpellCopy(TriggerSpellAbilityCastOrCopy.class), + Stationed(TriggerCrewedSaddled.class), Surveil(TriggerSurveil.class), TakesInitiative(TriggerTakesInitiative.class), TapAll(TriggerTapAll.class), diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index 6b20d52e089..37f0eec264b 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -331,6 +331,12 @@ public class MagicStack /* extends MyObservable */ implements Iterable crews = sp.getPaidList("Tapped", true); + if (crews != null) { + for (Card c : crews) { + Map stationParams = AbilityKey.mapFromCard(sp.getHostCard()); + stationParams.put(AbilityKey.Crew, c); + game.getTriggerHandler().runTrigger(TriggerType.Stationed, stationParams, false); + } + } + } } else { // Run Copy triggers if (sp.isSpell()) { @@ -508,7 +518,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable players = game.getPlayersInTurnOrder(playerTurn); + boolean result = false; + // CR 603.3b for (Player p : players) { - if (p.hasLost()) { - continue; - } result |= chooseOrderOfSimultaneousStackEntry(p, false); } - for (Player p : players) { - if (p.hasLost()) { - continue; - } result |= chooseOrderOfSimultaneousStackEntry(p, true); } @@ -880,10 +884,12 @@ public class MagicStack /* extends MyObservable */ implements Iterable { // might support different order via preference later private static final Comparator COMPARATOR = Comparator.comparingInt((Card c) -> c.getCMC()) - .thenComparing(c -> c.getColor()) + .thenComparing(c -> c.getColor().getOrderWeight()) .thenComparing(Comparator.comparing(Card::getName)) .thenComparing(Card::hasPerpetual); @@ -111,8 +111,7 @@ public class Zone implements java.io.Serializable, Iterable { final Zone oldZone = game.getZoneOf(c); final ZoneType zt = oldZone == null ? ZoneType.Stack : oldZone.getZoneType(); - // only if the zoneType differs from this - // don't go in there is its a control change + // don't go in there if its a control change if (zt != zoneType) { c.setTurnInController(getPlayer()); c.setTurnInZone(game.getPhaseHandler().getTurn()); diff --git a/forge-game/src/main/java/forge/trackable/TrackableTypes.java b/forge-game/src/main/java/forge/trackable/TrackableTypes.java index 4b711552ecf..fe2464d90bf 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableTypes.java +++ b/forge-game/src/main/java/forge/trackable/TrackableTypes.java @@ -464,7 +464,7 @@ public class TrackableTypes { public static final TrackableType ColorSetType = new TrackableType() { @Override public ColorSet getDefaultValue() { - return ColorSet.getNullColor(); + return ColorSet.C; } @Override diff --git a/forge-game/src/test/java/forge/game/card/CardCopyServiceTest.java b/forge-game/src/test/java/forge/game/card/CardCopyServiceTest.java deleted file mode 100644 index dcaa6712580..00000000000 --- a/forge-game/src/test/java/forge/game/card/CardCopyServiceTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package forge.game.card; - -import org.testng.annotations.Test; - -public class CardCopyServiceTest { - - @Test - public void testCopyCard() { - - } - - @Test - public void testCopyState() { - - } - - @Test - public void testCopyStats() { - - } - - @Test - public void testGetLKICopy() { - - } -} diff --git a/forge-gui-android/assets/database.json b/forge-gui-android/assets/database.json new file mode 100644 index 00000000000..6545eda2047 --- /dev/null +++ b/forge-gui-android/assets/database.json @@ -0,0 +1,770 @@ +{ + "a32x":{ + "CPU":"2x Cortex-A76 @ 2GHz 6x Cortex-A55 @ 2GHz", + "SoC":"MediaTek Dimensity 720 (MT6853)" + }, + "angler":{ + "CPU":"4x Cortex-A57 @ 1.95GHz 4x Cortex-A53 @ 1.55GHz", + "SoC":"Snapdragon 810 MSM8994" + }, + "ane":{ + "CPU":"4x Cortex-A54 @ 2.3GHz 4x Cortex-A54 @ 1.7GHz", + "SoC":"HiSilicon Kirin 659" + }, + "amar_row_wifi":{ + "CPU":"8x Cortex-A53 @ 1.8GHz", + "SoC":"Mediatek MT8768" + }, + "atlas":{ + "CPU":"-", + "SoC":"Intel(R) Core(TM) i5-8200Y @ 1.3GHz" + }, + "apq8084":{ + "CPU":"4x Krait 450 @ 2.65GHz", + "SoC":"Snapdragon 805 APQ8084" + }, + "atoll":{ + "CPU":"2x Kryo 465 Gold @ 2.3GHz 6x Kryo 465 Silver @ 1.8GHz", + "SoC":"Snapdragon 720G (SM7125)" + }, + "art-l29":{ + "CPU":"4x Cortex-A73 @ 2.2GHz 4x Cortex-A53 @ 1.7GHz", + "SoC":"HiSilicon Kirin 710F" + }, + "baylake":{ + "CPU":"Atom Z3745 @ 1.3GHz", + "SoC":"Intel Atom Z3745" + }, + "begonia":{ + "CPU":"2x Cortex-176 @ 2GHz 6x Cortex-A55 @ 2GHz", + "SoC":"MediaTek Helios G90T MT6785T" + }, + "blueline":{ + "CPU":"4x Kryo 385 Gold @ 2.8GHz 4x Kryo 385 Silver @ 1.75GHz", + "SoC":"Snapdragon 845" + }, + "bullhead":{ + "CPU":"4x Cortex-A57 @ 1.8GHz 4x Cortex-A53 @ 1.44GHz", + "SoC":"Snapdragon 808" + }, + "capri":{ + "CPU":"2x Cortex-A9 @ 1.2GHz", + "SoC":"Broadcom BCM28155" + }, + "cepheus":{ + "CPU":"1x Kryo 485 Gold @ 2.8GHz 3x Kryo 485 Gold @ 2.4GHz 4x Kryo 485 Silver @ 1.7GHz", + "SoC":"Snapdragon 855 SM8150" + }, + "cheryl":{ + "CPU":"4x Kryo 280 HP @ 2.45GHz 4x Kryo 280 LP @ 1.9GHz", + "SoC":"Snapdragon 835 MSM8998" + }, + "cheryl2":{ + "CPU":"4x Kryo 385 Gold @ 2.8GHz 4x Kryo 385 Silver @ 1.8GHz", + "SoC":"Snapdragon 845 SDM845" + }, + "cheetah":{ + "CPU":"2x Cortex-X1 @ 2.8GHz 2x Cortex-A78 @ 2.3GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"Google Tensor G2 (GS201)" + }, + "comet":{ + "CPU":"1x Cortex-X4 @ 31.GHz 3x Cortex-A720 @ 2.6GHz 4x Cortex-A520 @ 1.95GHz", + "SoC":"Google Tensor G4 (GS401)" + }, + "eureka":{ + "CPU":"6x Cortex-A78C @ 2.3 GHz", + "SoC":"Snapdragon XR2 Gen 2 (SM8550)" + }, + "felix":{ + "CPU":"2x Cortex-X1 @ 2.8GHz 2x Cortex-A78 @ 2.3GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"Google Tensor G2 (GS201)" + }, + "tangorpro":{ + "CPU":"2x Cortex-X1 @ 2.8GHz 2x Cortex-A78 @ 2.3GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"Google Tensor G2 (GS201)" + }, + "codina":{ + "CPU":"2x Cortex-A9 @ 1.0GHz", + "SoC":"NovaThor U8500" + }, + "clovertrail":{ + "CPU":"2x Atom Z2560 @ 1.6GHz", + "SoC":"Intel Atom Z2560" + }, + "clt":{ + "CPU":"4x Cortex-A73 @ 2.36GHz 4x Cortex-A53 @ 1.8GHz", + "SoC":"HiSilicon Kirin 970" + }, + "els":{ + "CPU":"4x Cortex-A76 @ 2.8GHz 4x Cortex-A55 @ 1.9GHz", + "SoC":"HiSilicon Kirin 990 5G" + }, + "dandelion":{ + "CPU":"8x Cortex-A54 @ 1.5GHz", + "SoC":"MediaTek Helio G25 (MT6762G)" + }, + "darcy":{ + "CPU":"4x ARM Cortex-A53 4x ARM Cortex-A57", + "SoC":"nVIDIA Tegra X1 T210" + }, + "db8520h":{ + "CPU":"2x Cortex-A9 @ 1.0GHz", + "SoC":"NovaThor U8500" + }, + "dragon":{ + "CPU":"4x Cortex-A57 @ 1.9GHz 4x Cortex-A53 @ 1.3GHz", + "SoC":"nVIDIA Tegra X1 T210" + }, + "douglas":{ + "CPU":"4x Cortex-A53 @ 1.3GHz", + "SoC":"MediaTek MT8163" + }, + "eeepad":{ + "CPU":"4x Atom Z2520 @ 1.2GHz", + "SoC":"Intel Atom Z2520" + }, + "endeavoru":{ + "CPU":"4x Cortex-A9 @ 1.5GHz 1x Cortex-A9 @ 0.5GHz", + "SoC":"nVIDIA Tegra 3 AP33" + }, + "eml":{ + "CPU":"4x Cortex-A73 @ 2.36GHz 4x Cortex-A53 @ 1.8GHz", + "SoC":"HiSilicon Kirin 970" + }, + "eve":{ + "CPU":"i5-7Y56 @ 1.2GHz", + "SoC":"Ambel Lake-Y / Kaby Lake-U/Y" + }, + "eva-l19":{ + "CPU":"4x Cortex-A72 @ 2.5GHz 4x Cortex-A53 @ 1.8GHz", + "SoC":"HiSilicon Kirin 955" + }, + "exynos990":{ + "CPU":"4x Cortex-A55 @ 2GHz 2x Cortex-A76 @ 2.5GHz 2x Exynos M5 @ 2.7GHz", + "SoC":"Exynos 990" + }, + "exynos9611":{ + "CPU":"4x Cortex-A73 @ 2.3GHz 4x Cortex-A53 @ 1.7GHz", + "SoC":"Exynos 7 Octa (9611)" + }, + "exynos2100":{ + "CPU":"1x Cortex-X1 @ 2.9GHz 3x Cortex-A78 @ 2.8GHz 4x Cortex-A55 @ 2.2GHz", + "SoC":"Exynos 2100" + }, + "exynos9810":{ + "CPU":"4x Exynos M3 @ 2.7GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"Exynos 9 (9810)" + }, + "exynos9820":{ + "CPU":"2 Exynos M4 @ 2.7GHz 2x Cortex-A75 @ 2.3GHz 4x Cortex-A55 @ 1.9GHz", + "SoC":"Exynos 9 (9820)" + }, + "ford":{ + "CPU":"4x Cortex-A7 @ 1.3GHz", + "SoC":"MediaTek MT8127" + }, + "flo":{ + "CPU":"4x Krait 300 @ 1.5GHz", + "SoC":"Snapdragon 600 APQ8064-FLO" + }, + "flame":{ + "CPU":"1x Kryo 485 Gold @ 2.8GHz 3x Kryo 485 Gold @ 2.4GHz 4x Kryo 485 Silver @ 1.7GHz", + "SoC":"Snapdragon 855 SM8150" + }, + "fleur":{ + "CPU":"2x Cortex-A76 @ 2GHz 6x Cortex-A55 @ 2GHz", + "SoC":"MediaTek Helio G96 (MT6781)" + }, + "flounder":{ + "CPU":"2x nVidia Denver @ 2.5GHz", + "SoC":"nVIDIA Tegra K1 T132" + }, + "g3u":{ + "CPU":"2x Cortex-A5 @ 1.0GHz", + "SoC":"Snapdragon S4 Play MSM8225" + }, + "gee":{ + "CPU":"4x Krait 300 @ 1.5GHz", + "SoC":"Snapdragon S4 Pro APQ8064" + }, + "grouper":{ + "CPU":"4x Cortex-A9 @ 1.3GHz 1x Cortex-A9 @ 0.5GHz", + "SoC":"nVIDIA Tegra 3 T30L" + }, + "hammerhead":{ + "CPU":"4x Krait 400 @ 2.26GHz", + "SoC":"Snapdragon 800 MSM8974" + }, + "hawaii_ss_kylepro":{ + "CPU":"2x Cortex-A9 @ 1.2GHz", + "SoC":"Broadcom BCM21664T" + }, + "herring":{ + "CPU":"1x Cortex-A8 @ 1.0GHz", + "SoC":"Exynos 3 Single 3110" + }, + "hollywood":{ + "CPU":"4x Kryo 280 HP @ 2.4GHz 4x Kryo 280 LP @ 1.9GHz", + "SoC":"Snapdragon XR2" + }, + "k6853v1_64_titan":{ + "CPU":"2x Cortex-A76 @ 2Ghz 6x Cortex-A55 @ 2GHz", + "SoC":"MediaTek Dimensity 720 (MT6853)" + }, + "kalama":{ + "CPU":"1x Cortex-X3 @ 3.3GHz 2x Cortex-A710 @ 2.8GHz 2x Cortex-A715 @ 2.8GHz 3x Cortex-A510 @ 2.0GHz", + "SoC":"Snapdragon 8 Gen 2 (SM8550)" + }, + "kona":{ + "CPU":"1x Cortex-A77 @ 3.1GHz 3x Cortex-A77 @ 2.4GHz 4x Kryo 585 Silver @ 1.8GHz", + "SoC":"Snapdragon 865 SM8250" + }, + "kohaku":{ + "CPU":"i5-10210U @ 1.6GHz", + "SoC":"Comet Lake-U" + }, + "lahaina":{ + "CPU":"1x Cortex-X1 @ 2.8GHz 3x Cortex-A78 @ 2.4GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"Snapdragon 888" + }, + "liara":{ + "CPU":"5 Compute cores 2C+3G", + "SoC":"AMD A4-9120C Radeon R4" + }, + "lito":{ + "CPU":"2x Cortex-A77 @ 2.1GHz 6x Kryo 560 Silver @ 1.7GHz", + "SoC":"Snapdragon 690 SM6350" + }, + "lya":{ + "CPU":"2x Cortex-A76 @ 2.6GHz 2x Cortex-A76 @ 1.9GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"HiSilicon Kirin 980" + }, + "lyo-l01":{ + "CPU":"4x Cortex-153 @ 1.3GHz", + "SoC":"MediaTek MT6735" + }, + "mako":{ + "CPU":"4x Krait @ 1.5GHz", + "SoC":"Snapdragon S4 Pro APQ8064" + }, + "marlin":{ + "CPU":"2x Kryo HP @ 2.15GHz 2x Kryo @ 1.6GHz", + "SoC":"Snapdragon 821 MSM8996 Pro" + }, + "med":{ + "CPU":"8x Cortex-A54 @ 1.5GHz", + "SoC":"MediaTek MT6762R" + }, + "mocha":{ + "CPU":"4x Cortex-A15 @2.2GHz", + "SoC":"nVIDIA Tegra K1 T124" + }, + "msm8225":{ + "CPU":"2x Cortex-A5 @ 1.2GHz", + "SoC":"Snapdragon S4 MSM8225" + }, + "msm8226":{ + "CPU":"4x Cortex-A7 @ 1.19GHz", + "SoC":"Snapdragon 400 MSM8226" + }, + "msm8625":{ + "CPU":"2x Cortex-A5 @ 1.2GHz", + "SoC":"Snapdragon S4 MSM8625" + }, + "MSM8227":{ + "CPU":"2x Krait @ 1GHz", + "SoC":"Snapdragon S4 Plus MSM8227" + }, + "msm8627":{ + "CPU":"2x Krait @ 1GHz", + "SoC":"Snapdragon S4 Plus MSM8627" + }, + "apq8030":{ + "CPU":"2x Krait @ 1.2GHz", + "SoC":"Snapdragon S4 Plus APQ8030" + }, + "msm8230":{ + "CPU":"2x Krait @ 1.2GHz", + "SoC":"Snapdragon S4 Plus MSM8230" + }, + "msm8660_surf":{ + "CPU":"2x Scorpion @ 1.5GHz", + "SoC":"Snapdragon S3 MSM8260" + }, + "msm8630":{ + "CPU":"2x Krait @ 1.2GHz", + "SoC":"Snapdragon S4 Plus MSM8630" + }, + "msm8930":{ + "CPU":"2x Krait @ 1.2GHz", + "SoC":"Snapdragon S4 Plus MSM8930" + }, + "msm8937":{ + "CPU":"4x Cortex-A53 @ 1.4GHz", + "SoC":"Snapdragon 425 MSM8917" + }, + "apq8060a":{ + "CPU":"2x Krait @ 1.5GHz", + "SoC":"Snapdragon S4 Plus APQ8060A" + }, + "msm8260a":{ + "CPU":"2x Krait @ 1.5GHz", + "SoC":"Snapdragon S4 Plus MSM8260A" + }, + "msm8660a":{ + "CPU":"2x Krait @ 1.5GHz", + "SoC":"Snapdragon S4 Plus MSM8660A" + }, + "msm8960":{ + "CPU":"2x Krait @ 1.5GHz", + "SoC":"Snapdragon S4 Plus MSM8960" + }, + "msm8260a-pro":{ + "CPU":"2x Krait 300 @ 1.7GHz", + "SoC":"Snapdragon S4 Pro MSM8260A Pro" + }, + "msm8960t":{ + "CPU":"2x Krait 300 @ 1.7GHz", + "SoC":"Snapdragon S4 Pro MSM8960T" + }, + "msm8960t-pro":{ + "CPU":"2x Krait 300 @ 1.7GHz", + "SoC":"Snapdragon S4 Pro MSM8960T Pro" + }, + "msm8960ab":{ + "CPU":"2x Krait 300 @ 1.7GHz", + "SoC":"Snapdragon S4 Pro MSM8960AB" + }, + "msm8960dt":{ + "CPU":"2x Krait 300 @ 1.7GHz", + "SoC":"Snapdragon S4 Pro MSM8960DT" + }, + "apq8064":{ + "CPU":"4x Krait 300 @ 1.5GHz", + "SoC":"Snapdragon 600 APQ8064" + }, + "msm8916":{ + "CPU":"4x Cortex-A53 @ 1.2GHz", + "SoC":"Snapdragon 410 MSM8916" + }, + "msm8953":{ + "CPU":"8x Cortex-A53 @ 2.0GHz", + "SoC":"Snapdragon 625 MSM8953" + }, + "msm8952":{ + "CPU":"8x Cortex-A53 @ 1.2GHz", + "SoC":"Snapdragon 617 MSM8952" + }, + "msm8956":{ + "CPU":"2x Cortex-A72 @ 1.8GHz 4x Cortex-A53 @ 1.4GHz", + "SoC":"Snapdragon 650 MSM8956" + }, + "msm8974":{ + "CPU":"4x Krait 400 @ 2.15GHz", + "SoC":"Snapdragon 800 MSM8974" + }, + "msm8974pro-ab":{ + "CPU":"4x Krait 400 @ 2.26GHz", + "SoC":"Snapdragon 801 MSM8974PRO-AB" + }, + "msm8974pro-ac":{ + "CPU":"4x Krait 400 @ 2.45GHz", + "SoC":"Snapdragon 801 MSM8974AC" + }, + "msm8976":{ + "CPU":"4x Cortex-A53 @ 1.4GHz 4x Cortex-A72 @ 1.8GHz", + "SoC":"Snapdragon 652 MSM8976" + }, + "msm8976pro":{ + "CPU":"4x Cortex-A53 @ 1.4GHz 4x Cortex-A72 @ 1.9GHz", + "SoC":"Snapdragon 653 MSM8976 Pro" + }, + "msm8992":{ + "CPU":"4x Cortex-A57 @ 1.8GHz 4x Cortex-A53 @ 1.4GHz", + "SoC":"Snapdragon 808 MSM8992" + }, + "msm8994":{ + "CPU":"4x Cortex-A57 @ 1.95GHz 4x Cortex-A53 @ 1.5GHz", + "SoC":"Snapdragon 810 MSM8994" + }, + "msm8996":{ + "CPU":"2x Kryo HP @ 1.8GHz 2x Kryo LP @ 1.36GHz", + "SoC":"Snapdragon 820 MSM8996" + }, + "msm8996pro":{ + "CPU":"2x Kryo HP @ 2.34GHz 2x Kryo LP @ 2.18GHz", + "SoC":"Snapdragon 821 MSM8996 Pro" + }, + "msm8998":{ + "CPU":"4x Kryo 280 HP @ 2.4GHz 4x Kryo 280 LP @ 1.9GHz", + "SoC":"Snapdragon 835 MSM8998" + }, + "msmnile":{ + "CPU":"1x Kryo 485 Gold @ 2.8GHz 3x Kryo 485 Gold @ 2.4GHz 4x Kryo 485 Silver @ 1.8GHz", + "SoC":"Snapdragon 855 SM8150" + }, + "mt6795t":{ + "CPU":"8x Cortex-A53 @ 2.1GHz", + "SoC":"MediaTek Helio X10 MT6795T" + }, + "mt6797m":{ + "CPU":"2x Cortex-A72 @ 2.1GHz 4x Cortex-A53 @ 1.85GHz 4x Cortex-A53 @ 1.4GHz", + "SoC":"MediaTek Helio X20 MT6797M" + }, + "mt6750t":{ + "CPU":"8x Cortex-A53 @ 1.5GHz", + "SoC":"MediaTek MT6750T" + }, + "mx5":{ + "CPU":"8x Cortex-A53 @ 2.1GHz", + "SoC":"MediaTek Helio X10 MT6795T" + }, + "mtk6575":{ + "CPU":"1x Cortex-A9 @ 1.0GHz", + "SoC":"MediaTek MT6575" + }, + "noh":{ + "CPU":"4x Cortex-A77 @ 3.1GHz 4x Cortex-A55 @ 2.0GHz", + "SoC":"HiSilicon Kirin 9000" + }, + "sdm710":{ + "CPU":"2x Kryo 385 Gold @ 2.2GHz 6x Kryo 385 Silver @ 1.7GHz", + "SoC":"Snapdragon 710 SDM710" + }, + "sdm845":{ + "CPU":"4x Kryo 385 Gold @ 2.8GHz 4x Kryo 385 Silver @ 1.7GHz", + "SoC":"Snapdragon 845 SDM845" + }, + "monterey":{ + "CPU":"4x Kryo 280 HP @ 2.45GHz 4x Kryo 280 LP @ 1.9GHz", + "SoC":"Snapdragon 835 MSM8998" + }, + "oriole":{ + "CPU":"2x Cortex-X1 @ 2.8GHz 2x Cortex-A76 @ 2.2GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"Google Tensor (Whitechapel)" + }, + "oppo6779_18073":{ + "CPU":"2x Cortex-A75 @ 2.2GHz 6x Cortex-A55 @ 2GHz", + "SoC":"Mediatek MT6779 Helio P90" + }, + "gt-p7510":{ + "CPU":"2x Cortex-A9 @ 1.0GHz", + "SoC":"nVIDIA Tegra 2 T20" + }, + "pacific":{ + "CPU":"2x @ 2.1GHz 2x @ 1.6GHz", + "SoC":"Snapdragon 820E Embedded" + }, + "piranha":{ + "CPU":"2x Cortex-A9 @ 1.0GHz", + "SoC":"Texas Instruments OMAP4430" + }, + "pro5":{ + "CPU":"4xCortex-56 @ 2.1GHz 4x Cortex-53 @ 1.5GHz", + "SoC":"Exynos 7 Octa 7420" + }, + "pro7plus":{ + "CPU":"2x Cortex-A73 @ 2.6GHz 4x Cortex-A53 @ 2.2GHz 4x Cortex-A35 @ 1.9GHz", + "SoC":"MediaTek Helio X30 MT6799" + }, + "pxa986":{ + "CPU":"2x Cortex-A9 @ 1.2GHz", + "SoC":"Marvell PXA988" + }, + "pxa19xx":{ + "CPU":"4x Cortex-A53 @ 1.25GHz", + "SoC":"Maxvell Armada PXA1908" + }, + "rhea_ss_corsicass":{ + "CPU":"1x Cortex-A9 @ 0.9GHz", + "SoC":"Broadcom BCM21654" + }, + "panther":{ + "CPU":"1x Cortex-X1 @ 2.8GHz 2x Cortex-A78 @ 2.3GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"Google Tensor G2" + }, + "prada":{ + "CPU":"4x Cortex-A53 @ 1.4 GHz 4x Cortex-A53 @ 1.1GHz", + "SoC":"Snapdragon 430 MSM8937" + }, + "pro6plus":{ + "CPU":"4x Exynos M1 @ 1.97GHz 4x Cortex-A53 @ 1.48GHz", + "SoC":"Exynos 8 Octa 8890" + }, + "universal3110":{ + "CPU":"1x Cortex-A8 @ 1.2GHz", + "SoC":"Exynos 3 Single 3110" + }, + "universal9820":{ + "CPU":"2x Exynos M4 @ 2.7GHz 2x Cortex-A75 @ 2.3GHz 4x Cortex-A55 @ 1.95GHz", + "SoC":"Exynos 9 9820" + }, + "redfin":{ + "CPU":"2x Kryo 475 Gold @ 2.4GHz 2x Kryo 475 Gold 2.2GHz 6x Kryo 475 Silver @ 1.8GHz ", + "SoC":"Snapdragon 765/765G" + }, + "s5e9925":{ + "CPU":"1x Cortex-X2 @ 2.8GHZ 3x Cortex-A710 @ 2.5GHz 4x Cortex-A510 @ 1.8GHz", + "SoC":"Exynos 2200" + }, + "saturn":{ + "CPU":"4x Krait 450 @ 2.45GHz", + "SoC":"Snapdragon 805 APQ8084" + }, + "sailfish":{ + "CPU":"2x Kryo HP @ 2.15GHz 2x Kryo @ 1.6GHz", + "SoC":"Snapdragon 821 MSM8996 Pro" + }, + "sdm660":{ + "CPU":"4x Kryo 260 HP @ 2.2GHz 4x Kryo 260 LP @ 1.8GHz", + "SoC":"Snapdragon 660" + }, + "sc-02b":{ + "CPU":"1x Cortex-A8 @ 1.2GHz", + "SoC":"Exynos 3 Single 3110" + }, + "sch-i905":{ + "CPU":"2x Cortex-A9 @ 1.0GHz", + "SoC":"nVIDIA Tegra 2 T20" + }, + "sc7730s":{ + "CPU":"4x Cortex-A7 @ 1.3GHz", + "SoC":"Spreadtrum SC7730S" + }, + "shamu":{ + "CPU":"4x Krai 450 @ 2.65GHz", + "SoC":"Snapdragon 805 APQ8084AB" + }, + "shiba":{ + "CPU":"1x Cortex-X3 @ 2.9GHz 4x Cortex-A715 @ 2.3GHz 4x Cortex-A510 @ 1.7GHz", + "SoC":"Google Tensor G3 (GS301)" + }, + "sm6150":{ + "CPU":"2x Kryo 460 Gold @ 2GHz 6x 460 Kryo Silver @ 1.7GHz", + "SoC":"Snapdragon 675 SM6150" + }, + "smdkc110":{ + "CPU":"1x Cortex-A8 @ 1.2GHz", + "SoC":"Exynos 3 Single 3110" + }, + "sun":{ + "CPU":"2x Oryon @ 4.5GHz 6x Oryon @ 3.5GHz", + "SoC":"Snapdragon 9 Elite (SM8750)" + }, + "ums512_25c10": { + "CPU":"2 Cortex-A75 @2GHz 6x Cortex-A55 @ 2 GHz", + "SoC":"Unisoc Tiger T618" + }, + "universal3250":{ + "CPU":"2x Cortex-A7 @ 1.0GHz", + "SoC":"Exynos 2 Dual 3250" + }, + "universal3470":{ + "CPU":"4x Cortex-A7 @ 1.4GHz", + "SoC":"Exynos 3 Quad 3470" + }, + "universal3475":{ + "CPU":"4x Cortex-A7 @ 1.3GHz", + "SoC":"Exynos 3 Quad 3475" + }, + "universal4210":{ + "CPU":"2x Cortex-A9 @ 1.4GHz", + "SoC":"Exynos 4 Dual 4210" + }, + "universal4212":{ + "CPU":"2x Cortex-A9 @ 1.5GHz", + "SoC":"Exynos 4 Dual 4212" + }, + "universal4412":{ + "CPU":"4x Cortex-A9 @ 1.4GHz", + "SoC":"Exynos 4 Quad 4412" + }, + "smdk4x12":{ + "CPU":"4x Cortex-A9 @ 1.4GHz", + "SoC":"Exynos 4 Quad 4412" + }, + "universal4415":{ + "CPU":"4x Cortex-A9 @ 1.5GHz", + "SoC":"Exynos 4 Quad 4415" + }, + "universal5250":{ + "CPU":"2x Cortex-A15 @ 1.7GHz", + "SoC":"Exynos 5 Dual 5250" + }, + "universal5260":{ + "CPU":"2x Cortex-A15 @ 1.7GHz 4x Cortex-A7 @ 1.3GHz", + "SoC":"Exynos 5 Hexa 5260" + }, + "universal5410":{ + "CPU":"4x Cortex-A15 @ 1.6GHz 4x Cortex-A7 @ 1.2GHz", + "SoC":"Exynos 5 Octa 5410" + }, + "universal5420":{ + "CPU":"4x Cortex-A15 @ 1.9GHz 4x Cortex-A7 @ 1.3GHz", + "SoC":"Exynos 5 Octa 5420" + }, + "universal5422":{ + "CPU":"4x Cortex-A15 @ 2.1GHz 4x Cortex-A7 @ 1.5GHz", + "SoC":"Exynos 5 Octa 5422" + }, + "universal5430":{ + "CPU":"4x Cortex-A15 @ 1.8GHz 4x Cortex-A7 @ 1.3GHz", + "SoC":"Exynos 5 Octa 5430" + }, + "universal5800":{ + "CPU":"4x Cortex-A15 @ 2.0GHz 4x Cortex-A7 @ 1.3GHz", + "SoC":"Exynos 5 Octa 5800" + }, + "universal5433":{ + "CPU":"4x Cortex-A57 @ 1.9GHz 4x Cortex-A53 @ 1.3GHz", + "SoC":"Exynos 7 Octa 5433" + }, + "universal7420":{ + "CPU":"4x Cortex-A57 @ 2.1GHz 4x Cortex-A53 @ 1.5GHz", + "SoC":"Exynos 7 Octa 7420" + }, + "universal7570":{ + "CPU":"8x Cortex-A53 @ 1.4GHz", + "SoC":"Exynos 7 Quad 7570" + }, + "universal7580":{ + "CPU":"8x Cortex-A53 @ 1.6GHz", + "SoC":"Exynos 7 Octa 7580" + }, + "universal7870":{ + "CPU":"8x Cortex-A53 @ 1.6GHz", + "SoC":"Exynos 7 Octa 7870" + }, + "universal7880":{ + "CPU":"8x Cortex-A53 @ 1.9GHz", + "SoC":"Exynos 7 Octa 7880" + }, + "universal7872":{ + "CPU":"2x Cortex-A73 @ 2.0GHz 4x Cortex-A53 @ 1.6GHz", + "SoC":"Exynos 7 Hexa 7872" + }, + "universal7885":{ + "CPU":"2x Cortex-A73 @ 2.2GHz 6x Cortex-A53 @ 1.6GHz", + "SoC":"Exynos 7 Octa 7885" + }, + "universal8890":{ + "CPU":"4x Cortex-A53 @ 1.6GHz 4x Samsung Exynos M1 @ 2.6GHz", + "SoC":"Exynos 8 Octa 8890" + }, + "universal8895":{ + "CPU":"4x Samsung Exynos M1 @ 2.3GHz 4x Cortex-A53 @ 1.6GHz", + "SoC":"Exynos 9 Octa 8895" + }, + "universal9810":{ + "CPU":"4x Samsung Exynos M3 @ 2.8GHz 4x Cortex-A55 @ 1.7GHz", + "SoC":"Exynos 9 Series 9810" + }, + "universal9825":{ + "CPU":"2x Samsung Exynos M4 @ 2.7GHz 2x Cortex-A75 @ 2.4GHz 4x Cortex-A55 @ 1.9GHz", + "SoC":"Exynos 9 Series 9825" + }, + "ville":{ + "CPU":"2x Krait @ 1.5GHz", + "SoC":"Snapdragon S4 MSM8290" + }, + "vog":{ + "CPU":"2x Cortex-A76 @ 2.6GHz 2x Cortex-A76 @ 1.9GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"HiSilicon Kirin 980" + }, + "venus":{ + "CPU":"1x Cortex-X1 @ 2.8GHz 3x Cortex-A78 @ 2.4GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"Snapdragon 888 SM8350" + }, + "vtr":{ + "CPU":"4x Cortex-A73 @ 2.4GHz 4x Cortex-A53 @ 1.8GHz", + "SoC":"HiSilicon Kirin 960" + }, + "taimen":{ + "CPU":"4x Kryo 280 HP @ 2.45GHz 4x Kryo 280 LP @ 1.9GHz", + "SoC":"Snapdragon 835 MSM8998" + }, + "tn8":{ + "CPU":"4x Cortex-A15 @2.2GHz", + "SoC":"nVIDIA Tegra K1 T124" + }, + "taro":{ + "CPU":"1x Cortex-X2 @ 3GHz 3x Cortex-A710 @ 2.5GHz 4x Cortex-A510 @ 1.8GHz", + "SoC":"Snapdragon 8 Gen 1 (SM8450)" + }, + "thebes":{ + "CPU":"2x Cortex-A15 @ 1.5GHz 2x Cortex-A7 @ 1.2GHz", + "SoC":"MediaTek MT8135" + }, + "trinket":{ + "CPU":"4x Kryo 260 HP @ 2GHz 4x Kryo 260 LP @ 1.8GHz", + "SoC":"Snapdragon 665 SM6125" + }, + "tuna":{ + "CPU":"2x Cortex-A9 @ 1.2GHz", + "SoC":"TI OMAP 4460" + }, + "vu2kt":{ + "CPU":"2x Krait @ 1.5GHz", + "SoC":"Snapdragon S4 Plus MSM8960" + }, + "walleye":{ + "CPU":"4x Kryo 280 HP @ 2.45GHz 4x Kryo 280 LP @ 1.9GHz", + "SoC":"Snapdragon 835 MSM8998" + }, + "wikipad":{ + "CPU":"4x Cortex-A9 @ 1.3GHz 1x Cortex-A9 @ 0.5GHz", + "SoC":"nVIDIA Tegra 3 T30L" + }, + "qc_reference_phone":{ + "CPU":"2x Kryo HP @ 2.15GHz 2x Kryo LP @ 1.36GHz", + "SoC":"Snapdragon 820 MSM8996" + }, + "z4u":{ + "CPU":"4x Cortex-A5 @ 1.2GHz", + "SoC":"Snapdragon 200 MSM8225Q" + }, + "zs600kl":{ + "CPU":"4x Kryo 385 Gold @ 3GHz 4x Kryo 385 Silver @ 1.8GHz", + "SoC":"Snapdragon 845" + }, + "mt6765v":{ + "CPU":"4x Cortex-A53 @ 2.3GHz 4x Cortex-A53 @ 1.8GHz", + "SoC":"Mediatek MT6765G Helio G35 (12 nm)" + }, + "k65v1_64_bsp_titan_rat":{ + "CPU":"4x Cortex-A53 @ 2.3GHz 4x Cortex-A53 @ 1.8GHz", + "SoC":"Mediatek MT6765 Helio P35 (12nm)" + }, + "careena":{ + "CPU":"AMD 670F00h", + "SoC":"AMD A4-9120C RADEON R4, 5 COMPUTE CORES 2C+3G" + }, + "sion":{ + "CPU":"i3-8130U CPU 2.2GHz SoC", + "SoC":"i3-8130U CPU 2,2 GHz soc" + }, + "biloba":{ + "CPU":"2x Cortex-A75 @ 2.0GHz 6x Cortex-A55 @ 1.8GHz", + "SoC":"Mediatek MT6769Z Helio G85 (12nm)" + }, + "ele":{ + "CPU":"2x Cortex-A76 @ 2.6GHz 2x Cortex-A76 @ 1.9GHz 4x Cortex-A55 @ 1.8GHz", + "SoC":"Kirin 980 (7 nm)" + }, + "krane":{ + "CPU":"4x Cortex-A73 @ 2.0GHz + 4x Cortex-A53 @ 2.0GHz", + "SoC":"MediaTekHelio P60T (12nm)" + }, + "pineapple":{ + "CPU":"1x Cortex-X4 @ 3.3GHz 3x Cortex-A720 @ 3.2GHz 2x Cortex-A720 @ 2.6GHz 4x Cortex-A520 @ 2.3GHz", + "SoC":"Qualcomm SM8650-AB Snapdragon 8 Gen 3" + }, + "s5e9945":{ + "CPU":"1x Cortex-X4 @ 3.2GHz 2x Cortex-A720 @ 2.9GHz 3x Cortex-A720 @ 2.6GHz 4x Cortex-A520 @ 2.0GHz", + "SoC":"Exynos 2400" + }, + "a12":{ + "CPU":"4x Cortex-A53 @ 2.35GHz 4x Cortex-A53 @ 1.8GHz", + "SoC":"MediaTek Helio P35 (MT6765)" + } +} diff --git a/forge-gui-android/pom.xml b/forge-gui-android/pom.xml index 2447d905457..0de3daae9eb 100644 --- a/forge-gui-android/pom.xml +++ b/forge-gui-android/pom.xml @@ -97,7 +97,7 @@ org.robolectric android-all - 15-robolectric-12650502 + 15-robolectric-13954326 provided @@ -156,7 +156,7 @@ io.sentry sentry-android - 8.19.1 + 8.21.1 aar @@ -177,7 +177,7 @@ io.sentry sentry-android-core - 8.19.1 + 8.21.1 aar @@ -201,7 +201,7 @@ io.sentry sentry-android-ndk - 8.19.1 + 8.21.1 aar @@ -487,7 +487,7 @@ - + diff --git a/forge-gui-android/src/forge/app/Main.java b/forge-gui-android/src/forge/app/Main.java index a3f90977885..4e3e988bbdf 100644 --- a/forge-gui-android/src/forge/app/Main.java +++ b/forge-gui-android/src/forge/app/Main.java @@ -67,10 +67,12 @@ import forge.util.ThreadUtil; import io.sentry.protocol.Device; import io.sentry.protocol.OperatingSystem; import org.apache.commons.lang3.tuple.Pair; +import org.json.JSONObject; import org.jupnp.DefaultUpnpServiceConfiguration; import org.jupnp.android.AndroidUpnpServiceConfiguration; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Date; @@ -188,7 +190,26 @@ public class Main extends AndroidApplication { boolean permissiongranted = checkPermission(); Gadapter = new AndroidAdapter(getContext()); - + String cpu = ""; + String soc = ""; + boolean getChipset = false; + // database.json source: https://github.com/xTheEc0/Android-Device-Hardware-Specs-Database + try { + InputStream is = getAssets().open("database.json"); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + JSONObject db = new JSONObject(new String(buffer, StandardCharsets.UTF_8)); + JSONObject board = db.getJSONObject(Build.BOARD); + cpu = board.get("CPU").toString(); + soc = board.get("SoC").toString(); + getChipset = true; + } catch (Exception e) { + cpu = getCpuName(); + soc = Build.BOARD; + getChipset = false; + } // Device Info Device device = new Device(); device.setId(Build.ID); @@ -197,8 +218,8 @@ public class Main extends AndroidApplication { device.setBrand(Build.BRAND); device.setManufacturer(Build.MANUFACTURER); device.setMemorySize(memInfo.totalMem); - device.setCpuDescription(getCpuName()); - device.setChipset(Build.HARDWARE + " " + Build.BOARD); + device.setCpuDescription(cpu); + device.setChipset(soc); // OS Info OperatingSystem os = new OperatingSystem(); os.setName("Android"); @@ -206,7 +227,7 @@ public class Main extends AndroidApplication { os.setBuild(Build.DISPLAY); os.setRawDescription(getAndroidOSName()); - initForge(Gadapter, new HWInfo(device, os), permissiongranted, totalMemory, isTabletDevice(getContext())); + initForge(Gadapter, new HWInfo(device, os, getChipset), permissiongranted, totalMemory, isTabletDevice(getContext())); } private void crossfade(View contentView, View previousView) { diff --git a/forge-gui-desktop/pom.xml b/forge-gui-desktop/pom.xml index dfe902e6b9a..4e6b5c911a9 100644 --- a/forge-gui-desktop/pom.xml +++ b/forge-gui-desktop/pom.xml @@ -35,7 +35,6 @@ timestamp-property - month.date MM.dd @@ -47,7 +46,6 @@ regex-property - snapshot-version ${revision} -SNAPSHOT @@ -189,7 +187,7 @@ java.base/java.nio java.base/java.math java.base/java.util.concurrent - java.base/java.net + java.base/java.net ]]> forge.view.Main @@ -208,6 +206,46 @@ + + + org.codehaus.gmaven + groovy-maven-plugin + 2.1.1 + + + get-changelog-from-tag + initialize + + execute + + + + 1) { + tag = tagList[1] + } else if (!isOnTag && tagList.size() > 0) { + tag = tagList[0] + } + + project.properties['changelog.from.tag'] = tag + println "Using changelog from tag: ${tag}" +} catch (Exception e) { + project.properties['changelog.from.tag'] = "HEAD" + println "Using changelog from tag: HEAD (fallback)" +} +]]> + + + + + + se.bjurr.gitchangelog git-changelog-maven-plugin @@ -220,9 +258,9 @@ git-changelog - - forge-1.6.65 - ../forge-gui/release-files/CHANGES.txt + + refs/tags/${changelog.from.tag} + ${project.build.directory}/CHANGES.txt - + diff --git a/forge-gui-desktop/src/main/java/forge/GuiDesktop.java b/forge-gui-desktop/src/main/java/forge/GuiDesktop.java index bcd38dd843d..4a57d8fa84a 100644 --- a/forge-gui-desktop/src/main/java/forge/GuiDesktop.java +++ b/forge-gui-desktop/src/main/java/forge/GuiDesktop.java @@ -15,6 +15,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; import java.util.List; +import java.util.function.Consumer; import java.util.function.Function; import javax.swing.ImageIcon; @@ -53,7 +54,6 @@ import forge.toolbox.FOptionPane; import forge.toolbox.FSkin; import forge.toolbox.FSkin.SkinImage; import forge.util.BuildInfo; -import forge.util.Callback; import forge.util.FileUtil; import forge.util.ImageFetcher; import forge.util.OperatingSystem; @@ -264,7 +264,7 @@ public class GuiDesktop implements IGuiBase { } @Override - public void download(final GuiDownloadService service, final Callback callback) { + public void download(final GuiDownloadService service, final Consumer callback) { new GuiDownloader(service, callback).show(); } diff --git a/forge-gui-desktop/src/main/java/forge/download/GuiDownloader.java b/forge-gui-desktop/src/main/java/forge/download/GuiDownloader.java index 9c53c99f482..04671d2e01e 100644 --- a/forge-gui-desktop/src/main/java/forge/download/GuiDownloader.java +++ b/forge-gui-desktop/src/main/java/forge/download/GuiDownloader.java @@ -37,9 +37,10 @@ import forge.toolbox.FRadioButton; import forge.toolbox.FSkin; import forge.toolbox.FTextField; import forge.toolbox.JXButtonPanel; -import forge.util.Callback; import net.miginfocom.swing.MigLayout; +import java.util.function.Consumer; + @SuppressWarnings("serial") public class GuiDownloader extends DefaultBoundedRangeModel { // Swing components @@ -57,7 +58,7 @@ public class GuiDownloader extends DefaultBoundedRangeModel { SOverlayUtils.hideOverlay(); if (callback != null) { - callback.run(btnStart.getText() == "OK"); //determine result based on whether download finished + callback.accept(btnStart.getText() == "OK"); //determine result based on whether download finished } } }; @@ -70,12 +71,12 @@ public class GuiDownloader extends DefaultBoundedRangeModel { private final FRadioButton radProxyHTTP = new FRadioButton("HTTP Proxy"); private final GuiDownloadService service; - private final Callback callback; + private final Consumer callback; public GuiDownloader(final GuiDownloadService service0) { this(service0, null); } - public GuiDownloader(final GuiDownloadService service0, final Callback callback0) { + public GuiDownloader(final GuiDownloadService service0, final Consumer callback0) { service = service0; callback = callback0; diff --git a/forge-gui-desktop/src/main/java/forge/itemmanager/views/ColorSetRenderer.java b/forge-gui-desktop/src/main/java/forge/itemmanager/views/ColorSetRenderer.java index 6735e15f1ec..f449f6e8418 100644 --- a/forge-gui-desktop/src/main/java/forge/itemmanager/views/ColorSetRenderer.java +++ b/forge-gui-desktop/src/main/java/forge/itemmanager/views/ColorSetRenderer.java @@ -6,7 +6,7 @@ import java.awt.Graphics; import javax.swing.JTable; import forge.card.ColorSet; -import forge.card.mana.ManaCostShard; +import forge.card.MagicColor; import forge.toolbox.CardFaceSymbols; public class ColorSetRenderer extends ItemCellRenderer { @@ -33,7 +33,7 @@ public class ColorSetRenderer extends ItemCellRenderer { this.cs = (ColorSet) value; } else { - this.cs = ColorSet.getNullColor(); + this.cs = ColorSet.C; } this.setToolTipText(cs.toString()); return super.getTableCellRendererComponent(table, "", isSelected, hasFocus, row, column); @@ -57,8 +57,8 @@ public class ColorSetRenderer extends ItemCellRenderer { final int offsetIfNoSpace = cntGlyphs > 1 ? (cellWidth - padding0 - elemtWidth) / (cntGlyphs - 1) : elemtWidth + elemtGap; final int dx = Math.min(elemtWidth + elemtGap, offsetIfNoSpace); - for (final ManaCostShard s : cs.getOrderedShards()) { - CardFaceSymbols.drawManaSymbol(s.getImageKey(), g, x, y); + for (final MagicColor.Color s : cs.getOrderedColors()) { + CardFaceSymbols.drawManaSymbol(s.getShortName(), g, x, y); x += dx; } } diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/AddBasicLandsDialog.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/AddBasicLandsDialog.java index 7cff8386751..e2aa334956a 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/AddBasicLandsDialog.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/AddBasicLandsDialog.java @@ -68,7 +68,7 @@ public class AddBasicLandsDialog { private static final int LAND_PANEL_PADDING = 3; private final FComboBoxPanel cbLandSet = new FComboBoxPanel<>(Localizer.getInstance().getMessage("lblLandSet") + ":", FlowLayout.CENTER, - IterableUtil.filter(StaticData.instance().getSortedEditions(), CardEdition.Predicates.hasBasicLands)); + IterableUtil.filter(StaticData.instance().getSortedEditions(), CardEdition::hasBasicLands)); private final MainPanel panel = new MainPanel(); private final LandPanel pnlPlains = new LandPanel("Plains"); diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java index a572911f12a..54bfaff992e 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java @@ -46,7 +46,6 @@ import forge.toolbox.*; import forge.util.Localizer; import forge.view.FDialog; import net.miginfocom.swing.MigLayout; -import org.apache.commons.lang3.StringUtils; import static forge.deck.DeckRecognizer.TokenType.*; @@ -523,7 +522,7 @@ public class DeckImport extends FDialog { else deck.setName(currentDeckName); } - host.getDeckController().loadDeck(deck, controller.getCreateNewDeck()); + host.getDeckController().loadDeck(deck, controller.getImportBehavior() != DeckImportController.ImportBehavior.MERGE); processWindowEvent(new WindowEvent(DeckImport.this, WindowEvent.WINDOW_CLOSING)); }); @@ -531,7 +530,7 @@ public class DeckImport extends FDialog { this.createNewDeckCheckbox.setSelected(false); this.createNewDeckCheckbox.addActionListener(e -> { boolean createNewDeck = createNewDeckCheckbox.isSelected(); - controller.setCreateNewDeck(createNewDeck); + controller.setImportBehavior(createNewDeck ? DeckImportController.ImportBehavior.CREATE_NEW : DeckImportController.ImportBehavior.MERGE); String cmdAcceptLabel = createNewDeck ? CREATE_NEW_DECK_CMD_LABEL : IMPORT_CARDS_CMD_LABEL; cmdAcceptButton.setText(cmdAcceptLabel); String smartCardArtChboxTooltip = createNewDeck ? SMART_CARDART_TT_NO_DECK : SMART_CARDART_TT_WITH_DECK; @@ -600,7 +599,7 @@ public class DeckImport extends FDialog { if (token.getType() == LIMITED_CARD) cssClass = WARN_MSG_CLASS; String statusMsg = String.format("%s", cssClass, - getTokenStatusMessage(token)); + controller.getTokenStatusMessage(token)); statusLbl.append(statusMsg); } @@ -740,12 +739,12 @@ public class DeckImport extends FDialog { private String toHTML(final DeckRecognizer.Token token) { if (token == null) return ""; - String tokenMsg = getTokenMessage(token); + String tokenMsg = controller.getTokenMessage(token); if (tokenMsg == null) return ""; - String tokenStatus = getTokenStatusMessage(token); + String tokenStatus = controller.getTokenStatusMessage(token); String cssClass = getTokenCSSClass(token.getType()); - if (tokenStatus.length() == 0) + if (tokenStatus.isEmpty()) tokenMsg = padEndWithHTMLSpaces(tokenMsg, 2*PADDING_TOKEN_MSG_LENGTH+10); else { tokenMsg = padEndWithHTMLSpaces(tokenMsg, PADDING_TOKEN_MSG_LENGTH); @@ -755,11 +754,6 @@ public class DeckImport extends FDialog { tokenMsg = String.format("%s", cssClass, token.getKey().toString(), tokenMsg); - if (tokenStatus == null) { - String tokenTag = String.format("%s", cssClass, tokenMsg); - return String.format("%s", tokenTag); - } - String tokenTag = "%s"; String tokenMsgTag = String.format(tokenTag, cssClass, tokenMsg); String tokenStatusTag; @@ -776,97 +770,6 @@ public class DeckImport extends FDialog { return String.format("%s%s", targetMsg, spacer); } - private String getTokenMessage(DeckRecognizer.Token token) { - switch (token.getType()) { - case LEGAL_CARD: - case LIMITED_CARD: - case CARD_FROM_NOT_ALLOWED_SET: - case CARD_FROM_INVALID_SET: - return String.format("%s x %s %s", token.getQuantity(), token.getText(), getTokenFoilLabel(token)); - // Card Warning Msgs - case UNKNOWN_CARD: - case UNSUPPORTED_CARD: - return token.getQuantity() > 0 ? String.format("%s x %s", token.getQuantity(), token.getText()) - : token.getText(); - - case UNSUPPORTED_DECK_SECTION: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"), - Localizer.getInstance() - .getMessage("lblWarnDeckSectionNotAllowedInEditor", token.getText(), - this.currentGameType)); - - // Special Case of Card moved into another section (e.g. Commander from Sideboard) - case WARNING_MESSAGE: - return String.format("%s: %s", Localizer.getInstance() - .getMessage("lblWarningMsgPrefix"), token.getText()); - - // Placeholders - case DECK_SECTION_NAME: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblDeckSection"), - token.getText()); - - case CARD_RARITY: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblRarity"), - token.getText()); - - case CARD_TYPE: - case CARD_CMC: - case MANA_COLOUR: - case COMMENT: - return token.getText(); - - case DECK_NAME: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblDeckName"), - token.getText()); - - case UNKNOWN_TEXT: - default: - return null; - - } - } - - private String getTokenStatusMessage(DeckRecognizer.Token token){ - if (token == null) - return ""; - - switch (token.getType()) { - case LIMITED_CARD: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"), - Localizer.getInstance().getMessage("lblWarnLimitedCard", - StringUtils.capitalize(token.getLimitedCardType().name()), getGameFormatLabel())); - - case CARD_FROM_NOT_ALLOWED_SET: - return Localizer.getInstance().getMessage("lblErrNotAllowedCard", getGameFormatLabel()); - - case CARD_FROM_INVALID_SET: - return Localizer.getInstance().getMessage("lblErrCardEditionDate"); - - case UNSUPPORTED_CARD: - return Localizer.getInstance().getMessage("lblErrUnsupportedCard", this.currentGameType); - - case UNKNOWN_CARD: - return String.format("%s: %s", Localizer.getInstance().getMessage("lblWarningMsgPrefix"), - Localizer.getInstance().getMessage("lblWarnUnknownCardMsg")); - - case UNSUPPORTED_DECK_SECTION: - case WARNING_MESSAGE: - case COMMENT: - case CARD_CMC: - case MANA_COLOUR: - case CARD_TYPE: - case DECK_SECTION_NAME: - case CARD_RARITY: - case DECK_NAME: - case LEGAL_CARD: - case UNKNOWN_TEXT: - default: - return ""; - - } - - } - private String getTokenCSSClass(DeckRecognizer.TokenType tokenType){ switch (tokenType){ case LEGAL_CARD: @@ -899,17 +802,6 @@ public class DeckImport extends FDialog { return ""; } } - - private String getTokenFoilLabel(DeckRecognizer.Token token) { - if (!token.isCardToken()) - return ""; - final String foilMarker = "- (Foil)"; - return token.getCard().isFoil() ? foilMarker : ""; - } - - private String getGameFormatLabel() { - return String.format("\"%s\"", this.controller.getCurrentGameFormatName()); - } } class GameFormatDropdownRenderer extends JLabel implements ListCellRenderer { diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/sanctioned/CSubmenuDraft.java b/forge-gui-desktop/src/main/java/forge/screens/home/sanctioned/CSubmenuDraft.java index fca5a6aa053..757935d7602 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/sanctioned/CSubmenuDraft.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/sanctioned/CSubmenuDraft.java @@ -228,9 +228,9 @@ public enum CSubmenuDraft implements ICDoc { return; } + final DeckGroup opponentDecks = FModel.getDecks().getDraft().get(humanDeck.getName()); if (VSubmenuDraft.SINGLETON_INSTANCE.isSingleSelected()) { // Single opponent - final DeckGroup opponentDecks = FModel.getDecks().getDraft().get(humanDeck.getName()); int indx = 0; for (@SuppressWarnings("unused") Deck d : opponentDecks.getAiDecks()) { indx++; @@ -242,10 +242,16 @@ public enum CSubmenuDraft implements ICDoc { combo.addItem("Gauntlet"); //combo.addItem("Tournament"); } else { + int size = opponentDecks.getAiDecks().size(); combo.addItem("2"); - combo.addItem("3"); - combo.addItem("4"); - combo.addItem("5"); + if (size > 2) { + combo.addItem("3"); + } + + if (size >= 4) { + combo.addItem("4"); + combo.addItem("5"); + } } } } diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/sanctioned/VSubmenuSealed.java b/forge-gui-desktop/src/main/java/forge/screens/home/sanctioned/VSubmenuSealed.java index ccdee8c7efe..2fcecd97e60 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/sanctioned/VSubmenuSealed.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/sanctioned/VSubmenuSealed.java @@ -120,6 +120,7 @@ public enum VSubmenuSealed implements IVSubmenu { grpPanel.add(radAll, "w 200px!, h 30px!"); radSingle.setSelected(true); grpPanel.add(cbOpponent, "w 200px!, h 30px!"); + pnlStart.removeAll(); pnlStart.setLayout(new MigLayout("insets 0, gap 0, wrap 2")); pnlStart.setOpaque(false); pnlStart.add(grpPanel, "gapright 20"); diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java index 79b3652b991..bd76f15756b 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java @@ -1269,15 +1269,15 @@ public final class CMatchUI @Override public void notifyStackAddition(GameEventSpellAbilityCast event) { - SpellAbility sa = event.sa; + SpellAbility sa = event.sa(); String stackNotificationPolicy = FModel.getPreferences().getPref(FPref.UI_STACK_EFFECT_NOTIFICATION_POLICY); boolean isAi = sa.getActivatingPlayer().isAI(); boolean isTrigger = sa.isTrigger(); - int stackIndex = event.stackIndex; + int stackIndex = event.stackIndex(); if (stackIndex == nextNotifiableStackIndex) { if (ForgeConstants.STACK_EFFECT_NOTIFICATION_ALWAYS.equals(stackNotificationPolicy) || (ForgeConstants.STACK_EFFECT_NOTIFICATION_AI_AND_TRIGGERED.equals(stackNotificationPolicy) && (isAi || isTrigger))) { // We can go and show the modal - SpellAbilityStackInstance si = event.si; + SpellAbilityStackInstance si = event.si(); MigLayout migLayout = new MigLayout("insets 15, left, gap 30, fill"); JPanel mainPanel = new JPanel(migLayout); diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/VAssignGenericAmount.java b/forge-gui-desktop/src/main/java/forge/screens/match/VAssignGenericAmount.java index 07f46812b09..379a6604456 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/VAssignGenericAmount.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/VAssignGenericAmount.java @@ -214,22 +214,8 @@ public class VAssignGenericAmount { pnlTargets.add(mp, "w 145px!, h 170px!, gap 5px 5px 3px 3px, ax center"); mp.addMouseListener(mad); targetsMap.put(mp, at); - } else if (at.entity instanceof Byte) { - SkinImage manaSymbol; - byte color = (Byte) at.entity; - if (color == MagicColor.WHITE) { - manaSymbol = FSkin.getImage(FSkinProp.IMG_MANA_W); - } else if (color == MagicColor.BLUE) { - manaSymbol = FSkin.getImage(FSkinProp.IMG_MANA_U); - } else if (color == MagicColor.BLACK) { - manaSymbol = FSkin.getImage(FSkinProp.IMG_MANA_B); - } else if (color == MagicColor.RED) { - manaSymbol = FSkin.getImage(FSkinProp.IMG_MANA_R); - } else if (color == MagicColor.GREEN) { - manaSymbol = FSkin.getImage(FSkinProp.IMG_MANA_G); - } else { // Should never come here, but add this to avoid compile error - manaSymbol = FSkin.getImage(FSkinProp.IMG_MANA_COLORLESS); - } + } else if (at.entity instanceof MagicColor.Color color) { + SkinImage manaSymbol = FSkin.getImage(FSkinProp.MANA_IMG.get(color.getShortName())); final MiscCardPanel mp = new MiscCardPanel(matchUI, "", manaSymbol); mp.setCardBounds(0, 0, 70, 70); pnlTargets.add(mp, "w 100px!, h 150px!, gap 5px 5px 3px 3px, ax center"); diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/CardFaceSymbols.java b/forge-gui-desktop/src/main/java/forge/toolbox/CardFaceSymbols.java index 5ba9090ad71..59dcd09375f 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/CardFaceSymbols.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/CardFaceSymbols.java @@ -8,6 +8,7 @@ import java.util.StringTokenizer; import com.esotericsoftware.minlog.Log; import forge.card.ColorSet; +import forge.card.MagicColor; import forge.card.mana.ManaCost; import forge.card.mana.ManaCostShard; import forge.gui.GuiBase; @@ -204,9 +205,9 @@ public class CardFaceSymbols { } public static void drawColorSet(Graphics g, ColorSet colorSet, int x, int y, int imageSize, boolean vertical) { - for (final ManaCostShard s : colorSet.getOrderedShards()) { - if (DECK_COLORSET.get(s.getImageKey())!=null) - FSkin.drawImage(g, DECK_COLORSET.get(s.getImageKey()), x, y, imageSize, imageSize); + for (final MagicColor.Color s : colorSet.getOrderedColors()) { + if (DECK_COLORSET.get(s.getShortName())!=null) + FSkin.drawImage(g, DECK_COLORSET.get(s.getShortName()), x, y, imageSize, imageSize); if (!vertical) x += imageSize; else diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/special/PlayerDetailsPanel.java b/forge-gui-desktop/src/main/java/forge/toolbox/special/PlayerDetailsPanel.java index d04d889a77d..df0d987c483 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/special/PlayerDetailsPanel.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/special/PlayerDetailsPanel.java @@ -69,22 +69,7 @@ public class PlayerDetailsPanel extends JPanel { } public static FSkinProp iconFromZone(ZoneType zoneType) { - switch (zoneType) { - case Hand: return FSkinProp.IMG_ZONE_HAND; - case Library: return FSkinProp.IMG_ZONE_LIBRARY; - case Graveyard: return FSkinProp.IMG_ZONE_GRAVEYARD; - case Exile: return FSkinProp.IMG_ZONE_EXILE; - case Sideboard: return FSkinProp.IMG_ZONE_SIDEBOARD; - case Flashback: return FSkinProp.IMG_ZONE_FLASHBACK; - case Command: return FSkinProp.IMG_ZONE_COMMAND; //IMG_PLANESWALKER - case PlanarDeck: return FSkinProp.IMG_ZONE_PLANAR; - case SchemeDeck: return FSkinProp.IMG_ZONE_SCHEME; - case AttractionDeck: return FSkinProp.IMG_ZONE_ATTRACTION; - case ContraptionDeck: return FSkinProp.IMG_ZONE_CONTRAPTION; - case Ante: return FSkinProp.IMG_ZONE_ANTE; - case Junkyard: return FSkinProp.IMG_ZONE_JUNKYARD; - default: return FSkinProp.IMG_HDZONE_LIBRARY; - } + return FSkinProp.iconFromZone(zoneType, false); } /** Adds various labels to pool area JPanel container. */ diff --git a/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java b/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java index 6fe9bd2e68b..b573dadf430 100644 --- a/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java +++ b/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java @@ -130,6 +130,10 @@ public class SimulateMatch { } } + if (params.containsKey("c")) { + rules.setSimTimeout(Integer.parseInt(params.get("c").get(0))); + } + sb.append(" - ").append(Lang.nounWithNumeral(nGames, "game")).append(" of ").append(type); System.out.println(sb.toString()); @@ -163,6 +167,7 @@ public class SimulateMatch { System.out.println("\tT - Type of tournament to run with all provided decks (Bracket, RoundRobin, Swiss)"); System.out.println("\tP - Amount of players per match (used only with Tournaments, defaults to 2)"); System.out.println("\tF - format of games, defaults to constructed"); + System.out.println("\tc - Clock flag. Set the maximum time in seconds before calling the match a draw, defaults to 120."); System.out.println("\tq - Quiet flag. Output just the game result, not the entire game log."); } @@ -176,7 +181,7 @@ public class SimulateMatch { TimeLimitedCodeBlock.runWithTimeout(() -> { mc.startGame(g1); sw.stop(); - }, 120, TimeUnit.SECONDS); + }, mc.getRules().getSimTimeout(), TimeUnit.SECONDS); } catch (TimeoutException e) { System.out.println("Stopping slow match as draw"); } catch (Exception | StackOverflowError e) { diff --git a/forge-gui-desktop/src/test/java/forge/ai/AIIntegrationTests.java b/forge-gui-desktop/src/test/java/forge/ai/AIIntegrationTests.java new file mode 100644 index 00000000000..9f24117977f --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/ai/AIIntegrationTests.java @@ -0,0 +1,80 @@ +package forge.ai; + +import forge.game.Game; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.zone.ZoneType; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +public class AIIntegrationTests extends AITest { + @Test + public void testSwingForLethal() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + p.setTeam(0); + addCard("Nest Robber", p); + addCard("Nest Robber", p); + + Player opponent = game.getPlayers().get(0); + opponent.setTeam(1); + + addCard("Runeclaw Bear", opponent); + opponent.setLife(2, null); + + this.playUntilPhase(game, PhaseType.END_OF_TURN); + + AssertJUnit.assertTrue(game.isGameOver()); + } + + @Test + public void testSuspendAI() { + // Test that the AI can cast a suspend spell + // They should suspend it, then deal three damage to their opponent + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + p.setTeam(0); + addCard("Mountain", p); + addCardToZone("Rift Bolt", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + opponent.setTeam(1); + + // Fill deck with lands so we can goldfish a few turns + for (int i = 0; i < 60; i++) { + addCardToZone("Island", opponent, ZoneType.Library); + // Add something they can't cast + addCardToZone("Stone Golem", p, ZoneType.Library); + } + + for (int i = 0; i < 3; i++) { + this.playUntilNextTurn(game); + } + + AssertJUnit.assertEquals(17, opponent.getLife()); + } + + @Test + public void testAttackTriggers() { + // Test that attack triggers actually trigger + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + p.setTeam(0); + addCard("Hellrider", p); + addCard("Raging Goblin", p); + + Player opponent = game.getPlayers().get(0); + opponent.setTeam(1); + + // Fill deck with lands so we can goldfish a few turns + for (int i = 0; i < 60; i++) { + addCardToZone("Island", opponent, ZoneType.Library); + // Add something they can't cast + addCardToZone("Stone Golem", p, ZoneType.Library); + } + + this.playUntilNextTurn(game); + + AssertJUnit.assertEquals(14, opponent.getLife()); + } +} diff --git a/forge-gui-desktop/src/test/java/forge/ai/AITest.java b/forge-gui-desktop/src/test/java/forge/ai/AITest.java new file mode 100644 index 00000000000..23588815a25 --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/ai/AITest.java @@ -0,0 +1,190 @@ +package forge.ai; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.Lists; + +import forge.GuiDesktop; +import forge.StaticData; +import forge.deck.Deck; +import forge.game.Game; +import forge.game.GameRules; +import forge.game.GameStage; +import forge.game.GameType; +import forge.game.Match; +import forge.game.card.Card; +import forge.game.card.CardCollectionView; +import forge.game.card.CardFactory; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.player.RegisteredPlayer; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; +import forge.gui.GuiBase; +import forge.item.IPaperCard; +import forge.item.PaperToken; +import forge.localinstance.properties.ForgePreferences.FPref; +import forge.model.FModel; + +public class AITest { + private static boolean initialized = false; + + public Game resetGame() { + // need to be done after FModel.initialize, or the Localizer isn't loaded yet + List players = Lists.newArrayList(); + Deck d1 = new Deck(); + players.add(new RegisteredPlayer(d1).setPlayer(new LobbyPlayerAi("p2", null))); + players.add(new RegisteredPlayer(d1).setPlayer(new LobbyPlayerAi("p1", null))); + GameRules rules = new GameRules(GameType.Constructed); + Match match = new Match(rules, players, "Test"); + Game game = new Game(players, rules, match); + Player p = game.getPlayers().get(1); + game.setAge(GameStage.Play); + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getPhaseHandler().onStackResolved(); + // game.getAction().checkStateEffects(true); + + return game; + } + + protected Game initAndCreateGame() { + if (!initialized) { + GuiBase.setInterface(new GuiDesktop()); + FModel.initialize(null, preferences -> { + preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false); + preferences.setPref(FPref.UI_LANGUAGE, "en-US"); + return null; + }); + initialized = true; + } + + return resetGame(); + } + + protected int countCardsWithName(Game game, String name) { + int i = 0; + for (Card c : game.getCardsIn(ZoneType.Battlefield)) { + if (c.getName().equals(name)) { + i++; + } + } + return i; + } + + protected Card findCardWithName(Game game, String name) { + for (Card c : game.getCardsIn(ZoneType.Battlefield)) { + if (c.getName().equals(name)) { + return c; + } + } + return null; + } + + + protected SpellAbility findSAWithPrefix(Card c, String prefix) { + return findSAWithPrefix(c.getSpellAbilities(), prefix); + } + + protected SpellAbility findSAWithPrefix(Iterable abilities, String prefix) { + for (SpellAbility sa : abilities) { + if (sa.getDescription().startsWith(prefix)) { + return sa; + } + } + return null; + } + + protected Card createCard(String name, Player p) { + IPaperCard paperCard = FModel.getMagicDb().getCommonCards().getCard(name); + if (paperCard == null) { + StaticData.instance().attemptToLoadCard(name); + paperCard = FModel.getMagicDb().getCommonCards().getCard(name); + } + return Card.fromPaperCard(paperCard, p); + } + + protected Card addCardToZone(String name, Player p, ZoneType zone) { + Card c = createCard(name, p); + // card need a new Timestamp otherwise Static Abilities might collide + c.setGameTimestamp(p.getGame().getNextTimestamp()); + p.getZone(zone).add(c); + return c; + } + + protected Card addCard(String name, Player p) { + return addCardToZone(name, p, ZoneType.Battlefield); + } + + protected List addCards(String name, int count, Player p) { + List cards = Lists.newArrayList(); + for (int i = 0; i < count; i++) { + cards.add(addCard(name, p)); + } + return cards; + } + + protected Card createToken(String name, Player p) { + PaperToken token = FModel.getMagicDb().getAllTokens().getToken(name); + if (token == null) { + System.out.println("Failed to find token name " + name); + return null; + } + return CardFactory.getCard(token, p, p.getGame()); + } + + protected List addTokens(String name, int amount, Player p) { + List cards = new ArrayList<>(); + + for(int i = 0; i < amount; i++) { + cards.add(addToken(name, p)); + } + + return cards; + } + + protected Card addToken(String name, Player p) { + Card c = createToken(name, p); + // card need a new Timestamp otherwise Static Abilities might collide + c.setGameTimestamp(p.getGame().getNextTimestamp()); + p.getZone(ZoneType.Battlefield).add(c); + return c; + } + + void playUntilStackClear(Game game) { + do { + game.getPhaseHandler().mainLoopStep(); + } while (!game.isGameOver() && !game.getStack().isEmpty()); + } + + void playUntilPhase(Game game, PhaseType phase) { + do { + game.getPhaseHandler().mainLoopStep(); + } while (!game.isGameOver() && !game.getPhaseHandler().is(phase)); + } + + void playUntilNextTurn(Game game) { + Player current = game.getPhaseHandler().getPlayerTurn(); + do { + game.getPhaseHandler().mainLoopStep(); + } while (!game.isGameOver() && game.getPhaseHandler().getPlayerTurn().equals(current)); + } + + protected String gameStateToString(Game game) { + StringBuilder sb = new StringBuilder(); + for (ZoneType zone : ZoneType.values()) { + CardCollectionView cards = game.getCardsIn(zone); + if (!cards.isEmpty()) { + sb.append("Zone ").append(zone.name()).append(":\n"); + for (Card c : game.getCardsIn(zone)) { + sb.append(" ").append(c); + if (c.isTapped()) { + sb.append(" (T)"); + } + sb.append("\n"); + } + } + } + return sb.toString(); + } +} diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTest.java index d3481d2b46b..89a5551180e 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTest.java @@ -1,15 +1,13 @@ package forge.ai.simulation; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import com.google.common.collect.Lists; -import forge.GuiDesktop; -import forge.StaticData; import forge.ai.AIOption; +import forge.ai.AITest; import forge.ai.LobbyPlayerAi; import forge.ai.simulation.GameStateEvaluator.Score; import forge.deck.Deck; @@ -18,21 +16,12 @@ import forge.game.GameRules; import forge.game.GameStage; import forge.game.GameType; import forge.game.Match; -import forge.game.card.Card; -import forge.game.card.CardCollectionView; -import forge.game.card.CardFactory; import forge.game.player.Player; import forge.game.player.RegisteredPlayer; -import forge.game.spellability.SpellAbility; -import forge.game.zone.ZoneType; -import forge.gui.GuiBase; -import forge.item.IPaperCard; -import forge.item.PaperToken; import forge.localinstance.properties.ForgePreferences.FPref; import forge.model.FModel; -public class SimulationTest { - private static boolean initialized = false; +public class SimulationTest extends AITest { public Game resetGame() { // need to be done after FModel.initialize, or the Localizer isn't loaded yet @@ -53,19 +42,6 @@ public class SimulationTest { return game; } - protected Game initAndCreateGame() { - if (!initialized) { - GuiBase.setInterface(new GuiDesktop()); - FModel.initialize(null, preferences -> { - preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false); - preferences.setPref(FPref.UI_LANGUAGE, "en-US"); - return null; - }); - initialized = true; - } - - return resetGame(); - } protected GameSimulator createSimulator(Game game, Player p) { return new GameSimulator(new SimulationController(new Score(0)) { @@ -75,106 +51,4 @@ public class SimulationTest { } }, game, p, null); } - - protected int countCardsWithName(Game game, String name) { - int i = 0; - for (Card c : game.getCardsIn(ZoneType.Battlefield)) { - if (c.getName().equals(name)) { - i++; - } - } - return i; - } - - protected Card findCardWithName(Game game, String name) { - for (Card c : game.getCardsIn(ZoneType.Battlefield)) { - if (c.getName().equals(name)) { - return c; - } - } - return null; - } - - protected String gameStateToString(Game game) { - StringBuilder sb = new StringBuilder(); - for (ZoneType zone : ZoneType.values()) { - CardCollectionView cards = game.getCardsIn(zone); - if (!cards.isEmpty()) { - sb.append("Zone ").append(zone.name()).append(":\n"); - for (Card c : game.getCardsIn(zone)) { - sb.append(" ").append(c).append("\n"); - } - } - } - return sb.toString(); - } - - protected SpellAbility findSAWithPrefix(Card c, String prefix) { - return findSAWithPrefix(c.getSpellAbilities(), prefix); - } - - protected SpellAbility findSAWithPrefix(Iterable abilities, String prefix) { - for (SpellAbility sa : abilities) { - if (sa.getDescription().startsWith(prefix)) { - return sa; - } - } - return null; - } - - protected Card createCard(String name, Player p) { - IPaperCard paperCard = FModel.getMagicDb().getCommonCards().getCard(name); - if (paperCard == null) { - StaticData.instance().attemptToLoadCard(name); - paperCard = FModel.getMagicDb().getCommonCards().getCard(name); - } - return Card.fromPaperCard(paperCard, p); - } - - protected Card addCardToZone(String name, Player p, ZoneType zone) { - Card c = createCard(name, p); - // card need a new Timestamp otherwise Static Abilities might collide - c.setGameTimestamp(p.getGame().getNextTimestamp()); - p.getZone(zone).add(c); - return c; - } - - protected Card addCard(String name, Player p) { - return addCardToZone(name, p, ZoneType.Battlefield); - } - - protected List addCards(String name, int count, Player p) { - List cards = Lists.newArrayList(); - for (int i = 0; i < count; i++) { - cards.add(addCard(name, p)); - } - return cards; - } - - protected Card createToken(String name, Player p) { - PaperToken token = FModel.getMagicDb().getAllTokens().getToken(name); - if (token == null) { - System.out.println("Failed to find token name " + name); - return null; - } - return CardFactory.getCard(token, p, p.getGame()); - } - - protected List addTokens(String name, int amount, Player p) { - List cards = new ArrayList<>(); - - for(int i = 0; i < amount; i++) { - cards.add(addToken(name, p)); - } - - return cards; - } - - protected Card addToken(String name, Player p) { - Card c = createToken(name, p); - // card need a new Timestamp otherwise Static Abilities might collide - c.setGameTimestamp(p.getGame().getNextTimestamp()); - p.getZone(ZoneType.Battlefield).add(c); - return c; - } } diff --git a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java index f4869226e1b..4d84a599a72 100644 --- a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java +++ b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java @@ -471,12 +471,12 @@ public class PlayerControllerForTests extends PlayerController { @Override public byte chooseColor(String message, SpellAbility sa, ColorSet colors) { - return Iterables.getFirst(colors, MagicColor.WHITE); + return Iterables.getFirst(colors, MagicColor.Color.WHITE).getColorMask(); } @Override public byte chooseColorAllowColorless(String message, Card card, ColorSet colors) { - return Iterables.getFirst(colors, (byte)0); + return Iterables.getFirst(colors, MagicColor.Color.COLORLESS).getColorMask(); } private CardCollection chooseItems(CardCollectionView items, int amount) { @@ -556,7 +556,7 @@ public class PlayerControllerForTests extends PlayerController { @Override public CounterType chooseCounterType(List options, SpellAbility sa, String prompt, Map params) { - return Iterables.getFirst(options, CounterType.get(CounterEnumType.P1P1)); + return Iterables.getFirst(options, CounterEnumType.P1P1); } @Override @@ -664,6 +664,11 @@ public class PlayerControllerForTests extends PlayerController { // TODO test this! } + @Override + public void revealUnsupported(Map> unsupported) { + // test this! + } + @Override public List chooseCardsYouWonToAddToDeck(List losses) { // TODO Auto-generated method stub diff --git a/forge-gui-mobile-dev/pom.xml b/forge-gui-mobile-dev/pom.xml index 1c287064cdb..34ac5a2cfca 100644 --- a/forge-gui-mobile-dev/pom.xml +++ b/forge-gui-mobile-dev/pom.xml @@ -242,7 +242,7 @@ com.github.oshi oshi-core - 6.8.3 + 6.9.0 diff --git a/forge-gui-mobile-dev/src/forge/app/GameLauncher.java b/forge-gui-mobile-dev/src/forge/app/GameLauncher.java index 08950da1eca..e6cda554356 100644 --- a/forge-gui-mobile-dev/src/forge/app/GameLauncher.java +++ b/forge-gui-mobile-dev/src/forge/app/GameLauncher.java @@ -51,7 +51,7 @@ public class GameLauncher { os.setBuild(si.getOperatingSystem().getVersionInfo().getBuildNumber()); os.setRawDescription(si.getOperatingSystem() + " x" + si.getOperatingSystem().getBitness()); totalRAM = Math.round(si.getHardware().getMemory().getTotal() / 1024f / 1024f); - hw = new HWInfo(device, os); + hw = new HWInfo(device, os, false); } catch (Exception e) { e.printStackTrace(); } diff --git a/forge-gui-mobile/src/forge/Forge.java b/forge-gui-mobile/src/forge/Forge.java index d04cb4c62d8..70baa9480c6 100644 --- a/forge-gui-mobile/src/forge/Forge.java +++ b/forge-gui-mobile/src/forge/Forge.java @@ -53,6 +53,7 @@ import io.sentry.Sentry; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; +import java.util.function.Consumer; public class Forge implements ApplicationListener { private static ApplicationListener app = null; @@ -100,6 +101,7 @@ public class Forge implements ApplicationListener { public static boolean allowCardBG = false; public static boolean altPlayerLayout = false; public static boolean altZoneTabs = false; + public static String altZoneTabMode = "Off"; public static boolean animatedCardTapUntap = false; public static String enableUIMask = "Crop"; public static String selector = "Default"; @@ -149,7 +151,7 @@ public class Forge implements ApplicationListener { scope.getContexts().setOperatingSystem(hwInfo.os()); }); } - GuiBase.setDeviceInfo(hwInfo, AndroidAPI, totalRAM); + GuiBase.setDeviceInfo(hwInfo, AndroidAPI, totalRAM, deviceAdapter.getDownloadsDir()); } return app; } @@ -170,18 +172,7 @@ public class Forge implements ApplicationListener { //install our error handler ExceptionHandler.registerErrorHandling(); //init hwInfo to log - HWInfo info = GuiBase.getHWInfo(); - if (info != null) { - System.out.println( - "##########################################\n" + - "APP: Forge v." + GuiBase.getInterface().getCurrentVersion() + - "\nDEV: " + info.device().getName() + - "\nCPU: " + info.device().getCpuDescription() + - "\nRAM: " + GuiBase.getDeviceRAM() + " MB" + - "\nOS: " + info.os().getRawDescription() + - "\n##########################################" - ); - } + System.out.println(GuiBase.getHWInfo()); // closeSplashScreen() is called early on non-Windows OS so it will not crash, LWJGL3 bug on AWT Splash. if (OperatingSystem.isWindows()) getDeviceAdapter().closeSplashScreen(); @@ -232,7 +223,7 @@ public class Forge implements ApplicationListener { reversedPrompt = getForgePreferences().getPrefBoolean(FPref.UI_REVERSE_PROMPT_BUTTON); autoAIDeckSelection = getForgePreferences().getPrefBoolean(FPref.UI_AUTO_AIDECK_SELECTION); altPlayerLayout = getForgePreferences().getPrefBoolean(FPref.UI_ALT_PLAYERINFOLAYOUT); - altZoneTabs = getForgePreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS); + setAltZoneTabMode(getForgePreferences().getPref(FPref.UI_ALT_PLAYERZONETABS)); animatedCardTapUntap = getForgePreferences().getPrefBoolean(FPref.UI_ANIMATED_CARD_TAPUNTAP); enableUIMask = getForgePreferences().getPref(FPref.UI_ENABLE_BORDER_MASKING); if (getForgePreferences().getPref(FPref.UI_ENABLE_BORDER_MASKING).equals("true")) //override old settings if not updated @@ -270,6 +261,17 @@ public class Forge implements ApplicationListener { FThreads.invokeInBackgroundThread(() -> AssetsDownloader.checkForUpdates(exited, runnable)); } } + public static void setAltZoneTabMode(String mode) { + Forge.altZoneTabMode = mode; + switch (Forge.altZoneTabMode) { + case "Vertical", "Horizontal" -> Forge.altZoneTabs = true; + case "Off" -> Forge.altZoneTabs = false; + default -> Forge.altZoneTabs = false; + } + } + public static boolean isHorizontalTabLayout() { + return Forge.altZoneTabs && "Horizontal".equalsIgnoreCase(Forge.altZoneTabMode); + } public static boolean hasGamepad() { //Classic Mode Various Screen GUI are not yet supported, needs control mapping for each screens if (isMobileAdventureMode) { @@ -347,8 +349,11 @@ public class Forge implements ApplicationListener { GuiBase.setIsAdventureMode(true); advStartup = false; isMobileAdventureMode = true; - if (GuiBase.isAndroid()) //force it for adventure mode - altZoneTabs = true; + //force it for adventure mode if the prefs is not updated from boolean value to string value + if ("true".equalsIgnoreCase(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS)) || + "false".equalsIgnoreCase(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS))) { + setAltZoneTabMode("Vertical"); + } //pixl cursor for adventure setCursor(null, "0"); if (!GuiBase.isAndroid() || !getDeviceAdapter().getGamepads().isEmpty()) @@ -599,22 +604,19 @@ public class Forge implements ApplicationListener { } if(currentScreen == null) return; - currentScreen.onClose(new Callback<>() { - @Override - public void run(Boolean result) { - if (result) { - Dscreens.pollFirst(); - setCurrentScreen(Dscreens.peekFirst()); - if (clearLastMatch) { - try { - Dscreens.remove(lastMatch); - } catch (Exception e) { - e.printStackTrace(); - } - //check - /*for (FScreen fScreen : Dscreens) - System.out.println(fScreen.toString());*/ + currentScreen.onClose(result -> { + if (result) { + Dscreens.pollFirst(); + setCurrentScreen(Dscreens.peekFirst()); + if (clearLastMatch) { + try { + Dscreens.remove(lastMatch); + } catch (Exception e) { + e.printStackTrace(); } + //check + /*for (FScreen fScreen : Dscreens) + System.out.println(fScreen.toString());*/ } } }); @@ -639,19 +641,16 @@ public class Forge implements ApplicationListener { return; } //don't allow exiting multiple times - Callback callback = new Callback() { - @Override - public void run(Boolean result) { - if (result) { - exited = true; - exitAnimation(true); - } + Consumer callback = result -> { + if (result) { + exited = true; + exitAnimation(true); } }; if (silent) { - callback.run(true); + callback.accept(true); } else { FOptionPane.showConfirmDialog( getLocalizer().getMessage("lblAreYouSureYouWishRestartForge"), getLocalizer().getMessage("lblRestartForge"), @@ -668,18 +667,15 @@ public class Forge implements ApplicationListener { options.add(getLocalizer().getMessage("lblExit")); options.add(getLocalizer().getMessage("lblCancel")); - Callback callback = new Callback() { - @Override - public void run(Integer result) { - if (result == 0) { - exited = true; - exitAnimation(false); - } + Consumer callback = result -> { + if (result == 0) { + exited = true; + exitAnimation(false); } }; if (silent) { - callback.run(0); + callback.accept(0); } else { FOptionPane.showOptionDialog(getLocalizer().getMessage("lblAreYouSureYouWishExitForge"), "", FOptionPane.QUESTION_ICON, options, 0, callback); @@ -701,30 +697,27 @@ public class Forge implements ApplicationListener { return; } - currentScreen.onSwitchAway(new Callback() { - @Override - public void run(Boolean result) { - if (result) { - if (replaceBackScreen && !Dscreens.isEmpty()) { - Dscreens.removeFirst(); - } - if (Dscreens.peekFirst() != screen0) { //prevent screen being its own back screen - Dscreens.addFirst(screen0); - } - setCurrentScreen(screen0); - if (screen0 instanceof MatchScreen) { - //set cursor for classic mode - if (!isMobileAdventureMode) { - if (magnifyToggle) { - setCursor(FSkin.getCursor().get(1), "1"); - } else { - setCursor(FSkin.getCursor().get(2), "2"); - } + currentScreen.onSwitchAway(result -> { + if (result) { + if (replaceBackScreen && !Dscreens.isEmpty()) { + Dscreens.removeFirst(); + } + if (Dscreens.peekFirst() != screen0) { //prevent screen being its own back screen + Dscreens.addFirst(screen0); + } + setCurrentScreen(screen0); + if (screen0 instanceof MatchScreen) { + //set cursor for classic mode + if (!isMobileAdventureMode) { + if (magnifyToggle) { + setCursor(FSkin.getCursor().get(1), "1"); + } else { + setCursor(FSkin.getCursor().get(2), "2"); } } - deltaTime = 0f; - hueFragTime = 0f; } + deltaTime = 0f; + hueFragTime = 0f; } }); } @@ -777,7 +770,7 @@ public class Forge implements ApplicationListener { isMobileAdventureMode = false; GuiBase.setIsAdventureMode(false); setCursor(FSkin.getCursor().get(0), "0"); - altZoneTabs = FModel.getPreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS); + setAltZoneTabMode(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS)); Gdx.input.setInputProcessor(getInputProcessor()); clearTransitionScreen(); openHomeDefault(); @@ -1490,7 +1483,7 @@ public class Forge implements ApplicationListener { boolean handled; if (KeyInputAdapter.isShiftKeyDown()) { - handled = pan(mouseMovedX, mouseMovedY, -Utils.AVG_FINGER_WIDTH * amountX, 0, false); + handled = pan(mouseMovedX, mouseMovedY, -Utils.AVG_FINGER_WIDTH * amountY, 0, false); } else { handled = pan(mouseMovedX, mouseMovedY, 0, -Utils.AVG_FINGER_HEIGHT * amountY, true); } diff --git a/forge-gui-mobile/src/forge/GuiMobile.java b/forge-gui-mobile/src/forge/GuiMobile.java index 2af4a82bf0a..e63362a1182 100644 --- a/forge-gui-mobile/src/forge/GuiMobile.java +++ b/forge-gui-mobile/src/forge/GuiMobile.java @@ -32,6 +32,7 @@ import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.function.Consumer; import java.util.function.Function; import org.jupnp.DefaultUpnpServiceConfiguration; @@ -282,7 +283,7 @@ public class GuiMobile implements IGuiBase { } @Override - public void download(final GuiDownloadService service, final Callback callback) { + public void download(final GuiDownloadService service, final Consumer callback) { new GuiDownloader(service, callback).show(); } diff --git a/forge-gui-mobile/src/forge/adventure/character/EntryActor.java b/forge-gui-mobile/src/forge/adventure/character/EntryActor.java index 94d30e9de2f..299b26b7e75 100644 --- a/forge-gui-mobile/src/forge/adventure/character/EntryActor.java +++ b/forge-gui-mobile/src/forge/adventure/character/EntryActor.java @@ -43,7 +43,7 @@ public class EntryActor extends MapActor{ { if(targetMap==null||targetMap.isEmpty()) { - stage.exitDungeon(false); + stage.exitDungeon(false, false); } else { diff --git a/forge-gui-mobile/src/forge/adventure/character/PortalActor.java b/forge-gui-mobile/src/forge/adventure/character/PortalActor.java index b3ad332b674..99d1dba680a 100644 --- a/forge-gui-mobile/src/forge/adventure/character/PortalActor.java +++ b/forge-gui-mobile/src/forge/adventure/character/PortalActor.java @@ -39,7 +39,7 @@ public class PortalActor extends EntryActor { } if (currentAnimationType == PortalAnimationTypes.Active) { if (targetMap == null || targetMap.isEmpty()) { - stage.exitDungeon(false); + stage.exitDungeon(false, false); } else { if (targetMap.equals(currentMap)) { stage.spawn(entryTargetObject); diff --git a/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java b/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java index fa96f36809d..9faae9e41d4 100644 --- a/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java +++ b/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java @@ -33,6 +33,7 @@ import java.util.stream.Collectors; public class AdventureEventData implements Serializable { @Serial private static final long serialVersionUID = 1L; + private static final int JUMPSTART_TO_PICK_FROM = 6; public transient BoosterDraft draft; public AdventureEventParticipant[] participants; public int rounds; @@ -80,7 +81,6 @@ public class AdventureEventData implements Serializable { matchesLost = other.matchesLost; } - public Deck[] getRewardPacks(int count) { Deck[] ret = new Deck[count]; for (int i = 0; i < count; i++) { @@ -113,164 +113,12 @@ public class AdventureEventData implements Serializable { return; cardBlockName = cardBlock.getName(); if (format == AdventureEventController.EventFormat.Draft) { - //Below all to be fully generated in later release - rewardPacks = getRewardPacks(3); - generateParticipants(7); - packConfiguration = getBoosterConfiguration(cardBlock); - - rewards = new AdventureEventReward[4]; - AdventureEventReward r0 = new AdventureEventReward(); - AdventureEventReward r1 = new AdventureEventReward(); - AdventureEventReward r2 = new AdventureEventReward(); - AdventureEventReward r3 = new AdventureEventReward(); - r0.minWins = 0; - r0.maxWins = 0; - r0.cardRewards = new Deck[]{rewardPacks[0]}; - rewards[0] = r0; - r1.minWins = 1; - r1.maxWins = 3; - r1.cardRewards = new Deck[]{rewardPacks[1], rewardPacks[2]}; - rewards[1] = r1; - r2.minWins = 2; - r2.maxWins = 3; - r2.itemRewards = new String[]{"Challenge Coin"}; - rewards[2] = r2; + setupDraftRewards(); } else if (format == AdventureEventController.EventFormat.Jumpstart) { - int numPacksToPickFrom = 6; - generateParticipants(7); - jumpstartBoosters = AdventureEventController.instance().getJumpstartBoosters(cardBlock, numPacksToPickFrom); - + jumpstartBoosters = AdventureEventController.instance().getJumpstartBoosters(cardBlock, JUMPSTART_TO_PICK_FROM); packConfiguration = new String[]{cardBlock.getLandSet().getCode(), cardBlock.getLandSet().getCode(), cardBlock.getLandSet().getCode()}; - for (AdventureEventParticipant participant : participants) { - List availableOptions = AdventureEventController.instance().getJumpstartBoosters(cardBlock, numPacksToPickFrom); - List chosenPacks = new ArrayList<>(); - - Map> themeMap = new HashMap<>(); - - //1. Search for matching themes from deck names, fill deck with them if possible - for (Deck option : availableOptions) { - // This matches up theme for all except DMU - with only 2 per color the next part will handle that - String theme = option.getName().replaceAll("\\d$", "").trim(); - if (!themeMap.containsKey(theme)) { - themeMap.put(theme, new ArrayList<>()); - } - themeMap.get(theme).add(option); - } - - String themeAdded = ""; - boolean done = false; - while (!done) { - for (int i = packConfiguration.length - chosenPacks.size(); i > 1; i--) { - if (themeAdded.isEmpty()) { - for (String theme : themeMap.keySet()) { - if (themeMap.get(theme).size() >= i) { - themeAdded = theme; - break; - } - } - } - } - if (themeAdded.isEmpty()) { - done = true; - } else { - chosenPacks.addAll(themeMap.get(themeAdded).subList(0, Math.min(themeMap.get(themeAdded).size(), packConfiguration.length - chosenPacks.size()))); - availableOptions.removeAll(themeMap.get(themeAdded)); - themeMap.remove(themeAdded); - themeAdded = ""; - } - } - - //2. Fill remaining slots with colors already picked whenever possible - Map> colorMap = new HashMap<>(); - for (Deck option : availableOptions) { - if (option.getTags().contains("black")) - colorMap.computeIfAbsent("black", (k) -> new ArrayList<>()).add(option); - if (option.getTags().contains("blue")) - colorMap.computeIfAbsent("blue", (k) -> new ArrayList<>()).add(option); - if (option.getTags().contains("green")) - colorMap.computeIfAbsent("green", (k) -> new ArrayList<>()).add(option); - if (option.getTags().contains("red")) - colorMap.computeIfAbsent("red", (k) -> new ArrayList<>()).add(option); - if (option.getTags().contains("white")) - colorMap.computeIfAbsent("white", (k) -> new ArrayList<>()).add(option); - if (option.getTags().contains("multicolor")) - colorMap.computeIfAbsent("multicolor", (k) -> new ArrayList<>()).add(option); - if (option.getTags().contains("colorless")) - colorMap.computeIfAbsent("colorless", (k) -> new ArrayList<>()).add(option); - } - - done = false; - String colorAdded = ""; - while (!done) { - List colorsAlreadyPicked = new ArrayList<>(); - for (Deck picked : chosenPacks) { - if (picked.getTags().contains("black")) colorsAlreadyPicked.add("black"); - if (picked.getTags().contains("blue")) colorsAlreadyPicked.add("blue"); - if (picked.getTags().contains("green")) colorsAlreadyPicked.add("green"); - if (picked.getTags().contains("red")) colorsAlreadyPicked.add("red"); - if (picked.getTags().contains("white")) colorsAlreadyPicked.add("white"); - if (picked.getTags().contains("multicolor")) colorsAlreadyPicked.add("multicolor"); - if (picked.getTags().contains("colorless")) colorsAlreadyPicked.add("colorless"); - } - - while (colorAdded.isEmpty() && !colorsAlreadyPicked.isEmpty()) { - String colorToTry = Aggregates.removeRandom(colorsAlreadyPicked); - for (Deck toCheck : availableOptions) { - if (toCheck.getTags().contains(colorToTry)) { - colorAdded = colorToTry; - chosenPacks.add(toCheck); - availableOptions.remove(toCheck); - break; - } - } - } - //3. If no matching color found and need more packs, add any available at random. - if (packConfiguration.length > chosenPacks.size() && colorAdded.isEmpty() && !availableOptions.isEmpty()) { - chosenPacks.add(Aggregates.removeRandom(availableOptions)); - } else { - done = colorAdded.isEmpty() || packConfiguration.length <= chosenPacks.size(); - } - colorAdded = ""; - - } - participant.registeredDeck = new Deck(); - for (Deck chosen : chosenPacks) { - participant.registeredDeck.getMain().addAllFlat(chosen.getMain().toFlatList()); - } - } - - rewards = new AdventureEventData.AdventureEventReward[4]; - AdventureEventData.AdventureEventReward r0 = new AdventureEventData.AdventureEventReward(); - AdventureEventData.AdventureEventReward r1 = new AdventureEventData.AdventureEventReward(); - AdventureEventData.AdventureEventReward r2 = new AdventureEventData.AdventureEventReward(); - AdventureEventData.AdventureEventReward r3 = new AdventureEventData.AdventureEventReward(); - - RewardData r0gold = new RewardData(); - r0gold.count = 100; - r0gold.type = "gold"; - r0.rewards = new RewardData[]{r0gold}; - r0.minWins = 1; - r0.maxWins = 1; - rewards[0] = r0; - RewardData r1gold = new RewardData(); - r1gold.count = 200; - r1gold.type = "gold"; - r1.rewards = new RewardData[]{r1gold}; - r1.minWins = 2; - r1.maxWins = 2; - rewards[1] = r1; - r2.minWins = 3; - r2.maxWins = 3; - RewardData r2gold = new RewardData(); - r2gold.count = 500; - r2gold.type = "gold"; - r2.rewards = new RewardData[]{r2gold}; - rewards[2] = r2; - r3.minWins = 0; - r3.maxWins = 3; - rewards[3] = r3; - //r3 will be the selected card packs + setupJumpstartRewards(); } switch (style) { @@ -302,7 +150,7 @@ public class AdventureEventData implements Serializable { Random placeholder = MyRandom.getRandom(); MyRandom.setRandom(getEventRandom()); if (draft == null && (eventStatus == AdventureEventController.EventStatus.Available || eventStatus == AdventureEventController.EventStatus.Entered)) { - draft = BoosterDraft.createDraft(LimitedPoolType.Block, getCardBlock(), packConfiguration); + draft = BoosterDraft.createDraft(LimitedPoolType.Block, getCardBlock(), packConfiguration, participants.length); registeredDeck = draft.getHumanPlayer().getDeck(); assignPlayerNames(draft); } @@ -328,6 +176,7 @@ public class AdventureEventData implements Serializable { private static final Predicate filterStandard = FModel.getFormats().getStandard().editionLegalPredicate; public static Predicate selectSetPool() { + // Should we negate any of these to avoid overlap? final int rollD100 = MyRandom.getRandom().nextInt(100); Predicate rolledFilter; if (rollD100 < 30) { @@ -440,6 +289,66 @@ public class AdventureEventData implements Serializable { return legalBlocks.isEmpty() ? null : Aggregates.random(legalBlocks); } + private void setupDraftRewards() { + //Below all to be fully generated in later release + rewardPacks = getRewardPacks(3); + if (cardBlock != null) { + packConfiguration = getBoosterConfiguration(cardBlock); + + rewards = new AdventureEventData.AdventureEventReward[4]; + AdventureEventData.AdventureEventReward r0 = new AdventureEventData.AdventureEventReward(); + AdventureEventData.AdventureEventReward r1 = new AdventureEventData.AdventureEventReward(); + AdventureEventData.AdventureEventReward r2 = new AdventureEventData.AdventureEventReward(); + AdventureEventData.AdventureEventReward r3 = new AdventureEventData.AdventureEventReward(); + r0.minWins = 0; + r0.maxWins = 0; + r0.cardRewards = new Deck[]{rewardPacks[0]}; + rewards[0] = r0; + r1.minWins = 1; + r1.maxWins = 3; + r1.cardRewards = new Deck[]{rewardPacks[1], rewardPacks[2]}; + rewards[1] = r1; + r2.minWins = 2; + r2.maxWins = 3; + r2.itemRewards = new String[]{"Challenge Coin"}; + rewards[2] = r2; + } + } + + private void setupJumpstartRewards() { + rewards = new AdventureEventData.AdventureEventReward[4]; + AdventureEventData.AdventureEventReward r0 = new AdventureEventData.AdventureEventReward(); + AdventureEventData.AdventureEventReward r1 = new AdventureEventData.AdventureEventReward(); + AdventureEventData.AdventureEventReward r2 = new AdventureEventData.AdventureEventReward(); + AdventureEventData.AdventureEventReward r3 = new AdventureEventData.AdventureEventReward(); + + RewardData r0gold = new RewardData(); + r0gold.count = 100; + r0gold.type = "gold"; + r0.rewards = new RewardData[]{r0gold}; + r0.minWins = 1; + r0.maxWins = 1; + rewards[0] = r0; + RewardData r1gold = new RewardData(); + r1gold.count = 200; + r1gold.type = "gold"; + r1.rewards = new RewardData[]{r1gold}; + r1.minWins = 2; + r1.maxWins = 2; + rewards[1] = r1; + r2.minWins = 3; + r2.maxWins = 3; + RewardData r2gold = new RewardData(); + r2gold.count = 500; + r2gold.type = "gold"; + r2.rewards = new RewardData[]{r2gold}; + rewards[2] = r2; + r3.minWins = 0; + r3.maxWins = 3; + rewards[3] = r3; + //r3 will be the selected card packs + } + public String[] getBoosterConfiguration(CardBlock selectedBlock) { Random placeholder = MyRandom.getRandom(); @@ -473,6 +382,105 @@ public class AdventureEventData implements Serializable { } participants[numberOfOpponents] = getHumanPlayer(); + + if (format == AdventureEventController.EventFormat.Jumpstart) { + for (AdventureEventParticipant participant : participants) { + List availableOptions = AdventureEventController.instance().getJumpstartBoosters(cardBlock, JUMPSTART_TO_PICK_FROM); + List chosenPacks = new ArrayList<>(); + + Map> themeMap = new HashMap<>(); + + //1. Search for matching themes from deck names, fill deck with them if possible + for (Deck option : availableOptions) { + // This matches up theme for all except DMU - with only 2 per color the next part will handle that + String theme = option.getName().replaceAll("\\d$", "").trim(); + if (!themeMap.containsKey(theme)) { + themeMap.put(theme, new ArrayList<>()); + } + themeMap.get(theme).add(option); + } + + String themeAdded = ""; + boolean done = false; + while (!done) { + for (int i = packConfiguration.length - chosenPacks.size(); i > 1; i--) { + if (themeAdded.isEmpty()) { + for (String theme : themeMap.keySet()) { + if (themeMap.get(theme).size() >= i) { + themeAdded = theme; + break; + } + } + } + } + if (themeAdded.isEmpty()) { + done = true; + } else { + chosenPacks.addAll(themeMap.get(themeAdded).subList(0, Math.min(themeMap.get(themeAdded).size(), packConfiguration.length - chosenPacks.size()))); + availableOptions.removeAll(themeMap.get(themeAdded)); + themeMap.remove(themeAdded); + themeAdded = ""; + } + } + + //2. Fill remaining slots with colors already picked whenever possible + Map> colorMap = new HashMap<>(); + for (Deck option : availableOptions) { + if (option.getTags().contains("black")) + colorMap.computeIfAbsent("black", (k) -> new ArrayList<>()).add(option); + if (option.getTags().contains("blue")) + colorMap.computeIfAbsent("blue", (k) -> new ArrayList<>()).add(option); + if (option.getTags().contains("green")) + colorMap.computeIfAbsent("green", (k) -> new ArrayList<>()).add(option); + if (option.getTags().contains("red")) + colorMap.computeIfAbsent("red", (k) -> new ArrayList<>()).add(option); + if (option.getTags().contains("white")) + colorMap.computeIfAbsent("white", (k) -> new ArrayList<>()).add(option); + if (option.getTags().contains("multicolor")) + colorMap.computeIfAbsent("multicolor", (k) -> new ArrayList<>()).add(option); + if (option.getTags().contains("colorless")) + colorMap.computeIfAbsent("colorless", (k) -> new ArrayList<>()).add(option); + } + + done = false; + String colorAdded = ""; + while (!done) { + List colorsAlreadyPicked = new ArrayList<>(); + for (Deck picked : chosenPacks) { + if (picked.getTags().contains("black")) colorsAlreadyPicked.add("black"); + if (picked.getTags().contains("blue")) colorsAlreadyPicked.add("blue"); + if (picked.getTags().contains("green")) colorsAlreadyPicked.add("green"); + if (picked.getTags().contains("red")) colorsAlreadyPicked.add("red"); + if (picked.getTags().contains("white")) colorsAlreadyPicked.add("white"); + if (picked.getTags().contains("multicolor")) colorsAlreadyPicked.add("multicolor"); + if (picked.getTags().contains("colorless")) colorsAlreadyPicked.add("colorless"); + } + + while (colorAdded.isEmpty() && !colorsAlreadyPicked.isEmpty()) { + String colorToTry = Aggregates.removeRandom(colorsAlreadyPicked); + for (Deck toCheck : availableOptions) { + if (toCheck.getTags().contains(colorToTry)) { + colorAdded = colorToTry; + chosenPacks.add(toCheck); + availableOptions.remove(toCheck); + break; + } + } + } + //3. If no matching color found and need more packs, add any available at random. + if (packConfiguration.length > chosenPacks.size() && colorAdded.isEmpty() && !availableOptions.isEmpty()) { + chosenPacks.add(Aggregates.removeRandom(availableOptions)); + } else { + done = colorAdded.isEmpty() || packConfiguration.length <= chosenPacks.size(); + } + colorAdded = ""; + } + participant.registeredDeck = new Deck(); + for (Deck chosen : chosenPacks) { + participant.registeredDeck.getMain().addAllFlat(chosen.getMain().toFlatList()); + } + } + } } private void assignPlayerNames(BoosterDraft draft) { @@ -525,6 +533,31 @@ public class AdventureEventData implements Serializable { } matches.get(round).add(match); } + } else if (style == AdventureEventController.EventStyle.RoundRobin) { + // In a roundrobin everyone plays everyone else once + // We do have this logic already in ForgeTOurnament, we should see if we could reuse it + matches.put(round, new ArrayList<>()); + activePlayers = Arrays.stream(participants).collect(Collectors.toList()); + + if (round > 1) { + AdventureEventParticipant pivot = activePlayers.remove(0); + for(int i = 1; i < round; i++) { + // Rotate X amount of players, where X is the current round-1 + AdventureEventParticipant rotate = activePlayers.remove(0); + activePlayers.add(rotate); + } + activePlayers.add(0, pivot); + } + + int numPlayers = activePlayers.size(); + for (int i = 0; i < numPlayers / 2; i++) { + AdventureEventMatch match = new AdventureEventMatch(); + match.p1 = activePlayers.get(i); + match.p2 = activePlayers.get(numPlayers - i - 1); + matches.get(round).add(match); + } + } else { + System.out.println(style + " not yet implemented!!!"); } return matches.get(currentRound); } @@ -587,7 +620,58 @@ public class AdventureEventData implements Serializable { } //todo: more robust logic for event types that can be won without perfect record (Swiss w/cut, round robin) - playerWon = matchesLost == 0 || matchesWon == rounds; + if (style == AdventureEventController.EventStyle.Bracket) { + playerWon = matchesLost == 0 || matchesWon == rounds; + } else if (style == AdventureEventController.EventStyle.RoundRobin) { + if (matchesWon == rounds) { + playerWon = true; + } else { + //If multiple players are tied for first, only the one with the best tiebreaker wins + List topPlayers = new ArrayList<>(); + int bestRecord = 0; + for (AdventureEventParticipant p : participants) { + if (p.wins > bestRecord) { + bestRecord = p.wins; + topPlayers.clear(); + topPlayers.add(p); + } else if (p.wins == bestRecord) { + topPlayers.add(p); + } + } + if (topPlayers.size() == 1) { + playerWon = topPlayers.get(0).getName().equals(getHumanPlayer().getName()); + } else { + //multiple players tied for first, use tiebreaker + Map tiebreakers = new HashMap<>(); + for (AdventureEventParticipant p : topPlayers) { + int tb = 0; + for (AdventureEventMatch m : matches.values().stream().flatMap(List::stream).collect(Collectors.toList())) { + if (m.p1 == p && m.winner != null && m.winner != p) { + tb += m.p2.wins; + } else if (m.p2 == p && m.winner != null && m.winner != p) { + tb += m.p1.wins; + } + } + tiebreakers.put(p, tb); + } + int bestTiebreaker = 0; + AdventureEventParticipant winner = null; + boolean tie = false; + for (AdventureEventParticipant p : tiebreakers.keySet()) { + if (tiebreakers.get(p) > bestTiebreaker) { + bestTiebreaker = tiebreakers.get(p); + winner = p; + tie = false; + } else if (tiebreakers.get(p) == bestTiebreaker) { + tie = true; + } + } + playerWon = !tie && winner != null && winner.getName().equals(getHumanPlayer().getName()); + } + } + } else { + playerWon = false; + } eventStatus = AdventureEventController.EventStatus.Awarded; } @@ -599,15 +683,18 @@ public class AdventureEventData implements Serializable { description += "Block: " + getCardBlock() + "\n"; description += "Boosters: " + String.join(", ", packConfiguration) + "\n"; description += "Competition Style: " + participants.length + " players, matches played as best of " + eventRules.gamesPerMatch + ", " + (eventRules.getPairingDescription()) + "\n\n"; - description += String.format("Pay 1 Entry Fee\n- Gold %d[][+Gold][BLACK]\n- Mana Shards %d[][+Shards][BLACK]\n", Math.round(eventRules.goldToEnter * townPriceModifier), Math.round(eventRules.shardsToEnter * townPriceModifier)); - if (eventRules.acceptsBronzeChallengeCoin) { - description += "- Bronze Challenge Coin [][+BronzeChallengeCoin][BLACK]\n\n"; - } else if (eventRules.acceptsSilverChallengeCoin) { - description += "- Silver Challenge Coin [][+SilverChallengeCoin][BLACK]\n\n"; - } else if (eventRules.acceptsChallengeCoin) { - description += "- Gold Challenge Coin [][+ChallengeCoin][BLACK]\n\n"; - } else { - description += "\n"; + + if (eventStatus == AdventureEventController.EventStatus.Available) { + description += String.format("Pay 1 Entry Fee\n- Gold %d[][+Gold][BLACK]\n- Mana Shards %d[][+Shards][BLACK]\n", Math.round(eventRules.goldToEnter * townPriceModifier), Math.round(eventRules.shardsToEnter * townPriceModifier)); + if (eventRules.acceptsBronzeChallengeCoin) { + description += "- Bronze Challenge Coin [][+BronzeChallengeCoin][BLACK]\n\n"; + } else if (eventRules.acceptsSilverChallengeCoin) { + description += "- Silver Challenge Coin [][+SilverChallengeCoin][BLACK]\n\n"; + } else if (eventRules.acceptsChallengeCoin) { + description += "- Gold Challenge Coin [][+ChallengeCoin][BLACK]\n\n"; + } else { + description += "\n"; + } } description += String.format("Prizes\nChampion: Keep drafted deck\n2+ round wins: Challenge Coin \n1+ round wins: %s Booster, %s Booster\n0 round wins: %s Booster", rewardPacks[0].getComment(), rewardPacks[1].getComment(), rewardPacks[2].getComment()); } else if (format == AdventureEventController.EventFormat.Jumpstart) { diff --git a/forge-gui-mobile/src/forge/adventure/data/DialogData.java b/forge-gui-mobile/src/forge/adventure/data/DialogData.java index 4064188e420..fedd70b6839 100644 --- a/forge-gui-mobile/src/forge/adventure/data/DialogData.java +++ b/forge-gui-mobile/src/forge/adventure/data/DialogData.java @@ -1,10 +1,9 @@ package forge.adventure.data; -import forge.util.Callback; - import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * Dialog Data JSON loader class. @@ -22,7 +21,7 @@ public class DialogData implements Serializable { public DialogData[] options = new DialogData[0]; //List of sub-dialogs. Show up as options in the current one. public boolean isDisabled = false; - public transient Callback callback; + public transient Consumer callback; public DialogData(){} public DialogData(DialogData other){ diff --git a/forge-gui-mobile/src/forge/adventure/data/RewardData.java b/forge-gui-mobile/src/forge/adventure/data/RewardData.java index b0dc93ce141..6ae9bbe6bda 100644 --- a/forge-gui-mobile/src/forge/adventure/data/RewardData.java +++ b/forge-gui-mobile/src/forge/adventure/data/RewardData.java @@ -2,12 +2,14 @@ package forge.adventure.data; import com.badlogic.gdx.utils.Array; import com.google.common.collect.Iterables; +import forge.ImageKeys; import forge.StaticData; import forge.adventure.util.*; import forge.adventure.world.WorldSave; import forge.card.CardEdition; import forge.deck.Deck; import forge.item.PaperCard; +import forge.item.PaperCardPredicates; import forge.model.FModel; import forge.util.IterableUtil; import forge.util.StreamUtil; @@ -16,7 +18,6 @@ import java.io.Serializable; import java.util.*; import java.util.function.Predicate; - /** * Data class that will be used to read Json configuration files * BiomeData @@ -96,11 +97,23 @@ public class RewardData implements Serializable { 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 - else - allCards = IterableUtil.filter(CardUtil.getFullCardPool(false), new CardUtil.CardPredicate(legals, true)); - //Filter out specific cards. + allCards = CardUtil.getFullCardPool(false); + + if(legals != null) + allCards = IterableUtil.filter(allCards, new CardUtil.CardPredicate(legals, true)); + + // Filter out by editions and obtainability + if (configData.allowedEditions != null && configData.allowedEditions.length > 0) { + allCards = IterableUtil.filter(allCards, PaperCardPredicates.printedInAnyEditions(configData.allowedEditions)); + } else if (configData.restrictedEditions != null && configData.restrictedEditions.length > 0) { + allCards = IterableUtil.filter(allCards, PaperCardPredicates.onlyPrintedInEditions(configData.restrictedEditions).negate()); + } else { + allCards = IterableUtil.filter(allCards, PaperCardPredicates.isObtainableAnyEdition()); + } + + Set restrictedCards = new HashSet<>(Arrays.asList(configData.restrictedCards)); + + // Filter out specific cards. allCards = IterableUtil.filter(allCards, input -> { if (input == null) return false; @@ -108,16 +121,13 @@ public class RewardData implements Serializable { return false; if (input.getRules().getAiHints().getRemNonCommanderDecks()) return false; - if (configData.allowedEditions != null) { - if (!Arrays.asList(configData.allowedEditions).contains(input.getEdition())) - return false; - } else if (Arrays.asList(configData.restrictedEditions).contains(input.getEdition())) + if (input.getRules().isCustom() && + input.getImageKey(false).startsWith(ImageKeys.ADVENTURECARD_PREFIX)) { return false; - return !Arrays.asList(configData.restrictedCards).contains(input.getName()); - }); + } - // Only allow obtainable cards - allCards = IterableUtil.filter(allCards, input -> StaticData.instance().getCardEdition(input.getEdition()).isCardObtainable(input.getCardName())); + return !restrictedCards.contains(input.getName()); + }); //Filter AI cards for enemies. allEnemyCards = IterableUtil.filter(allCards, input -> { diff --git a/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java b/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java index ccc0cd279c2..450d98d328e 100644 --- a/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java +++ b/forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java @@ -31,6 +31,7 @@ import forge.util.ItemPool; import java.io.Serializable; import java.util.*; +import java.util.function.Predicate; /** * Class that represents the player (not the player sprite) @@ -44,7 +45,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent { private int heroRace; private int avatarIndex; private boolean isFemale; - private ColorSet colorIdentity = ColorSet.ALL_COLORS; + private ColorSet colorIdentity = ColorSet.WUBRG; // Deck data private Deck deck; @@ -70,6 +71,9 @@ public class AdventurePlayer implements Serializable, SaveFileContent { private final HashMap equippedItems = new HashMap<>(); private final List quests = new ArrayList<>(); private final List events = new ArrayList<>(); + private final Set unsupportedCards = new HashSet<>(); + private final Predicate isUnsupported = pc -> pc != null && pc.getRules() != null && pc.getRules().isUnsupported(); + private final Predicate isValid = pc -> pc != null && pc.getRules() != null && !pc.getRules().isUnsupported(); // Fantasy/Chaos mode settings. private boolean fantasyMode = false; @@ -130,6 +134,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent { favoriteCards.clear(); AdventureEventController.clear(); AdventureQuestController.clear(); + unsupportedCards.clear(); } static public AdventurePlayer current() { @@ -307,6 +312,10 @@ public class AdventurePlayer implements Serializable, SaveFileContent { return colorIdentity.toString(); } + public Collection getUnsupportedCards() { + return unsupportedCards; + } + //Setters public void setWorldPosX(float worldPosX) { @@ -387,9 +396,9 @@ public class AdventurePlayer implements Serializable, SaveFileContent { if (temp != null) setColorIdentity(temp); else - colorIdentity = ColorSet.ALL_COLORS; + colorIdentity = ColorSet.WUBRG; } else - colorIdentity = ColorSet.ALL_COLORS; + colorIdentity = ColorSet.WUBRG; gold = data.readInt("gold"); maxLife = data.readInt("maxLife"); @@ -478,14 +487,24 @@ public class AdventurePlayer implements Serializable, SaveFileContent { } deck = new Deck(data.readString("deckName")); - deck.getMain().addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("deckCards")))); - if (data.containsKey("sideBoardCards")) - deck.getOrCreate(DeckSection.Sideboard).addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("sideBoardCards")))); - if(data.containsKey("attractionDeckCards")) - deck.getOrCreate(DeckSection.Attractions).addAll(CardPool.fromCardList(List.of((String[]) data.readObject("attractionDeckCards")))); - if(data.containsKey("contraptionDeckCards")) //TODO: Generalize this. Can't we just serialize the whole deck? - deck.getOrCreate(DeckSection.Contraptions).addAll(CardPool.fromCardList(List.of((String[]) data.readObject("contraptionDeckCards")))); - + CardPool deckCards = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("deckCards"))); + deck.getMain().addAll(deckCards.getFilteredPool(isValid)); + unsupportedCards.addAll(deckCards.getFilteredPool(isUnsupported).toFlatList()); + if (data.containsKey("sideBoardCards")) { + CardPool sideBoardCards = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("sideBoardCards"))); + deck.getOrCreate(DeckSection.Sideboard).addAll(sideBoardCards.getFilteredPool(isValid)); + unsupportedCards.addAll(sideBoardCards.getFilteredPool(isUnsupported).toFlatList()); + } + if (data.containsKey("attractionDeckCards")) { + CardPool attractionDeckCards = CardPool.fromCardList(List.of((String[]) data.readObject("attractionDeckCards"))); + deck.getOrCreate(DeckSection.Attractions).addAll(attractionDeckCards.getFilteredPool(isValid)); + unsupportedCards.addAll(attractionDeckCards.getFilteredPool(isUnsupported).toFlatList()); + } + if (data.containsKey("contraptionDeckCards")) {//TODO: Generalize this. Can't we just serialize the whole deck? + CardPool contraptionDeckCards = CardPool.fromCardList(List.of((String[]) data.readObject("contraptionDeckCards"))); + deck.getOrCreate(DeckSection.Contraptions).addAll(contraptionDeckCards.getFilteredPool(isValid)); + unsupportedCards.addAll(contraptionDeckCards.getFilteredPool(isUnsupported).toFlatList()); + } if (data.containsKey("characterFlagsKey") && data.containsKey("characterFlagsValue")) { String[] keys = (String[]) data.readObject("characterFlagsKey"); Byte[] values = (Byte[]) data.readObject("characterFlagsValue"); @@ -536,13 +555,24 @@ public class AdventurePlayer implements Serializable, SaveFileContent { else { decks.add(new Deck(data.readString("deck_name_" + i))); } - decks.get(i).getMain().addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("deck_" + i)))); - if (data.containsKey("sideBoardCards_" + i)) - decks.get(i).getOrCreate(DeckSection.Sideboard).addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("sideBoardCards_" + i)))); - if (data.containsKey("attractionDeckCards_" + i)) - decks.get(i).getOrCreate(DeckSection.Attractions).addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("attractionDeckCards_" + i)))); - if (data.containsKey("contraptionDeckCards_" + i)) - decks.get(i).getOrCreate(DeckSection.Contraptions).addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("contraptionDeckCards_" + i)))); + CardPool mainCards = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("deck_" + i))); + decks.get(i).getMain().addAll(mainCards.getFilteredPool(isValid)); + unsupportedCards.addAll(mainCards.getFilteredPool(isUnsupported).toFlatList()); + if (data.containsKey("sideBoardCards_" + i)) { + CardPool sideBoardCards = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("sideBoardCards_" + i))); + decks.get(i).getOrCreate(DeckSection.Sideboard).addAll(sideBoardCards.getFilteredPool(isValid)); + unsupportedCards.addAll(sideBoardCards.getFilteredPool(isUnsupported).toFlatList()); + } + if (data.containsKey("attractionDeckCards_" + i)) { + CardPool attractionCards = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("attractionDeckCards_" + i))); + decks.get(i).getOrCreate(DeckSection.Attractions).addAll(attractionCards.getFilteredPool(isValid)); + unsupportedCards.addAll(attractionCards.getFilteredPool(isUnsupported).toFlatList()); + } + if (data.containsKey("contraptionDeckCards_" + i)) { + CardPool contraptionCards = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("contraptionDeckCards_" + i))); + decks.get(i).getOrCreate(DeckSection.Contraptions).addAll(contraptionCards.getFilteredPool(isValid)); + unsupportedCards.addAll(contraptionCards.getFilteredPool(isUnsupported).toFlatList()); + } } // In case we allow removing decks from the deck selection GUI, populate up to the minimum for (int i = dynamicDeckCount++; i < MIN_DECK_COUNT; i++) { @@ -557,26 +587,43 @@ public class AdventurePlayer implements Serializable, SaveFileContent { continue; } decks.set(i, new Deck(data.readString("deck_name_" + i))); - decks.get(i).getMain().addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("deck_" + i)))); - if (data.containsKey("sideBoardCards_" + i)) - decks.get(i).getOrCreate(DeckSection.Sideboard).addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("sideBoardCards_" + i)))); + CardPool mainCards = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("deck_" + i))); + decks.get(i).getMain().addAll(mainCards.getFilteredPool(isValid)); + unsupportedCards.addAll(mainCards.getFilteredPool(isUnsupported).toFlatList()); + if (data.containsKey("sideBoardCards_" + i)) { + CardPool sideBoardCards = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("sideBoardCards_" + i))); + decks.get(i).getOrCreate(DeckSection.Sideboard).addAll(sideBoardCards.getFilteredPool(isValid)); + unsupportedCards.addAll(sideBoardCards.getFilteredPool(isUnsupported).toFlatList()); + } } } setSelectedDeckSlot(data.readInt("selectedDeckIndex")); - cards.addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("cards")))); + CardPool cardPool = CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("cards"))); + cards.addAll(cardPool.getFilteredPool(isValid)); + unsupportedCards.addAll(cardPool.getFilteredPool(isUnsupported).toFlatList()); if (data.containsKey("newCards")) { InventoryItem[] items = (InventoryItem[]) data.readObject("newCards"); for (InventoryItem item : items) { - newCards.add((PaperCard) item); + if (item instanceof PaperCard pc) { + if (isUnsupported.test(pc)) + unsupportedCards.add(pc); + else + newCards.add(pc); + } } } if (data.containsKey("noSellCards")) { // Legacy list of unsellable cards. Now done via CardRequest flags. Convert the corresponding cards. PaperCard[] items = (PaperCard[]) data.readObject("noSellCards"); CardPool noSellPool = new CardPool(); - noSellPool.addAllFlat(List.of(items)); + for (PaperCard pc : items) { + if (isUnsupported.test(pc)) + unsupportedCards.add(pc); + else + noSellPool.add(pc); + } for (Map.Entry noSellEntry : noSellPool) { PaperCard item = noSellEntry.getKey(); if (item == null) @@ -614,11 +661,21 @@ public class AdventurePlayer implements Serializable, SaveFileContent { } if (data.containsKey("autoSellCards")) { PaperCard[] items = (PaperCard[]) data.readObject("autoSellCards"); - autoSellCards.addAllFlat(Arrays.asList(items)); + for (PaperCard pc : items) { + if (isUnsupported.test(pc)) + unsupportedCards.add(pc); + else + autoSellCards.add(pc); + } } if (data.containsKey("favoriteCards")) { PaperCard[] items = (PaperCard[]) data.readObject("favoriteCards"); - favoriteCards.addAll(Arrays.asList(items)); + for (PaperCard pc : items) { + if (isUnsupported.test(pc)) + unsupportedCards.add(pc); + else + favoriteCards.add(pc); + } } fantasyMode = data.containsKey("fantasyMode") && data.readBool("fantasyMode"); @@ -1020,6 +1077,25 @@ public class AdventurePlayer implements Serializable, SaveFileContent { return items.random(); } + public boolean hasEquippedItem() { + for (Long id : equippedItems.values()) { + ItemData item = getEquippedItem(id); + if (item == null) + continue; + if (isHardorInsaneDifficulty()) { + return true; + } else { + switch (item.equipmentSlot) { + // limit to these for easy and normal + case "Boots", "Body", "Neck" -> { + return true; + } + } + } + } + return false; + } + public ItemData getEquippedAbility1() { for (Long id : equippedItems.values()) { ItemData data = getEquippedItem(id); @@ -1118,9 +1194,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent { } public void removeItem(String name) { - ItemData item = ItemListData.getItem(name); - if (item != null) - removeItem(item); + inventoryItems.stream().filter(itemData -> name.equalsIgnoreCase(itemData.name)).findFirst().ifPresent(this::removeItem); } public void removeItem(ItemData item) { @@ -1134,7 +1208,8 @@ public class AdventurePlayer implements Serializable, SaveFileContent { } public void equip(ItemData item) { - if (equippedItems.get(item.equipmentSlot) != null && equippedItems.get(item.equipmentSlot) == item.longID) { + Long itemID = equippedItems.get(item.equipmentSlot); + if (itemID != null && itemID.equals(item.longID)) { item.isEquipped = false; equippedItems.remove(item.equipmentSlot); } else { @@ -1186,16 +1261,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent { } public int countItem(String name) { - int count = 0; - if (!hasItem(name)) - return count; - for (ItemData i : inventoryItems) { - if (i == null) - continue; - if (i.name.equals(name)) - count++; - } - return count; + return (int) inventoryItems.stream().filter(Objects::nonNull).filter(i -> i.name.equals(name)).count(); } public boolean addItem(String name) { @@ -1212,11 +1278,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent { } public void removeAllQuestItems(){ - for (ItemData data : inventoryItems) { - if(data != null && data.questItem){ - removeItem(data); - } - } + inventoryItems.removeIf(data -> data != null && data.questItem); } public boolean addBooster(Deck booster) { @@ -1374,6 +1436,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent { */ public int copyDeck() { for (int i = 0; i < MAX_DECK_COUNT; i++) { + if (i >= getDeckCount()) addDeck(); if (isEmptyDeck(i)) { decks.set(i, (Deck) deck.copyTo(deck.getName() + " (" + Forge.getLocalizer().getMessage("lblCopy") + ")")); return i; diff --git a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java index dc32be79757..d3425a17e33 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java +++ b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java @@ -27,27 +27,41 @@ import forge.item.PaperCard; import forge.itemmanager.*; import forge.itemmanager.filters.CardColorFilter; import forge.itemmanager.filters.CardTypeFilter; -import forge.localinstance.properties.ForgePreferences; -import forge.menu.FCheckBoxMenuItem; import forge.menu.FDropDownMenu; import forge.menu.FMenuItem; import forge.menu.FPopupMenu; import forge.model.FModel; import forge.toolbox.*; -import forge.util.Callback; import forge.util.ItemPool; import forge.util.Localizer; import forge.util.Utils; import java.util.*; +import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; public class AdventureDeckEditor extends FDeckEditor { protected static class AdventureEditorConfig extends DeckEditorConfig { - @Override public GameType getGameType() { return GameType.Adventure; } - @Override public DeckFormat getDeckFormat() { return DeckFormat.Adventure; } - @Override protected IDeckController getController() { return ADVENTURE_DECK_CONTROLLER; } - @Override public boolean usePlayerInventory() { return true; } + @Override + public GameType getGameType() { + return GameType.Adventure; + } + + @Override + public DeckFormat getDeckFormat() { + return DeckFormat.Adventure; + } + + @Override + protected IDeckController getController() { + return ADVENTURE_DECK_CONTROLLER; + } + + @Override + public boolean usePlayerInventory() { + return true; + } @Override protected DeckEditorPage[] getInitialPages() { @@ -111,18 +125,34 @@ public class AdventureDeckEditor extends FDeckEditor { protected static class DeckPreviewConfig extends AdventureEditorConfig { private final Deck deckToPreview; + public DeckPreviewConfig(Deck deckToPreview) { this.deckToPreview = deckToPreview; } - @Override public boolean usePlayerInventory() { return false; } - @Override public boolean isLimited() { return true; } - @Override public ItemPool getCardPool(boolean wantUnique) { return deckToPreview.getAllCardsInASinglePool(true, true); } - @Override public boolean allowsCardReplacement() { return false; } + @Override + public boolean usePlayerInventory() { + return false; + } + + @Override + public boolean isLimited() { + return true; + } + + @Override + public ItemPool getCardPool(boolean wantUnique) { + return deckToPreview.getAllCardsInASinglePool(true, true); + } + + @Override + public boolean allowsCardReplacement() { + return false; + } @Override protected DeckEditorPage[] getInitialPages() { - return new DeckEditorPage[] {new ContentPreviewPage(deckToPreview)}; + return new DeckEditorPage[]{new ContentPreviewPage(deckToPreview)}; } } @@ -135,19 +165,39 @@ public class AdventureDeckEditor extends FDeckEditor { this.controller = new AdventureEventDeckController(event); } - @Override public GameType getGameType() { return GameType.AdventureEvent; } - @Override public DeckFormat getDeckFormat() { return DeckFormat.Limited; } - @Override public boolean isLimited() { return true; } - @Override public boolean isDraft() { return event.getDraft() != null; } - @Override protected IDeckController getController() { return this.controller; } + @Override + public GameType getGameType() { + return GameType.AdventureEvent; + } + + @Override + public DeckFormat getDeckFormat() { + return DeckFormat.Limited; + } + + @Override + public boolean isLimited() { + return true; + } + + @Override + public boolean isDraft() { + return event.getDraft() != null; + } + + @Override + protected IDeckController getController() { + return this.controller; + } @Override public List getBasicLandSets(Deck currentDeck) { - if(event.cardBlock != null) { - if(event.cardBlock.getLandSet() != null) + if (event.cardBlock != null) { + if (event.cardBlock.getLandSet() != null) return List.of(event.cardBlock.getLandSet()); - List eventSets = event.cardBlock.getSets(); - if(!eventSets.isEmpty()) + List eventSets = new ArrayList<>(event.cardBlock.getSets()); + eventSets.removeIf(Predicate.not(CardEdition::hasBasicLands)); + if (!eventSets.isEmpty()) return eventSets; } return List.of(DeckProxy.getDefaultLandSet(event.registeredDeck)); @@ -170,9 +220,9 @@ public class AdventureDeckEditor extends FDeckEditor { case Entered: if (event.getDraft() != null) return new DeckEditorPage[]{ - new DraftPackPage(new AdventureCardManager()), - new AdventureDeckSectionPage(DeckSection.Main, ItemManagerConfig.DRAFT_POOL), - new AdventureDeckSectionPage(DeckSection.Sideboard, ItemManagerConfig.SIDEBOARD) + new DraftPackPage(new AdventureCardManager()), + new AdventureDeckSectionPage(DeckSection.Main, ItemManagerConfig.DRAFT_POOL), + new AdventureDeckSectionPage(DeckSection.Sideboard, ItemManagerConfig.SIDEBOARD) }; default: return new DeckEditorPage[]{ @@ -242,7 +292,7 @@ public class AdventureDeckEditor extends FDeckEditor { Localizer localizer = Forge.getLocalizer(); String label = localizer.getMessage("lblSellFor") + " " + Current.player().cardSellPrice(card); int sellable = cardManager.getItemCount(card); - if(sellable <= 0) + if (sellable <= 0) return; String prompt = card + " - " + label + " " + localizer.getMessage("lblHowMany"); @@ -250,7 +300,7 @@ public class AdventureDeckEditor extends FDeckEditor { int sold = Current.player().sellCard(card, result); removeCard(card, sold); })); - if(cardIsFavorite(card)) + if (cardIsFavorite(card)) sellItem.setTextColor(255, 0, 0); menu.addItem(sellItem); } @@ -292,16 +342,13 @@ public class AdventureDeckEditor extends FDeckEditor { value += Current.player().cardSellPrice(entry.getKey()) * entry.getValue(); } - if(toSell.isEmpty()) + if (toSell.isEmpty()) return; - FOptionPane.showConfirmDialog(Forge.getLocalizer().getMessage("lblSellAllConfirm", toSell.countAll(), value), Forge.getLocalizer().getMessage("lblSellCurrentFilters"), Forge.getLocalizer().getMessage("lblSell"), Forge.getLocalizer().getMessage("lblCancel"), false, new Callback<>() { - @Override - public void run(Boolean result) { - if (result) { - Current.player().doBulkSell(toSell); - refresh(); - } + FOptionPane.showConfirmDialog(Forge.getLocalizer().getMessage("lblSellAllConfirm", toSell.countAll(), value), Forge.getLocalizer().getMessage("lblSellCurrentFilters"), Forge.getLocalizer().getMessage("lblSell"), Forge.getLocalizer().getMessage("lblCancel"), false, result -> { + if (result) { + Current.player().doBulkSell(toSell); + refresh(); } }); } @@ -309,13 +356,21 @@ public class AdventureDeckEditor extends FDeckEditor { @Override public void setCardFavorited(PaperCard card, boolean isFavorite) { AdventurePlayer player = Current.player(); - if(isFavorite) + if (isFavorite) player.favoriteCards.add(card); else player.favoriteCards.remove(card); } - @Override protected boolean cardIsFavorite(PaperCard card) { return Current.player().favoriteCards.contains(card); } - @Override protected boolean allowFavoriteCards() { return true; } + + @Override + protected boolean cardIsFavorite(PaperCard card) { + return Current.player().favoriteCards.contains(card); + } + + @Override + protected boolean allowFavoriteCards() { + return true; + } } private static class CollectionCatalogPage extends CatalogPage { @@ -354,9 +409,18 @@ public class AdventureDeckEditor extends FDeckEditor { int autoSellCount = Current.player().autoSellCards.count(card); //Number currently in auto-sell. int canMoveToAutoSell = safeToSellCount - autoSellCount; //Number that can be moved to auto-sell from here. + if (card.getRules().isUnsupported()) { + menu.clearItems(); + FMenuItem removeItem = new FMenuItem(localizer.getMessage("lblRemoveUnsupportedCard"), FSkinImage.HDDELETE, e -> + removeCard(card, safeToSellCount)); + menu.addItem(removeItem); + return; + } + if (copiesUsedInDecks > 0) { String text = localizer.getMessage("lblCopiesInUse", copiesUsedInDecks); - FMenuItem usedHint = new FMenuItem(text, FSkinImage.HDLIBRARY, n -> {}); + FMenuItem usedHint = new FMenuItem(text, FSkinImage.HDLIBRARY, n -> { + }); usedHint.setEnabled(false); menu.addItem(usedHint); } @@ -393,13 +457,21 @@ public class AdventureDeckEditor extends FDeckEditor { @Override public void setCardFavorited(PaperCard card, boolean isFavorite) { AdventurePlayer player = Current.player(); - if(isFavorite) + if (isFavorite) player.favoriteCards.add(card); else player.favoriteCards.remove(card); } - @Override protected boolean cardIsFavorite(PaperCard card) { return Current.player().favoriteCards.contains(card); } - @Override protected boolean allowFavoriteCards() { return true; } + + @Override + protected boolean cardIsFavorite(PaperCard card) { + return Current.player().favoriteCards.contains(card); + } + + @Override + protected boolean allowFavoriteCards() { + return true; + } @Override public void buildDeckMenu(FPopupMenu menu) { @@ -420,15 +492,12 @@ public class AdventureDeckEditor extends FDeckEditor { continue; toMove.add(entry.getKey(), entry.getValue()); } - if(toMove.isEmpty()) + if (toMove.isEmpty()) return; - FOptionPane.showConfirmDialog(Forge.getLocalizer().getMessage("lblAutoSellCurrentFiltersConfirm", toMove.countAll()), Forge.getLocalizer().getMessage("lblAutoSellCurrentFilters"), Forge.getLocalizer().getMessage("lblAutoSell"), Forge.getLocalizer().getMessage("lblCancel"), false, new Callback<>() { - @Override - public void run(Boolean result) { - if (result) { - moveCards(toMove, autoSellPage); - } + FOptionPane.showConfirmDialog(Forge.getLocalizer().getMessage("lblAutoSellCurrentFiltersConfirm", toMove.countAll()), Forge.getLocalizer().getMessage("lblAutoSellCurrentFilters"), Forge.getLocalizer().getMessage("lblAutoSell"), Forge.getLocalizer().getMessage("lblCancel"), false, result -> { + if (result) { + moveCards(toMove, autoSellPage); } }); } @@ -478,20 +547,20 @@ public class AdventureDeckEditor extends FDeckEditor { super.buildMenu(menu, card); Localizer localizer = Forge.getLocalizer(); AdventurePlayer player = Current.player(); - if(isShop()) { + if (isShop()) { String label = localizer.getMessage("lblSellFor") + " " + player.cardSellPrice(card); int sellable = cardManager.getItemCount(card); - if(sellable <= 0) + if (sellable <= 0) return; String prompt = card + " - " + label + " " + localizer.getMessage("lblHowMany"); menu.addItem(new FMenuItem(label, SIDEBOARD_ICON, new MoveQuantityPrompt(prompt, sellable, result -> { - int sold = player.sellCard(card, result); - removeCard(card, sold); - }) + int sold = player.sellCard(card, result); + removeCard(card, sold); + }) )); } - if(parentScreen instanceof AdventureDeckEditor adventureEditor && adventureEditor.getCatalogPage() != null) { + if (parentScreen instanceof AdventureDeckEditor adventureEditor && adventureEditor.getCatalogPage() != null) { CatalogPage catalogPage = adventureEditor.getCatalogPage(); int autoSellCount = cardManager.getItemCount(card); int amountInCollection = player.getCards().count(card); @@ -499,9 +568,7 @@ public class AdventureDeckEditor extends FDeckEditor { String action = localizer.getMessage("lblFromAutoSell", autoSellCount, safeToSellCount); String prompt = String.format("%s - %s %s", card, action, localizer.getMessage("lblHowMany")); - FMenuItem moveToCatalog = new FMenuItem(action, CATALOG_ICON, new MoveQuantityPrompt(prompt, autoSellCount, amount -> { - moveCard(card, catalogPage, amount); - })); + FMenuItem moveToCatalog = new FMenuItem(action, CATALOG_ICON, new MoveQuantityPrompt(prompt, autoSellCount, amount -> moveCard(card, catalogPage, amount))); menu.addItem(moveToCatalog); } @@ -509,7 +576,7 @@ public class AdventureDeckEditor extends FDeckEditor { @Override protected void onCardActivated(PaperCard card) { - if(isShop()) { + if (isShop()) { Current.player().sellCard(card, 1); removeCard(card, 1); } @@ -519,7 +586,7 @@ public class AdventureDeckEditor extends FDeckEditor { public AdventureEventData getCurrentEvent() { IDeckController controller = getDeckController(); - if(!(controller instanceof AdventureEventDeckController eventController)) + if (!(controller instanceof AdventureEventDeckController eventController)) return null; return eventController.currentEvent; } @@ -527,7 +594,7 @@ public class AdventureDeckEditor extends FDeckEditor { @Override public BoosterDraft getDraft() { AdventureEventData currentEvent = getCurrentEvent(); - if(currentEvent == null) + if (currentEvent == null) return null; return currentEvent.getDraft(); } @@ -535,7 +602,7 @@ public class AdventureDeckEditor extends FDeckEditor { @Override public boolean isDrafting() { AdventureEventData currentEvent = getCurrentEvent(); - if(currentEvent == null) + if (currentEvent == null) return false; return currentEvent.draft != null && !currentEvent.isDraftComplete; } @@ -550,7 +617,7 @@ public class AdventureDeckEditor extends FDeckEditor { public void completeDraft() { super.completeDraft(); AdventureEventData currentEvent = getCurrentEvent(); - if(currentEvent == null) + if (currentEvent == null) return; currentEvent.isDraftComplete = true; Deck[] opponentDecks = currentEvent.getDraft().getComputerDecks(); @@ -558,7 +625,7 @@ public class AdventureDeckEditor extends FDeckEditor { currentEvent.participants[i].setDeck(opponentDecks[i]); } currentEvent.draftedDeck = (Deck) currentEvent.registeredDeck.copyTo("Draft Deck"); - if (allowsAddBasic()) { + if (allowAddBasic()) { showAddBasicLandsDialog(); //Might be annoying if you haven't pruned your deck yet, but best to remind player that //this probably needs to be done since it's there since it's not normally part of Adventure @@ -624,9 +691,9 @@ public class AdventureDeckEditor extends FDeckEditor { private static final FImage AUTO_SELL_ICON = FSkinImage.HDEXILE; //to-maybe-do: Custom adventure icon for this? Adventure should really just have its own skin. public static FImage iconFromDeckSection(DeckSection deckSection) { - if(deckSection == DeckSection.Main) + if (deckSection == DeckSection.Main) return MAIN_DECK_ICON; - if(deckSection == DeckSection.Sideboard) + if (deckSection == DeckSection.Sideboard) return FDeckEditor.SIDEBOARD_ICON; return FDeckEditor.iconFromDeckSection(deckSection); } @@ -656,6 +723,15 @@ public class AdventureDeckEditor extends FDeckEditor { // if (currentEvent.registeredDeck!=null && !currentEvent.registeredDeck.isEmpty()){ // //Use this deck instead of selected deck // } + + for (TabPage page : tabPages) { + if (page instanceof CollectionCatalogPage) { + if (!Current.player().getUnsupportedCards().isEmpty()) + GuiChoose.getChoices(Forge.getLocalizer().getMessage("lblRemoveAllUnsupportedCards"), + -1, -1, Current.player().getUnsupportedCards(), result -> Current.player().getUnsupportedCards().clear()); + break; + } + } } public void refresh() { @@ -679,7 +755,7 @@ public class AdventureDeckEditor extends FDeckEditor { public AdventureDeckEditor(boolean createAsShop) { super(createAsShop ? new ShopConfig() : new AdventureEditorConfig(), createAsShop ? null : Current.player().getSelectedDeck()); - if(createAsShop) + if (createAsShop) setHeaderText(Forge.getLocalizer().getMessage("lblSell")); } @@ -687,7 +763,7 @@ public class AdventureDeckEditor extends FDeckEditor { super(new AdventureEventEditorConfig(event), event.registeredDeck); currentEvent = event; - if(event.getDraft() != null && event.getDraft().shouldShowDraftLog()) { + if (event.getDraft() != null && event.getDraft().shouldShowDraftLog()) { this.draftLog = new FDraftLog(); event.getDraft().setLogEntry(this.draftLog); deckHeader.initDraftLog(this.draftLog, this); @@ -705,68 +781,58 @@ public class AdventureDeckEditor extends FDeckEditor { } @Override - protected FPopupMenu createMoreOptionsMenu() { - return new FPopupMenu() { - @Override - protected void buildMenu() { - Localizer localizer = Forge.getLocalizer(); - addItem(new FMenuItem(localizer.getMessage("btnCopyToClipboard"), Forge.hdbuttons ? FSkinImage.HDEXPORT : FSkinImage.BLANK, e1 -> FDeckViewer.copyDeckToClipboard(getDeck()))); - if (allowsAddBasic()) { - FMenuItem addBasic = new FMenuItem(localizer.getMessage("lblAddBasicLands"), FSkinImage.LANDLOGO, e1 -> showAddBasicLandsDialog()); - addItem(addBasic); - } - if(FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.DEV_MODE_ENABLED)) { - addItem(new FCheckBoxMenuItem(localizer.getMessage("cbEnforceDeckLegality"), shouldEnforceConformity(), e -> toggleConformity())); - String devSuffix = " (" + localizer.getMessage("lblDev") + ")"; - addItem(new FMenuItem(localizer.getMessage("lblAddcard") + devSuffix, FSkinImage.HDPLUS, e -> showDevAddCardDialog())); - } - ((DeckEditorPage) getSelectedPage()).buildDeckMenu(this); + protected void addChosenBasicLands(CardPool landsToAdd) { + if (isLimitedEditor()) + super.addChosenBasicLands(landsToAdd); + + //Take the basic lands from the player's collection if they have them. If they need more, create unsellable copies. + CatalogPage catalog = getCatalogPage(); + if (catalog != null) { // TODO find out why this is null on some devices since it shouldn't be null + CardPool requiredNewLands = new CardPool(); + CardPool landsToMove = new CardPool(); + ItemPool availablePool = catalog.getCardPool(); + for (Map.Entry entry : landsToAdd) { + int needed = entry.getValue(); + PaperCard card = entry.getKey(); + int moveableSellable = Math.min(availablePool.count(card), needed); + landsToMove.add(card, moveableSellable); + needed -= moveableSellable; + if (needed <= 0) + continue; + PaperCard unsellable = card.getNoSellVersion(); + //It'd probably be better to do some kind of fuzzy search that matches prints but ignores flags. + //But for now, unsellable is the only one that should matter here. + int moveableUnsellable = Math.min(availablePool.count(unsellable), needed); + landsToMove.add(unsellable, needed); //We'll acquire the rest later. + if (needed > moveableUnsellable) + requiredNewLands.add(unsellable, needed - moveableUnsellable); } - }; + if (!requiredNewLands.isEmpty()) + Current.player().addCards(requiredNewLands); + catalog.refresh(); + catalog.moveCards(landsToMove, getMainDeckPage()); + } } @Override - protected void addChosenBasicLands(CardPool landsToAdd) { - if(isLimitedEditor()) - super.addChosenBasicLands(landsToAdd); - //Take the basic lands from the player's collection if they have them. If they need more, create unsellable copies. - CardPool requiredNewLands = new CardPool(); - CardPool landsToMove = new CardPool(); - CatalogPage catalog = getCatalogPage(); - ItemPool availablePool = catalog.getCardPool(); - for(Map.Entry entry : landsToAdd) { - int needed = entry.getValue(); - PaperCard card = entry.getKey(); - int moveableSellable = Math.min(availablePool.count(card), needed); - landsToMove.add(card, moveableSellable); - needed -= moveableSellable; - if(needed <= 0) - continue; - PaperCard unsellable = card.getNoSellVersion(); - //It'd probably be better to do some kind of fuzzy search that matches prints but ignores flags. - //But for now, unsellable is the only one that should matter here. - int moveableUnsellable = Math.min(availablePool.count(unsellable), needed); - landsToMove.add(unsellable, needed); //We'll acquire the rest later. - if(needed > moveableUnsellable) - requiredNewLands.add(unsellable, needed - moveableUnsellable); - } - if(!requiredNewLands.isEmpty()) - Current.player().addCards(requiredNewLands); - catalog.refresh(); - catalog.moveCards(landsToMove, getMainDeckPage()); + protected PaperCard supplyPrintForImporter(PaperCard missingCard) { + PaperCard out = super.supplyPrintForImporter(missingCard); + return out == null ? null : out.getNoSellVersion(); } @Override protected void cacheTabPages() { super.cacheTabPages(); - for(TabPage page : tabPages) { - if(page instanceof CollectionAutoSellPage) + for (TabPage page : tabPages) { + if (page instanceof CollectionAutoSellPage) this.autoSellPage = (CollectionAutoSellPage) page; } } @Override - protected boolean allowsAddBasic() { + protected boolean allowAddBasic() { + if (getEditorConfig() instanceof DeckPreviewConfig) + return false; AdventureEventData currentEvent = getCurrentEvent(); if (currentEvent == null) return true; @@ -815,23 +881,17 @@ public class AdventureDeckEditor extends FDeckEditor { } @Override - public void onClose(final Callback canCloseCallback) { - if(canCloseCallback == null) { + public void onClose(final Consumer canCloseCallback) { + if (canCloseCallback == null) { resolveClose(null, true); return; } Localizer localizer = Forge.getLocalizer(); if (isDrafting()) { - FOptionPane.showConfirmDialog(localizer.getMessage("lblEndAdventureEventConfirm"), localizer.getMessage("lblLeaveDraft"), localizer.getMessage("lblLeave"), localizer.getMessage("lblCancel"), false, new Callback<>() { - @Override - public void run(Boolean result) { - resolveClose(canCloseCallback, result == true); - } - }); + FOptionPane.showConfirmDialog(localizer.getMessage("lblEndAdventureEventConfirm"), localizer.getMessage("lblLeaveDraft"), localizer.getMessage("lblLeave"), localizer.getMessage("lblCancel"), false, result -> resolveClose(canCloseCallback, result == true)); return; - } - else if(getEditorConfig().isLimited() || getDeck().isEmpty()) { + } else if (getEditorConfig().isLimited() || getDeck().isEmpty()) { resolveClose(canCloseCallback, true); return; } @@ -840,31 +900,26 @@ public class AdventureDeckEditor extends FDeckEditor { if (deckError != null) { //Allow the player to close the editor with an invalid deck, but warn them that cards may be swapped out. String warning = localizer.getMessage("lblAdventureDeckError", deckError); - FOptionPane.showConfirmDialog(warning, localizer.getMessage("lblInvalidDeck"), false, new Callback<>() { - @Override - public void run(Boolean result) { - resolveClose(canCloseCallback, result == true); - } - }); + FOptionPane.showConfirmDialog(warning, localizer.getMessage("lblInvalidDeck"), false, result -> resolveClose(canCloseCallback, result == true)); return; } resolveClose(canCloseCallback, true); } - private void resolveClose(final Callback canCloseCallback, boolean result) { - if(result) { + private void resolveClose(final Consumer canCloseCallback, boolean result) { + if (result) { Current.player().newCards.clear(); - if(isDrafting()) + if (isDrafting()) getCurrentEvent().eventStatus = AdventureEventController.EventStatus.Abandoned; } - if(canCloseCallback != null) - canCloseCallback.run(result); + if (canCloseCallback != null) + canCloseCallback.accept(result); } @Override protected void devAddCards(CardPool cards) { - if(!getEditorConfig().usePlayerInventory()) { + if (!getEditorConfig().usePlayerInventory()) { //Drafting. super.devAddCards(cards); return; @@ -889,9 +944,9 @@ public class AdventureDeckEditor extends FDeckEditor { protected String getItemSuffix(Map.Entry item) { PaperCard card = item.getKey(); String parentSuffix = super.getItemSuffix(item); - if(card.hasNoSellValue()) { + if (card.hasNoSellValue()) { String valueText = " [NO VALUE]"; - if(parentSuffix == null) + if (parentSuffix == null) return valueText; return String.join(" ", valueText, parentSuffix); } @@ -905,7 +960,7 @@ public class AdventureDeckEditor extends FDeckEditor { public void drawValue(Graphics g, Map.Entry value, FSkinFont font, FSkinColor foreColor, FSkinColor backColor, boolean pressed, float x, float y, float w, float h) { super.drawValue(g, value, font, foreColor, backColor, pressed, x, y, w, h); - if(showPriceInfo()) { + if (showPriceInfo()) { float totalHeight = h + 2 * FList.PADDING; float cardArtWidth = totalHeight * CardRenderer.CARD_ART_RATIO; @@ -972,6 +1027,7 @@ public class AdventureDeckEditor extends FDeckEditor { } private static final AdventureDeckController ADVENTURE_DECK_CONTROLLER = new AdventureDeckController(); + /** * Barebones deck controller. Doesn't really need to do anything since Adventure Decks are updated in real time * while they're edited, and they're only saved when the adventure is saved. @@ -983,29 +1039,41 @@ public class AdventureDeckEditor extends FDeckEditor { @Override public void setEditor(FDeckEditor editor) { this.editor = editor; - if(editor != null) - editor.notifyNewControllerModel(); - } - - @Override public void setDeck(Deck deck) { - this.currentDeck = deck; - if(editor != null) + if (editor != null) editor.notifyNewControllerModel(); } - @Override public Deck getDeck() { return currentDeck; } - @Override public void newDeck() { + + @Override + public void setDeck(Deck deck) { + this.currentDeck = deck; + if (editor != null) + editor.notifyNewControllerModel(); + } + + @Override + public Deck getDeck() { + return currentDeck; + } + + @Override + public void newDeck() { setDeck(new Deck("Adventure Deck")); } @Override public String getDeckDisplayName() { - if(currentDeck == null) + if (currentDeck == null) return "New Deck"; return currentDeck.getName(); } - @Override public void notifyModelChanged() {} // - @Override public void exitWithoutSaving() {} //Too many external variables to just revert the deck. Not supported for now. + @Override + public void notifyModelChanged() { + } // + + @Override + public void exitWithoutSaving() { + } //Too many external variables to just revert the deck. Not supported for now. } private static class AdventureEventDeckController implements IDeckController { @@ -1019,26 +1087,40 @@ public class AdventureDeckEditor extends FDeckEditor { @Override public void setEditor(FDeckEditor editor) { this.editor = editor; - if(editor != null) + if (editor != null) editor.notifyNewControllerModel(); } - @Override public void setDeck(Deck deck) {this.newDeck();} //Deck is supplied by the event. - @Override public void newDeck() { - if(editor != null) + @Override + public void setDeck(Deck deck) { + this.newDeck(); + } //Deck is supplied by the event. + + @Override + public void newDeck() { + if (editor != null) editor.notifyNewControllerModel(); } - @Override public Deck getDeck() { return currentEvent.registeredDeck; } + + @Override + public Deck getDeck() { + return currentEvent.registeredDeck; + } @Override public String getDeckDisplayName() { - if(getDeck() == null) + if (getDeck() == null) return "Uninitialized Deck"; return getDeck().getName(); } - @Override public void notifyModelChanged() {} // - @Override public void exitWithoutSaving() {} //Too many external variables to just revert the deck. Not supported for now. + @Override + public void notifyModelChanged() { + } // + + @Override + public void exitWithoutSaving() { + } //Too many external variables to just revert the deck. Not supported for now. } } diff --git a/forge-gui-mobile/src/forge/adventure/scene/ArenaScene.java b/forge-gui-mobile/src/forge/adventure/scene/ArenaScene.java index ba43cc72113..691dc35c0c4 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/ArenaScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/ArenaScene.java @@ -157,7 +157,7 @@ public class ArenaScene extends UIScene implements IAfterMatch { } @Override - public void setWinner(boolean winner) { + public void setWinner(boolean winner, boolean isArena) { enable = false; Array winners = new Array<>(); Array winnersEnemies = new Array<>(); diff --git a/forge-gui-mobile/src/forge/adventure/scene/DeckSelectScene.java b/forge-gui-mobile/src/forge/adventure/scene/DeckSelectScene.java index a1f3e853834..a6da1bb0d81 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/DeckSelectScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/DeckSelectScene.java @@ -83,7 +83,7 @@ public class DeckSelectScene extends UIScene { private void layoutDeckButtons() { for (int i = 0; i < AdventurePlayer.current().getDeckCount(); i++) - addDeckButton(Forge.getLocalizer().getMessage("lblDeck") + ": " + (i + 1), i); + addDeckButton(i); } private void addDeck(){ @@ -144,6 +144,7 @@ public class DeckSelectScene extends UIScene { } private void updateDeckButton(int index) { + if (!buttons.containsKey(index)) addDeckButton(index); buttons.get(index).setText(Current.player().getDeck(index).getName()); buttons.get(index).getTextraLabel().layout(); buttons.get(index).layout(); @@ -177,8 +178,9 @@ public class DeckSelectScene extends UIScene { } } - private TextraButton addDeckButton(String name, int i) { + private TextraButton addDeckButton(int i) { TextraButton button = Controls.newTextButton("-"); + String name = Forge.getLocalizer().getMessage("lblDeck") + ": " + (i + 1); button.addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { diff --git a/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java b/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java index 74afd0a9732..c974b02e9c1 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/DuelScene.java @@ -39,7 +39,6 @@ import forge.sound.MusicPlaylist; import forge.toolbox.FOptionPane; import forge.trackable.TrackableCollection; import forge.util.Aggregates; -import forge.util.Callback; import org.apache.commons.lang3.tuple.Pair; import java.util.*; @@ -143,7 +142,7 @@ public class DuelScene extends ForgeScene { Current.player().getStatistic().setResult(enemyName, winner); if (last instanceof IAfterMatch) { - ((IAfterMatch) last).setWinner(winner); + ((IAfterMatch) last).setWinner(winner, isArena); } }); } @@ -153,12 +152,9 @@ public class DuelScene extends ForgeScene { } private FOptionPane createFOption(String message, String title, FBufferedImage icon, Runnable runnable) { - return new FOptionPane(message, null, title, icon, null, ImmutableList.of(Forge.getLocalizer().getMessage("lblOK")), -1, new Callback() { - @Override - public void run(Integer result) { - if (runnable != null) - runnable.run(); - } + return new FOptionPane(message, null, title, icon, null, ImmutableList.of(Forge.getLocalizer().getMessage("lblOK")), -1, result -> { + if (runnable != null) + runnable.run(); }); } @@ -196,18 +192,21 @@ public class DuelScene extends ForgeScene { @Override public void enter() { GameHUD.getInstance().unloadAudio(); - Set appliedVariants = new HashSet<>(); + GameType mainGameType; + boolean isDeckMissing = false; + String isDeckMissingMsg = ""; if (eventData != null && eventData.eventRules != null) { - appliedVariants.add(eventData.eventRules.gameType); + mainGameType = eventData.eventRules.gameType; } else { - appliedVariants.add(GameType.Adventure); + mainGameType = GameType.Adventure; } + Set appliedVariants = EnumSet.of(mainGameType); AdventurePlayer advPlayer = Current.player(); List players = new ArrayList<>(); - applyAdventureDeckRules(); + applyAdventureDeckRules(mainGameType.getDeckFormat()); int playerCount = 1; EnemyData currentEnemy = enemy.getData(); for (int i = 0; i < 8 && currentEnemy != null; i++) { @@ -278,7 +277,7 @@ public class DuelScene extends ForgeScene { currentEnemy = enemy.getData(); boolean bossBattle = currentEnemy.boss; - for (int i = 0; i < 8 && currentEnemy != null; i++) { + for (int i = 0; i < playerCount && currentEnemy != null; i++) { Deck deck; if (this.chaosBattle) { //random challenge for chaos mode @@ -298,6 +297,12 @@ public class DuelScene extends ForgeScene { } else { deck = currentEnemy.copyPlayerDeck ? this.playerDeck : currentEnemy.generateDeck(Current.player().isFantasyMode(), Current.player().isUsingCustomDeck() || Current.player().isHardorInsaneDifficulty()); } + if (deck == null) { + isDeckMissing = true; + isDeckMissingMsg = "Deck for " + currentEnemy.getName() + " is missing! " + (this.eventData == null ? "Genetic AI deck will be used." : "Player deck will be used."); + System.err.println(isDeckMissingMsg); + deck = this.eventData == null ? Aggregates.random(DeckProxy.getAllGeneticAIDecks()).getDeck() : this.playerDeck; + } RegisteredPlayer aiPlayer = RegisteredPlayer.forVariants(playerCount, appliedVariants, deck, null, false, null, null); LobbyPlayer enemyPlayer = GamePlayerUtil.createAiPlayer(currentEnemy.getName(), selectAI(currentEnemy.ai)); @@ -371,9 +376,9 @@ public class DuelScene extends ForgeScene { hostedMatch.startMatch(rules, appliedVariants, players, guiMap, bossBattle ? MusicPlaylist.BOSS : MusicPlaylist.MATCH); MatchController.instance.setGameView(hostedMatch.getGameView()); boolean showMessages = enemy.getData().boss || (enemy.getData().copyPlayerDeck && Current.player().isUsingCustomDeck()); - if (chaosBattle || showMessages) { + if (chaosBattle || showMessages || isDeckMissing) { final FBufferedImage fb = getFBEnemyAvatar(); - bossDialogue = createFOption(Forge.getLocalizer().getMessage("AdvBossIntro" + Aggregates.randomInt(1, 35)), + bossDialogue = createFOption(isDeckMissing ? isDeckMissingMsg : Forge.getLocalizer().getMessage("AdvBossIntro" + Aggregates.randomInt(1, 35)), enemy.getName(), fb, fb::dispose); matchOverlay = new LoadingOverlay(() -> FThreads.delayInEDT(300, () -> FThreads.invokeInEdtNowOrLater(() -> bossDialogue.show())), false, true); @@ -397,16 +402,25 @@ public class DuelScene extends ForgeScene { private static final String PLACEHOLDER_ATTRACTION = "Coin-Operated Pony"; private static final String PLACEHOLDER_CONTRAPTION = "Automatic Fidget Spinner"; - private void applyAdventureDeckRules() { + private void applyAdventureDeckRules(DeckFormat format) { //Can't just keep the player from entering a battle if their deck is invalid. So instead we'll just edit their deck. CardPool mainSection = playerDeck.getMain(), attractions = playerDeck.get(DeckSection.Attractions), contraptions = playerDeck.get(DeckSection.Contraptions); - DeckFormat format = DeckFormat.Adventure; removeExcessCopies(mainSection, format); removeExcessCopies(attractions, format); removeExcessCopies(contraptions, format); - int missingCards = Config.instance().getConfigData().minDeckSize - mainSection.countAll(); + int mainSize = mainSection.countAll(); + + int maxDeckSize = format == DeckFormat.Adventure ? Integer.MAX_VALUE : format.getMainRange().getMaximum(); + int excessCards = mainSize - maxDeckSize; + if (excessCards > 0) { + List removals = Aggregates.random(mainSection.toFlatList(), excessCards); + mainSection.removeAllFlat(removals); + } + + int minDeckSize = format == DeckFormat.Adventure ? Config.instance().getConfigData().minDeckSize : format.getMainRange().getMinimum(); + int missingCards = minDeckSize - mainSize; if (missingCards > 0) //Replace unknown cards for a Wastes. mainSection.add(PLACEHOLDER_MAIN, missingCards); diff --git a/forge-gui-mobile/src/forge/adventure/scene/EventScene.java b/forge-gui-mobile/src/forge/adventure/scene/EventScene.java index 0d7e692b61a..1c7c1828d6b 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/EventScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/EventScene.java @@ -29,7 +29,6 @@ import forge.adventure.world.WorldSave; import forge.deck.Deck; import forge.gui.FThreads; import forge.screens.TransitionScreen; -import forge.util.Callback; import forge.util.MyRandom; import java.util.Arrays; @@ -52,6 +51,7 @@ public class EventScene extends MenuScene implements IAfterMatch { static PointOfInterestChanges changes; private Array entryDialog; + private AdventureEventData.AdventureEventMatch humanMatch = null; private int packsSelected = 0; //Used for meta drafts, booster drafts will use existing logic. @@ -124,26 +124,17 @@ public class EventScene extends MenuScene implements IAfterMatch { //todo: add translation decline.name = "Do not enter event"; - enterWithCoin.callback = new Callback() { - @Override - public void run(Boolean result) { - currentEvent.eventStatus = AdventureEventController.EventStatus.Entered; - refresh(); - } + enterWithCoin.callback = (result) -> { + currentEvent.eventStatus = AdventureEventController.EventStatus.Entered; + refresh(); }; - enterWithShards.callback = new Callback() { - @Override - public void run(Boolean result) { - currentEvent.eventStatus = AdventureEventController.EventStatus.Entered; - refresh(); - } + enterWithShards.callback = (result) -> { + currentEvent.eventStatus = AdventureEventController.EventStatus.Entered; + refresh(); }; - enterWithGold.callback = new Callback() { - @Override - public void run(Boolean result) { - currentEvent.eventStatus = AdventureEventController.EventStatus.Entered; - refresh(); - } + enterWithGold.callback = (result) -> { + currentEvent.eventStatus = AdventureEventController.EventStatus.Entered; + refresh(); }; introDialog.options = new DialogData[4]; @@ -500,7 +491,7 @@ public class EventScene extends MenuScene implements IAfterMatch { } public void startRound() { - for (AdventureEventData.AdventureEventMatch match : currentEvent.matches.get(currentEvent.currentRound)) { + for (AdventureEventData.AdventureEventMatch match : currentEvent.getMatches(currentEvent.currentRound)) { match.round = currentEvent.currentRound; if (match.winner != null) continue; @@ -549,9 +540,7 @@ public class EventScene extends MenuScene implements IAfterMatch { } } - AdventureEventData.AdventureEventMatch humanMatch = null; - - public void setWinner(boolean winner) { + public void setWinner(boolean winner, boolean isArena) { if (winner) { humanMatch.winner = humanMatch.p1; humanMatch.p1.wins++; diff --git a/forge-gui-mobile/src/forge/adventure/scene/HudScene.java b/forge-gui-mobile/src/forge/adventure/scene/HudScene.java index c832752a584..a157ad24fda 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/HudScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/HudScene.java @@ -181,8 +181,8 @@ public abstract class HudScene extends Scene implements InputProcessor, IAfterMa } @Override - public void setWinner(boolean winner) { - stage.setWinner(winner); + public void setWinner(boolean winner, boolean isArena) { + stage.setWinner(winner, isArena); } public boolean isInHudOnlyMode() { diff --git a/forge-gui-mobile/src/forge/adventure/scene/MenuScene.java b/forge-gui-mobile/src/forge/adventure/scene/MenuScene.java index 41e1930bd73..4f8a1a1b1c3 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/MenuScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/MenuScene.java @@ -157,7 +157,7 @@ public class MenuScene extends UIScene { loadDialog(option); if (option.callback != null) { - option.callback.run(true); + option.callback.accept(true); } }); B.getTextraLabel().setWrap(true); //We want this to wrap in case it's a wordy choice. diff --git a/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java b/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java index dac7c6279ad..d91ece8176f 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java @@ -259,7 +259,7 @@ public class SettingsScene extends UIScene { if (!GuiBase.isAndroid()) { addCheckBox(Forge.getLocalizer().getMessage("lblBattlefieldTextureFiltering"), ForgePreferences.FPref.UI_LIBGDX_TEXTURE_FILTERING); - addCheckBox(Forge.getLocalizer().getMessage("lblAltZoneTabs"), ForgePreferences.FPref.UI_ALT_PLAYERZONETABS); + //addCheckBox(Forge.getLocalizer().getMessage("lblAltZoneTabs"), ForgePreferences.FPref.UI_ALT_PLAYERZONETABS); } else { addCheckBox(Forge.getLocalizer().getMessage("lblLandscapeMode") + " (" + Forge.getLocalizer().getMessage("lblRestartRequired") + ")", diff --git a/forge-gui-mobile/src/forge/adventure/scene/ShopScene.java b/forge-gui-mobile/src/forge/adventure/scene/ShopScene.java index 58f3bbdf5de..fc3ebbfe8f8 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/ShopScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/ShopScene.java @@ -10,7 +10,6 @@ import forge.localinstance.properties.ForgePreferences; import forge.model.FModel; import forge.screens.FScreen; import forge.toolbox.FOptionPane; -import forge.util.Callback; import forge.util.ItemPool; /** @@ -65,12 +64,9 @@ public class ShopScene extends ForgeScene { return; FOptionPane.showConfirmDialog(Forge.getLocalizer().getMessage("lblSellAllConfirm", cards, profit), Forge.getLocalizer().getMessage("lblAutoSellable"), Forge.getLocalizer().getMessage("lblSell"), - Forge.getLocalizer().getMessage("lblCancel"), false, new Callback() { - @Override - public void run(Boolean result) { - if (result) { - doAutosell(); - } + Forge.getLocalizer().getMessage("lblCancel"), false, result -> { + if (result) { + doAutosell(); } } ); diff --git a/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java b/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java index 31d77f9e2dd..690d7cbc271 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java @@ -401,7 +401,7 @@ public class SpellSmithScene extends UIScene { if (cost_low > -1) totalCost *= 2.5f; //And CMC cost multiplier. cardPool = StreamUtil.stream(P).collect(Collectors.toList()); - poolSize.setText(((cardPool.size() > 0 ? "[/][FOREST]" : "[/][RED]")) + cardPool.size() + " possible card" + (cardPool.size() != 1 ? "s" : "")); + poolSize.setText(((cardPool.size() > 0 ? "[/][FOREST]" : "[/][RED]")) + cardPool.size() + " possible card" + (cardPool.size() > 1 ? "s" : "")); currentPrice = (int) totalCost; currentShardPrice = (int) (totalCost * 0.2f); //Intentionally rounding up via the cast to int pullUsingGold.setText("[+Pull][+goldcoin] "+ currentPrice); diff --git a/forge-gui-mobile/src/forge/adventure/stage/Console.java b/forge-gui-mobile/src/forge/adventure/stage/Console.java index 2d320614c98..5bc67052d15 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/Console.java +++ b/forge-gui-mobile/src/forge/adventure/stage/Console.java @@ -1,22 +1,26 @@ package forge.adventure.stage; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.utils.Align; +import com.badlogic.gdx.utils.Array; import forge.Forge; import forge.adventure.util.Controls; public class Console extends Window { private final ScrollPane scroll; - private String last = ""; + private String last = ""; private final InputLine input; private final Table content; public void toggle() { - if(isVisible()) { + if (isVisible()) { setVisible(false); getStage().unfocus(input); + Gdx.input.setOnscreenKeyboardVisible(false); } else { if (!Forge.advFreezePlayerControls) { setVisible(true); @@ -27,47 +31,117 @@ public class Console extends Window { static class InputLine extends TextField { private final Console console; + private final Array commands = new Array<>(); + private int index; + private final TextField textField; public InputLine(Console console) { super("", Controls.getSkin()); this.console = console; - writeEnters=true; + writeEnters = true; + textField = this; } @Override - protected InputListener createInputListener () { - TextField self = this; - return new TextFieldClickListener() - { + protected InputListener createInputListener() { + return new TextFieldClickListener() { @Override - public boolean keyTyped (InputEvent event, char character) { - // Disallow "typing" most ASCII control characters, which would show up as a space when onlyFontChars is true. - switch (character) { - case BACKSPACE: - break; - case TAB: - if(self.getText().isEmpty()) - { - self.setText(console.last); - } - else - { - self.setText(console.complete(self.getText())); - self.setCursorPosition(Integer.MAX_VALUE); - } - break; - case NEWLINE: - case CARRIAGE_RETURN: - console.command(self.getText()); - self.setText(""); - return false; - default: - if (character < 32) return false; + public boolean keyUp(InputEvent event, int keycode) { + switch (keycode) { + case Input.Keys.UP: + if (!textField.getText().isEmpty()) { + index = commands.indexOf(textField.getText(), false) - 1; + if (index >= 0 && index < commands.size) { + textField.setText(commands.get(index)); + console.last = textField.getText(); + index = commands.indexOf(console.last, false); + textField.setCursorPosition(Integer.MAX_VALUE); + } else { + index = 0; + textField.setText(commands.get(index)); + textField.setCursorPosition(Integer.MAX_VALUE); + } + } else if (!commands.isEmpty()) { + textField.setText(commands.get(commands.size - 1)); + console.last = textField.getText(); + index = commands.indexOf(console.last, false); + textField.setCursorPosition(Integer.MAX_VALUE); + } + break; + case Input.Keys.DOWN: + if (!textField.getText().isEmpty()) { + index = commands.indexOf(textField.getText(), false) + 1; + if (index >= 0 && index < commands.size) { + textField.setText(commands.get(index)); + console.last = textField.getText(); + index = commands.indexOf(console.last, false); + textField.setCursorPosition(Integer.MAX_VALUE); + } else { + index = commands.size - 1; + textField.setText(commands.get(index)); + textField.setCursorPosition(Integer.MAX_VALUE); + } + } else if (!commands.isEmpty()) { + textField.setText(commands.get(0)); + console.last = textField.getText(); + index = commands.indexOf(console.last, false); + textField.setCursorPosition(Integer.MAX_VALUE); + } + break; + case Input.Keys.LEFT: + if (!textField.getText().isEmpty()) { + if ((textField.getCursorPosition() - 1) >= 0) + textField.setCursorPosition(textField.getCursorPosition() - 1); + } + break; + case Input.Keys.RIGHT: + if (!textField.getText().isEmpty()) { + if ((textField.getCursorPosition() + 1) <= textField.getText().length()) + textField.setCursorPosition(textField.getCursorPosition() + 1); + } + break; + case Input.Keys.V: + if (Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT)) { + if (Forge.getClipboard().hasContents()) { + textField.appendText(Forge.getClipboard().getContents()); + textField.setCursorPosition(Integer.MAX_VALUE); + } + } + default: + break; + } + return super.keyUp(event, keycode); } - return super.keyTyped(event,character); + + @Override + public boolean keyTyped(InputEvent event, char character) { + // Disallow "typing" most ASCII control characters, which would show up as a space when onlyFontChars is true. + switch (character) { + case BACKSPACE: + break; + case TAB: + if (textField.getText().isEmpty()) { + textField.setText(console.last); + } else { + textField.setText(console.complete(textField.getText())); + textField.setCursorPosition(Integer.MAX_VALUE); + } + break; + case NEWLINE: + case CARRIAGE_RETURN: + commands.add(textField.getText()); + console.command(textField.getText()); + textField.setText(""); + return false; + default: + if (character < 32) + return false; + } + return super.keyTyped(event, character); } }; } + } private String complete(String text) { @@ -79,13 +153,13 @@ public class Console extends Window { toggle(); return; } - Cell