-
Notifications
You must be signed in to change notification settings - Fork 883
Add card info tooltips with keyword explanations and related cards #9806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MostCromulent
wants to merge
47
commits into
Card-Forge:master
Choose a base branch
from
MostCromulent:hoveroptions
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
ec1c136
Add card info popup with card image, spellbook, and size controls
MostCromulent 51cff1d
Improve card info popup: single-instance enforcement, focus handling,…
MostCromulent 4aa4a40
Constrain CardInfoPopup to Forge window bounds instead of screen bounds
MostCromulent 8573d9a
Improve card info popup: layout, sizing, and related card categories
MostCromulent c2527b0
Restyle card info popup with dark pill panels and proper text rendering
MostCromulent 41e8057
Add keyword actions to card info popup and improve layout
MostCromulent 5ac627c
Merge branch 'Card-Forge:master' into hoveroptions
MostCromulent f9f51c1
Clean up card info popup: remove dead code and duplicate state
MostCromulent 24a7fab
Fix popup during zoom and related card rendering without art
MostCromulent 80af802
Improve keyword popup text and ordering
MostCromulent 27bf7f1
Add keyword and related card info to card zoom view
MostCromulent 6f5e495
Fix granted keyword detection and clean up popup labels
MostCromulent f47e697
Improve zoom view layout and fix related card image loading
MostCromulent 5db88c6
Add translation keys for keyword action names and reminder text
MostCromulent 59b0115
Remove duplicate English strings from KeywordAction enum
MostCromulent 6709ad0
Move keyword detection logic to shared forge-gui module
MostCromulent a54d3dc
Fix Assemble/Set In Motion descriptions and mark scheme actions as basic
MostCromulent be48e91
Handle scry conjugation in keyword matching and fix zoom scroll layout
MostCromulent c79e6f6
Address PR reviewer feedback on keyword display and zoom view
MostCromulent 8a2ce7d
Fix tooltip flickering on tapped cards by using component bounds for …
MostCromulent bc46a36
Improve hover tooltip and zoom view layout and fix keyword display is…
MostCromulent b1e6ca3
Fix specialize face rendering and Scryfall download for hover tooltip
MostCromulent c9da360
Add dynamic keyword count annotations to hover tooltips and zoom view
MostCromulent 4f15496
Improve Affinity keyword matching and add stack hover tooltips
MostCromulent adfee29
Add card overlay painting to hover tooltip image
MostCromulent b2b0498
Remove dead code from CardInfoPopup
MostCromulent efc1247
Merge branch 'Card-Forge:master' into hoveroptions
MostCromulent 9f1f468
Add menu options to hide card picture and detail panels
MostCromulent 6600fe0
Fix card panel re-enable to create right column instead of stacking i…
MostCromulent f1b0fd9
Fix face-down info leak, enable keyword popup by default, and cleanup…
MostCromulent 7e52afd
Adjust tooltip overlay positions and hide popup on game end
MostCromulent 150c7d7
Merge upstream/master into hoveroptions
MostCromulent cfcd562
Show hover tooltip on game log inline card images
MostCromulent 6820fbf
Add Card Overlay Settings dialog with independent hover overlay prefs
MostCromulent 2843e96
Improve hover tooltip keywords, related cards, and preference handling
MostCromulent 5af05ac
Show Amass Army tokens as related cards in hover tooltip
MostCromulent cbdfa8e
Merge upstream/master into hoveroptions
MostCromulent 2ce6e02
Minor comment cleanup
MostCromulent e415e74
Rework tooltip preferences: master toggles, hotkeys, and updated defa…
MostCromulent 3b990c0
Fix devotion tooltip: support dual-color devotion and immediate hotke…
MostCromulent 2d8d828
Remove dead code: unused JSeparator and unreachable overload
MostCromulent 985075c
Address PR review feedback: keyword display and layout fixes
MostCromulent 00f5230
Merge upstream/master into hoveroptions
MostCromulent 7c6552f
Fix Escape/Craft keyword headers: wrapping and full cost display
MostCromulent da9e502
Hover/zoom tooltip: 1-card-wide keywords + compact multi-group relate…
MostCromulent 9fbd234
Fix Support false positives, suppress Manifest when Manifest Dread pr…
MostCromulent e18a9f0
Add graveyard count tooltips for Tarmogoyf/Lhurgoyf-family cards
MostCromulent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
134 changes: 134 additions & 0 deletions
134
forge-game/src/main/java/forge/game/keyword/KeywordAction.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| package forge.game.keyword; | ||
|
|
||
| /** | ||
| * Keyword actions — verbs that appear in rules text but aren't keyword abilities. | ||
| * Descriptions are based on the MTG comprehensive rules (section 701). | ||
| * | ||
| * <p>Unlike {@link Keyword} (keyword abilities that grant continuous effects or | ||
| * triggered/static abilities), keyword actions are one-shot game actions performed | ||
| * when instructed by a spell or ability.</p> | ||
| * | ||
| * <p>Actions marked {@code basic=true} are fundamental game actions (destroy, exile, | ||
| * sacrifice, etc.) that every player knows — UI code may choose to omit these from | ||
| * tooltips to avoid clutter.</p> | ||
| */ | ||
| public enum KeywordAction { | ||
| // 701.2 – 701.13: Basic game actions | ||
| ACTIVATE("Activate", "Put an activated ability on the stack.", true), | ||
| ATTACH("Attach", "Move an Aura, Equipment, or Fortification onto another object or player.", true), | ||
| BEHOLD("Behold", "Reveal a card with the required quality from your hand, or choose a permanent you control with that quality.", false), | ||
| CAST("Cast", "Put a spell on the stack.", true), | ||
| COUNTER("Counter", "Remove a spell or ability from the stack. It doesn't resolve and none of its effects happen.", true), | ||
| CREATE("Create", "Put a token onto the battlefield.", true), | ||
| DESTROY("Destroy", "Move a permanent from the battlefield to its owner's graveyard.", true), | ||
| DISCARD("Discard", "Put a card from your hand into its owner's graveyard.", true), | ||
| DOUBLE("Double", "Double a creature's power and toughness, double the number of counters, or double the number of tokens.", true), | ||
| TRIPLE("Triple", "Triple a value — for example, triple a creature's power and toughness.", true), | ||
| EXCHANGE("Exchange", "Swap values or control of objects between two players or permanents.", true), | ||
| EXILE("Exile", "Move an object to the exile zone.", true), | ||
|
|
||
| // 701.14 – 701.17: Combat & graveyard actions | ||
| FIGHT("Fight", "Each creature deals damage equal to its power to the other.", false), | ||
| GOAD("Goad", "A goaded creature attacks each combat if able and attacks a player other than you if able.", false), | ||
| INVESTIGATE("Investigate", "Create a Clue token. It's an artifact with \"{2}, Sacrifice this: Draw a card.\"", false), | ||
| MILL("Mill", "Put the top N cards of your library into your graveyard.", false), | ||
|
|
||
| // 701.18 – 701.21: More basic actions | ||
| PLAY("Play", "Play a land or cast a spell.", true), | ||
| REGENERATE("Regenerate", "Instead of being destroyed, tap this permanent, remove all damage from it, and remove it from combat.", false), | ||
| REVEAL("Reveal", "Show a card to all players.", true), | ||
| SACRIFICE("Sacrifice", "Move a permanent you control from the battlefield to its owner's graveyard.", true), | ||
|
|
||
| // 701.22 – 701.28: Library & transform actions | ||
| SCRY("Scry", "Look at the top N cards of your library, then put any number on the bottom and the rest on top in any order.", false), | ||
| SEARCH("Search", "Look through a zone for a card meeting certain criteria.", true), | ||
| SHUFFLE("Shuffle", "Randomize the order of cards in a library.", true), | ||
| SURVEIL("Surveil", "Look at the top N cards of your library, then put any number into your graveyard and the rest on top in any order.", false), | ||
| TAP_UNTAP("Tap/Untap", "Rotate a permanent sideways to show it's been used, or straighten it to show it's ready.", true), | ||
| TRANSFORM("Transform", "Turn this double-faced card over to its other face.", false), | ||
| CONVERT("Convert", "Turn this double-faced card over to its other face. Unlike transform, convert can change a card from front to back or back to front.", false), | ||
|
|
||
| // 701.29 – 701.30: Opponent manipulation | ||
| FATESEAL("Fateseal", "Look at the top N cards of an opponent's library, then put any number on the bottom and the rest on top in any order.", false), | ||
| CLASH("Clash", "Each clashing player reveals the top card of their library, then puts it on the top or bottom. You win if your card's mana value is higher.", false), | ||
|
|
||
| // 701.31 – 701.33: Supplemental format actions | ||
| PLANESWALK("Planeswalk", "Move to a new plane by turning over the next card in the planar deck.", false), | ||
| SET_IN_MOTION("Set in motion", "Turn a scheme face up and follow its instructions.", false), | ||
| ABANDON("Abandon", "Turn a face-up ongoing scheme face down and put it on the bottom of its owner's scheme deck.", false), | ||
|
|
||
| // 701.34 – 701.36: Counter & token actions | ||
| PROLIFERATE("Proliferate", "Choose any number of permanents and/or players, then give each another counter of each kind already there.", false), | ||
| DETAIN("Detain", "Until your next turn, that permanent can't attack or block and its activated abilities can't be activated.", false), | ||
| POPULATE("Populate", "Create a token that's a copy of a creature token you control.", false), | ||
|
|
||
| // 701.37 – 701.39: Creature enhancement | ||
| MONSTROSITY("Monstrosity", "If this creature isn't monstrous, put N +1/+1 counters on it and it becomes monstrous.", false), | ||
| VOTE("Vote", "Each player votes for one of the given options. The outcome depends on which option gets more votes.", false), | ||
| BOLSTER("Bolster", "Choose a creature you control with the least toughness and put N +1/+1 counters on it.", false), | ||
|
|
||
| // 701.40 – 701.44: Face-down & explore | ||
| MANIFEST("Manifest", "Put the top card of your library onto the battlefield face down as a 2/2 creature. Turn it face up any time for its mana cost if it's a creature card.", false), | ||
| SUPPORT("Support", "Put a +1/+1 counter on each of up to N target creatures.", false), | ||
| MELD("Meld", "Exile two specific cards and combine them into one oversized card on the battlefield.", false), | ||
| EXERT("Exert", "An exerted creature won't untap during your next untap step.", false), | ||
| EXPLORE("Explore", "Reveal the top card of your library. Put it into your hand if it's a land. Otherwise, put a +1/+1 counter on this creature, then you may put the card back or into your graveyard.", false), | ||
|
|
||
| // 701.45 – 701.48: Un-set & learning | ||
| ASSEMBLE("Assemble", "Put a Contraption you own from outside the game onto the battlefield on one of your sprockets.", false), | ||
| ADAPT("Adapt", "If this creature has no +1/+1 counters on it, put N +1/+1 counters on it.", false), | ||
| AMASS("Amass", "Put N +1/+1 counters on an Army you control. If you don't control one, create a 0/0 black Zombie Army creature token first.", false), | ||
| LEARN("Learn", "You may reveal a Lesson card from outside the game and put it into your hand, or discard a card to draw a card.", false), | ||
|
|
||
| // 701.49 – 701.50: Dungeon & connive | ||
| VENTURE("Venture", "Move to the next room of a dungeon. If you're not in one, enter the first room of a dungeon of your choice.", false), | ||
| CONNIVE("Connive", "Draw a card, then discard a card. If you discarded a nonland card, put a +1/+1 counter on this creature.", false), | ||
|
|
||
| // 701.51 – 701.52: Attraction actions | ||
| OPEN_AN_ATTRACTION("Open an Attraction", "Put the top card of your Attraction deck onto the battlefield face up.", false), | ||
| ROLL_TO_VISIT("Roll to visit your Attractions", "Roll a six-sided die. Each Attraction you control whose lit-up numbers include the result is visited.", false), | ||
|
|
||
| // 701.53 – 701.54: Incubate & ring | ||
| INCUBATE("Incubate", "Create an Incubator token with N +1/+1 counters on it. It has \"{2}: Transform this artifact.\" It transforms into a 0/0 Phyrexian artifact creature.", false), | ||
| THE_RING_TEMPTS_YOU("The Ring tempts you", "Choose a creature you control as your Ring-bearer. Your Ring gains its next ability.", false), | ||
|
|
||
| // 701.55 – 701.56: Villainous choice & time travel | ||
| FACE_A_VILLAINOUS_CHOICE("Face a villainous choice", "Choose one of two options presented by an opponent. The chosen option's effects happen.", false), | ||
| TIME_TRAVEL("Time travel", "For each suspended card you own and each permanent you control with a time counter, you may add or remove a time counter.", false), | ||
|
|
||
| // 701.57 – 701.60: Discover, cloak, evidence, suspect | ||
| DISCOVER("Discover", "Exile cards from the top of your library until you exile a nonland card with lower mana value. Cast it without paying its mana cost or put it into your hand.", false), | ||
| CLOAK("Cloak", "Put a card onto the battlefield face down as a 2/2 creature with ward {2}. Turn it face up any time for its mana cost if it's a creature card.", false), | ||
| COLLECT_EVIDENCE("Collect evidence", "Exile cards from your graveyard with total mana value N or greater.", false), | ||
| SUSPECT("Suspect", "A suspected creature has menace and can't block.", false), | ||
|
|
||
| // 701.61 – 701.64: Bloomburrow & beyond | ||
| FORAGE("Forage", "Exile three cards from your graveyard or sacrifice a Food.", false), | ||
| MANIFEST_DREAD("Manifest dread", "Look at the top two cards of your library. Manifest one and put the other into your graveyard.", false), | ||
| ENDURE("Endure", "Choose to either put N +1/+1 counters on this creature or create an N/N white Spirit creature token.", false), | ||
| HARNESS("Harness", "This permanent becomes harnessed. It stays harnessed until it leaves the battlefield.", false), | ||
|
|
||
| // 701.65 – 701.68: Avatar & Lorwyn Eclipsed | ||
| AIRBEND("Airbend", "Exile a permanent. Its owner may cast it for {2} as long as it remains exiled.", false), | ||
| EARTHBEND("Earthbend", "Target land you control becomes a 0/0 creature with haste. Put N +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.", false), | ||
| WATERBEND("Waterbend", "Pay a mana cost, but for each mana symbol you may tap an untapped artifact or creature you control instead of paying that mana.", false), | ||
| BLIGHT("Blight", "Put N -1/-1 counters on a creature you control.", false); | ||
|
|
||
| /** Display name as it appears in rules text. */ | ||
| public final String displayName; | ||
| /** Reminder text describing what the action does. */ | ||
| public final String reminderText; | ||
| /** True for fundamental game actions (destroy, exile, sacrifice, etc.) that don't need tooltip explanations. */ | ||
| public final boolean basic; | ||
|
|
||
| KeywordAction(String displayName, String reminderText, boolean basic) { | ||
| this.displayName = displayName; | ||
| this.reminderText = reminderText; | ||
| this.basic = basic; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return displayName; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
forge-gui-desktop/src/main/java/forge/screens/match/menus/CardInfoPopupMenu.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| package forge.screens.match.menus; | ||
|
|
||
| import java.awt.BorderLayout; | ||
| import java.awt.Color; | ||
| import java.awt.Font; | ||
| import java.awt.event.MouseEvent; | ||
|
|
||
| import javax.swing.JCheckBoxMenuItem; | ||
| import javax.swing.JMenu; | ||
| import javax.swing.JMenuItem; | ||
| import javax.swing.JPanel; | ||
| import javax.swing.JSeparator; | ||
| import javax.swing.SwingConstants; | ||
|
|
||
| import forge.localinstance.properties.ForgePreferences; | ||
| import forge.localinstance.properties.ForgePreferences.FPref; | ||
| import forge.model.FModel; | ||
| import forge.toolbox.FSkin; | ||
| import forge.toolbox.FSkin.SkinnedLabel; | ||
| import forge.toolbox.FSkin.SkinnedSlider; | ||
| import forge.util.Localizer; | ||
|
|
||
| /** | ||
| * Submenu under the Game menu for toggling card info popup sections | ||
| * (keyword explanations and related cards) during a match. | ||
| */ | ||
| public final class CardInfoPopupMenu { | ||
| private static final ForgePreferences prefs = FModel.getPreferences(); | ||
|
|
||
| public CardInfoPopupMenu() { | ||
| } | ||
|
|
||
| public JMenu getMenu() { | ||
| final Localizer localizer = Localizer.getInstance(); | ||
| final JMenu menu = new JMenu(localizer.getMessage("lblCardInfoPopups")); | ||
|
|
||
| // --- Hover Tooltip section --- | ||
| menu.add(createSectionHeader(localizer.getMessage("lblHoverTooltip"))); | ||
| menu.add(getCheckboxItem(localizer.getMessage("lblCardImage"), | ||
| FPref.UI_POPUP_CARD_IMAGE)); | ||
| menu.add(getCheckboxItem(localizer.getMessage("lblRelatedCards"), | ||
| FPref.UI_POPUP_RELATED_CARDS)); | ||
| menu.add(getCheckboxItem(localizer.getMessage("lblKeywordExplanations"), | ||
| FPref.UI_POPUP_KEYWORD_INFO)); | ||
|
|
||
| menu.add(new JSeparator()); | ||
|
|
||
| // --- Card Zoom View section --- | ||
| menu.add(createSectionHeader(localizer.getMessage("lblCardZoomView"))); | ||
| menu.add(getCheckboxItem(localizer.getMessage("lblRelatedCards"), | ||
| FPref.UI_ZOOM_RELATED_CARDS)); | ||
| menu.add(getCheckboxItem(localizer.getMessage("lblKeywordExplanations"), | ||
| FPref.UI_ZOOM_KEYWORD_INFO)); | ||
|
|
||
| menu.add(new JSeparator()); | ||
| menu.add(buildImageSizePanel(localizer)); | ||
| return menu; | ||
| } | ||
|
|
||
| private static JMenuItem createSectionHeader(final String text) { | ||
| final JMenuItem header = new JMenuItem(text); | ||
| header.setEnabled(false); | ||
| header.setFont(header.getFont().deriveFont(Font.BOLD)); | ||
| return header; | ||
| } | ||
|
|
||
| private static JCheckBoxMenuItem getCheckboxItem(final String label, final FPref pref) { | ||
| final JCheckBoxMenuItem item = new JCheckBoxMenuItem(label) { | ||
| @Override | ||
| protected void processMouseEvent(final MouseEvent e) { | ||
| if (e.getID() == MouseEvent.MOUSE_RELEASED && contains(e.getPoint())) { | ||
| doClick(0); | ||
| setArmed(true); | ||
| } else { | ||
| super.processMouseEvent(e); | ||
| } | ||
| } | ||
| }; | ||
| item.setState(prefs.getPrefBoolean(pref)); | ||
| item.addActionListener(e -> { | ||
| final boolean newState = !prefs.getPrefBoolean(pref); | ||
| prefs.setPref(pref, newState); | ||
| prefs.save(); | ||
| }); | ||
| return item; | ||
| } | ||
|
|
||
| private static JPanel buildImageSizePanel(final Localizer localizer) { | ||
| final Color bg = FSkin.getColor(FSkin.Colors.CLR_THEME2).getColor(); | ||
| final Color fg = FSkin.getColor(FSkin.Colors.CLR_TEXT).getColor(); | ||
| final int rawValue = prefs.getPrefInt(FPref.UI_POPUP_IMAGE_SIZE); | ||
| final int currentValue = Math.max(100, Math.min(500, rawValue)); | ||
|
|
||
| final JPanel panel = new JPanel(new BorderLayout()); | ||
| panel.setBackground(bg); | ||
|
|
||
| final SkinnedLabel label = new SkinnedLabel(); | ||
| label.setText(localizer.getMessage("lblImageSize")); | ||
| label.setForeground(fg); | ||
| label.setFont(FSkin.getFont()); | ||
| label.setOpaque(true); | ||
| label.setBackground(bg); | ||
|
|
||
| final SkinnedSlider slider = new SkinnedSlider(SwingConstants.HORIZONTAL, 100, 500, currentValue); | ||
| slider.setMajorTickSpacing(100); | ||
| slider.setMinorTickSpacing(20); | ||
| slider.setPaintTicks(true); | ||
| slider.setPaintLabels(true); | ||
| slider.setBackground(bg); | ||
| slider.setForeground(fg); | ||
| slider.setFont(FSkin.getFont()); | ||
|
|
||
| slider.addChangeListener(e -> { | ||
| slider.repaint(); | ||
| prefs.setPref(FPref.UI_POPUP_IMAGE_SIZE, String.valueOf(slider.getValue())); | ||
| prefs.save(); | ||
| }); | ||
|
|
||
| panel.add(label, BorderLayout.NORTH); | ||
| panel.add(slider, BorderLayout.CENTER); | ||
| return panel; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.