Skip to content

Commit b9546b6

Browse files
MostCromulentclaude
andcommitted
Auto-trigger persistence: desktop + mobile UI
Wire the trigger Always-Yes / Always-No tier-aware persistence into the desktop Swing and mobile libGDX UIs. - StackItemView: add SourceTriggerYieldKey TrackableProperty + getSourceTriggerYieldKey() accessor, populated from the source Trigger.getYieldKey() in updateSourceTrigger. - VStack (desktop + mobile): convert the Always-Yes / Always-No context-menu items from int triggerID to String triggerYieldKey, computing abilityScope from UI_AUTO_TRIGGER_MODE. Master's "Yield to entire stack" item is preserved unchanged. - KeyboardShortcuts (desktop) + MatchScreen (mobile): same int to String migration for the Y / N hotkeys. - VAutoTriggers (desktop + mobile): new dialog mirroring the VAutoYields layout. Lists trigger decisions via getYieldController().getAutoTriggers(). Disable-all checkbox toggles getDisableAutoTriggers / setDisableAutoTriggers. Remove button calls setShouldAlwaysAskTrigger with abilityScope computed from UI_AUTO_TRIGGER_MODE. - GameMenu (desktop) + VGameMenu (mobile): single Auto-Triggers entry beside the existing Auto-Yields entry. - VSubmenuPreferences + CSubmenuPreferences (desktop) + SettingsPage (mobile): radio group for UI_AUTO_TRIGGER_MODE, same four tier options as auto-yields, default Per Card. - en-US.properties: cbpAutoTriggerMode, nlpAutoTriggerMode, lblAutoTriggers, lblDisableAllAutoTriggers, lblRemoveTrigger, lblNoActiveAutoTrigger, lblNoAutoTrigger. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1cdeaf6 commit b9546b6

14 files changed

Lines changed: 370 additions & 50 deletions

File tree

