From c5c4a886271b1108247d191327571643f95b9e6e Mon Sep 17 00:00:00 2001 From: Jeff T Date: Mon, 29 Dec 2025 17:04:57 -0500 Subject: [PATCH 1/5] Added castability highlighting for cards in hand --- .../home/settings/CSubmenuPreferences.java | 1 + .../home/settings/VSubmenuPreferences.java | 8 ++ .../java/forge/view/arcane/CardPanel.java | 83 +++++++++++++++++++ forge-gui/res/languages/en-US.properties | 2 + .../properties/ForgePreferences.java | 1 + 5 files changed, 95 insertions(+) diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java index bc2fee612c6..f03a3875120 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java @@ -155,6 +155,7 @@ public void initialize() { lstControls.add(Pair.of(view.getCbOpenPacksIndiv(), FPref.UI_OPEN_PACKS_INDIV)); lstControls.add(Pair.of(view.getCbTokensInSeparateRow(), FPref.UI_TOKENS_IN_SEPARATE_ROW)); lstControls.add(Pair.of(view.getCbStackCreatures(), FPref.UI_STACK_CREATURES)); + lstControls.add(Pair.of(view.getCbShowCastableBorder(), FPref.UI_SHOW_CASTABLE_BORDER)); lstControls.add(Pair.of(view.getCbManaLostPrompt(), FPref.UI_MANA_LOST_PROMPT)); lstControls.add(Pair.of(view.getCbEscapeEndsTurn(), FPref.UI_ALLOW_ESC_TO_END_TURN)); lstControls.add(Pair.of(view.getCbDetailedPaymentDesc(), FPref.UI_DETAILED_SPELLDESC_IN_PROMPT)); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java index 469eb15cfdd..ade7177bec1 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java @@ -109,6 +109,7 @@ public enum VSubmenuPreferences implements IVSubmenu { private final JCheckBox cbOpenPacksIndiv = new OptionsCheckBox(localizer.getMessage("cbOpenPacksIndiv")); private final JCheckBox cbTokensInSeparateRow = new OptionsCheckBox(localizer.getMessage("cbTokensInSeparateRow")); private final JCheckBox cbStackCreatures = new OptionsCheckBox(localizer.getMessage("cbStackCreatures")); + private final JCheckBox cbShowCastableBorder = new OptionsCheckBox(localizer.getMessage("cbShowCastableBorder")); private final JCheckBox cbFilterLandsByColorId = new OptionsCheckBox(localizer.getMessage("cbFilterLandsByColorId")); private final JCheckBox cbShowStormCount = new OptionsCheckBox(localizer.getMessage("cbShowStormCount")); private final JCheckBox cbRemindOnPriority = new OptionsCheckBox(localizer.getMessage("cbRemindOnPriority")); @@ -423,6 +424,9 @@ public enum VSubmenuPreferences implements IVSubmenu { pnlPrefs.add(cbStackCreatures, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlStackCreatures")), descriptionConstraints); + pnlPrefs.add(cbShowCastableBorder, titleConstraints); + pnlPrefs.add(new NoteLabel(localizer.getMessage("nlShowCastableBorder")), descriptionConstraints); + pnlPrefs.add(cbTimedTargOverlay, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlTimedTargOverlay")), descriptionConstraints); @@ -973,6 +977,10 @@ public final JCheckBox getCbStackCreatures() { return cbStackCreatures; } + public final JCheckBox getCbShowCastableBorder() { + return cbShowCastableBorder; + } + public final JCheckBox getCbManaLostPrompt() { return cbManaLostPrompt; } diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index d2377eb3739..39ced0ab8d0 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java @@ -45,6 +45,7 @@ import javax.swing.JRootPane; import javax.swing.SwingUtilities; +import forge.ai.ComputerUtilMana; import forge.CachedCardImage; import forge.StaticData; import forge.card.CardEdition; @@ -55,6 +56,7 @@ import forge.game.card.CardView.CardStateView; import forge.game.card.CounterType; import forge.game.keyword.Keyword; +import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.gui.CardContainer; import forge.gui.FThreads; @@ -299,6 +301,13 @@ protected final void paintComponent(final Graphics g) { g2d.fillRoundRect(cardXOffset - n2, (cardYOffset - n2) + offset, cardWidth + (n2 * 2), cardHeight + (n2 * 2), cornerSize + n2, cornerSize + n2); } + // Yellow border for cards in hand that can be cast with available mana + if (canCastCard() && isPreferenceEnabled(FPref.UI_SHOW_CASTABLE_BORDER)) { + g2d.setColor(Color.YELLOW); + final int n = Math.max(1, Math.round(cardWidth * CardPanel.SELECTED_BORDER_SIZE)); + g2d.fillRoundRect(cardXOffset - n, (cardYOffset - n) + offset, cardWidth + (n * 2), cardHeight + (n * 2), cornerSize + n , cornerSize + n); + } + // Green outline for hover if (isSelected) { g2d.setColor(Color.green); @@ -352,6 +361,80 @@ protected final void paintComponent(final Graphics g) { } } + /* + * Checks if the card in this panel can be cast with available mana. + * Returns true if the card is in hand and has at least one spell ability that can be played. + * This method is used to highlight castable cards with a yellow border. + */ + private boolean canCastCard() { + // Only check cards in hand + if (card == null || !ZoneType.Hand.equals(card.getZone())) { + return false; + } + + // Get the game view to access the actual Game object + final var gameView = matchUI.getGameView(); + if (gameView == null) { + return false; + } + + final var game = gameView.getGame(); + if (game == null) { + return false; + } + + // Find the actual Card object in the player's hand by matching ID + Card actualCard = null; + for (final var player : game.getPlayers()) { + final var hand = player.getZone(ZoneType.Hand); + if (hand != null) { + for (final Card c : hand) { + if (c.getId() == card.getId()) { + actualCard = c; + break; + } + } + if (actualCard != null) { + break; + } + } + } + + if (actualCard == null) { + return false; + } + + // Get all spell abilities for this card + final var abilities = actualCard.getSpellAbilities(); + if (abilities == null || abilities.isEmpty()) { + return false; + } + + // Get the player who owns this card + final var player = actualCard.getOwner(); + if (player == null) { + return false; + } + + for (final SpellAbility sa : abilities) { + // First check if the spell can be played (timing, restrictions, etc.) + if (!sa.canPlay(true)) { + continue; + } + + // Set the activating player for the spell ability + sa.setActivatingPlayer(player); + + // Check if the player can actually pay the mana cost from available sources + if (ComputerUtilMana.canPayManaCost(sa, player, 0, false)) { + return true; + } + } + //If no costs can be met + return false; + } + + private void drawManaCost(final Graphics g, final ManaCost cost, final int deltaY) { final int width = CardFaceSymbols.getWidth(cost); final int height = CardFaceSymbols.getHeight(); diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index ebbbf217683..b89221a1943 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -135,6 +135,7 @@ cbCardTextHideReminder=Hide Reminder Text for Card Text Renderer cbOpenPacksIndiv=Open Packs Individually cbTokensInSeparateRow=Display Tokens in a Separate Row cbStackCreatures=Stack Creatures +cbShowCastableBorder=Enable Castability Highlighting cbFilterLandsByColorId=Filter Lands by Color in Activated Abilities cbShowStormCount=Show Storm Count in Prompt Pane cbRemindOnPriority=Visually Alert on Receipt of Priority @@ -240,6 +241,7 @@ nlCardTextHideReminder=When render card images, skip rendering reminder text. nlOpenPacksIndiv=When opening Fat Packs and Booster Boxes, booster packs will be opened and displayed one at a time. nlTokensInSeparateRow=Displays tokens in a separate row on the battlefield below the non-token creatures. nlStackCreatures=Stacks identical creatures on the battlefield like lands, artifacts, and enchantments. +nlShowCastableBorder=Enables a yellow border that highlights cards that can currently be cast (considering available mana, timing, etc.) nlTimedTargOverlay=Enables throttling-based optimization of targeting overlay to reduce CPU use (only disable if you experience choppiness on older hardware, requires starting a new match). nlCounterDisplayType=Selects the style of the in-game counter display for cards. Text-based is a new tab-like display on the cards. Image-based is the old counter image. Hybrid displays both at once. nlCounterDisplayLocation=Determines where to position the text-based counters on the card: close to the top or close to the bottom. diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java index 55d702a3d6a..93426a00cc2 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java @@ -77,6 +77,7 @@ public enum FPref implements PreferencesStore.IPref { UI_AVATARS ("0,1"), UI_SLEEVES ("0,1"), UI_SHOW_CARD_OVERLAYS ("true"), + UI_SHOW_CASTABLE_BORDER ("false"), UI_OVERLAY_CARD_NAME ("true"), UI_OVERLAY_CARD_POWER ("true"), UI_OVERLAY_CARD_MANA_COST ("true"), From 2417ef8c22f2be2d63d7f484bf374edfddef3a2b Mon Sep 17 00:00:00 2001 From: Jeff T Date: Tue, 30 Dec 2025 08:46:05 -0500 Subject: [PATCH 2/5] Addressed PR feedback: now checks preference before running canCastCard() check --- .../src/main/java/forge/view/arcane/CardPanel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index 39ced0ab8d0..99e36a57335 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java @@ -302,7 +302,7 @@ protected final void paintComponent(final Graphics g) { } // Yellow border for cards in hand that can be cast with available mana - if (canCastCard() && isPreferenceEnabled(FPref.UI_SHOW_CASTABLE_BORDER)) { + if (isPreferenceEnabled(FPref.UI_SHOW_CASTABLE_BORDER) && canCastCard()) { g2d.setColor(Color.YELLOW); final int n = Math.max(1, Math.round(cardWidth * CardPanel.SELECTED_BORDER_SIZE)); g2d.fillRoundRect(cardXOffset - n, (cardYOffset - n) + offset, cardWidth + (n * 2), cardHeight + (n * 2), cornerSize + n , cornerSize + n); From f0d71ece300d2d5857f198006814ffaff0856921 Mon Sep 17 00:00:00 2001 From: Jeff T Date: Tue, 30 Dec 2025 09:30:19 -0500 Subject: [PATCH 3/5] Addressed PR feedback: refactored nested loop to use game.findByView instead --- .../java/forge/view/arcane/CardPanel.java | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index 99e36a57335..a3ebbccebfd 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java @@ -383,23 +383,8 @@ private boolean canCastCard() { return false; } - // Find the actual Card object in the player's hand by matching ID - Card actualCard = null; - for (final var player : game.getPlayers()) { - final var hand = player.getZone(ZoneType.Hand); - if (hand != null) { - for (final Card c : hand) { - if (c.getId() == card.getId()) { - actualCard = c; - break; - } - } - if (actualCard != null) { - break; - } - } - } - + // Find the actual Card object + Card actualCard = game.findByView(card); if (actualCard == null) { return false; } From 2aefcf6fb09aec99f0eb0f3cb619f653ea59c934 Mon Sep 17 00:00:00 2001 From: Jeff T Date: Thu, 1 Jan 2026 18:02:02 -0500 Subject: [PATCH 4/5] Wraps method in try/catch to avoids errors thrown during exceptions --- .../java/forge/view/arcane/CardPanel.java | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index a3ebbccebfd..c5581d96685 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java @@ -367,56 +367,62 @@ protected final void paintComponent(final Graphics g) { * This method is used to highlight castable cards with a yellow border. */ private boolean canCastCard() { - // Only check cards in hand - if (card == null || !ZoneType.Hand.equals(card.getZone())) { - return false; - } - - // Get the game view to access the actual Game object - final var gameView = matchUI.getGameView(); - if (gameView == null) { + try { + // Only check cards in hand + if (card == null || !ZoneType.Hand.equals(card.getZone())) { return false; - } + } - final var game = gameView.getGame(); - if (game == null) { - return false; - } + // Get the game view to access the actual Game object + final var gameView = matchUI.getGameView(); + if (gameView == null) { + return false; + } - // Find the actual Card object - Card actualCard = game.findByView(card); - if (actualCard == null) { - return false; - } + final var game = gameView.getGame(); + if (game == null) { + return false; + } - // Get all spell abilities for this card - final var abilities = actualCard.getSpellAbilities(); - if (abilities == null || abilities.isEmpty()) { - return false; - } + // Find the actual Card object + Card actualCard = game.findByView(card); + if (actualCard == null) { + return false; + } - // Get the player who owns this card - final var player = actualCard.getOwner(); - if (player == null) { - return false; - } + // Get all spell abilities for this card + final var abilities = actualCard.getSpellAbilities(); + if (abilities == null || abilities.isEmpty()) { + return false; + } - for (final SpellAbility sa : abilities) { - // First check if the spell can be played (timing, restrictions, etc.) - if (!sa.canPlay(true)) { - continue; + // Get the player who owns this card + final var player = actualCard.getOwner(); + if (player == null) { + return false; } - // Set the activating player for the spell ability - sa.setActivatingPlayer(player); + for (final SpellAbility sa : abilities) { + // First check if the spell can be played (timing, restrictions, etc.) + if (!sa.canPlay(true)) { + continue; + } + + // Set the activating player for the spell ability + sa.setActivatingPlayer(player); - // Check if the player can actually pay the mana cost from available sources - if (ComputerUtilMana.canPayManaCost(sa, player, 0, false)) { - return true; + // Check if the player can actually pay the mana cost from available sources + if (ComputerUtilMana.canPayManaCost(sa, player, 0, false)) { + return true; + } } + //If no costs can be met + return false; + + } catch (Exception e) { + // If any exception occurs (NullPointerException, etc.), safely return false + return false; } - //If no costs can be met - return false; } From e8f175d2c5ae94d270b7b8b2cd0262b480b82881 Mon Sep 17 00:00:00 2001 From: Jeff T Date: Tue, 13 Jan 2026 10:43:37 -0500 Subject: [PATCH 5/5] Addressed PR Feedback: Fixed spacing --- .../src/main/java/forge/view/arcane/CardPanel.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index c5581d96685..2022a7fec13 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java @@ -375,13 +375,13 @@ private boolean canCastCard() { // Get the game view to access the actual Game object final var gameView = matchUI.getGameView(); - if (gameView == null) { - return false; + if (gameView == null) { + return false; } final var game = gameView.getGame(); - if (game == null) { - return false; + if (game == null) { + return false; } // Find the actual Card object