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..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 @@ -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 (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); + } + // Green outline for hover if (isSelected) { g2d.setColor(Color.green); @@ -352,6 +361,71 @@ 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() { + try { + // 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 + Card actualCard = game.findByView(card); + 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; + + } catch (Exception e) { + // If any exception occurs (NullPointerException, etc.), safely return false + 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"),