Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public enum VSubmenuPreferences implements IVSubmenu<CSubmenuPreferences> {
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"));
Expand Down Expand Up @@ -423,6 +424,9 @@ public enum VSubmenuPreferences implements IVSubmenu<CSubmenuPreferences> {
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);

Expand Down Expand Up @@ -973,6 +977,10 @@ public final JCheckBox getCbStackCreatures() {
return cbStackCreatures;
}

public final JCheckBox getCbShowCastableBorder() {
return cbShowCastableBorder;
}

public final JCheckBox getCbManaLostPrompt() {
return cbManaLostPrompt;
}
Expand Down
74 changes: 74 additions & 0 deletions forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit of a misnomer here, since the AI not being able to pay the mana cost doesn't mean its not able to be paid. Also there could be circumstances where there's no legal targets, or other costs are unable to be paid that this would bypass.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it's really to show if you have the available mana to cast something so you don't have to count in your head. But it doesn't mean there's necessarily a valid target, or that you have enough life to pay the additional cost , or that playing the card won't trigger a game loss, etc. It also doesn't highlight cards with no mana cost that can be played (lands, zero CMC, etc.). It really is just a helper so you don't have to do a manual mana check/count each turn to see if you finally have enough mana of the right kind to cast that creature you've been waiting to cast, or if you've tapped 2 Plains and 3 Islands this turn, do you still have enough available to cast your other UW. Do you think renaming the feature is in order? Something other than castability highlighting like "Mana available" or somthing like that? Lmk what you think.

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();
Expand Down
2 changes: 2 additions & 0 deletions forge-gui/res/languages/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down