forge-game/src/main/java/forge/game/spellability/StackItemView.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import forge.game.card.CardView;
44
import forge.game.card.IHasCardView;
55
import forge.game.player.PlayerView;
6+
import forge.game.trigger.Trigger;
67
import forge.trackable.TrackableCollection;
78
import forge.trackable.TrackableObject;
89
import forge.trackable.Tracker;
@@ -113,8 +114,14 @@ public boolean isTrigger() {
113114
public int getSourceTrigger() {
114115
return get(TrackableProperty.SourceTrigger);
115116
}
117+
public String getSourceTriggerYieldKey() {
118+
String s = get(TrackableProperty.SourceTriggerYieldKey);
119+
return s == null ? "" : s;
120+
}
116121
void updateSourceTrigger(SpellAbilityStackInstance si) {
117122
set(TrackableProperty.SourceTrigger, si.getSpellAbility().getSourceTrigger());
123+
Trigger t = si.getSpellAbility().getTrigger();
124+
set(TrackableProperty.SourceTriggerYieldKey, t == null ? "" : t.getYieldKey());
118125
}
119126

120127
public String getText() {

forge-game/src/main/java/forge/trackable/TrackableProperty.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ public enum TrackableProperty {
257257
//StackItem
258258
Key(TrackableTypes.StringType),
259259
SourceTrigger(TrackableTypes.IntegerType),
260+
SourceTriggerYieldKey(TrackableTypes.StringType),
260261
SourceCard(TrackableTypes.CardViewType),
261262
ActivatingPlayer(TrackableTypes.PlayerViewType),
262263
TargetCards(TrackableTypes.CardViewCollectionType),

forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,11 @@ public void actionPerformed(final ActionEvent e) {
162162
boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals(
163163
forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE));
164164
matchUI.getGameController().setShouldAutoYield(si.getKey(), true, abilityScope);
165-
int triggerID = si.getSourceTrigger();
166-
if (si.isOptionalTrigger() && matchUI.isLocalPlayer(si.getActivatingPlayer())) {
167-
matchUI.getGameController().setShouldAlwaysAcceptTrigger(triggerID);
165+
String triggerYieldKey = si.getSourceTriggerYieldKey();
166+
if (si.isOptionalTrigger() && matchUI.isLocalPlayer(si.getActivatingPlayer()) && !triggerYieldKey.isEmpty()) {
167+
boolean triggerAbilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_TRIGGER_PER_CARD.equals(
168+
forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_TRIGGER_MODE));
169+
matchUI.getGameController().setShouldAlwaysAcceptTrigger(triggerYieldKey, triggerAbilityScope);
168170
}
169171
matchUI.getGameController().passPriority();
170172
}
@@ -182,9 +184,11 @@ public void actionPerformed(final ActionEvent e) {
182184
boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals(
183185
forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE));
184186
matchUI.getGameController().setShouldAutoYield(si.getKey(), true, abilityScope);
185-
int triggerID = si.getSourceTrigger();
186-
if (si.isOptionalTrigger() && matchUI.isLocalPlayer(si.getActivatingPlayer())) {
187-
matchUI.getGameController().setShouldAlwaysDeclineTrigger(triggerID);
187+
String triggerYieldKey = si.getSourceTriggerYieldKey();
188+
if (si.isOptionalTrigger() && matchUI.isLocalPlayer(si.getActivatingPlayer()) && !triggerYieldKey.isEmpty()) {
189+
boolean triggerAbilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_TRIGGER_PER_CARD.equals(
190+
forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_TRIGGER_MODE));
191+
matchUI.getGameController().setShouldAlwaysDeclineTrigger(triggerYieldKey, triggerAbilityScope);
188192
}
189193
matchUI.getGameController().passPriority();
190194
}

forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ public void initialize() {
228228
initializeColorIdentityCombobox();
229229
initializeSwitchStatesCombobox();
230230
initializeAutoYieldModeComboBox();
231+
initializeAutoTriggerModeComboBox();
231232
initializeStackGroupPermanentsComboBox();
232233
initializeMaxStackDepthComboBox();
233234
initializeCounterDisplayTypeComboBox();
@@ -585,6 +586,21 @@ private void initializeAutoYieldModeComboBox() {
585586
panel.setComboBox(comboBox, selectedItem);
586587
}
587588

589+
private void initializeAutoTriggerModeComboBox() {
590+
final String[] elems = {
591+
ForgeConstants.AUTO_TRIGGER_PER_CARD,
592+
ForgeConstants.AUTO_TRIGGER_PER_ABILITY,
593+
ForgeConstants.AUTO_TRIGGER_PER_ABILITY_SESSION,
594+
ForgeConstants.AUTO_TRIGGER_PER_ABILITY_INSTALL,
595+
};
596+
final FPref userSetting = FPref.UI_AUTO_TRIGGER_MODE;
597+
final FComboBoxPanel<String> panel = this.view.getAutoTriggerModeComboBoxPanel();
598+
final FComboBox<String> comboBox = createComboBox(elems, userSetting);
599+
final String selectedItem = this.prefs.getPref(userSetting);
600+
comboBox.setSelectedItem(selectedItem == null ? elems[0] : selectedItem);
601+
panel.setComboBox(comboBox, selectedItem);
602+
}
603+
588604
private void initializeGraveyardOrderingComboBox() {
589605
final String[] elems = {ForgeConstants.GRAVEYARD_ORDERING_NEVER, ForgeConstants.GRAVEYARD_ORDERING_OWN_CARDS,
590606
ForgeConstants.GRAVEYARD_ORDERING_ALWAYS};

forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ public enum VSubmenuPreferences implements IVSubmenu<CSubmenuPreferences> {
153153
private final FComboBoxPanel<String> cbpLandPlayed = new FComboBoxPanel<>(localizer.getMessage("cbpLandPlayed")+":");
154154
private final FComboBoxPanel<String> cbpDisplayCurrentCardColors = new FComboBoxPanel<>(localizer.getMessage("cbpDisplayCurrentCardColors")+":");
155155
private final FComboBoxPanel<String> cbpAutoYieldMode = new FComboBoxPanel<>(localizer.getMessage("cbpAutoYieldMode")+":");
156+
private final FComboBoxPanel<String> cbpAutoTriggerMode = new FComboBoxPanel<>(localizer.getMessage("cbpAutoTriggerMode")+":");
156157
private final FComboBoxPanel<String> cbpStackGroupPermanents = new FComboBoxPanel<>(localizer.getMessage("cbpStackGroupPermanents")+":");
157158
private final FComboBoxPanel<Integer> cbpMaxStackDepth = new FComboBoxPanel<>(localizer.getMessage("cbpMaxStackDepth")+":");
158159
private final FComboBoxPanel<String> cbpCounterDisplayType = new FComboBoxPanel<>(localizer.getMessage("cbpCounterDisplayType")+":");
@@ -304,6 +305,9 @@ public enum VSubmenuPreferences implements IVSubmenu<CSubmenuPreferences> {
304305
pnlPrefs.add(cbpAutoYieldMode, comboBoxConstraints);
305306
pnlPrefs.add(new NoteLabel(localizer.getMessage("nlpAutoYieldMode")), descriptionConstraints);
306307

308+
pnlPrefs.add(cbpAutoTriggerMode, comboBoxConstraints);
309+
pnlPrefs.add(new NoteLabel(localizer.getMessage("nlpAutoTriggerMode")), descriptionConstraints);
310+
307311
//Server Preferences
308312
pnlPrefs.add(new SectionLabel(localizer.getMessage("ServerPreferences")), sectionConstraints);
309313

@@ -907,6 +911,10 @@ public FComboBoxPanel<String> getAutoYieldModeComboBoxPanel() {
907911
return cbpAutoYieldMode;
908912
}
909913

914+
public FComboBoxPanel<String> getAutoTriggerModeComboBoxPanel() {
915+
return cbpAutoTriggerMode;
916+
}
917+
910918
public FComboBoxPanel<String> getCbpStackGroupPermanents() {
911919
return cbpStackGroupPermanents;
912920
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package forge.screens.match;
2+
3+
import java.awt.Dimension;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
import javax.swing.AbstractListModel;
9+
10+
import forge.Singletons;
11+
import forge.gui.UiCommand;
12+
import forge.player.AutoYieldStore;
13+
import forge.toolbox.FButton;
14+
import forge.toolbox.FCheckBox;
15+
import forge.toolbox.FList;
16+
import forge.toolbox.FOptionPane;
17+
import forge.toolbox.FScrollPane;
18+
import forge.util.Localizer;
19+
import forge.view.FDialog;
20+
21+
@SuppressWarnings("serial")
22+
public class VAutoTriggers extends FDialog {
23+
private static final int PADDING = 10;
24+
private static final int BUTTON_WIDTH = 150;
25+
private static final int BUTTON_HEIGHT = 26;
26+
27+
private static final String ACCEPT_PREFIX = "[" + Localizer.getInstance().getMessage("lblAlwaysYes") + "] ";
28+
private static final String DECLINE_PREFIX = "[" + Localizer.getInstance().getMessage("lblAlwaysNo") + "] ";
29+
30+
private final FButton btnOk;
31+
private final FButton btnRemove;
32+
private final FList<String> lstAutoTriggers;
33+
private final FScrollPane listScroller;
34+
private final FCheckBox chkDisableAll;
35+
private final List<String> autoTriggers;
36+
37+
public VAutoTriggers(final CMatchUI matchUI) {
38+
super();
39+
setTitle(Localizer.getInstance().getMessage("lblAutoTriggers"));
40+
41+
autoTriggers = new ArrayList<>();
42+
for (final Map.Entry<String, AutoYieldStore.TriggerDecision> entry : matchUI.getGameController().getYieldController().getAutoTriggers()) {
43+
autoTriggers.add(formatEntry(entry));
44+
}
45+
lstAutoTriggers = new FList<>(new AutoTriggersListModel());
46+
47+
int x = PADDING;
48+
int y = PADDING;
49+
int width = Singletons.getView().getFrame().getWidth() * 2 / 3;
50+
int w = width - 2 * PADDING;
51+
52+
listScroller = new FScrollPane(lstAutoTriggers, true);
53+
54+
chkDisableAll = new FCheckBox(Localizer.getInstance().getMessage("lblDisableAllAutoTriggers"), matchUI.getGameController().getDisableAutoTriggers());
55+
chkDisableAll.addChangeListener(e -> matchUI.getGameController().setDisableAutoTriggers(chkDisableAll.isSelected()));
56+
57+
btnOk = new FButton(Localizer.getInstance().getMessage("lblOK"));
58+
btnOk.setCommand((UiCommand) () -> setVisible(false));
59+
btnRemove = new FButton(Localizer.getInstance().getMessage("lblRemoveTrigger"));
60+
btnRemove.setCommand((UiCommand) () -> {
61+
String selected = lstAutoTriggers.getSelectedValue();
62+
if (selected != null) {
63+
autoTriggers.remove(selected);
64+
btnRemove.setEnabled(autoTriggers.size() > 0);
65+
String key = stripPrefix(selected);
66+
boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_TRIGGER_PER_CARD.equals(
67+
forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_TRIGGER_MODE));
68+
matchUI.getGameController().setShouldAlwaysAskTrigger(key, abilityScope);
69+
VAutoTriggers.this.revalidate();
70+
lstAutoTriggers.repaint();
71+
}
72+
});
73+
if (autoTriggers.size() > 0) {
74+
lstAutoTriggers.setSelectedIndex(0);
75+
}
76+
else {
77+
btnRemove.setEnabled(false);
78+
}
79+
80+
Dimension checkBoxSize = chkDisableAll.getPreferredSize();
81+
int listHeight = lstAutoTriggers.getMinimumSize().height + 2 * PADDING;
82+
83+
add(listScroller, x, y, w, listHeight);
84+
y += listHeight + PADDING;
85+
add(chkDisableAll, x, y, checkBoxSize.width, checkBoxSize.height);
86+
x = w - 2 * BUTTON_WIDTH - PADDING;
87+
add(btnOk, x, y, BUTTON_WIDTH, BUTTON_HEIGHT);
88+
x += BUTTON_WIDTH + PADDING;
89+
add(btnRemove, x, y, BUTTON_WIDTH, BUTTON_HEIGHT);
90+
91+
this.pack();
92+
this.setSize(width, getHeight());
93+
}
94+
95+
private static String formatEntry(final Map.Entry<String, AutoYieldStore.TriggerDecision> entry) {
96+
String prefix = entry.getValue() == AutoYieldStore.TriggerDecision.ACCEPT ? ACCEPT_PREFIX : DECLINE_PREFIX;
97+
return prefix + entry.getKey();
98+
}
99+
100+
private static String stripPrefix(final String display) {
101+
if (display.startsWith(ACCEPT_PREFIX)) return display.substring(ACCEPT_PREFIX.length());
102+
if (display.startsWith(DECLINE_PREFIX)) return display.substring(DECLINE_PREFIX.length());
103+
return display;
104+
}
105+
106+
private class AutoTriggersListModel extends AbstractListModel<String> {
107+
@Override
108+
public int getSize() {
109+
return autoTriggers.size();
110+
}
111+
112+
@Override
113+
public String getElementAt(final int index) {
114+
return autoTriggers.get(index);
115+
}
116+
}
117+
118+
public void showAutoTriggers() {
119+
if (lstAutoTriggers.getCount() > 0) {
120+
setVisible(true);
121+
dispose();
122+
} else {
123+
FOptionPane.showMessageDialog(Localizer.getInstance().getMessage("lblNoActiveAutoTrigger"), Localizer.getInstance().getMessage("lblNoAutoTrigger"), FOptionPane.INFORMATION_ICON);
124+
}
125+
}
126+
}

forge-gui-desktop/src/main/java/forge/screens/match/menus/GameMenu.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import forge.menus.MenuUtil;
1919
import forge.model.FModel;
2020
import forge.screens.match.CMatchUI;
21+
import forge.screens.match.VAutoTriggers;
2122
import forge.screens.match.VAutoYields;
2223
import forge.screens.match.views.VField;
2324
import forge.screens.match.controllers.CDock.ArcState;
@@ -57,6 +58,7 @@ public JMenu getMenu() {
5758
menu.add(getMenuItem_TokensSeparateRow());
5859
menu.add(getMenuItem_SeparateCombatStacks());
5960
menu.add(getMenuItem_AutoYields());
61+
menu.add(getMenuItem_AutoTriggers());
6062
menu.addSeparator();
6163
menu.add(getMenuItem_ViewDeckList());
6264
return menu;
@@ -189,6 +191,18 @@ private ActionListener getAutoYieldsAction() {
189191
};
190192
}
191193

194+
private SkinnedMenuItem getMenuItem_AutoTriggers() {
195+
final Localizer localizer = Localizer.getInstance();
196+
final SkinnedMenuItem menuItem = new SkinnedMenuItem(localizer.getMessage("lblAutoTriggers"));
197+
menuItem.setIcon((showIcons ? MenuUtil.getMenuIcon(FSkinProp.ICO_WARNING) : null));
198+
menuItem.addActionListener(getAutoTriggersAction());
199+
return menuItem;
200+
}
201+
202+
private ActionListener getAutoTriggersAction() {
203+
return e -> new VAutoTriggers(matchUI).showAutoTriggers();
204+
}
205+
192206
private SkinnedMenuItem getMenuItem_ViewDeckList() {
193207
final Localizer localizer = Localizer.getInstance();
194208
final SkinnedMenuItem menuItem = new SkinnedMenuItem(localizer.getMessage("lblDeckList"));

forge-gui-desktop/src/main/java/forge/screens/match/views/VStack.java

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ private final class AbilityMenu extends JPopupMenu {
301301
private final JMenuItem jmiYieldToEntireStack;
302302
private StackItemView item;
303303

304-
private Integer triggerID = 0;
304+
private String triggerYieldKey = "";
305+
private boolean triggerAbilityScope;
305306

306307
public AbilityMenu(){
307308
jmiAutoYield = new JCheckBoxMenuItem(Localizer.getInstance().getMessage("cbpAutoYieldMode"));
@@ -320,22 +321,24 @@ public AbilityMenu(){
320321

321322
jmiAlwaysYes = new JCheckBoxMenuItem(Localizer.getInstance().getMessage("lblAlwaysYes"));
322323
jmiAlwaysYes.addActionListener(arg0 -> {
323-
if (controller.getMatchUI().getGameController().shouldAlwaysAcceptTrigger(triggerID)) {
324-
controller.getMatchUI().getGameController().setShouldAlwaysAskTrigger(triggerID);
324+
if (triggerYieldKey.isEmpty()) return;
325+
if (controller.getMatchUI().getGameController().shouldAlwaysAcceptTrigger(triggerYieldKey)) {
326+
controller.getMatchUI().getGameController().setShouldAlwaysAskTrigger(triggerYieldKey, triggerAbilityScope);
325327
}
326328
else {
327-
controller.getMatchUI().getGameController().setShouldAlwaysAcceptTrigger(triggerID);
329+
controller.getMatchUI().getGameController().setShouldAlwaysAcceptTrigger(triggerYieldKey, triggerAbilityScope);
328330
}
329331
});
330332
add(jmiAlwaysYes);
331333

332334
jmiAlwaysNo = new JCheckBoxMenuItem(Localizer.getInstance().getMessage("lblAlwaysNo"));
333335
jmiAlwaysNo.addActionListener(arg0 -> {
334-
if (controller.getMatchUI().getGameController().shouldAlwaysDeclineTrigger(triggerID)) {
335-
controller.getMatchUI().getGameController().setShouldAlwaysAskTrigger(triggerID);
336+
if (triggerYieldKey.isEmpty()) return;
337+
if (controller.getMatchUI().getGameController().shouldAlwaysDeclineTrigger(triggerYieldKey)) {
338+
controller.getMatchUI().getGameController().setShouldAlwaysAskTrigger(triggerYieldKey, triggerAbilityScope);
336339
}
337340
else {
338-
controller.getMatchUI().getGameController().setShouldAlwaysDeclineTrigger(triggerID);
341+
controller.getMatchUI().getGameController().setShouldAlwaysDeclineTrigger(triggerYieldKey, triggerAbilityScope);
339342
}
340343
});
341344
add(jmiAlwaysNo);
@@ -352,15 +355,17 @@ public AbilityMenu(){
352355

353356
public void setStackInstance(final StackItemView item0) {
354357
item = item0;
355-
triggerID = item.getSourceTrigger();
358+
triggerYieldKey = item.getSourceTriggerYieldKey();
359+
triggerAbilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_TRIGGER_PER_CARD.equals(
360+
forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_TRIGGER_MODE));
356361

357362
jmiAutoYield.setVisible(item.isAbility());
358363
jmiAutoYield.setSelected(item.isAbility()
359364
&& controller.getMatchUI().getGameController().shouldAutoYield(item.getKey()));
360365

361-
if (item.isOptionalTrigger() && controller.getMatchUI().isLocalPlayer(item.getActivatingPlayer())) {
362-
jmiAlwaysYes.setSelected(controller.getMatchUI().getGameController().shouldAlwaysAcceptTrigger(triggerID));
363-
jmiAlwaysNo.setSelected(controller.getMatchUI().getGameController().shouldAlwaysDeclineTrigger(triggerID));
366+
if (item.isOptionalTrigger() && controller.getMatchUI().isLocalPlayer(item.getActivatingPlayer()) && !triggerYieldKey.isEmpty()) {
367+
jmiAlwaysYes.setSelected(controller.getMatchUI().getGameController().shouldAlwaysAcceptTrigger(triggerYieldKey));
368+
jmiAlwaysNo.setSelected(controller.getMatchUI().getGameController().shouldAlwaysDeclineTrigger(triggerYieldKey));
364369
jmiAlwaysYes.setVisible(true);
365370
jmiAlwaysNo.setVisible(true);
366371
} else {

0 commit comments

Comments
 (0)