From 751a0ea4779fc25869960cdf269fa679139cf9e7 Mon Sep 17 00:00:00 2001 From: Madwand99 Date: Fri, 22 May 2026 20:55:32 -0700 Subject: [PATCH 1/7] Add macro window --- .../player/actions/ActivateAbilityAction.java | 4 +- .../game/player/actions/CastSpellAction.java | 5 + .../player/actions/ColorChoiceAction.java | 4 +- .../game/player/actions/ConfirmAction.java | 6 +- .../player/actions/FinishTargetingAction.java | 7 +- .../game/player/actions/ManaComboAction.java | 28 +- .../game/player/actions/ModeChoiceAction.java | 9 +- .../player/actions/PassPriorityAction.java | 17 +- .../game/player/actions/PayCostAction.java | 5 + .../player/actions/PayManaFromPoolAction.java | 10 +- .../game/player/actions/PlayerAction.java | 44 +- .../forge/game/player/actions/ScryAction.java | 7 +- .../game/player/actions/SelectCardAction.java | 7 +- .../player/actions/SelectPlayerAction.java | 4 + .../game/player/actions/StackOrderAction.java | 8 +- .../player/actions/TargetEntityAction.java | 5 + .../java/forge/control/KeyboardShortcuts.java | 3 +- .../main/java/forge/gui/framework/EDocID.java | 1 + .../java/forge/screens/match/CMatchUI.java | 20 +- .../screens/match/controllers/CDock.java | 11 +- .../screens/match/controllers/CMacro.java | 261 ++++++++++ .../forge/screens/match/views/VMacro.java | 280 +++++++++++ forge-gui/res/languages/de-DE.properties | 41 ++ forge-gui/res/languages/en-US.properties | 41 ++ forge-gui/res/languages/es-ES.properties | 41 ++ forge-gui/res/languages/fr-FR.properties | 41 ++ forge-gui/res/languages/it-IT.properties | 41 ++ forge-gui/res/languages/ja-JP.properties | 41 ++ forge-gui/res/languages/ko-KR.properties | 41 ++ forge-gui/res/languages/pt-BR.properties | 41 ++ forge-gui/res/languages/zh-CN.properties | 41 ++ .../gamemodes/match/input/InputAttack.java | 6 + .../gamemodes/match/input/InputConfirm.java | 11 + .../gamemodes/match/input/InputPayMana.java | 2 + .../match/input/InputSelectTargets.java | 14 + .../java/forge/interfaces/IMacroSystem.java | 5 + .../forge/player/PlayerControllerHuman.java | 9 +- .../player/RecordActionsMacroSystem.java | 455 ++++++++++++++++-- 38 files changed, 1526 insertions(+), 91 deletions(-) create mode 100644 forge-gui-desktop/src/main/java/forge/screens/match/controllers/CMacro.java create mode 100644 forge-gui-desktop/src/main/java/forge/screens/match/views/VMacro.java diff --git a/forge-game/src/main/java/forge/game/player/actions/ActivateAbilityAction.java b/forge-game/src/main/java/forge/game/player/actions/ActivateAbilityAction.java index e3329f6cefa..acde65dc31c 100644 --- a/forge-game/src/main/java/forge/game/player/actions/ActivateAbilityAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/ActivateAbilityAction.java @@ -15,7 +15,7 @@ public String getAbilityDescription() { } @Override - protected void appendDetails(final StringBuilder sb) { - sb.append(" ability=\"").append(abilityDescription).append("\""); + public String describe() { + return localize("lblMacroActionActivateAbility", describeEntity(), abilityDescription); } } diff --git a/forge-game/src/main/java/forge/game/player/actions/CastSpellAction.java b/forge-game/src/main/java/forge/game/player/actions/CastSpellAction.java index bf7b6b3328d..f80ef46ef4d 100644 --- a/forge-game/src/main/java/forge/game/player/actions/CastSpellAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/CastSpellAction.java @@ -6,4 +6,9 @@ public class CastSpellAction extends PlayerAction { public CastSpellAction(GameEntityView cardView) { super(cardView, "Cast spell"); } + + @Override + public String describe() { + return localize("lblMacroActionCastSpell", describeEntity()); + } } diff --git a/forge-game/src/main/java/forge/game/player/actions/ColorChoiceAction.java b/forge-game/src/main/java/forge/game/player/actions/ColorChoiceAction.java index bb2cbfa2c3d..5a2a1ddd1fc 100644 --- a/forge-game/src/main/java/forge/game/player/actions/ColorChoiceAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/ColorChoiceAction.java @@ -15,7 +15,7 @@ public byte getColor() { } @Override - protected void appendDetails(final StringBuilder sb) { - sb.append(" color=").append(MagicColor.toShortString(color)); + public String describe() { + return localize("lblMacroActionChooseColor", MagicColor.toLongString(color)); } } diff --git a/forge-game/src/main/java/forge/game/player/actions/ConfirmAction.java b/forge-game/src/main/java/forge/game/player/actions/ConfirmAction.java index e39c7e5305a..58c0be980b7 100644 --- a/forge-game/src/main/java/forge/game/player/actions/ConfirmAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/ConfirmAction.java @@ -15,7 +15,9 @@ public boolean isConfirmed() { } @Override - protected void appendDetails(final StringBuilder sb) { - sb.append(" confirmed=").append(confirmed); + public String describe() { + final String action = localize(confirmed ? "lblMacroActionConfirm" : "lblMacroActionDecline"); + final String entity = describeEntity(); + return entity.isEmpty() ? action : localize("lblMacroActionChoiceFor", action, entity); } } diff --git a/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java b/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java index 63df3040f07..7e22216df7c 100644 --- a/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java @@ -1,7 +1,12 @@ package forge.game.player.actions; -public class FinishTargetingAction extends PlayerAction{ +public class FinishTargetingAction extends PlayerAction { public FinishTargetingAction() { super(null, "Finish game entity"); } + + @Override + public String describe() { + return localize("lblMacroActionFinishSelecting"); + } } diff --git a/forge-game/src/main/java/forge/game/player/actions/ManaComboAction.java b/forge-game/src/main/java/forge/game/player/actions/ManaComboAction.java index 39dc0aba2c1..4cf3c8d7a0e 100644 --- a/forge-game/src/main/java/forge/game/player/actions/ManaComboAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/ManaComboAction.java @@ -1,5 +1,8 @@ package forge.game.player.actions; +import forge.card.MagicColor; + +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -8,7 +11,7 @@ public class ManaComboAction extends PlayerAction { public ManaComboAction(final Map manaCombo) { super(null, "Choose mana combination"); - this.manaCombo = new LinkedHashMap<>(manaCombo); + this.manaCombo = Collections.unmodifiableMap(new LinkedHashMap<>(manaCombo)); } public Map getManaCombo() { @@ -16,7 +19,26 @@ public Map getManaCombo() { } @Override - protected void appendDetails(final StringBuilder sb) { - sb.append(" manaCombo=").append(manaCombo); + public String describe() { + final StringBuilder sb = new StringBuilder(); + boolean needComma = false; + for (final Map.Entry entry : manaCombo.entrySet()) { + final int amount = entry.getValue() == null ? 0 : entry.getValue(); + if (amount <= 0) { + continue; + } + if (needComma) { + sb.append(", "); + } + sb.append(describeColor(entry.getKey(), amount)); + needComma = true; + } + return localize("lblMacroActionChooseManaCombination", + needComma ? sb.toString() : localize("lblMacroNone")); + } + + private static String describeColor(final byte color, final int amount) { + final String colorName = MagicColor.toLongString(color); + return localize(amount == 1 ? "lblMacroManaAmount" : "lblMacroManaAmountPlural", amount, colorName); } } diff --git a/forge-game/src/main/java/forge/game/player/actions/ModeChoiceAction.java b/forge-game/src/main/java/forge/game/player/actions/ModeChoiceAction.java index 97e4a9ae4f6..1254715607e 100644 --- a/forge-game/src/main/java/forge/game/player/actions/ModeChoiceAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/ModeChoiceAction.java @@ -1,5 +1,7 @@ package forge.game.player.actions; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class ModeChoiceAction extends PlayerAction { @@ -7,7 +9,7 @@ public class ModeChoiceAction extends PlayerAction { public ModeChoiceAction(final List modeDescriptions) { super(null, "Choose mode"); - this.modeDescriptions = modeDescriptions; + this.modeDescriptions = Collections.unmodifiableList(new ArrayList<>(modeDescriptions)); } public List getModeDescriptions() { @@ -15,7 +17,8 @@ public List getModeDescriptions() { } @Override - protected void appendDetails(final StringBuilder sb) { - sb.append(" modes=").append(modeDescriptions); + public String describe() { + return localize(modeDescriptions.size() == 1 ? "lblMacroActionChooseMode" : "lblMacroActionChooseModes", + describeList(modeDescriptions)); } } diff --git a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java index 1145400c2b5..12b1ccb159d 100644 --- a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java @@ -25,10 +25,17 @@ public PhaseType getPhase() { } @Override - protected void appendDetails(final StringBuilder sb) { - sb.append(" stackWasEmpty=").append(stackWasEmpty); - if (phase != null) { - sb.append(" phase=").append(phase); - } + public String describe() { + final String phaseText = phase == null ? "" : " " + localize("lblMacroActionDuringPhase", describePhase(phase)); + final String stackText = localize(stackWasEmpty ? "lblMacroStackEmpty" : "lblMacroStackNotEmpty"); + return localize("lblMacroActionPassPriority", phaseText, stackText); + } + + private static String describePhase(final PhaseType phase) { + return switch (phase) { + case MAIN1 -> "Main Phase 1"; + case MAIN2 -> "Main Phase 2"; + default -> phase.nameForScripts; + }; } } diff --git a/forge-game/src/main/java/forge/game/player/actions/PayCostAction.java b/forge-game/src/main/java/forge/game/player/actions/PayCostAction.java index 314643d3982..90034b4e1c8 100644 --- a/forge-game/src/main/java/forge/game/player/actions/PayCostAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/PayCostAction.java @@ -6,4 +6,9 @@ public class PayCostAction extends PlayerAction { public PayCostAction(GameEntityView cardView) { super(cardView, "Pay cost"); } + + @Override + public String describe() { + return localize("lblMacroActionPayCost", describeEntity()); + } } diff --git a/forge-game/src/main/java/forge/game/player/actions/PayManaFromPoolAction.java b/forge-game/src/main/java/forge/game/player/actions/PayManaFromPoolAction.java index d3251833988..3272b26bf22 100644 --- a/forge-game/src/main/java/forge/game/player/actions/PayManaFromPoolAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/PayManaFromPoolAction.java @@ -1,8 +1,10 @@ package forge.game.player.actions; +import forge.card.MagicColor; + +public class PayManaFromPoolAction extends PlayerAction { + private final byte colorSelected; -public class PayManaFromPoolAction extends PlayerAction{ - private byte colorSelected; public PayManaFromPoolAction(byte colorCode) { super(null, "Pay mana"); colorSelected = colorCode; @@ -13,7 +15,7 @@ public byte getSelectedColor() { } @Override - protected void appendDetails(final StringBuilder sb) { - sb.append(" mana=").append(colorSelected); + public String describe() { + return localize("lblMacroActionPayManaFromPool", MagicColor.toSymbol(colorSelected)); } } diff --git a/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java b/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java index 8df08d04075..9ec1c0263bb 100644 --- a/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java @@ -2,17 +2,25 @@ import forge.game.GameEntityView; import forge.game.player.PlayerController; +import forge.util.Localizer; + +import java.util.List; +import java.util.regex.Pattern; public abstract class PlayerAction { - protected String name; - protected GameEntityView gameEntityView = null; + private static final Pattern ENTITY_ID_SUFFIX = Pattern.compile(" \\((\\d+)\\)$"); + private static final Localizer LOCALIZER = Localizer.getInstance(); + + private final String name; + private final GameEntityView gameEntityView; public PlayerAction(GameEntityView cardView) { gameEntityView = cardView; + name = null; } public PlayerAction(final GameEntityView cardView, final String actionName) { - this(cardView); + gameEntityView = cardView; name = actionName; } @@ -26,14 +34,38 @@ public GameEntityView getGameEntityView() { } public String describe() { - final StringBuilder sb = new StringBuilder(getClass().getSimpleName()); - if (gameEntityView != null) { - sb.append("(").append(gameEntityView).append(")"); + final StringBuilder sb = new StringBuilder(name == null ? getClass().getSimpleName() : name); + final String entity = describeEntity(); + if (!entity.isEmpty()) { + sb.append(": ").append(entity); } appendDetails(sb); return sb.toString(); } + protected String describeEntity() { + return gameEntityView == null ? "" : describeEntity(gameEntityView); + } + + protected static String describeEntity(final GameEntityView entity) { + return entity == null ? "" : ENTITY_ID_SUFFIX.matcher(String.valueOf(entity)).replaceAll(" $1"); + } + + protected static String describeList(final List values) { + if (values == null || values.isEmpty()) { + return localize("lblMacroNone"); + } + final StringBuilder sb = new StringBuilder(); + for (final String value : values) { + sb.append('\n').append("- ").append(value); + } + return sb.toString(); + } + + protected static String localize(final String key, final Object... args) { + return LOCALIZER.getMessage(key, args); + } + protected void appendDetails(final StringBuilder sb) { } diff --git a/forge-game/src/main/java/forge/game/player/actions/ScryAction.java b/forge-game/src/main/java/forge/game/player/actions/ScryAction.java index 96dba62e45c..bf218a9bc54 100644 --- a/forge-game/src/main/java/forge/game/player/actions/ScryAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/ScryAction.java @@ -25,7 +25,7 @@ private static List namesOf(final CardCollectionView cards) { for (final Card card : cards) { names.add(card.getName()); } - return names; + return Collections.unmodifiableList(names); } public List getTopCardNames() { @@ -37,8 +37,7 @@ public List getBottomCardNames() { } @Override - protected void appendDetails(final StringBuilder sb) { - sb.append(" top=").append(topCardNames); - sb.append(" bottom=").append(bottomCardNames); + public String describe() { + return localize("lblMacroActionScry", describeList(topCardNames), describeList(bottomCardNames)); } } diff --git a/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java b/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java index e6280ec2cf8..8970d3d3220 100644 --- a/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java @@ -2,10 +2,13 @@ import forge.game.GameEntityView; -public class SelectCardAction extends PlayerAction{ +public class SelectCardAction extends PlayerAction { public SelectCardAction(GameEntityView cardView) { super(cardView, "Select card"); } - + @Override + public String describe() { + return localize("lblMacroActionSelectCard", describeEntity()); + } } diff --git a/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java b/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java index 636e1848e2a..4469992ed12 100644 --- a/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java @@ -7,4 +7,8 @@ public SelectPlayerAction(GameEntityView playerView) { super(playerView, "Select player"); } + @Override + public String describe() { + return localize("lblMacroActionSelectPlayer", describeEntity()); + } } diff --git a/forge-game/src/main/java/forge/game/player/actions/StackOrderAction.java b/forge-game/src/main/java/forge/game/player/actions/StackOrderAction.java index 703c430ba34..7151183ca1d 100644 --- a/forge-game/src/main/java/forge/game/player/actions/StackOrderAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/StackOrderAction.java @@ -1,5 +1,7 @@ package forge.game.player.actions; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class StackOrderAction extends PlayerAction { @@ -7,7 +9,7 @@ public class StackOrderAction extends PlayerAction { public StackOrderAction(final List abilityDescriptions) { super(null, "Order simultaneous abilities"); - this.abilityDescriptions = abilityDescriptions; + this.abilityDescriptions = Collections.unmodifiableList(new ArrayList<>(abilityDescriptions)); } public List getAbilityDescriptions() { @@ -15,7 +17,7 @@ public List getAbilityDescriptions() { } @Override - protected void appendDetails(final StringBuilder sb) { - sb.append(" order=").append(abilityDescriptions); + public String describe() { + return localize("lblMacroActionOrderAbilities", describeList(abilityDescriptions)); } } diff --git a/forge-game/src/main/java/forge/game/player/actions/TargetEntityAction.java b/forge-game/src/main/java/forge/game/player/actions/TargetEntityAction.java index 420c23e3359..d5ff5c9434c 100644 --- a/forge-game/src/main/java/forge/game/player/actions/TargetEntityAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/TargetEntityAction.java @@ -7,4 +7,9 @@ public class TargetEntityAction extends PlayerAction { public TargetEntityAction(GameEntityView cardView) { super(cardView, "Target game entity"); } + + @Override + public String describe() { + return localize("lblMacroActionChooseTarget", describeEntity()); + } } diff --git a/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java b/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java index 216cdebaaec..19ce769acfe 100644 --- a/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java +++ b/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java @@ -193,7 +193,7 @@ public void actionPerformed(final ActionEvent e) { } }; - final Action actMacroRecord = macroAction(matchUI, ui -> ui.getGameController().macros().setRememberedActions()); + final Action actMacroRecord = macroAction(matchUI, ui -> ui.getCDock().toggleMacroRecording()); final Action actMacroNextAction = macroAction(matchUI, ui -> ui.getGameController().macros().nextRememberedAction()); @@ -306,6 +306,7 @@ public void actionPerformed(final ActionEvent e) { } command.accept(matchUI); matchUI.getCDock().refreshMacroButtons(); + matchUI.getCMacro().update(); } }; } diff --git a/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java b/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java index dabd0d5968e..3a18e4b0a91 100644 --- a/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java +++ b/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java @@ -93,6 +93,7 @@ public enum EDocID { REPORT_STACK (), REPORT_COMBAT (), REPORT_DEPENDENCIES (), + REPORT_MACRO (), REPORT_LOG (), DEV_MODE (), diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java index 729ef85b70f..3eb562ddb65 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java @@ -104,6 +104,7 @@ import forge.screens.match.controllers.CDev; import forge.screens.match.controllers.CDock; import forge.screens.match.controllers.CLog; +import forge.screens.match.controllers.CMacro; import forge.screens.match.controllers.CPrompt; import forge.screens.match.controllers.CStack; import forge.screens.match.menus.CMatchUIMenus; @@ -172,6 +173,7 @@ public final class CMatchUI private final CDev cDev = new CDev(this); private final CDock cDock = new CDock(this); private final CLog cLog = new CLog(this); + private final CMacro cMacro = new CMacro(this); private final CPrompt cPrompt = new CPrompt(this); private final CStack cStack = new CStack(this); private int nextNotifiableStackIndex = 0; @@ -193,6 +195,7 @@ public CMatchUI() { this.myDocs.put(EDocID.REPORT_STACK, getCStack().getView()); this.myDocs.put(EDocID.REPORT_COMBAT, cCombat.getView()); this.myDocs.put(EDocID.REPORT_DEPENDENCIES, cDependencies.getView()); + this.myDocs.put(EDocID.REPORT_MACRO, cMacro.getView()); this.myDocs.put(EDocID.REPORT_LOG, cLog.getView()); this.myDocs.put(EDocID.DEV_MODE, getCDev().getView()); this.myDocs.put(EDocID.BUTTON_DOCK, getCDock().getView()); @@ -293,7 +296,11 @@ public CDev getCDev() { public CDock getCDock() { return cDock; } - CPrompt getCPrompt() { + + public CMacro getCMacro() { + return cMacro; + } + public CPrompt getCPrompt() { return cPrompt; } public CStack getCStack() { @@ -737,6 +744,7 @@ public void initialize() { FloatingZone.closeAll(); updatePlayerControl(); KeyboardShortcuts.attachKeyboardShortcuts(this); + hideStartupHiddenDoc(cMacro.getView()); for (final IVDoc view : myDocs.values()) { if (view == null) { continue; @@ -747,6 +755,14 @@ public void initialize() { } } + private void hideStartupHiddenDoc(final IVDoc view) { + final DragCell parent = view.getParentCell(); + if (parent != null) { + parent.removeDoc(view); + view.setParentCell(null); + } + } + @Override public void update() { } @@ -915,6 +931,7 @@ public void enableOverlay() { @Override public void finishGame() { + cMacro.closeForMatchEnd(); FloatingZone.closeAll(); //ensure floating card areas cleared and closed after the game if (isNetGame()) { writeMatchPreferences(); @@ -1247,6 +1264,7 @@ static List sortPlayersForMultiplayer( @Override public void afterGameEnd() { super.afterGameEnd(); + cMacro.closeForMatchEnd(); Singletons.getView().getLpnDocument().remove(targetingOverlay.getPanel()); FThreads.invokeInEdtNowOrLater(() -> { Singletons.getView().getNavigationBar().closeTab(screen); diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CDock.java b/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CDock.java index 7f44e580b0b..648fed2c217 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CDock.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CDock.java @@ -115,15 +115,22 @@ private void toggleAutoPass() { update(); } - private void toggleMacroRecording() { + public void toggleMacroRecording() { + final boolean wasRecording = matchUI.getGameController().macros().isRecording(); matchUI.getGameController().macros().setRememberedActions(); refreshMacroButtons(); - showPromptTab(); + if (!wasRecording && matchUI.getGameController().macros().isRecording()) { + matchUI.getCMacro().showWindow(); + } else { + matchUI.getCMacro().update(); + showPromptTab(); + } } private void playMacro() { matchUI.getGameController().macros().repeatRememberedActions(); refreshMacroButtons(); + matchUI.getCMacro().update(); showPromptTab(); } diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CMacro.java b/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CMacro.java new file mode 100644 index 00000000000..487e658c20d --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CMacro.java @@ -0,0 +1,261 @@ +package forge.screens.match.controllers; + +import java.awt.Color; +import java.awt.Component; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; + +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.border.Border; + +import forge.gui.FThreads; +import forge.gui.framework.DragCell; +import forge.gui.framework.ICDoc; +import forge.gui.framework.SLayoutConstants; +import forge.interfaces.IMacroSystem; +import forge.screens.match.CMatchUI; +import forge.screens.match.views.VMacro; +import forge.view.FDialog; +import forge.view.FFrame; +import forge.view.FView; +import forge.Singletons; +import forge.util.Localizer; + +public class CMacro implements ICDoc { + private final CMatchUI matchUI; + private final VMacro view; + private final FDialog floatingWindow = new FDialog(false, true, "0") { + @Override + public void setLocationRelativeTo(final Component c) { + if (floatingLocationPrepared) { + return; + } + super.setLocationRelativeTo(c); + } + }; + private final JPanel floatingContent = new JPanel(); + private IMacroSystem observedMacroSystem; + private DragCell dockTargetCell; + private DragCell highlightedCell; + private Border dockOriginalBorder; + private boolean updateQueued; + private boolean dockDetectionInstalled; + private boolean floatingLocationPrepared; + private boolean closedForMatchEnd; + + private final Runnable updateView = this::queueUpdate; + + public CMacro(final CMatchUI matchUI) { + this.matchUI = matchUI; + this.view = new VMacro(this); + floatingContent.setOpaque(false); + floatingWindow.setTitle(Localizer.getInstance().getMessage("lblMacroWindow")); + floatingWindow.add(floatingContent, "grow, push"); + } + + public CMatchUI getMatchUI() { + return matchUI; + } + + public VMacro getView() { + return view; + } + + @Override + public void register() { + } + + @Override + public void initialize() { + closedForMatchEnd = false; + floatingLocationPrepared = false; + observeCurrentMacroSystem(); + } + + public void showWindow() { + closedForMatchEnd = false; + observeCurrentMacroSystem(); + if (view.getParentCell() != null) { + view.getParentCell().setSelected(view); + } else { + showFloatingWindow(); + } + update(); + } + + public void showFloatingWindow() { + closedForMatchEnd = false; + observeCurrentMacroSystem(); + undockFromCurrentCell(); + view.populateContainer(floatingContent); + ensureDockDetectionInstalled(); + if (!floatingWindow.isVisible()) { + setDefaultFloatingBounds(); + floatingWindow.setVisible(true); + } + update(); + } + + public void closeForMatchEnd() { + closedForMatchEnd = true; + clearDockHighlight(); + dockTargetCell = null; + floatingLocationPrepared = false; + floatingWindow.setVisible(false); + undockFromCurrentCell(); + + IMacroSystem macros = observedMacroSystem; + if (macros == null && matchUI.getGameController() != null) { + macros = matchUI.getGameController().macros(); + } + if (macros != null) { + macros.cancelCurrentMacro(); + macros.removeStatusListener(updateView); + } + observedMacroSystem = null; + updateQueued = false; + } + + private void setDefaultFloatingBounds() { + final FFrame mainFrame = Singletons.getView().getFrame(); + final int width = Math.max(260, mainFrame.getWidth() / 5); + final int height = Math.max(260, mainFrame.getHeight() / 2); + final int x = mainFrame.getX() + 20; + final int y = mainFrame.getY() + Math.max(40, (mainFrame.getHeight() - height) / 2); + floatingWindow.setBounds(x, y, width, height); + floatingLocationPrepared = true; + } + + private void dockIntoCell(final DragCell target) { + if (target == null) { + return; + } + clearDockHighlight(); + floatingWindow.setVisible(false); + undockFromCurrentCell(); + target.addDoc(view); + target.setSelected(view); + update(); + } + + private void undockFromCurrentCell() { + for (final DragCell cell : FView.SINGLETON_INSTANCE.getDragCells()) { + if (cell.getDocs().contains(view)) { + cell.removeDoc(view); + } + } + view.setParentCell(null); + } + + private void ensureDockDetectionInstalled() { + if (dockDetectionInstalled) { + return; + } + dockDetectionInstalled = true; + floatingWindow.getTitleBar().addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(final MouseEvent e) { + if (!SwingUtilities.isLeftMouseButton(e)) { + return; + } + if (dockTargetCell != null) { + final DragCell target = dockTargetCell; + dockTargetCell = null; + dockIntoCell(target); + } + } + }); + floatingWindow.getTitleBar().addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseDragged(final MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e)) { + detectDockTarget(e); + } + } + }); + } + + private void detectDockTarget(final MouseEvent e) { + final int ex = (int) e.getLocationOnScreen().getX(); + final int ey = (int) e.getLocationOnScreen().getY(); + + DragCell newTarget = null; + for (final DragCell cell : FView.SINGLETON_INSTANCE.getDragCells()) { + final int cx = cell.getAbsX(); + final int cy = cell.getAbsY(); + final int cw = cell.getW(); + if (ex >= cx && ey >= cy && ex <= cx + cw && ey <= cy + SLayoutConstants.HEAD_H * 3 / 2) { + newTarget = cell; + break; + } + } + + if (newTarget != dockTargetCell) { + clearDockHighlight(); + dockTargetCell = newTarget; + applyDockHighlight(); + } + } + + private void applyDockHighlight() { + if (dockTargetCell == null) { + return; + } + highlightedCell = dockTargetCell; + dockOriginalBorder = highlightedCell.getBody().getBorder(); + highlightedCell.getBody().setBorder(BorderFactory.createLineBorder(new Color(70, 130, 230), 2)); + } + + private void clearDockHighlight() { + if (highlightedCell != null) { + highlightedCell.getBody().setBorder(dockOriginalBorder); + highlightedCell = null; + } + dockOriginalBorder = null; + } + + private void observeCurrentMacroSystem() { + if (closedForMatchEnd) { + return; + } + if (matchUI.getGameController() == null) { + return; + } + + final IMacroSystem macros = matchUI.getGameController().macros(); + if (macros == observedMacroSystem) { + return; + } + if (observedMacroSystem != null) { + observedMacroSystem.removeStatusListener(updateView); + } + observedMacroSystem = macros; + observedMacroSystem.addStatusListener(updateView); + } + + private void queueUpdate() { + if (updateQueued) { + return; + } + updateQueued = true; + FThreads.invokeInEdtLater(() -> { + updateQueued = false; + if (closedForMatchEnd) { + return; + } + update(); + }); + } + + @Override + public void update() { + if (closedForMatchEnd) { + return; + } + observeCurrentMacroSystem(); + view.updateMacroStatus(); + } +} diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/views/VMacro.java b/forge-gui-desktop/src/main/java/forge/screens/match/views/VMacro.java new file mode 100644 index 00000000000..d47232f7cb0 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/match/views/VMacro.java @@ -0,0 +1,280 @@ +package forge.screens.match.views; + +import java.awt.Font; +import java.awt.Container; +import java.awt.Color; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSplitPane; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultHighlighter; +import javax.swing.text.Highlighter; + +import forge.gui.framework.DragCell; +import forge.gui.framework.DragTab; +import forge.gui.framework.EDocID; +import forge.gui.framework.IVDoc; +import forge.interfaces.IMacroSystem; +import forge.screens.match.controllers.CMacro; +import forge.toolbox.FLabel; +import forge.toolbox.FScrollPane; +import forge.toolbox.FTextArea; +import forge.util.Localizer; +import net.miginfocom.swing.MigLayout; + +public class VMacro implements IVDoc { + private final CMacro controller; + private final Localizer localizer = Localizer.getInstance(); + private final DragTab tab = new DragTab(localizer.getMessage("lblMacroWindow")); + private final JPanel contentPanel = new JPanel(); + private final JLabel statusLabel; + private final JLabel detailLabel; + private final JLabel logLabel; + private final JPanel emptyLogPanel = new JPanel(); + private final JPanel logPanel = new JPanel(); + private final FTextArea actionText = new FTextArea(); + private final FTextArea logText = new FTextArea(); + private final FScrollPane actionScroller = new FScrollPane(actionText, false, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + private final FScrollPane logScroller = new FScrollPane(logText, false, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + private final JSplitPane actionLogSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, actionScroller, logPanel); + private final Highlighter.HighlightPainter activeStepPainter = + new DefaultHighlighter.DefaultHighlightPainter(new Color(255, 225, 120, 120)); + private List displayedActions; + private List displayedMessages; + private final List actionStartOffsets = new ArrayList<>(); + private final List actionEndOffsets = new ArrayList<>(); + private int displayedActiveActionIndex = -2; + private DragCell parentCell; + + public VMacro(final CMacro controller) { + this.controller = controller; + statusLabel = new FLabel.Builder() + .fontSize(13) + .fontStyle(Font.BOLD) + .fontAlign(SwingConstants.LEFT) + .opaque() + .build(); + detailLabel = new FLabel.Builder() + .fontSize(12) + .fontStyle(Font.PLAIN) + .fontAlign(SwingConstants.LEFT) + .opaque() + .build(); + logLabel = new FLabel.Builder() + .fontSize(12) + .fontStyle(Font.BOLD) + .fontAlign(SwingConstants.LEFT) + .opaque() + .build(); + statusLabel.setOpaque(false); + detailLabel.setOpaque(false); + logLabel.setOpaque(false); + logLabel.setText(localizer.getMessage("lblMacroLog")); + contentPanel.setOpaque(false); + emptyLogPanel.setOpaque(false); + logPanel.setOpaque(false); + logPanel.setLayout(new MigLayout("wrap 1, gap 2px!, insets 0")); + logPanel.add(logLabel, "w 100%, h 20px!"); + logPanel.add(logScroller, "w 100%, h 0:100%"); + configureCopyTextArea(actionText); + configureCopyTextArea(logText); + actionLogSplit.setOpaque(false); + actionLogSplit.setBorder(null); + actionLogSplit.setResizeWeight(0.75); + actionLogSplit.setBottomComponent(emptyLogPanel); + actionLogSplit.setDividerSize(0); + actionLogSplit.setOneTouchExpandable(false); + tab.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(final MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + final JPopupMenu menu = new JPopupMenu(); + final JMenuItem undock = new JMenuItem(localizer.getMessage("lblUndock")); + undock.addActionListener(ev -> controller.showFloatingWindow()); + menu.add(undock); + menu.show(tab, e.getX(), e.getY()); + } + } + }); + } + + private void configureCopyTextArea(final FTextArea textArea) { + textArea.setLineWrap(true); + textArea.setFocusable(false); + textArea.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(final MouseEvent e) { + textArea.setFocusable(true); + textArea.requestFocusInWindow(); + } + }); + textArea.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(final FocusEvent e) { + textArea.setFocusable(false); + } + }); + } + + @Override + public void populate() { + populateContainer(parentCell.getBody()); + } + + public void populateContainer(final Container container) { + contentPanel.removeAll(); + contentPanel.setLayout(new MigLayout("wrap 1, gap 4px!, insets 4px")); + contentPanel.add(statusLabel, "w 100%, h 24px!"); + contentPanel.add(detailLabel, "w 100%, h 22px!"); + contentPanel.add(actionLogSplit, "w 100%, h 0:100%"); + if (container instanceof JPanel panel) { + panel.removeAll(); + panel.setLayout(new MigLayout("insets 0, gap 0")); + panel.add(contentPanel, "w 100%, h 100%"); + } else { + container.remove(contentPanel); + container.add(contentPanel); + } + SwingUtilities.invokeLater(() -> actionLogSplit.setDividerLocation(0.75)); + updateMacroStatus(); + } + + public void updateMacroStatus() { + final IMacroSystem macros = controller.getMatchUI().getGameController() == null + ? null : controller.getMatchUI().getGameController().macros(); + if (macros == null) { + setStatus(localizer.getMessage("lblMacroIdle"), localizer.getMessage("lblMacroNoActionsRecorded")); + setActions(List.of(), -1); + setMessages(List.of()); + return; + } + + final String playbackText = macros.playbackText(); + final List actionDescriptions = macros.getRememberedActionDescriptions(); + if (macros.isRecording()) { + setStatus(localizer.getMessage("lblMacroRecordingStatus"), + localizer.getMessage("lblMacroActionCount", actionDescriptions.size())); + } else if (macros.isReplaying() || playbackText != null) { + setStatus(localizer.getMessage("lblMacroPlaybackStatus"), + playbackText == null ? "" : playbackText); + } else if (macros.hasRememberedActions()) { + setStatus(localizer.getMessage("lblMacroReadyStatus"), + localizer.getMessage("lblMacroActionCount", actionDescriptions.size())); + } else { + setStatus(localizer.getMessage("lblMacroIdle"), localizer.getMessage("lblMacroNoActionsRecorded")); + } + + setActions(actionDescriptions, macros.getActiveActionIndex()); + setMessages(macros.getPlaybackMessages()); + } + + private void setStatus(final String status, final String detail) { + statusLabel.setText(status); + detailLabel.setText(detail); + } + + private void setActions(final List actions, final int activeActionIndex) { + if (displayedActions != null && displayedActions.equals(actions) + && displayedActiveActionIndex == activeActionIndex) { + return; + } + displayedActions = new ArrayList<>(actions); + displayedActiveActionIndex = activeActionIndex; + actionStartOffsets.clear(); + actionEndOffsets.clear(); + if (actions.isEmpty()) { + actionText.setText(localizer.getMessage("lblMacroNoActionsRecorded")); + actionText.getHighlighter().removeAllHighlights(); + return; + } + + final StringBuilder text = new StringBuilder(); + for (int i = 0; i < actions.size(); i++) { + if (text.length() > 0) { + text.append('\n'); + } + actionStartOffsets.add(text.length()); + text.append(i + 1).append(". ").append(actions.get(i)); + actionEndOffsets.add(text.length()); + } + actionText.setText(text.toString()); + highlightActiveAction(activeActionIndex); + } + + private void highlightActiveAction(final int activeActionIndex) { + actionText.getHighlighter().removeAllHighlights(); + if (activeActionIndex < 0 || activeActionIndex >= actionStartOffsets.size()) { + actionText.setCaretPosition(actionText.getDocument().getLength()); + return; + } + try { + final int start = actionStartOffsets.get(activeActionIndex); + final int end = actionEndOffsets.get(activeActionIndex); + actionText.getHighlighter().addHighlight(start, end, activeStepPainter); + actionText.setCaretPosition(start); + } catch (final BadLocationException ex) { + actionText.setCaretPosition(actionText.getDocument().getLength()); + } + } + + private void setMessages(final List messages) { + if (displayedMessages != null && displayedMessages.equals(messages)) { + return; + } + displayedMessages = new ArrayList<>(messages); + final boolean hasMessages = !messages.isEmpty(); + if (hasMessages && actionLogSplit.getBottomComponent() != logPanel) { + actionLogSplit.setBottomComponent(logPanel); + actionLogSplit.setDividerSize(8); + actionLogSplit.setOneTouchExpandable(true); + SwingUtilities.invokeLater(() -> actionLogSplit.setDividerLocation(0.75)); + } else if (!hasMessages && actionLogSplit.getBottomComponent() != emptyLogPanel) { + actionLogSplit.setBottomComponent(emptyLogPanel); + actionLogSplit.setDividerSize(0); + actionLogSplit.setOneTouchExpandable(false); + } + logText.setText(messages.isEmpty() ? "" : String.join("\n", messages)); + logText.setCaretPosition(logText.getDocument().getLength()); + contentPanel.revalidate(); + contentPanel.repaint(); + } + + @Override + public void setParentCell(final DragCell cell0) { + parentCell = cell0; + } + + @Override + public DragCell getParentCell() { + return parentCell; + } + + @Override + public EDocID getDocumentID() { + return EDocID.REPORT_MACRO; + } + + @Override + public DragTab getTabLabel() { + return tab; + } + + @Override + public CMacro getLayoutControl() { + return controller; + } +} diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index 9d46122b2ff..fa412df9ee2 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -464,6 +464,47 @@ lblMacroRecordStopTooltip=Makroaufnahme beenden und Wiederholungsanzahl wählen lblMacroPlayTooltip=Makro abspielen lblMacroPlayUnavailableTooltip=Nimm vor der Wiedergabe ein Makro auf lblMacroPlaybackActiveTooltip=Makrowiedergabe läuft +lblMacroWindow=Makrofenster +lblMacroIdle=Makro inaktiv +lblMacroRecordingStatus=Makroaufnahme läuft +lblMacroPlaybackStatus=Makro wird abgespielt +lblMacroReadyStatus=Makro bereit +lblMacroNoActionsRecorded=Noch keine Aktionen aufgezeichnet. +lblMacroActionCount={0} aufgezeichnete Aktion(en) +lblMacroLog=Makroprotokoll +lblMacroPlaybackProgress={0} / {1} +lblMacroNone=keine +lblMacroActionActivateAbility=Aktiviere Fähigkeit von {0}: {1} +lblMacroActionCastSpell=Wirke Zauberspruch: {0} +lblMacroActionChooseColor=Wähle Farbe: {0} +lblMacroActionConfirm=Bestätigen +lblMacroActionDecline=Ablehnen +lblMacroActionChoiceFor={0}-Entscheidung für {1} +lblMacroActionFinishSelecting=Zielauswahl beenden +lblMacroActionChooseManaCombination=Wähle Manakombination: {0} +lblMacroManaAmount={0} {1} +lblMacroManaAmountPlural={0} {1} Mana +lblMacroActionChooseMode=Wähle Modus: {0} +lblMacroActionChooseModes=Wähle Modi:{0} +lblMacroActionDuringPhase=während {0} +lblMacroActionPassPriority=Priorität abgeben{0} mit {1} +lblMacroStackEmpty=einem leeren Stapel +lblMacroStackNotEmpty=Zaubersprüchen oder Fähigkeiten auf dem Stapel +lblMacroActionPayCost=Kosten mit {0} bezahlen +lblMacroActionPayManaFromPool={0} aus dem Manavorrat bezahlen +lblMacroActionScry=Hellsicht:\nOben drauf legen: {0}\nUnten drunter legen: {1} +lblMacroActionSelectCard=Karte wählen: {0} +lblMacroActionSelectPlayer=Spieler wählen: {0} +lblMacroActionOrderAbilities=Gleichzeitige Fähigkeiten ordnen:{0} +lblMacroActionChooseTarget=Ziel wählen: {0} +lblMacroPlaybackStoppedAt={0} bei {1}, während {2} angezeigt wird +lblMacroPlaybackStoppedWaitingAfterFinalAction=wurde nach der letzten aufgezeichneten Aktion beim Warten angehalten +lblMacroPlaybackStoppedWaiting=wurde beim Warten angehalten +lblMacroPlaybackStoppedGeneric=wurde angehalten +lblMacroPlaybackStoppedWaitingBetweenIterations=wurde zwischen Wiederholungen beim Warten auf einen leeren Stapel angehalten +lblMacroCouldNotReplay=Konnte nicht wiedergeben: {0}, während {1} angezeigt wird +lblMacroStepDescription=Schritt {0} ({1}) +lblMacroNextRecordedStep=der nächste aufgezeichnete Schritt lblRepeatActionSequence=Aktionsfolge wiederholen lblHowManyTimesToRepeatSequence=Wie oft soll die gespeicherte Aktionsfolge zusätzlich wiederholt werden? lblFinishRecordingBeforePlayback=Beende die Makroaufnahme, bevor du sie abspielst. diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 3ef3b9d9afc..34fd9077b80 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -530,6 +530,47 @@ lblMacroRecordStopTooltip=Stop recording macro and choose repeat count lblMacroPlayTooltip=Play Macro lblMacroPlayUnavailableTooltip=Record a macro before playback lblMacroPlaybackActiveTooltip=Macro playback is running +lblMacroWindow=Macro Window +lblMacroIdle=Macro idle +lblMacroRecordingStatus=Recording macro +lblMacroPlaybackStatus=Playing macro +lblMacroReadyStatus=Macro ready +lblMacroNoActionsRecorded=No recorded actions yet. +lblMacroActionCount={0} recorded action(s) +lblMacroLog=Macro log +lblMacroPlaybackProgress={0} / {1} +lblMacroNone=none +lblMacroActionActivateAbility=Activate ability of {0}: {1} +lblMacroActionCastSpell=Cast spell: {0} +lblMacroActionChooseColor=Choose color: {0} +lblMacroActionConfirm=Confirm +lblMacroActionDecline=Decline +lblMacroActionChoiceFor={0} choice for {1} +lblMacroActionFinishSelecting=Finish selecting targets +lblMacroActionChooseManaCombination=Choose mana combination: {0} +lblMacroManaAmount={0} {1} +lblMacroManaAmountPlural={0} {1} mana +lblMacroActionChooseMode=Choose mode: {0} +lblMacroActionChooseModes=Choose modes:{0} +lblMacroActionDuringPhase=during {0} +lblMacroActionPassPriority=Pass priority{0} with {1} +lblMacroStackEmpty=an empty stack +lblMacroStackNotEmpty=spells or abilities on the stack +lblMacroActionPayCost=Pay cost with {0} +lblMacroActionPayManaFromPool=Pay {0} from mana pool +lblMacroActionScry=Scry:\nPut on top: {0}\nPut on bottom: {1} +lblMacroActionSelectCard=Select card: {0} +lblMacroActionSelectPlayer=Select player: {0} +lblMacroActionOrderAbilities=Order simultaneous abilities:{0} +lblMacroActionChooseTarget=Choose target: {0} +lblMacroPlaybackStoppedAt={0} at {1} while showing {2} +lblMacroPlaybackStoppedWaitingAfterFinalAction=stopped waiting after final recorded action +lblMacroPlaybackStoppedWaiting=stopped waiting +lblMacroPlaybackStoppedGeneric=stopped +lblMacroPlaybackStoppedWaitingBetweenIterations=stopped waiting between iterations for stack to clear +lblMacroCouldNotReplay=Could not replay: {0} while showing {1} +lblMacroStepDescription=step {0} ({1}) +lblMacroNextRecordedStep=the next recorded step lblRepeatActionSequence=Repeat Action Sequence lblHowManyTimesToRepeatSequence=How many additional times should the recorded action sequence be repeated? lblFinishRecordingBeforePlayback=Finish recording the macro before playing it back. diff --git a/forge-gui/res/languages/es-ES.properties b/forge-gui/res/languages/es-ES.properties index 6d2810a0d82..92b5b9d7a67 100644 --- a/forge-gui/res/languages/es-ES.properties +++ b/forge-gui/res/languages/es-ES.properties @@ -441,6 +441,47 @@ lblMacroRecordStopTooltip=Detener grabación de macro y elegir repeticiones lblMacroPlayTooltip=Reproducir macro lblMacroPlayUnavailableTooltip=Graba una macro antes de reproducirla lblMacroPlaybackActiveTooltip=La reproducción de macro está en curso +lblMacroWindow=Ventana de macro +lblMacroIdle=Macro inactiva +lblMacroRecordingStatus=Grabando macro +lblMacroPlaybackStatus=Reproduciendo macro +lblMacroReadyStatus=Macro lista +lblMacroNoActionsRecorded=Aún no hay acciones grabadas. +lblMacroActionCount={0} acción(es) grabada(s) +lblMacroLog=Registro de macro +lblMacroPlaybackProgress={0} / {1} +lblMacroNone=ninguno +lblMacroActionActivateAbility=Activar habilidad de {0}: {1} +lblMacroActionCastSpell=Lanzar hechizo: {0} +lblMacroActionChooseColor=Elegir color: {0} +lblMacroActionConfirm=Confirmar +lblMacroActionDecline=Rechazar +lblMacroActionChoiceFor=Elección {0} para {1} +lblMacroActionFinishSelecting=Terminar selección de objetivos +lblMacroActionChooseManaCombination=Elegir combinación de maná: {0} +lblMacroManaAmount={0} {1} +lblMacroManaAmountPlural={0} maná {1} +lblMacroActionChooseMode=Elegir modo: {0} +lblMacroActionChooseModes=Elegir modos:{0} +lblMacroActionDuringPhase=durante {0} +lblMacroActionPassPriority=Pasar prioridad{0} con {1} +lblMacroStackEmpty=la pila vacía +lblMacroStackNotEmpty=hechizos o habilidades en la pila +lblMacroActionPayCost=Pagar coste con {0} +lblMacroActionPayManaFromPool=Pagar {0} de la reserva de maná +lblMacroActionScry=Adivinar:\nPoner encima: {0}\nPoner debajo: {1} +lblMacroActionSelectCard=Seleccionar carta: {0} +lblMacroActionSelectPlayer=Seleccionar jugador: {0} +lblMacroActionOrderAbilities=Ordenar habilidades simultáneas:{0} +lblMacroActionChooseTarget=Elegir objetivo: {0} +lblMacroPlaybackStoppedAt={0} en {1} mientras se muestra {2} +lblMacroPlaybackStoppedWaitingAfterFinalAction=se detuvo esperando después de la acción grabada final +lblMacroPlaybackStoppedWaiting=se detuvo esperando +lblMacroPlaybackStoppedGeneric=se detuvo +lblMacroPlaybackStoppedWaitingBetweenIterations=se detuvo esperando entre iteraciones a que se vacíe la pila +lblMacroCouldNotReplay=No se pudo reproducir: {0} mientras se muestra {1} +lblMacroStepDescription=paso {0} ({1}) +lblMacroNextRecordedStep=el siguiente paso grabado lblRepeatActionSequence=Repetir secuencia de acciones lblHowManyTimesToRepeatSequence=¿Cuántas veces adicionales debe repetirse la secuencia de acciones grabada? lblFinishRecordingBeforePlayback=Termina de grabar la macro antes de reproducirla. diff --git a/forge-gui/res/languages/fr-FR.properties b/forge-gui/res/languages/fr-FR.properties index 9d5231075b2..f53ec041300 100644 --- a/forge-gui/res/languages/fr-FR.properties +++ b/forge-gui/res/languages/fr-FR.properties @@ -439,6 +439,47 @@ lblMacroRecordStopTooltip=Arrêter l'enregistrement de la macro et choisir le no lblMacroPlayTooltip=Lire la macro lblMacroPlayUnavailableTooltip=Enregistrez une macro avant la lecture lblMacroPlaybackActiveTooltip=La lecture de la macro est en cours +lblMacroWindow=Fenêtre de macro +lblMacroIdle=Macro inactive +lblMacroRecordingStatus=Enregistrement de la macro +lblMacroPlaybackStatus=Lecture de la macro +lblMacroReadyStatus=Macro prête +lblMacroNoActionsRecorded=Aucune action enregistrée. +lblMacroActionCount={0} action(s) enregistrée(s) +lblMacroLog=Journal de macro +lblMacroPlaybackProgress={0} / {1} +lblMacroNone=aucun +lblMacroActionActivateAbility=Activer la capacité de {0}: {1} +lblMacroActionCastSpell=Lancer le sort: {0} +lblMacroActionChooseColor=Choisir une couleur: {0} +lblMacroActionConfirm=Confirmer +lblMacroActionDecline=Refuser +lblMacroActionChoiceFor=Choix {0} pour {1} +lblMacroActionFinishSelecting=Terminer la sélection des cibles +lblMacroActionChooseManaCombination=Choisir la combinaison de mana: {0} +lblMacroManaAmount={0} {1} +lblMacroManaAmountPlural={0} mana {1} +lblMacroActionChooseMode=Choisir un mode: {0} +lblMacroActionChooseModes=Choisir des modes:{0} +lblMacroActionDuringPhase=pendant {0} +lblMacroActionPassPriority=Passer la priorité{0} avec {1} +lblMacroStackEmpty=une pile vide +lblMacroStackNotEmpty=des sorts ou capacités sur la pile +lblMacroActionPayCost=Payer le coût avec {0} +lblMacroActionPayManaFromPool=Payer {0} depuis la réserve de mana +lblMacroActionScry=Regard:\nMettre au-dessus: {0}\nMettre au-dessous: {1} +lblMacroActionSelectCard=Sélectionner la carte: {0} +lblMacroActionSelectPlayer=Sélectionner le joueur: {0} +lblMacroActionOrderAbilities=Ordonner les capacités simultanées:{0} +lblMacroActionChooseTarget=Choisir la cible: {0} +lblMacroPlaybackStoppedAt={0} à {1} pendant l''affichage de {2} +lblMacroPlaybackStoppedWaitingAfterFinalAction=arrêt en attente après la dernière action enregistrée +lblMacroPlaybackStoppedWaiting=arrêt en attente +lblMacroPlaybackStoppedGeneric=arrêt +lblMacroPlaybackStoppedWaitingBetweenIterations=arrêt en attente entre les itérations que la pile se vide +lblMacroCouldNotReplay=Impossible de rejouer: {0} pendant l''affichage de {1} +lblMacroStepDescription=étape {0} ({1}) +lblMacroNextRecordedStep=la prochaine étape enregistrée lblRepeatActionSequence=Répéter la séquence d'actions lblHowManyTimesToRepeatSequence=Combien de fois supplémentaires la séquence d'actions enregistrée doit-elle être répétée ? lblFinishRecordingBeforePlayback=Terminez l'enregistrement de la macro avant de la lire. diff --git a/forge-gui/res/languages/it-IT.properties b/forge-gui/res/languages/it-IT.properties index d41d69f75d0..3d4e543ceea 100644 --- a/forge-gui/res/languages/it-IT.properties +++ b/forge-gui/res/languages/it-IT.properties @@ -438,6 +438,47 @@ lblMacroRecordStopTooltip=Interrompi la registrazione della macro e scegli il nu lblMacroPlayTooltip=Riproduci macro lblMacroPlayUnavailableTooltip=Registra una macro prima della riproduzione lblMacroPlaybackActiveTooltip=La riproduzione della macro è in corso +lblMacroWindow=Finestra macro +lblMacroIdle=Macro inattiva +lblMacroRecordingStatus=Registrazione macro +lblMacroPlaybackStatus=Riproduzione macro +lblMacroReadyStatus=Macro pronta +lblMacroNoActionsRecorded=Nessuna azione registrata. +lblMacroActionCount={0} azione/i registrata/e +lblMacroLog=Registro macro +lblMacroPlaybackProgress={0} / {1} +lblMacroNone=nessuno +lblMacroActionActivateAbility=Attiva abilità di {0}: {1} +lblMacroActionCastSpell=Lancia magia: {0} +lblMacroActionChooseColor=Scegli colore: {0} +lblMacroActionConfirm=Conferma +lblMacroActionDecline=Rifiuta +lblMacroActionChoiceFor=Scelta {0} per {1} +lblMacroActionFinishSelecting=Termina selezione dei bersagli +lblMacroActionChooseManaCombination=Scegli combinazione di mana: {0} +lblMacroManaAmount={0} {1} +lblMacroManaAmountPlural={0} mana {1} +lblMacroActionChooseMode=Scegli modalità: {0} +lblMacroActionChooseModes=Scegli modalità:{0} +lblMacroActionDuringPhase=durante {0} +lblMacroActionPassPriority=Cedi priorità{0} con {1} +lblMacroStackEmpty=una pila vuota +lblMacroStackNotEmpty=magie o abilità in pila +lblMacroActionPayCost=Paga costo con {0} +lblMacroActionPayManaFromPool=Paga {0} dalla riserva di mana +lblMacroActionScry=Profetizza:\nMetti in cima: {0}\nMetti in fondo: {1} +lblMacroActionSelectCard=Seleziona carta: {0} +lblMacroActionSelectPlayer=Seleziona giocatore: {0} +lblMacroActionOrderAbilities=Ordina abilità simultanee:{0} +lblMacroActionChooseTarget=Scegli bersaglio: {0} +lblMacroPlaybackStoppedAt={0} al {1} mentre è visualizzato {2} +lblMacroPlaybackStoppedWaitingAfterFinalAction=fermata in attesa dopo l''ultima azione registrata +lblMacroPlaybackStoppedWaiting=fermata in attesa +lblMacroPlaybackStoppedGeneric=fermata +lblMacroPlaybackStoppedWaitingBetweenIterations=fermata in attesa tra le iterazioni che la pila si svuoti +lblMacroCouldNotReplay=Impossibile riprodurre: {0} mentre è visualizzato {1} +lblMacroStepDescription=passo {0} ({1}) +lblMacroNextRecordedStep=il prossimo passo registrato lblRepeatActionSequence=Ripeti sequenza di azioni lblHowManyTimesToRepeatSequence=Quante volte aggiuntive deve essere ripetuta la sequenza di azioni registrata? lblFinishRecordingBeforePlayback=Termina la registrazione della macro prima di riprodurla. diff --git a/forge-gui/res/languages/ja-JP.properties b/forge-gui/res/languages/ja-JP.properties index 83e786228b9..7704a5c552b 100644 --- a/forge-gui/res/languages/ja-JP.properties +++ b/forge-gui/res/languages/ja-JP.properties @@ -439,6 +439,47 @@ lblMacroRecordStopTooltip=マクロ記録を停止して繰り返し回数を選 lblMacroPlayTooltip=マクロを再生 lblMacroPlayUnavailableTooltip=再生する前にマクロを記録してください lblMacroPlaybackActiveTooltip=マクロを再生中です +lblMacroWindow=マクロウィンドウ +lblMacroIdle=マクロ待機中 +lblMacroRecordingStatus=マクロを記録中 +lblMacroPlaybackStatus=マクロを再生中 +lblMacroReadyStatus=マクロ準備完了 +lblMacroNoActionsRecorded=記録されたアクションはまだありません。 +lblMacroActionCount=記録済みアクション {0} 件 +lblMacroLog=マクロログ +lblMacroPlaybackProgress={0} / {1} +lblMacroNone=なし +lblMacroActionActivateAbility={0}の能力を起動: {1} +lblMacroActionCastSpell=呪文を唱える: {0} +lblMacroActionChooseColor=色を選ぶ: {0} +lblMacroActionConfirm=確認 +lblMacroActionDecline=拒否 +lblMacroActionChoiceFor={1}で{0}を選択 +lblMacroActionFinishSelecting=対象の選択を完了 +lblMacroActionChooseManaCombination=マナの組み合わせを選ぶ: {0} +lblMacroManaAmount={0} {1} +lblMacroManaAmountPlural={0} {1}マナ +lblMacroActionChooseMode=モードを選ぶ: {0} +lblMacroActionChooseModes=モードを選ぶ:{0} +lblMacroActionDuringPhase=({0}中) +lblMacroActionPassPriority={1}で優先権をパス{0} +lblMacroStackEmpty=空のスタック +lblMacroStackNotEmpty=スタック上にある呪文や能力 +lblMacroActionPayCost={0}でコストを支払う +lblMacroActionPayManaFromPool=マナ・プールから{0}を支払う +lblMacroActionScry=占術:\n一番上に置く: {0}\n一番下に置く: {1} +lblMacroActionSelectCard=カードを選択: {0} +lblMacroActionSelectPlayer=プレイヤーを選択: {0} +lblMacroActionOrderAbilities=同時に誘発した能力の順番を決める:{0} +lblMacroActionChooseTarget=対象を選ぶ: {0} +lblMacroPlaybackStoppedAt={2}の表示中に{1}で{0} +lblMacroPlaybackStoppedWaitingAfterFinalAction=最後の記録済みアクション後の待機で停止しました +lblMacroPlaybackStoppedWaiting=待機中に停止しました +lblMacroPlaybackStoppedGeneric=停止しました +lblMacroPlaybackStoppedWaitingBetweenIterations=反復の間にスタックが空になるのを待って停止しました +lblMacroCouldNotReplay=再生できません: {0}(表示中: {1}) +lblMacroStepDescription=ステップ {0} ({1}) +lblMacroNextRecordedStep=次の記録済みステップ lblRepeatActionSequence=アクションシーケンスを繰り返す lblHowManyTimesToRepeatSequence=記録されたアクションシーケンスを追加で何回繰り返しますか? lblFinishRecordingBeforePlayback=再生する前にマクロの記録を終了してください。 diff --git a/forge-gui/res/languages/ko-KR.properties b/forge-gui/res/languages/ko-KR.properties index 15d05882c52..4d0d03fb3bd 100644 --- a/forge-gui/res/languages/ko-KR.properties +++ b/forge-gui/res/languages/ko-KR.properties @@ -473,6 +473,47 @@ lblMacroRecordStopTooltip=매크로 기록을 중지하고 반복 횟수 선택 lblMacroPlayTooltip=매크로 재생 lblMacroPlayUnavailableTooltip=재생하기 전에 매크로를 기록하세요 lblMacroPlaybackActiveTooltip=매크로 재생 중 +lblMacroWindow=매크로 창 +lblMacroIdle=매크로 대기 중 +lblMacroRecordingStatus=매크로 기록 중 +lblMacroPlaybackStatus=매크로 재생 중 +lblMacroReadyStatus=매크로 준비됨 +lblMacroNoActionsRecorded=아직 기록된 액션이 없습니다. +lblMacroActionCount=기록된 액션 {0}개 +lblMacroLog=매크로 로그 +lblMacroPlaybackProgress={0} / {1} +lblMacroNone=없음 +lblMacroActionActivateAbility={0}의 능력 활성화: {1} +lblMacroActionCastSpell=주문 시전: {0} +lblMacroActionChooseColor=색 선택: {0} +lblMacroActionConfirm=확인 +lblMacroActionDecline=거절 +lblMacroActionChoiceFor={1}에 대해 {0} 선택 +lblMacroActionFinishSelecting=대상 선택 완료 +lblMacroActionChooseManaCombination=마나 조합 선택: {0} +lblMacroManaAmount={0} {1} +lblMacroManaAmountPlural={0} {1} 마나 +lblMacroActionChooseMode=모드 선택: {0} +lblMacroActionChooseModes=모드 선택:{0} +lblMacroActionDuringPhase=({0} 중) +lblMacroActionPassPriority={1} 상태에서 우선권 넘기기{0} +lblMacroStackEmpty=빈 스택 +lblMacroStackNotEmpty=스택에 있는 주문 또는 능력 +lblMacroActionPayCost={0}(으)로 비용 지불 +lblMacroActionPayManaFromPool=마나 풀에서 {0} 지불 +lblMacroActionScry=점술:\n맨 위에 놓기: {0}\n맨 아래에 놓기: {1} +lblMacroActionSelectCard=카드 선택: {0} +lblMacroActionSelectPlayer=플레이어 선택: {0} +lblMacroActionOrderAbilities=동시에 유발된 능력 순서 정하기:{0} +lblMacroActionChooseTarget=대상 선택: {0} +lblMacroPlaybackStoppedAt={2} 표시 중 {1}에서 {0} +lblMacroPlaybackStoppedWaitingAfterFinalAction=마지막 기록된 액션 후 대기 중 중지됨 +lblMacroPlaybackStoppedWaiting=대기 중 중지됨 +lblMacroPlaybackStoppedGeneric=중지됨 +lblMacroPlaybackStoppedWaitingBetweenIterations=반복 사이에 스택이 비기를 기다리다 중지됨 +lblMacroCouldNotReplay=재생할 수 없음: {0} ({1} 표시 중) +lblMacroStepDescription={0}단계 ({1}) +lblMacroNextRecordedStep=다음 기록된 단계 lblRepeatActionSequence=액션 시퀀스 반복 lblHowManyTimesToRepeatSequence=기록된 액션 시퀀스를 추가로 몇 번 반복할까요? lblFinishRecordingBeforePlayback=재생하기 전에 매크로 기록을 끝내세요. diff --git a/forge-gui/res/languages/pt-BR.properties b/forge-gui/res/languages/pt-BR.properties index 0d55d7b765b..395f5bb941a 100644 --- a/forge-gui/res/languages/pt-BR.properties +++ b/forge-gui/res/languages/pt-BR.properties @@ -451,6 +451,47 @@ lblMacroRecordStopTooltip=Parar a gravação da macro e escolher a contagem de r lblMacroPlayTooltip=Reproduzir macro lblMacroPlayUnavailableTooltip=Grave uma macro antes da reprodução lblMacroPlaybackActiveTooltip=A reprodução da macro está em andamento +lblMacroWindow=Janela de macro +lblMacroIdle=Macro inativa +lblMacroRecordingStatus=Gravando macro +lblMacroPlaybackStatus=Reproduzindo macro +lblMacroReadyStatus=Macro pronta +lblMacroNoActionsRecorded=Nenhuma ação gravada ainda. +lblMacroActionCount={0} ação(ões) gravada(s) +lblMacroLog=Registro da macro +lblMacroPlaybackProgress={0} / {1} +lblMacroNone=nenhum +lblMacroActionActivateAbility=Ativar habilidade de {0}: {1} +lblMacroActionCastSpell=Conjurar mágica: {0} +lblMacroActionChooseColor=Escolher cor: {0} +lblMacroActionConfirm=Confirmar +lblMacroActionDecline=Recusar +lblMacroActionChoiceFor=Escolha {0} para {1} +lblMacroActionFinishSelecting=Terminar seleção de alvos +lblMacroActionChooseManaCombination=Escolher combinação de mana: {0} +lblMacroManaAmount={0} {1} +lblMacroManaAmountPlural={0} mana {1} +lblMacroActionChooseMode=Escolher modo: {0} +lblMacroActionChooseModes=Escolher modos:{0} +lblMacroActionDuringPhase=durante {0} +lblMacroActionPassPriority=Passar prioridade{0} com {1} +lblMacroStackEmpty=uma pilha vazia +lblMacroStackNotEmpty=mágicas ou habilidades na pilha +lblMacroActionPayCost=Pagar custo com {0} +lblMacroActionPayManaFromPool=Pagar {0} da reserva de mana +lblMacroActionScry=Vidência:\nColocar no topo: {0}\nColocar no fundo: {1} +lblMacroActionSelectCard=Selecionar carta: {0} +lblMacroActionSelectPlayer=Selecionar jogador: {0} +lblMacroActionOrderAbilities=Ordenar habilidades simultâneas:{0} +lblMacroActionChooseTarget=Escolher alvo: {0} +lblMacroPlaybackStoppedAt={0} em {1} enquanto {2} é exibido +lblMacroPlaybackStoppedWaitingAfterFinalAction=parou aguardando após a ação gravada final +lblMacroPlaybackStoppedWaiting=parou aguardando +lblMacroPlaybackStoppedGeneric=parou +lblMacroPlaybackStoppedWaitingBetweenIterations=parou aguardando entre iterações a pilha esvaziar +lblMacroCouldNotReplay=Não foi possível reproduzir: {0} enquanto {1} é exibido +lblMacroStepDescription=etapa {0} ({1}) +lblMacroNextRecordedStep=a próxima etapa gravada lblRepeatActionSequence=Repetir sequência de ações lblHowManyTimesToRepeatSequence=Quantas vezes adicionais a sequência de ações gravada deve ser repetida? lblFinishRecordingBeforePlayback=Termine de gravar a macro antes de reproduzi-la. diff --git a/forge-gui/res/languages/zh-CN.properties b/forge-gui/res/languages/zh-CN.properties index a914bd7bf6c..3f6c775780b 100644 --- a/forge-gui/res/languages/zh-CN.properties +++ b/forge-gui/res/languages/zh-CN.properties @@ -441,6 +441,47 @@ lblMacroRecordStopTooltip=停止录制宏并选择重复次数 lblMacroPlayTooltip=播放宏 lblMacroPlayUnavailableTooltip=请先录制宏再播放 lblMacroPlaybackActiveTooltip=宏正在播放 +lblMacroWindow=宏窗口 +lblMacroIdle=宏空闲 +lblMacroRecordingStatus=正在录制宏 +lblMacroPlaybackStatus=正在播放宏 +lblMacroReadyStatus=宏已就绪 +lblMacroNoActionsRecorded=尚未录制动作。 +lblMacroActionCount=已录制 {0} 个动作 +lblMacroLog=宏日志 +lblMacroPlaybackProgress={0} / {1} +lblMacroNone=无 +lblMacroActionActivateAbility=起动 {0} 的异能:{1} +lblMacroActionCastSpell=施放咒语:{0} +lblMacroActionChooseColor=选择颜色:{0} +lblMacroActionConfirm=确认 +lblMacroActionDecline=拒绝 +lblMacroActionChoiceFor=为 {1} 选择 {0} +lblMacroActionFinishSelecting=完成选择目标 +lblMacroActionChooseManaCombination=选择法术力组合:{0} +lblMacroManaAmount={0} {1} +lblMacroManaAmountPlural={0} 点{1}法术力 +lblMacroActionChooseMode=选择模式:{0} +lblMacroActionChooseModes=选择模式:{0} +lblMacroActionDuringPhase=({0}期间) +lblMacroActionPassPriority=在{1}时让过优先权{0} +lblMacroStackEmpty=堆叠为空 +lblMacroStackNotEmpty=堆叠上有咒语或异能 +lblMacroActionPayCost=用 {0} 支付费用 +lblMacroActionPayManaFromPool=从法术力池支付 {0} +lblMacroActionScry=占卜:\n置于牌库顶:{0}\n置于牌库底:{1} +lblMacroActionSelectCard=选择牌:{0} +lblMacroActionSelectPlayer=选择牌手:{0} +lblMacroActionOrderAbilities=排列同时触发的异能:{0} +lblMacroActionChooseTarget=选择目标:{0} +lblMacroPlaybackStoppedAt=显示 {2} 时在{1}{0} +lblMacroPlaybackStoppedWaitingAfterFinalAction=在最后一个已录制动作后等待时停止 +lblMacroPlaybackStoppedWaiting=等待时停止 +lblMacroPlaybackStoppedGeneric=停止 +lblMacroPlaybackStoppedWaitingBetweenIterations=在两次循环之间等待堆叠清空时停止 +lblMacroCouldNotReplay=无法播放:{0},当前显示 {1} +lblMacroStepDescription=步骤 {0}({1}) +lblMacroNextRecordedStep=下一个已录制步骤 lblRepeatActionSequence=重复动作序列 lblHowManyTimesToRepeatSequence=已录制的动作序列还要额外重复多少次? lblFinishRecordingBeforePlayback=请先完成宏录制,然后再播放。 diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputAttack.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputAttack.java index 9aca0381c69..a7000eff531 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputAttack.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputAttack.java @@ -29,6 +29,7 @@ import forge.game.event.GameEventCombatUpdate; import forge.game.keyword.Keyword; import forge.game.player.Player; +import forge.game.player.actions.FinishTargetingAction; import forge.game.staticability.StaticAbilityMustAttack; import forge.game.zone.ZoneType; import forge.gui.events.UiEventAttackerDeclared; @@ -97,12 +98,17 @@ private void disablePrompt() { @Override protected final void onOk() { + getController().macros().addRememberedAction(new FinishTargetingAction()); // Propaganda costs could have been paid here. setCurrentDefender(null); // remove highlights activateBand(null); stop(); } + public boolean isDeclaredAttackerForMacro(final Card card) { + return combat.isAttacking(card); + } + @Override protected final void onCancel() { //either alpha strike or undeclare all attackers based on whether any attackers have been declared diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputConfirm.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputConfirm.java index 78bcf24c536..67de46bc990 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputConfirm.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputConfirm.java @@ -150,6 +150,17 @@ public final boolean getResult() { return result; } + public CardView getCardViewForMacro() { + if (card != null) { + return card; + } + return sa == null ? null : sa.getCardView(); + } + + public String getMessageForMacro() { + return message; + } + @Override public String getActivateAction(Card card) { return null; diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java index 88c832d7116..3915a2a9f0a 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java @@ -448,6 +448,8 @@ public String toString() { public boolean isActivatingManaAbility() { return locked; } + public boolean canCancelPaymentForMacro() { return !mandatory; } + protected String messagePrefix; public void setMessagePrefix(String prompt) { // TODO Auto-generated method stub diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java index ca152203513..bd8355ab3fa 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java @@ -377,6 +377,20 @@ public boolean selectCardForMacro(final Card card, final ITriggerEvent triggerEv return targets.contains(card) && targets.size() > oldTargetCount; } + public Iterable getValidCardsForMacro() { + return choices; + } + + public boolean canFinishTargetingForMacro() { + if (!sa.isMinTargetChosen()) { + return false; + } + if (numTargets != null && targets.size() != numTargets) { + return false; + } + return divisionValues == null || divisionValues.isEmpty() || sa.getStillToDivide() <= 0; + } + protected Boolean onDividedAsYouChoose(GameObject go) { String apiBasedMessage = "Distribute how much to "; if (sa.getApi() == ApiType.DealDamage) { diff --git a/forge-gui/src/main/java/forge/interfaces/IMacroSystem.java b/forge-gui/src/main/java/forge/interfaces/IMacroSystem.java index e9d3ebcd095..54c054d9c0f 100644 --- a/forge-gui/src/main/java/forge/interfaces/IMacroSystem.java +++ b/forge-gui/src/main/java/forge/interfaces/IMacroSystem.java @@ -18,6 +18,11 @@ default void cancelPlayback() { } default boolean isRecording() { return false; } default boolean isReplaying() { return false; } default boolean hasRememberedActions() { return false; } + default List getRememberedActionDescriptions() { return List.of(); } + default int getActiveActionIndex() { return -1; } + default List getPlaybackMessages() { return List.of(); } + default void addStatusListener(final Runnable listener) { } + default void removeStatusListener(final Runnable listener) { } default Byte consumeRememberedColorChoice(final List choices) { return null; } default Map consumeRememberedManaCombo(final List choices, final int manaAmount, final boolean different) { diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index fcbb2a2f4d5..887881e0926 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -843,9 +843,13 @@ public boolean confirmTrigger(final WrappedAbility wrapper) { cardView = spellAbilityView.getHostCard(); else cardView = wrapper.getCardView(); - return this.getGui().confirm(cardView, buildQuestion.toString().replaceAll("\n", " ")); + final boolean result = this.getGui().confirm(cardView, buildQuestion.toString().replaceAll("\n", " ")); + recordConfirm(cardView, result); + return result; } - return InputConfirm.confirm(this, wrapper, buildQuestion.toString()); + final boolean result = InputConfirm.confirm(this, wrapper, buildQuestion.toString()); + recordConfirm(wrapper.getCardView(), result); + return result; } @Override @@ -2118,6 +2122,7 @@ public List orderSimultaneousSa(List activePlayerSAs for (final int index : savedOrder) { orderedSAs.add(activePlayerSAs.get(index)); } + macros().addRememberedAction(new StackOrderAction(describeAbilityOrder(orderedSAs))); return orderedSAs; } diff --git a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java index 0159c95979e..70254758d36 100644 --- a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java +++ b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java @@ -45,6 +45,8 @@ import java.util.Map; import java.util.function.Function; import java.util.function.IntSupplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; // Iteration on the current limited macro system. Instead of asking for IDs to click on // Instead we wrap the input queue in a way that we can record what the player is doing and @@ -57,6 +59,8 @@ public class RecordActionsMacroSystem implements IMacroSystem { private static final int WAIT_FOR_NEXT_INPUT = -2; private static final int MAX_REJECTED_ACTION_RETRIES = 40; private static final int MAX_WAIT_RETRIES = 400; + private static final Pattern CARD_ID_PATTERN = Pattern.compile(" \\((\\d+)\\)"); + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); private final PlayerControllerHuman playerControllerHuman; private final Localizer localizer = Localizer.getInstance(); @@ -64,11 +68,16 @@ public class RecordActionsMacroSystem implements IMacroSystem { private final List actions = Lists.newArrayList(); private final List playbackActions = Lists.newArrayList(); + private final List statusListeners = Lists.newArrayList(); private boolean recording; private int repeatIteration; private int repeatIterations; private int playbackRetries; + private int activeActionIndex = -1; + private int pendingStackOrderPriorityPasses; + private boolean waitingForSelectionAfterStackOrder; private ManaComboAction pendingRecordedManaCombo; + private final List playbackMessages = Lists.newArrayList(); public RecordActionsMacroSystem(final PlayerControllerHuman playerControllerHuman) { this.playerControllerHuman = playerControllerHuman; @@ -83,6 +92,39 @@ public RecordActionsMacroSystem(final PlayerControllerHuman playerControllerHuma @Override public boolean hasRememberedActions() { return !actions.isEmpty(); } + @Override + public List getRememberedActionDescriptions() { + return actions.stream().map(PlayerAction::describe).collect(Collectors.toList()); + } + + @Override + public int getActiveActionIndex() { + return activeActionIndex; + } + + @Override + public List getPlaybackMessages() { + return Lists.newArrayList(playbackMessages); + } + + @Override + public void addStatusListener(final Runnable listener) { + if (listener != null && !statusListeners.contains(listener)) { + statusListeners.add(listener); + } + } + + @Override + public void removeStatusListener(final Runnable listener) { + statusListeners.remove(listener); + } + + private void notifyStatusListeners() { + for (final Runnable listener : Lists.newArrayList(statusListeners)) { + listener.run(); + } + } + @Override public Byte consumeRememberedColorChoice(final List choices) { return consumeRememberedAction(ColorChoiceAction.class, @@ -147,21 +189,41 @@ private boolean isValidModeChoice(final List selectedModes, final List consumeRememberedAbilityOrder(final List choices) { return consumeRememberedAction(StackOrderAction.class, - action -> isValidAbilityOrder(action.getAbilityDescriptions(), choices) - ? Lists.newArrayList(action.getAbilityDescriptions()) : null); + action -> buildRememberedAbilityOrder(action.getAbilityDescriptions(), choices)); } - private boolean isValidAbilityOrder(final List selectedOrder, final List choices) { + private List buildRememberedAbilityOrder(final List selectedOrder, final List choices) { if (selectedOrder.size() != choices.size()) { - return false; + return null; } - final List available = Lists.newArrayList(choices); + final List available = normalizeAbilityDescriptions(choices); + final List availableChoices = Lists.newArrayList(choices); + final List orderedChoices = Lists.newArrayListWithCapacity(selectedOrder.size()); for (final String selected : selectedOrder) { - if (!available.remove(selected)) { - return false; + final int choiceIndex = available.indexOf(normalizeAbilityDescription(selected)); + if (choiceIndex < 0) { + return null; } + available.remove(choiceIndex); + orderedChoices.add(availableChoices.remove(choiceIndex)); } - return available.isEmpty(); + return available.isEmpty() ? orderedChoices : null; + } + + private List normalizeAbilityDescriptions(final List descriptions) { + final List normalized = Lists.newArrayListWithCapacity(descriptions.size()); + for (final String description : descriptions) { + normalized.add(normalizeAbilityDescription(description)); + } + return normalized; + } + + private String normalizeAbilityDescription(final String description) { + if (description == null) { + return ""; + } + return WHITESPACE_PATTERN.matcher(CARD_ID_PATTERN.matcher(description).replaceAll("")) + .replaceAll(" ").trim(); } @Override @@ -184,7 +246,10 @@ private R consumeRememberedAction(final Class act debug("accepted " + action.describe()); playbackActions.remove(actionIndex); playbackRetries = 0; + pendingStackOrderPriorityPasses = 0; + waitingForSelectionAfterStackOrder = action instanceof StackOrderAction && hasFutureSelectionAction(); playerControllerHuman.getInputQueue().updateObservers(); + notifyStatusListeners(); return result; } debug("rejected " + action.describe()); @@ -224,13 +289,14 @@ private CardCollection takeCardsByName(final CardCollection available, final Lis @Override public String playbackText() { if (repeatIterations > 0) { - return repeatIteration + " / " + repeatIterations; + return localizer.getMessage("lblMacroPlaybackProgress", repeatIteration, repeatIterations); } if (playbackActions.isEmpty()) { return null; } - return actions.size() - playbackActions.size() + " / " + actions.size(); + return localizer.getMessage("lblMacroPlaybackProgress", + actions.size() - playbackActions.size(), actions.size()); } public boolean startRecording() { @@ -242,9 +308,12 @@ public boolean startRecording() { finishPlayback(); } recording = true; + activeActionIndex = -1; actions.clear(); playbackActions.clear(); + playbackMessages.clear(); playerControllerHuman.getInputQueue().updateObservers(); + notifyStatusListeners(); return true; } @@ -257,6 +326,7 @@ public boolean finishRecording() { recording = false; flushPendingRecordedManaCombo(); playerControllerHuman.getInputQueue().updateObservers(); + notifyStatusListeners(); return true; } @@ -313,6 +383,7 @@ private void flushPendingRecordedManaCombo() { private void rememberAction(final PlayerAction action) { actions.add(action); playbackActions.add(action); + notifyStatusListeners(); } private void removeLastCardSelectionFor(final GameEntityView view, final List actionList) { @@ -386,7 +457,9 @@ public void nextRememberedAction() { @Override public void cancelPlayback() { if (repeatIterations > 0 || !playbackActions.isEmpty()) { - debug("cancelled input=" + describeInput() + " remaining=" + describeActions(playbackActions)); + if (DEBUG) { + debug("cancelled input=" + describeInput() + " remaining=" + describeActions(playbackActions)); + } finishPlayback(); } } @@ -394,22 +467,30 @@ public void cancelPlayback() { @Override public void cancelCurrentMacro() { if (recording) { - debug("cancelled recording actions=" + describeActions(actions)); + if (DEBUG) { + debug("cancelled recording actions=" + describeActions(actions)); + } recording = false; actions.clear(); } pendingRecordedManaCombo = null; cancelPlayback(); playerControllerHuman.getInputQueue().updateObservers(); + notifyStatusListeners(); } private void startPlayback(final int loops) { repeatIterations = loops; repeatIteration = 1; - playbackRetries = 0; + resetPlaybackProgress(); + activeActionIndex = -1; + playbackMessages.clear(); restartPlaybackActions(); - debug("start loops=" + loops + " actions=" + describeActions(playbackActions)); + if (DEBUG) { + debug("start loops=" + loops + " actions=" + describeActions(playbackActions)); + } playerControllerHuman.getInputQueue().updateObservers(); + notifyStatusListeners(); FThreads.delayInEDT(50, this::continuePlayback); } @@ -420,57 +501,122 @@ private void continuePlayback() { } if (playbackActions.isEmpty()) { + final int pendingInputResult = processPendingInputAfterActions(); + if (pendingInputResult == WAIT_FOR_NEXT_INPUT) { + if (++playbackRetries > MAX_WAIT_RETRIES) { + stopPlayback("lblMacroPlaybackStoppedWaitingAfterFinalAction"); + return; + } + FThreads.delayInEDT(50, this::continuePlayback); + return; + } if (repeatIteration >= repeatIterations) { finishPlayback(); return; } + if (waitForInterIterationStackClear()) { + if (repeatIterations > 0) { + FThreads.delayInEDT(50, this::continuePlayback); + } + return; + } repeatIteration++; restartPlaybackActions(); + resetPlaybackProgress(); + activeActionIndex = -1; playerControllerHuman.getInputQueue().updateObservers(); + notifyStatusListeners(); } + setActiveAction(playbackActions.isEmpty() ? null : playbackActions.get(0)); final int actionIndex = processNextAcceptedAction(); if (actionIndex >= 0) { - playbackActions.remove(actionIndex); + final PlayerAction acceptedAction = playbackActions.remove(actionIndex); playbackRetries = 0; + pendingStackOrderPriorityPasses = 0; + if (acceptedAction instanceof PassPriorityAction || isSelectionAction(acceptedAction)) { + waitingForSelectionAfterStackOrder = false; + } } else if (actionIndex == WAIT_FOR_NEXT_INPUT) { if (++playbackRetries > MAX_WAIT_RETRIES) { - stopPlayback("stopped waiting"); + stopPlayback("lblMacroPlaybackStoppedWaiting"); return; } } else if (++playbackRetries > MAX_REJECTED_ACTION_RETRIES) { - stopPlayback("stopped"); + stopPlayback("lblMacroPlaybackStoppedGeneric"); return; } FThreads.delayInEDT(50, this::continuePlayback); } + private int processPendingInputAfterActions() { + final Input input = playerControllerHuman.getInputProxy().getInput(); + if (input instanceof InputLockUI) { + debug("waiting for pending game action after final recorded action"); + return WAIT_FOR_NEXT_INPUT; + } + if (!isStackEmpty() && isPriorityInput(input)) { + debug("clearing pending stack after final recorded action"); + return waitForInput(input); + } + if (input instanceof InputPayMana || input instanceof InputConfirm) { + debug("handling pending " + describeInput() + " after final recorded action"); + return processNextAcceptedAction(); + } + if (input instanceof InputSelectEntitiesFromList selectInput) { + debug("finishing pending list selection after final recorded action"); + return finishListSelectionIfAny(selectInput); + } + return NO_ACTION_ACCEPTED; + } + private int processNextAcceptedAction() { final Input inp = playerControllerHuman.getInputProxy().getInput(); - debug("input=" + describeInput() + " remaining=" + describeActions(playbackActions)); + debugPlaybackState(); if (inp instanceof InputLockUI) { return WAIT_FOR_NEXT_INPUT; } else if (inp instanceof InputPassPriority passPriorityInput && passPriorityInput.getChosenSa() != null) { return WAIT_FOR_NEXT_INPUT; + } else if (waitingForSelectionAfterStackOrder && isPriorityInput(inp)) { + final int nextSelection = findNextAction(SelectCardAction.class, SelectPlayerAction.class); + if (nextSelection < 0) { + waitingForSelectionAfterStackOrder = false; + return NO_ACTION_ACCEPTED; + } + if (!isStackEmpty()) { + return waitForInput(inp, "waiting for post-order selection before " + + playbackActions.get(nextSelection).describe()); + } + waitingForSelectionAfterStackOrder = false; + debug("stack emptied before post-order selection " + playbackActions.get(nextSelection).describe()); + return NO_ACTION_ACCEPTED; } else if (inp instanceof InputAttack) { final int passPriority = findNextAction(PassPriorityAction.class); - final int playerAction = findNextPlayerSelectionBefore(passPriority); + final int finishAttack = findNextActionBefore(passPriority, FinishTargetingAction.class); + final int actionBoundary = finishAttack >= 0 ? finishAttack : passPriority; + final int playerAction = findNextPlayerSelectionBefore(actionBoundary); if (playerAction >= 0) { return processActionAt(playerAction); } - final int cardAction = findNextCardSelectionBefore(passPriority); + final int cardAction = findNextCardSelectionBefore(actionBoundary); final int cardResult = processActionAt(cardAction); if (cardResult >= 0) { return cardResult; } + if (cardAction >= 0) { + return waitForPendingAttackInputAction(inp, cardAction); + } + if (finishAttack >= 0) { + return processActionAt(finishAttack); + } final int passResult = processAttackPassPriority(passPriority, inp); if (passResult >= 0) { return passResult; } - } else if (inp instanceof InputSelectTargets) { + } else if (inp instanceof InputSelectTargets targetInput) { for (int i = 0; i < playbackActions.size(); i++) { final PlayerAction action = playbackActions.get(i); if (action instanceof PassPriorityAction && noRemainingTargetSelectionsBefore(i)) { @@ -486,6 +632,10 @@ private int processNextAcceptedAction() { if (processAction(action)) { return i; } + if (targetInput.canFinishTargetingForMacro()) { + debug("finished current target prompt before " + action.describe()); + return waitForInput(targetInput); + } } } } else if (inp instanceof InputSelectEntitiesFromList selectInput) { @@ -520,18 +670,35 @@ private int processNextAcceptedAction() { return WAIT_FOR_NEXT_INPUT; } return waitForInput(inp); - } else if (inp instanceof InputConfirm) { + } else if (inp instanceof InputConfirm confirmInput) { final int confirmAction = findNextAction(PayCostAction.class, ConfirmAction.class); if (confirmAction >= 0 && processAction(playbackActions.get(confirmAction))) { return confirmAction; } + if (canAcceptImplicitTriggerConfirm(confirmInput)) { + return waitForInput(confirmInput, "accepting trigger before " + + playbackActions.get(findNextAction(SelectCardAction.class, SelectPlayerAction.class)).describe()); + } return waitForInput(inp); } else if (inp instanceof InputPassPriority) { final int passPriority = findNextAction(PassPriorityAction.class); final int cardAction = findNextCardSelectionBefore(passPriority); + final int stackOrderAction = findNextActionBefore(cardAction, StackOrderAction.class); + if (stackOrderAction >= 0 && !isStackEmpty()) { + if (hasSimultaneousStackEntries()) { + return WAIT_FOR_NEXT_INPUT; + } + if (pendingStackOrderPriorityPasses > 0) { + debug("not passing priority again while waiting for simultaneous ability order"); + return NO_ACTION_ACCEPTED; + } + pendingStackOrderPriorityPasses++; + return waitForInput(inp, "waiting for simultaneous ability order before " + + playbackActions.get(stackOrderAction).describe()); + } final int activateAbility = findNextActionBefore(passPriority, ActivateAbilityAction.class); if (cardAction >= 0 && (activateAbility < 0 || cardAction < activateAbility)) { - return processActionAt(cardAction, () -> waitForPendingCardAction(inp, cardAction)); + return processActionAt(cardAction, () -> waitForPendingCardAction(inp, cardAction, stackOrderAction >= 0)); } final int activateResult = processActionAt(activateAbility); @@ -540,7 +707,7 @@ private int processNextAcceptedAction() { } if (cardAction >= 0) { - return processActionAt(cardAction, () -> waitForPendingCardAction(inp, cardAction)); + return processActionAt(cardAction, () -> waitForPendingCardAction(inp, cardAction, stackOrderAction >= 0)); } final int playerAction = findNextPlayerSelectionBefore(passPriority); @@ -563,6 +730,7 @@ private int processNextAcceptedAction() { continue; } if (shouldSkipPassPriorityAction(i)) { + setActiveAction(action); debug("skipped obsolete " + action.describe()); return i; } @@ -642,6 +810,14 @@ private boolean isSelectionAction(final PlayerAction action) { return action instanceof SelectCardAction || action instanceof SelectPlayerAction; } + private boolean hasFutureSelectionAction() { + return findNextAction(SelectCardAction.class, SelectPlayerAction.class) >= 0; + } + + private boolean isPriorityInput(final Input input) { + return input instanceof InputPassPriority || input instanceof InputAttack; + } + private boolean isObsoleteStackPass(final PlayerAction action) { return action instanceof PassPriorityAction passPriorityAction && !passPriorityAction.wasStackEmpty() @@ -650,7 +826,17 @@ private boolean isObsoleteStackPass(final PlayerAction action) { private boolean shouldSkipPassPriorityAction(final int actionIndex) { final PlayerAction action = playbackActions.get(actionIndex); - return isObsoleteStackPass(action) || isTrailingMainPhasePassBeforeNextIteration(actionIndex); + return isObsoleteStackPass(action) + || isStalePhasePass(action) + || isTrailingMainPhasePassBeforeNextIteration(actionIndex); + } + + private boolean isStalePhasePass(final PlayerAction action) { + if (!(action instanceof PassPriorityAction passPriorityAction) || passPriorityAction.getPhase() == null) { + return false; + } + final PhaseType currentPhase = getCurrentPhase(); + return currentPhase != null && passPriorityAction.getPhase().isBefore(currentPhase); } private boolean isTrailingMainPhasePassBeforeNextIteration(final int actionIndex) { @@ -694,6 +880,44 @@ private boolean isStackEmpty() { return playerControllerHuman.getGame().getStack().isEmpty(); } + private boolean hasSimultaneousStackEntries() { + return playerControllerHuman.getGame().getStack().hasSimultaneousStackEntries(); + } + + private boolean waitForInterIterationStackClear() { + if (isStackEmpty()) { + playbackRetries = 0; + return false; + } + + final Input input = playerControllerHuman.getInputProxy().getInput(); + if (canStartNextIterationBeforeStackClear(input)) { + playbackRetries = 0; + return false; + } + if (input instanceof InputPassPriority || input instanceof InputAttack) { + input.selectButtonOK(); + playbackRetries = 0; + return true; + } + if (++playbackRetries > MAX_WAIT_RETRIES) { + stopPlayback("lblMacroPlaybackStoppedWaitingBetweenIterations"); + return true; + } + return true; + } + + private boolean canStartNextIterationBeforeStackClear(final Input input) { + if (actions.isEmpty()) { + return false; + } + final PlayerAction firstAction = actions.get(0); + return (isSelectionAction(firstAction) + && (input instanceof InputSelectTargets || input instanceof InputSelectEntitiesFromList)) + || (firstAction instanceof ConfirmAction && input instanceof InputConfirm) + || (firstAction instanceof PayManaFromPoolAction && input instanceof InputPayMana); + } + private boolean canAdvanceTowardRecordedCombatPass(final PassPriorityAction action, final PhaseType currentPhase) { final PhaseType recordedPhase = action.getPhase(); return action.wasStackEmpty() @@ -704,10 +928,12 @@ && isCombatPhase(currentPhase) && (isCombatPhase(recordedPhase) || recordedPhase == PhaseType.MAIN2); } - private int waitForPendingCardAction(final Input input, final int actionIndex) { + private int waitForPendingCardAction(final Input input, final int actionIndex, final boolean waitingForRecordedStackOrder) { final PlayerAction action = playbackActions.get(actionIndex); if (!isStackEmpty()) { - return waitForInput(input, "waiting for stack before " + action.describe()); + final String reason = waitingForRecordedStackOrder ? "waiting for stack before " + : "waiting for pending stack item before "; + return waitForInput(input, reason + action.describe()); } if (isWaitingForPostCombatMainAction(actionIndex)) { return waitForInput(input, "waiting for postcombat main before " + action.describe()); @@ -718,6 +944,13 @@ private int waitForPendingCardAction(final Input input, final int actionIndex) { return NO_ACTION_ACCEPTED; } + private int waitForPendingAttackInputAction(final Input input, final int actionIndex) { + if (findPendingPlayerCard(actionIndex) == null) { + return NO_ACTION_ACCEPTED; + } + return waitForInput(input, "waiting for postcombat main before " + playbackActions.get(actionIndex).describe()); + } + private int waitForInput(final Input input, final String debugMessage) { debug(debugMessage); return waitForInput(input); @@ -904,11 +1137,19 @@ private boolean noRemainingTargetSelectionsBefore(final int endIndex) { } private void finishPlayback() { + finishPlayback(true); + } + + private void finishPlayback(final boolean clearActiveAction) { repeatIteration = 0; repeatIterations = 0; - playbackRetries = 0; + resetPlaybackProgress(); + if (clearActiveAction) { + activeActionIndex = -1; + } playbackActions.clear(); playerControllerHuman.getInputQueue().updateObservers(); + notifyStatusListeners(); } private void restartPlaybackActions() { @@ -916,15 +1157,29 @@ private void restartPlaybackActions() { playbackActions.addAll(actions); } - private void stopPlayback(final String reason) { - debug(reason + " input=" + describeInput() + " remaining=" + describeActions(playbackActions)); - finishPlayback(); + private void resetPlaybackProgress() { + playbackRetries = 0; + pendingStackOrderPriorityPasses = 0; + waitingForSelectionAfterStackOrder = false; + } + + private void stopPlayback(final String reasonKey) { + final String message = localizer.getMessage("lblMacroPlaybackStoppedAt", + localizer.getMessage(reasonKey), describeActiveAction(), describeInput()); + playbackMessages.add(message); + if (DEBUG) { + debug(message + " remaining=" + describeActions(playbackActions)); + } + finishPlayback(false); playerControllerHuman.getGui().message(localizer.getMessage("lblMacroPlaybackStopped"), localizer.getMessage("lblRepeatActionSequence")); } public boolean processAction(PlayerAction action) { - debug("try " + action.describe()); + setActiveAction(action); + if (DEBUG) { + debug("try " + action.describe()); + } final Input inp = playerControllerHuman.getInputProxy().getInput(); if (action instanceof ActivateAbilityAction activateAbilityAction) { if (inp instanceof InputPassPriority passPriorityInput) { @@ -940,7 +1195,7 @@ public boolean processAction(PlayerAction action) { return debugResult(action, true); } } else if (action instanceof FinishTargetingAction) { - if (inp instanceof InputSelectTargets) { + if (inp instanceof InputSelectTargets || inp instanceof InputAttack) { inp.selectButtonOK(); return debugResult(action, true); } @@ -955,7 +1210,12 @@ public boolean processAction(PlayerAction action) { return debugResult(action, true); } } else if (action instanceof ConfirmAction confirmAction) { - if (inp instanceof InputConfirm || inp instanceof InputPayMana) { + if (!confirmAction.isConfirmed() && inp instanceof InputPayMana manaInput + && manaInput.canCancelPaymentForMacro()) { + inp.selectButtonCancel(); + return debugResult(action, true); + } + if (inp instanceof InputConfirm confirmInput && canReplayConfirm(confirmInput, confirmAction)) { if (confirmAction.isConfirmed()) { inp.selectButtonOK(); } else { @@ -974,6 +1234,27 @@ public boolean processAction(PlayerAction action) { return debugResult(action, false); } + private boolean canReplayConfirm(final InputConfirm input, final ConfirmAction action) { + final GameEntityView recordedView = action.getGameEntityView(); + if (!(recordedView instanceof CardView recordedCard)) { + return true; + } + final CardView inputCard = input.getCardViewForMacro(); + if (inputCard != null && recordedCard.getName().equals(inputCard.getName())) { + return true; + } + final String message = input.getMessageForMacro(); + return message != null && message.contains(recordedCard.getName()); + } + + private boolean canAcceptImplicitTriggerConfirm(final InputConfirm input) { + final String message = input.getMessageForMacro(); + if (message == null || !message.startsWith(localizer.getMessage("lblUseTriggeredAbilityOf"))) { + return false; + } + return findNextAction(SelectCardAction.class, SelectPlayerAction.class) >= 0; + } + private boolean activateAbility(final InputPassPriority input, final ActivateAbilityAction action) { final GameEntityView view = action.getGameEntityView(); if (!(view instanceof CardView cardView)) { @@ -1011,7 +1292,8 @@ private boolean selectCard(final CardView cardView) { final Card directCard = findCard(cardView); if (inp instanceof InputSelectTargets targetInput) { - return directCard != null && targetInput.selectCardForMacro(directCard, replayTriggerEvent); + final Card targetChoice = findTargetChoice(cardView, directCard, targetInput); + return targetChoice != null && targetInput.selectCardForMacro(targetChoice, replayTriggerEvent); } if (inp instanceof InputPassPriority passPriorityInput) { @@ -1025,6 +1307,10 @@ private boolean selectCard(final CardView cardView) { return playerControllerHuman.selectCard(directCard.getView(), null, replayTriggerEvent) && passPriorityInput.getChosenSa() != null; } + if (inp instanceof InputAttack attackInput && directCard != null + && attackInput.isDeclaredAttackerForMacro(directCard)) { + return false; + } if (directCard != null && playerControllerHuman.selectCard(directCard.getView(), null, replayTriggerEvent)) { return true; } @@ -1032,31 +1318,75 @@ private boolean selectCard(final CardView cardView) { return false; } + private Card findTargetChoice(final CardView recordedChoice, final Card exactChoice, + final InputSelectTargets targetInput) { + final Card choice = findCardChoice(recordedChoice, exactChoice, targetInput.getValidCardsForMacro()); + debugNoMatch(choice, "target", recordedChoice, null); + return choice; + } + private Card findListChoice(final CardView recordedChoice, final InputSelectEntitiesFromList selectInput) { final Card exactChoice = playerControllerHuman.getCard(recordedChoice); - if (exactChoice != null && selectInput.getValidChoices().contains(exactChoice)) { - return exactChoice; + if (recordedChoice == null) { + return null; + } + final Card exactListChoice = findExactCardChoice(exactChoice, selectInput.getValidChoices()); + if (exactListChoice != null) { + return exactListChoice; } - if (recordedChoice == null || isRecordedBattlefieldLandChoice(recordedChoice, exactChoice, selectInput)) { + if (isRecordedBattlefieldLandChoice(recordedChoice, selectInput)) { return null; } - for (final GameEntity validChoice : selectInput.getValidChoices()) { - if (validChoice instanceof Card card && isEquivalentCard(recordedChoice, card)) { - return card; + final Card choice = findCardChoice(recordedChoice, exactChoice, selectInput.getValidChoices()); + debugNoMatch(choice, "list", recordedChoice, selectInput.getValidChoices()); + return choice; + } + + private Card findCardChoice(final CardView recordedChoice, final Card exactChoice, final Iterable choices) { + final Card exactListChoice = findExactCardChoice(exactChoice, choices); + if (exactListChoice != null) { + return exactListChoice; + } + if (recordedChoice == null) { + return null; + } + final Card equivalent = findCardChoice(recordedChoice, choices, false); + return equivalent == null ? findCardChoice(recordedChoice, choices, true) : equivalent; + } + + private Card findExactCardChoice(final Card exactChoice, final Iterable choices) { + if (exactChoice == null) { + return null; + } + for (final Object choice : choices) { + if (choice == exactChoice) { + return exactChoice; } } - for (final GameEntity validChoice : selectInput.getValidChoices()) { - if (validChoice instanceof Card card && isEquivalentToken(recordedChoice, card)) { + return null; + } + + private Card findCardChoice(final CardView recordedChoice, final Iterable choices, final boolean tokenMatch) { + for (final Object choice : choices) { + if (choice instanceof Card card + && (tokenMatch ? isEquivalentToken(recordedChoice, card) : isEquivalentCard(recordedChoice, card))) { return card; } } - debug("no list match for " + recordedChoice + " choices=" + selectInput.getValidChoices()); return null; } - private boolean isRecordedBattlefieldLandChoice(final CardView recordedChoice, final Card exactChoice, + private void debugNoMatch(final Card choice, final String choiceType, final CardView recordedChoice, + final Object choices) { + if (choice == null && recordedChoice != null) { + debug("no " + choiceType + " match for " + recordedChoice + + (choices == null ? "" : " choices=" + choices)); + } + } + + private boolean isRecordedBattlefieldLandChoice(final CardView recordedChoice, final InputSelectEntitiesFromList selectInput) { - if (!(exactChoice == null ? recordedChoice.getCurrentState().getType().isLand() : exactChoice.isLand())) { + if (exactRecordedLandManaSource(recordedChoice) == null) { return false; } for (final GameEntity validChoice : selectInput.getValidChoices()) { @@ -1117,16 +1447,45 @@ private CardView selectedCardView(final PlayerAction action) { } private boolean debugResult(final PlayerAction action, final boolean result) { - debug((result ? "accepted " : "rejected ") + action.describe()); + if (DEBUG) { + debug((result ? "accepted " : "rejected ") + action.describe()); + } + if (!result && repeatIterations > 0 && playbackRetries == MAX_REJECTED_ACTION_RETRIES) { + playbackMessages.add(localizer.getMessage("lblMacroCouldNotReplay", action.describe(), describeInput())); + notifyStatusListeners(); + } return result; } + private void setActiveAction(final PlayerAction action) { + final int index = action == null ? -1 : actions.indexOf(action); + if (activeActionIndex == index) { + return; + } + activeActionIndex = index; + notifyStatusListeners(); + } + + private String describeActiveAction() { + if (activeActionIndex >= 0 && activeActionIndex < actions.size()) { + return localizer.getMessage("lblMacroStepDescription", + activeActionIndex + 1, actions.get(activeActionIndex).describe()); + } + return localizer.getMessage("lblMacroNextRecordedStep"); + } + private void debug(final String message) { if (DEBUG) { System.out.println(DEBUG_PREFIX + message); } } + private void debugPlaybackState() { + if (DEBUG) { + debug("input=" + describeInput() + " remaining=" + describeActions(playbackActions)); + } + } + private String describeInput() { final Input inp = playerControllerHuman.getInputProxy().getInput(); return inp == null ? "none" : inp.getClass().getSimpleName(); @@ -1152,7 +1511,7 @@ public int getButton() { @Override public int getX() { - return 0; // Hopefully this doesn't do anything wonky! + return 0; } @Override From 5c7f9e1a41ac30835e84eb571dd6de76634ecbb8 Mon Sep 17 00:00:00 2001 From: Madwand99 Date: Fri, 22 May 2026 21:08:58 -0700 Subject: [PATCH 2/7] More localization --- .../java/forge/game/player/actions/PassPriorityAction.java | 6 +++--- forge-gui/res/languages/de-DE.properties | 2 ++ forge-gui/res/languages/en-US.properties | 2 ++ forge-gui/res/languages/es-ES.properties | 2 ++ forge-gui/res/languages/fr-FR.properties | 2 ++ forge-gui/res/languages/it-IT.properties | 2 ++ forge-gui/res/languages/ja-JP.properties | 2 ++ forge-gui/res/languages/ko-KR.properties | 2 ++ forge-gui/res/languages/pt-BR.properties | 2 ++ forge-gui/res/languages/zh-CN.properties | 2 ++ 10 files changed, 21 insertions(+), 3 deletions(-) diff --git a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java index 12b1ccb159d..ef4f9735000 100644 --- a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java @@ -33,9 +33,9 @@ public String describe() { private static String describePhase(final PhaseType phase) { return switch (phase) { - case MAIN1 -> "Main Phase 1"; - case MAIN2 -> "Main Phase 2"; - default -> phase.nameForScripts; + case MAIN1 -> localize("lblMacroPhaseMain1"); + case MAIN2 -> localize("lblMacroPhaseMain2"); + default -> phase.nameForUi; }; } } diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index fa412df9ee2..dd4467b6149 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -487,6 +487,8 @@ lblMacroManaAmountPlural={0} {1} Mana lblMacroActionChooseMode=Wähle Modus: {0} lblMacroActionChooseModes=Wähle Modi:{0} lblMacroActionDuringPhase=während {0} +lblMacroPhaseMain1=Hauptphase 1 +lblMacroPhaseMain2=Hauptphase 2 lblMacroActionPassPriority=Priorität abgeben{0} mit {1} lblMacroStackEmpty=einem leeren Stapel lblMacroStackNotEmpty=Zaubersprüchen oder Fähigkeiten auf dem Stapel diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 34fd9077b80..bbabb6c7caf 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -553,6 +553,8 @@ lblMacroManaAmountPlural={0} {1} mana lblMacroActionChooseMode=Choose mode: {0} lblMacroActionChooseModes=Choose modes:{0} lblMacroActionDuringPhase=during {0} +lblMacroPhaseMain1=Main Phase 1 +lblMacroPhaseMain2=Main Phase 2 lblMacroActionPassPriority=Pass priority{0} with {1} lblMacroStackEmpty=an empty stack lblMacroStackNotEmpty=spells or abilities on the stack diff --git a/forge-gui/res/languages/es-ES.properties b/forge-gui/res/languages/es-ES.properties index 92b5b9d7a67..eb01e199e0a 100644 --- a/forge-gui/res/languages/es-ES.properties +++ b/forge-gui/res/languages/es-ES.properties @@ -464,6 +464,8 @@ lblMacroManaAmountPlural={0} maná {1} lblMacroActionChooseMode=Elegir modo: {0} lblMacroActionChooseModes=Elegir modos:{0} lblMacroActionDuringPhase=durante {0} +lblMacroPhaseMain1=Fase principal 1 +lblMacroPhaseMain2=Fase principal 2 lblMacroActionPassPriority=Pasar prioridad{0} con {1} lblMacroStackEmpty=la pila vacía lblMacroStackNotEmpty=hechizos o habilidades en la pila diff --git a/forge-gui/res/languages/fr-FR.properties b/forge-gui/res/languages/fr-FR.properties index f53ec041300..4621a46f4fb 100644 --- a/forge-gui/res/languages/fr-FR.properties +++ b/forge-gui/res/languages/fr-FR.properties @@ -462,6 +462,8 @@ lblMacroManaAmountPlural={0} mana {1} lblMacroActionChooseMode=Choisir un mode: {0} lblMacroActionChooseModes=Choisir des modes:{0} lblMacroActionDuringPhase=pendant {0} +lblMacroPhaseMain1=Phase principale 1 +lblMacroPhaseMain2=Phase principale 2 lblMacroActionPassPriority=Passer la priorité{0} avec {1} lblMacroStackEmpty=une pile vide lblMacroStackNotEmpty=des sorts ou capacités sur la pile diff --git a/forge-gui/res/languages/it-IT.properties b/forge-gui/res/languages/it-IT.properties index 3d4e543ceea..54fa666d126 100644 --- a/forge-gui/res/languages/it-IT.properties +++ b/forge-gui/res/languages/it-IT.properties @@ -461,6 +461,8 @@ lblMacroManaAmountPlural={0} mana {1} lblMacroActionChooseMode=Scegli modalità: {0} lblMacroActionChooseModes=Scegli modalità:{0} lblMacroActionDuringPhase=durante {0} +lblMacroPhaseMain1=Fase principale 1 +lblMacroPhaseMain2=Fase principale 2 lblMacroActionPassPriority=Cedi priorità{0} con {1} lblMacroStackEmpty=una pila vuota lblMacroStackNotEmpty=magie o abilità in pila diff --git a/forge-gui/res/languages/ja-JP.properties b/forge-gui/res/languages/ja-JP.properties index 7704a5c552b..af1cbfb7d71 100644 --- a/forge-gui/res/languages/ja-JP.properties +++ b/forge-gui/res/languages/ja-JP.properties @@ -462,6 +462,8 @@ lblMacroManaAmountPlural={0} {1}マナ lblMacroActionChooseMode=モードを選ぶ: {0} lblMacroActionChooseModes=モードを選ぶ:{0} lblMacroActionDuringPhase=({0}中) +lblMacroPhaseMain1=第1メイン・フェイズ +lblMacroPhaseMain2=第2メイン・フェイズ lblMacroActionPassPriority={1}で優先権をパス{0} lblMacroStackEmpty=空のスタック lblMacroStackNotEmpty=スタック上にある呪文や能力 diff --git a/forge-gui/res/languages/ko-KR.properties b/forge-gui/res/languages/ko-KR.properties index 4d0d03fb3bd..0acb1fbff25 100644 --- a/forge-gui/res/languages/ko-KR.properties +++ b/forge-gui/res/languages/ko-KR.properties @@ -496,6 +496,8 @@ lblMacroManaAmountPlural={0} {1} 마나 lblMacroActionChooseMode=모드 선택: {0} lblMacroActionChooseModes=모드 선택:{0} lblMacroActionDuringPhase=({0} 중) +lblMacroPhaseMain1=첫 번째 메인 단계 +lblMacroPhaseMain2=두 번째 메인 단계 lblMacroActionPassPriority={1} 상태에서 우선권 넘기기{0} lblMacroStackEmpty=빈 스택 lblMacroStackNotEmpty=스택에 있는 주문 또는 능력 diff --git a/forge-gui/res/languages/pt-BR.properties b/forge-gui/res/languages/pt-BR.properties index 395f5bb941a..f53a1ce4acf 100644 --- a/forge-gui/res/languages/pt-BR.properties +++ b/forge-gui/res/languages/pt-BR.properties @@ -474,6 +474,8 @@ lblMacroManaAmountPlural={0} mana {1} lblMacroActionChooseMode=Escolher modo: {0} lblMacroActionChooseModes=Escolher modos:{0} lblMacroActionDuringPhase=durante {0} +lblMacroPhaseMain1=Fase principal 1 +lblMacroPhaseMain2=Fase principal 2 lblMacroActionPassPriority=Passar prioridade{0} com {1} lblMacroStackEmpty=uma pilha vazia lblMacroStackNotEmpty=mágicas ou habilidades na pilha diff --git a/forge-gui/res/languages/zh-CN.properties b/forge-gui/res/languages/zh-CN.properties index 3f6c775780b..62121214387 100644 --- a/forge-gui/res/languages/zh-CN.properties +++ b/forge-gui/res/languages/zh-CN.properties @@ -464,6 +464,8 @@ lblMacroManaAmountPlural={0} 点{1}法术力 lblMacroActionChooseMode=选择模式:{0} lblMacroActionChooseModes=选择模式:{0} lblMacroActionDuringPhase=({0}期间) +lblMacroPhaseMain1=第一行动阶段 +lblMacroPhaseMain2=第二行动阶段 lblMacroActionPassPriority=在{1}时让过优先权{0} lblMacroStackEmpty=堆叠为空 lblMacroStackNotEmpty=堆叠上有咒语或异能 From 10d33e5554f01c27355100a2c50a4751feb5971c Mon Sep 17 00:00:00 2001 From: Madwand99 Date: Sat, 23 May 2026 00:22:58 -0700 Subject: [PATCH 3/7] A couple language file fixes --- .../forge/game/player/actions/PassPriorityAction.java | 10 +--------- forge-gui/res/languages/de-DE.properties | 3 --- forge-gui/res/languages/en-US.properties | 3 --- forge-gui/res/languages/es-ES.properties | 3 --- forge-gui/res/languages/fr-FR.properties | 3 --- forge-gui/res/languages/it-IT.properties | 3 --- forge-gui/res/languages/ja-JP.properties | 3 --- forge-gui/res/languages/ko-KR.properties | 3 --- forge-gui/res/languages/pt-BR.properties | 3 --- forge-gui/res/languages/zh-CN.properties | 3 --- .../java/forge/player/RecordActionsMacroSystem.java | 9 ++++++--- 11 files changed, 7 insertions(+), 39 deletions(-) diff --git a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java index ef4f9735000..19930b5c97f 100644 --- a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java @@ -26,16 +26,8 @@ public PhaseType getPhase() { @Override public String describe() { - final String phaseText = phase == null ? "" : " " + localize("lblMacroActionDuringPhase", describePhase(phase)); + final String phaseText = phase == null ? "" : " " + localize("lblMacroActionDuringPhase", phase.nameForUi); final String stackText = localize(stackWasEmpty ? "lblMacroStackEmpty" : "lblMacroStackNotEmpty"); return localize("lblMacroActionPassPriority", phaseText, stackText); } - - private static String describePhase(final PhaseType phase) { - return switch (phase) { - case MAIN1 -> localize("lblMacroPhaseMain1"); - case MAIN2 -> localize("lblMacroPhaseMain2"); - default -> phase.nameForUi; - }; - } } diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index dd4467b6149..41e14e61676 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -472,7 +472,6 @@ lblMacroReadyStatus=Makro bereit lblMacroNoActionsRecorded=Noch keine Aktionen aufgezeichnet. lblMacroActionCount={0} aufgezeichnete Aktion(en) lblMacroLog=Makroprotokoll -lblMacroPlaybackProgress={0} / {1} lblMacroNone=keine lblMacroActionActivateAbility=Aktiviere Fähigkeit von {0}: {1} lblMacroActionCastSpell=Wirke Zauberspruch: {0} @@ -487,8 +486,6 @@ lblMacroManaAmountPlural={0} {1} Mana lblMacroActionChooseMode=Wähle Modus: {0} lblMacroActionChooseModes=Wähle Modi:{0} lblMacroActionDuringPhase=während {0} -lblMacroPhaseMain1=Hauptphase 1 -lblMacroPhaseMain2=Hauptphase 2 lblMacroActionPassPriority=Priorität abgeben{0} mit {1} lblMacroStackEmpty=einem leeren Stapel lblMacroStackNotEmpty=Zaubersprüchen oder Fähigkeiten auf dem Stapel diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 619afa0443f..e3b49d0db5d 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -538,7 +538,6 @@ lblMacroReadyStatus=Macro ready lblMacroNoActionsRecorded=No recorded actions yet. lblMacroActionCount={0} recorded action(s) lblMacroLog=Macro log -lblMacroPlaybackProgress={0} / {1} lblMacroNone=none lblMacroActionActivateAbility=Activate ability of {0}: {1} lblMacroActionCastSpell=Cast spell: {0} @@ -553,8 +552,6 @@ lblMacroManaAmountPlural={0} {1} mana lblMacroActionChooseMode=Choose mode: {0} lblMacroActionChooseModes=Choose modes:{0} lblMacroActionDuringPhase=during {0} -lblMacroPhaseMain1=Main Phase 1 -lblMacroPhaseMain2=Main Phase 2 lblMacroActionPassPriority=Pass priority{0} with {1} lblMacroStackEmpty=an empty stack lblMacroStackNotEmpty=spells or abilities on the stack diff --git a/forge-gui/res/languages/es-ES.properties b/forge-gui/res/languages/es-ES.properties index eb01e199e0a..4c3d1c8dcaa 100644 --- a/forge-gui/res/languages/es-ES.properties +++ b/forge-gui/res/languages/es-ES.properties @@ -449,7 +449,6 @@ lblMacroReadyStatus=Macro lista lblMacroNoActionsRecorded=Aún no hay acciones grabadas. lblMacroActionCount={0} acción(es) grabada(s) lblMacroLog=Registro de macro -lblMacroPlaybackProgress={0} / {1} lblMacroNone=ninguno lblMacroActionActivateAbility=Activar habilidad de {0}: {1} lblMacroActionCastSpell=Lanzar hechizo: {0} @@ -464,8 +463,6 @@ lblMacroManaAmountPlural={0} maná {1} lblMacroActionChooseMode=Elegir modo: {0} lblMacroActionChooseModes=Elegir modos:{0} lblMacroActionDuringPhase=durante {0} -lblMacroPhaseMain1=Fase principal 1 -lblMacroPhaseMain2=Fase principal 2 lblMacroActionPassPriority=Pasar prioridad{0} con {1} lblMacroStackEmpty=la pila vacía lblMacroStackNotEmpty=hechizos o habilidades en la pila diff --git a/forge-gui/res/languages/fr-FR.properties b/forge-gui/res/languages/fr-FR.properties index 4621a46f4fb..3fdf0f7ff2c 100644 --- a/forge-gui/res/languages/fr-FR.properties +++ b/forge-gui/res/languages/fr-FR.properties @@ -447,7 +447,6 @@ lblMacroReadyStatus=Macro prête lblMacroNoActionsRecorded=Aucune action enregistrée. lblMacroActionCount={0} action(s) enregistrée(s) lblMacroLog=Journal de macro -lblMacroPlaybackProgress={0} / {1} lblMacroNone=aucun lblMacroActionActivateAbility=Activer la capacité de {0}: {1} lblMacroActionCastSpell=Lancer le sort: {0} @@ -462,8 +461,6 @@ lblMacroManaAmountPlural={0} mana {1} lblMacroActionChooseMode=Choisir un mode: {0} lblMacroActionChooseModes=Choisir des modes:{0} lblMacroActionDuringPhase=pendant {0} -lblMacroPhaseMain1=Phase principale 1 -lblMacroPhaseMain2=Phase principale 2 lblMacroActionPassPriority=Passer la priorité{0} avec {1} lblMacroStackEmpty=une pile vide lblMacroStackNotEmpty=des sorts ou capacités sur la pile diff --git a/forge-gui/res/languages/it-IT.properties b/forge-gui/res/languages/it-IT.properties index 54fa666d126..9b65dc0fc52 100644 --- a/forge-gui/res/languages/it-IT.properties +++ b/forge-gui/res/languages/it-IT.properties @@ -446,7 +446,6 @@ lblMacroReadyStatus=Macro pronta lblMacroNoActionsRecorded=Nessuna azione registrata. lblMacroActionCount={0} azione/i registrata/e lblMacroLog=Registro macro -lblMacroPlaybackProgress={0} / {1} lblMacroNone=nessuno lblMacroActionActivateAbility=Attiva abilità di {0}: {1} lblMacroActionCastSpell=Lancia magia: {0} @@ -461,8 +460,6 @@ lblMacroManaAmountPlural={0} mana {1} lblMacroActionChooseMode=Scegli modalità: {0} lblMacroActionChooseModes=Scegli modalità:{0} lblMacroActionDuringPhase=durante {0} -lblMacroPhaseMain1=Fase principale 1 -lblMacroPhaseMain2=Fase principale 2 lblMacroActionPassPriority=Cedi priorità{0} con {1} lblMacroStackEmpty=una pila vuota lblMacroStackNotEmpty=magie o abilità in pila diff --git a/forge-gui/res/languages/ja-JP.properties b/forge-gui/res/languages/ja-JP.properties index af1cbfb7d71..69d1fec377e 100644 --- a/forge-gui/res/languages/ja-JP.properties +++ b/forge-gui/res/languages/ja-JP.properties @@ -447,7 +447,6 @@ lblMacroReadyStatus=マクロ準備完了 lblMacroNoActionsRecorded=記録されたアクションはまだありません。 lblMacroActionCount=記録済みアクション {0} 件 lblMacroLog=マクロログ -lblMacroPlaybackProgress={0} / {1} lblMacroNone=なし lblMacroActionActivateAbility={0}の能力を起動: {1} lblMacroActionCastSpell=呪文を唱える: {0} @@ -462,8 +461,6 @@ lblMacroManaAmountPlural={0} {1}マナ lblMacroActionChooseMode=モードを選ぶ: {0} lblMacroActionChooseModes=モードを選ぶ:{0} lblMacroActionDuringPhase=({0}中) -lblMacroPhaseMain1=第1メイン・フェイズ -lblMacroPhaseMain2=第2メイン・フェイズ lblMacroActionPassPriority={1}で優先権をパス{0} lblMacroStackEmpty=空のスタック lblMacroStackNotEmpty=スタック上にある呪文や能力 diff --git a/forge-gui/res/languages/ko-KR.properties b/forge-gui/res/languages/ko-KR.properties index 0acb1fbff25..ba0fd87ca30 100644 --- a/forge-gui/res/languages/ko-KR.properties +++ b/forge-gui/res/languages/ko-KR.properties @@ -481,7 +481,6 @@ lblMacroReadyStatus=매크로 준비됨 lblMacroNoActionsRecorded=아직 기록된 액션이 없습니다. lblMacroActionCount=기록된 액션 {0}개 lblMacroLog=매크로 로그 -lblMacroPlaybackProgress={0} / {1} lblMacroNone=없음 lblMacroActionActivateAbility={0}의 능력 활성화: {1} lblMacroActionCastSpell=주문 시전: {0} @@ -496,8 +495,6 @@ lblMacroManaAmountPlural={0} {1} 마나 lblMacroActionChooseMode=모드 선택: {0} lblMacroActionChooseModes=모드 선택:{0} lblMacroActionDuringPhase=({0} 중) -lblMacroPhaseMain1=첫 번째 메인 단계 -lblMacroPhaseMain2=두 번째 메인 단계 lblMacroActionPassPriority={1} 상태에서 우선권 넘기기{0} lblMacroStackEmpty=빈 스택 lblMacroStackNotEmpty=스택에 있는 주문 또는 능력 diff --git a/forge-gui/res/languages/pt-BR.properties b/forge-gui/res/languages/pt-BR.properties index f53a1ce4acf..dde4aa2f7af 100644 --- a/forge-gui/res/languages/pt-BR.properties +++ b/forge-gui/res/languages/pt-BR.properties @@ -459,7 +459,6 @@ lblMacroReadyStatus=Macro pronta lblMacroNoActionsRecorded=Nenhuma ação gravada ainda. lblMacroActionCount={0} ação(ões) gravada(s) lblMacroLog=Registro da macro -lblMacroPlaybackProgress={0} / {1} lblMacroNone=nenhum lblMacroActionActivateAbility=Ativar habilidade de {0}: {1} lblMacroActionCastSpell=Conjurar mágica: {0} @@ -474,8 +473,6 @@ lblMacroManaAmountPlural={0} mana {1} lblMacroActionChooseMode=Escolher modo: {0} lblMacroActionChooseModes=Escolher modos:{0} lblMacroActionDuringPhase=durante {0} -lblMacroPhaseMain1=Fase principal 1 -lblMacroPhaseMain2=Fase principal 2 lblMacroActionPassPriority=Passar prioridade{0} com {1} lblMacroStackEmpty=uma pilha vazia lblMacroStackNotEmpty=mágicas ou habilidades na pilha diff --git a/forge-gui/res/languages/zh-CN.properties b/forge-gui/res/languages/zh-CN.properties index 62121214387..a7a854c1845 100644 --- a/forge-gui/res/languages/zh-CN.properties +++ b/forge-gui/res/languages/zh-CN.properties @@ -449,7 +449,6 @@ lblMacroReadyStatus=宏已就绪 lblMacroNoActionsRecorded=尚未录制动作。 lblMacroActionCount=已录制 {0} 个动作 lblMacroLog=宏日志 -lblMacroPlaybackProgress={0} / {1} lblMacroNone=无 lblMacroActionActivateAbility=起动 {0} 的异能:{1} lblMacroActionCastSpell=施放咒语:{0} @@ -464,8 +463,6 @@ lblMacroManaAmountPlural={0} 点{1}法术力 lblMacroActionChooseMode=选择模式:{0} lblMacroActionChooseModes=选择模式:{0} lblMacroActionDuringPhase=({0}期间) -lblMacroPhaseMain1=第一行动阶段 -lblMacroPhaseMain2=第二行动阶段 lblMacroActionPassPriority=在{1}时让过优先权{0} lblMacroStackEmpty=堆叠为空 lblMacroStackNotEmpty=堆叠上有咒语或异能 diff --git a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java index 70254758d36..7a58633acf5 100644 --- a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java +++ b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java @@ -289,14 +289,17 @@ private CardCollection takeCardsByName(final CardCollection available, final Lis @Override public String playbackText() { if (repeatIterations > 0) { - return localizer.getMessage("lblMacroPlaybackProgress", repeatIteration, repeatIterations); + return playbackProgressText(repeatIteration, repeatIterations); } if (playbackActions.isEmpty()) { return null; } - return localizer.getMessage("lblMacroPlaybackProgress", - actions.size() - playbackActions.size(), actions.size()); + return playbackProgressText(actions.size() - playbackActions.size(), actions.size()); + } + + private String playbackProgressText(final int current, final int total) { + return current + " / " + total; } public boolean startRecording() { From 11bfd1b4378de9eb3bedd26cb11256f7ca8f40ef Mon Sep 17 00:00:00 2001 From: Madwand99 Date: Sat, 23 May 2026 00:40:45 -0700 Subject: [PATCH 4/7] Fix unnecessary refersh --- .../java/forge/control/KeyboardShortcuts.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java b/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java index 19ce769acfe..f4e44c82dd8 100644 --- a/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java +++ b/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java @@ -195,9 +195,10 @@ public void actionPerformed(final ActionEvent e) { final Action actMacroRecord = macroAction(matchUI, ui -> ui.getCDock().toggleMacroRecording()); - final Action actMacroNextAction = macroAction(matchUI, ui -> ui.getGameController().macros().nextRememberedAction()); + final Action actMacroNextAction = macroActionAndRefreshControls(matchUI, + ui -> ui.getGameController().macros().nextRememberedAction()); - final Action actMacroRepeatActions = macroAction(matchUI, + final Action actMacroRepeatActions = macroActionAndRefreshControls(matchUI, ui -> ui.getGameController().macros().repeatRememberedActions()); final Action actZoomCard = new AbstractAction() { @@ -305,12 +306,18 @@ public void actionPerformed(final ActionEvent e) { return; } command.accept(matchUI); - matchUI.getCDock().refreshMacroButtons(); - matchUI.getCMacro().update(); } }; } + private static Action macroActionAndRefreshControls(final CMatchUI matchUI, final Consumer command) { + return macroAction(matchUI, ui -> { + command.accept(ui); + ui.getCDock().refreshMacroButtons(); + ui.getCMacro().update(); + }); + } + /** * * Instantiates a shortcut key instance with various properties for use From 470d3c390ac3cabc9b3ba0b21a12a956e146c1a8 Mon Sep 17 00:00:00 2001 From: Madwand99 Date: Sat, 23 May 2026 09:07:09 -0700 Subject: [PATCH 5/7] Move more code into Action classes --- .../game/player/actions/ConfirmAction.java | 10 ++ .../player/actions/FinishTargetingAction.java | 5 + .../player/actions/PassPriorityAction.java | 53 +++++- .../game/player/actions/PlayerAction.java | 23 ++- .../game/player/actions/SelectCardAction.java | 11 ++ .../player/actions/SelectPlayerAction.java | 5 + .../player/RecordActionsMacroSystem.java | 159 ++++++------------ 7 files changed, 153 insertions(+), 113 deletions(-) diff --git a/forge-game/src/main/java/forge/game/player/actions/ConfirmAction.java b/forge-game/src/main/java/forge/game/player/actions/ConfirmAction.java index 58c0be980b7..46aad9359ad 100644 --- a/forge-game/src/main/java/forge/game/player/actions/ConfirmAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/ConfirmAction.java @@ -1,6 +1,7 @@ package forge.game.player.actions; import forge.game.GameEntityView; +import forge.game.card.CardView; public class ConfirmAction extends PlayerAction { private final boolean confirmed; @@ -14,6 +15,15 @@ public boolean isConfirmed() { return confirmed; } + public boolean matchesPrompt(final CardView inputCard, final String message) { + final GameEntityView recordedView = getGameEntityView(); + if (!(recordedView instanceof CardView recordedCard)) { + return true; + } + return (inputCard != null && recordedCard.getName().equals(inputCard.getName())) + || (message != null && message.contains(recordedCard.getName())); + } + @Override public String describe() { final String action = localize(confirmed ? "lblMacroActionConfirm" : "lblMacroActionDecline"); diff --git a/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java b/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java index 7e22216df7c..a8ea2e59486 100644 --- a/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java @@ -5,6 +5,11 @@ public FinishTargetingAction() { super(null, "Finish game entity"); } + @Override + public boolean isTargetSelectionAction() { + return true; + } + @Override public String describe() { return localize("lblMacroActionFinishSelecting"); diff --git a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java index 19930b5c97f..489af13c464 100644 --- a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java @@ -20,8 +20,57 @@ public boolean wasStackEmpty() { return stackWasEmpty; } - public PhaseType getPhase() { - return phase; + public boolean isObsoleteWhen(final boolean stackEmpty) { + return !stackWasEmpty && stackEmpty; + } + + public boolean isStaleFor(final PhaseType currentPhase) { + return phase != null && currentPhase != null && phase.isBefore(currentPhase); + } + + public boolean canReplay(final boolean currentStackEmpty, final PhaseType currentPhase) { + return stackWasEmpty == currentStackEmpty + && (phase == null || phase == currentPhase || canAdvanceTowardRecordedCombatPass(currentPhase)); + } + + public boolean canReplayDuringAttack(final PhaseType currentPhase) { + return phase == null || phase == currentPhase; + } + + public boolean isStackPassFor(final PhaseType currentPhase) { + return !stackWasEmpty && phase == currentPhase; + } + + public boolean isTrailingMainPhasePassCandidate(final PhaseType currentPhase) { + return stackWasEmpty && phase == PhaseType.MAIN1 && currentPhase == PhaseType.MAIN1; + } + + private boolean canAdvanceTowardRecordedCombatPass(final PhaseType currentPhase) { + return stackWasEmpty + && currentPhase != null + && phase != null + && currentPhase.isBefore(phase) + && isCombatPhase(currentPhase) + && (isCombatPhase(phase) || phase == PhaseType.MAIN2); + } + + private static boolean isCombatPhase(final PhaseType phase) { + return phase == PhaseType.COMBAT_BEGIN + || phase == PhaseType.COMBAT_DECLARE_ATTACKERS + || phase == PhaseType.COMBAT_DECLARE_BLOCKERS + || phase == PhaseType.COMBAT_FIRST_STRIKE_DAMAGE + || phase == PhaseType.COMBAT_DAMAGE + || phase == PhaseType.COMBAT_END; + } + + @Override + public boolean clearsPostStackOrderWait() { + return true; + } + + @Override + public PassPriorityAction asPassPriorityAction() { + return this; } @Override diff --git a/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java b/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java index 9ec1c0263bb..6d0e6036ac6 100644 --- a/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java @@ -1,7 +1,7 @@ package forge.game.player.actions; import forge.game.GameEntityView; -import forge.game.player.PlayerController; +import forge.game.card.CardView; import forge.util.Localizer; import java.util.List; @@ -24,9 +24,24 @@ public PlayerAction(final GameEntityView cardView, final String actionName) { name = actionName; } - public void run(PlayerController controller) { - // Turn this abstract soon - // This should try to replicate the recorded macro action + public boolean isSelectionAction() { + return false; + } + + public boolean isTargetSelectionAction() { + return isSelectionAction(); + } + + public boolean clearsPostStackOrderWait() { + return isSelectionAction(); + } + + public PassPriorityAction asPassPriorityAction() { + return null; + } + + public CardView getSelectedCardView() { + return null; } public GameEntityView getGameEntityView() { diff --git a/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java b/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java index 8970d3d3220..12e335cb58b 100644 --- a/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java @@ -1,12 +1,23 @@ package forge.game.player.actions; import forge.game.GameEntityView; +import forge.game.card.CardView; public class SelectCardAction extends PlayerAction { public SelectCardAction(GameEntityView cardView) { super(cardView, "Select card"); } + @Override + public boolean isSelectionAction() { + return true; + } + + @Override + public CardView getSelectedCardView() { + return getGameEntityView() instanceof CardView cardView ? cardView : null; + } + @Override public String describe() { return localize("lblMacroActionSelectCard", describeEntity()); diff --git a/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java b/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java index 4469992ed12..9ebe6d0b8ae 100644 --- a/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java @@ -7,6 +7,11 @@ public SelectPlayerAction(GameEntityView playerView) { super(playerView, "Select player"); } + @Override + public boolean isSelectionAction() { + return true; + } + @Override public String describe() { return localize("lblMacroActionSelectPlayer", describeEntity()); diff --git a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java index 7a58633acf5..75caac8f62b 100644 --- a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java +++ b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java @@ -394,7 +394,7 @@ private void removeLastCardSelectionFor(final GameEntityView view, final List selectInput) { for (int i = 0; i < playbackActions.size(); i++) { final PlayerAction action = playbackActions.get(i); - if (!isSelectionAction(action)) { + if (!action.isSelectionAction()) { continue; } if (processAction(action)) { @@ -805,14 +805,6 @@ private int finishListSelectionIfAny(final InputSelectEntitiesFromList input) return input.getSelected().isEmpty() ? NO_ACTION_ACCEPTED : waitForInput(input); } - private boolean isTargetSelectionAction(final PlayerAction action) { - return isSelectionAction(action) || action instanceof FinishTargetingAction; - } - - private boolean isSelectionAction(final PlayerAction action) { - return action instanceof SelectCardAction || action instanceof SelectPlayerAction; - } - private boolean hasFutureSelectionAction() { return findNextAction(SelectCardAction.class, SelectPlayerAction.class) >= 0; } @@ -822,9 +814,8 @@ private boolean isPriorityInput(final Input input) { } private boolean isObsoleteStackPass(final PlayerAction action) { - return action instanceof PassPriorityAction passPriorityAction - && !passPriorityAction.wasStackEmpty() - && isStackEmpty(); + final PassPriorityAction passPriorityAction = action.asPassPriorityAction(); + return passPriorityAction != null && passPriorityAction.isObsoleteWhen(isStackEmpty()); } private boolean shouldSkipPassPriorityAction(final int actionIndex) { @@ -835,11 +826,8 @@ private boolean shouldSkipPassPriorityAction(final int actionIndex) { } private boolean isStalePhasePass(final PlayerAction action) { - if (!(action instanceof PassPriorityAction passPriorityAction) || passPriorityAction.getPhase() == null) { - return false; - } - final PhaseType currentPhase = getCurrentPhase(); - return currentPhase != null && passPriorityAction.getPhase().isBefore(currentPhase); + final PassPriorityAction passPriorityAction = action.asPassPriorityAction(); + return passPriorityAction != null && passPriorityAction.isStaleFor(getCurrentPhase()); } private boolean isTrailingMainPhasePassBeforeNextIteration(final int actionIndex) { @@ -847,32 +835,21 @@ private boolean isTrailingMainPhasePassBeforeNextIteration(final int actionIndex return false; } final PlayerAction action = playbackActions.get(actionIndex); - if (!(action instanceof PassPriorityAction passPriorityAction) || !passPriorityAction.wasStackEmpty() - || passPriorityAction.getPhase() != PhaseType.MAIN1 - || getCurrentPhase() != PhaseType.MAIN1) { + final PassPriorityAction passPriorityAction = action.asPassPriorityAction(); + if (passPriorityAction == null || !passPriorityAction.isTrailingMainPhasePassCandidate(getCurrentPhase())) { return false; } for (int i = actionIndex + 1; i < playbackActions.size(); i++) { - if (!(playbackActions.get(i) instanceof PassPriorityAction remainingPass) - || !remainingPass.wasStackEmpty()) { + final PassPriorityAction remainingPass = playbackActions.get(i).asPassPriorityAction(); + if (remainingPass == null || !remainingPass.wasStackEmpty()) { return false; } } - return !actions.isEmpty() && !(actions.get(0) instanceof PassPriorityAction); + return !actions.isEmpty() && actions.get(0).asPassPriorityAction() == null; } private boolean canReplayPassPriority(final PassPriorityAction action) { - if (action.wasStackEmpty() != isStackEmpty()) { - return false; - } - final PhaseType currentPhase = getCurrentPhase(); - return action.getPhase() == null - || action.getPhase() == currentPhase - || canAdvanceTowardRecordedCombatPass(action, currentPhase); - } - - private boolean canReplayAttackPassPriority(final PassPriorityAction action) { - return action.getPhase() == null || action.getPhase() == getCurrentPhase(); + return action.canReplay(isStackEmpty(), getCurrentPhase()); } private PhaseType getCurrentPhase() { @@ -915,22 +892,12 @@ private boolean canStartNextIterationBeforeStackClear(final Input input) { return false; } final PlayerAction firstAction = actions.get(0); - return (isSelectionAction(firstAction) + return (firstAction.isSelectionAction() && (input instanceof InputSelectTargets || input instanceof InputSelectEntitiesFromList)) || (firstAction instanceof ConfirmAction && input instanceof InputConfirm) || (firstAction instanceof PayManaFromPoolAction && input instanceof InputPayMana); } - private boolean canAdvanceTowardRecordedCombatPass(final PassPriorityAction action, final PhaseType currentPhase) { - final PhaseType recordedPhase = action.getPhase(); - return action.wasStackEmpty() - && currentPhase != null - && recordedPhase != null - && currentPhase.isBefore(recordedPhase) - && isCombatPhase(currentPhase) - && (isCombatPhase(recordedPhase) || recordedPhase == PhaseType.MAIN2); - } - private int waitForPendingCardAction(final Input input, final int actionIndex, final boolean waitingForRecordedStackOrder) { final PlayerAction action = playbackActions.get(actionIndex); if (!isStackEmpty()) { @@ -964,10 +931,6 @@ private int waitForInput(final Input input) { return WAIT_FOR_NEXT_INPUT; } - private boolean isCombatPhase(final PhaseType phase) { - return phase.name().startsWith("COMBAT_"); - } - private boolean isWaitingForAttackDeclaration(final int actionIndex) { final Card card = findPendingPlayerCard(actionIndex); if (card == null || !card.isCreature()) { @@ -993,7 +956,7 @@ private Card findPendingPlayerCard(final int actionIndex) { if (actionIndex < 0 || actionIndex >= playbackActions.size()) { return null; } - final Card card = findCard(selectedCardView(playbackActions.get(actionIndex))); + final Card card = findCard(playbackActions.get(actionIndex).getSelectedCardView()); return isControlledByPlayer(card) ? card : null; } @@ -1056,7 +1019,7 @@ private int activateLeadingRecordedManaSource() { final int size = findLeadingManaSourceSearchSize(); for (int i = 0; i < size; i++) { final PlayerAction action = playbackActions.get(i); - if (activateRecordedManaSource(findCard(selectedCardView(action)), action)) { + if (activateRecordedManaSource(findCard(action.getSelectedCardView()), action)) { return i; } } @@ -1066,7 +1029,7 @@ private int activateLeadingRecordedManaSource() { private int findLeadingManaSourceSearchSize() { for (int i = 0; i < playbackActions.size(); i++) { final PlayerAction action = playbackActions.get(i); - if (!(action instanceof SelectCardAction)) { + if (action.getSelectedCardView() == null) { return i; } } @@ -1077,7 +1040,7 @@ private boolean activateFutureRecordedLandManaSource() { final int start = findLeadingManaSourceSearchSize(); for (int i = start; i < playbackActions.size(); i++) { final PlayerAction action = playbackActions.get(i); - if (activateRecordedManaSource(exactRecordedLandManaSource(selectedCardView(action)), action)) { + if (activateRecordedManaSource(exactRecordedLandManaSource(action.getSelectedCardView()), action)) { return true; } } @@ -1087,7 +1050,7 @@ private boolean activateFutureRecordedLandManaSource() { private boolean hasFutureUsableRecordedLandSelection() { final int start = findLeadingManaSourceSearchSize(); for (int i = start; i < playbackActions.size(); i++) { - if (exactRecordedLandManaSource(selectedCardView(playbackActions.get(i))) != null) { + if (exactRecordedLandManaSource(playbackActions.get(i).getSelectedCardView()) != null) { return true; } } @@ -1132,7 +1095,7 @@ private int findNextActionBefore(final int endIndex, final Class... actionCla private boolean noRemainingTargetSelectionsBefore(final int endIndex) { final int size = Math.min(endIndex, playbackActions.size()); for (int i = 0; i < size; i++) { - if (isSelectionAction(playbackActions.get(i))) { + if (playbackActions.get(i).isSelectionAction()) { return false; } } @@ -1183,73 +1146,59 @@ public boolean processAction(PlayerAction action) { if (DEBUG) { debug("try " + action.describe()); } - final Input inp = playerControllerHuman.getInputProxy().getInput(); + final Input input = playerControllerHuman.getInputProxy().getInput(); if (action instanceof ActivateAbilityAction activateAbilityAction) { - if (inp instanceof InputPassPriority passPriorityInput) { + if (input instanceof InputPassPriority passPriorityInput) { return debugResult(action, activateAbility(passPriorityInput, activateAbilityAction)); } } else if (action instanceof PassPriorityAction passPriorityAction) { - if (inp instanceof InputPassPriority && canReplayPassPriority(passPriorityAction)) { - inp.selectButtonOK(); + if (input instanceof InputPassPriority && canReplayPassPriority(passPriorityAction)) { + input.selectButtonOK(); return debugResult(action, true); } - if (inp instanceof InputAttack && canReplayAttackPassPriority(passPriorityAction)) { - inp.selectButtonOK(); + if (input instanceof InputAttack && passPriorityAction.canReplayDuringAttack(getCurrentPhase())) { + input.selectButtonOK(); return debugResult(action, true); } } else if (action instanceof FinishTargetingAction) { - if (inp instanceof InputSelectTargets || inp instanceof InputAttack) { - inp.selectButtonOK(); + if (input instanceof InputSelectTargets || input instanceof InputAttack) { + input.selectButtonOK(); return debugResult(action, true); } - } else if (action instanceof PayManaFromPoolAction) { - if (inp instanceof InputPayMana) { - ((InputPayMana) inp).useManaFromPool(((PayManaFromPoolAction) action).getSelectedColor()); + } else if (action instanceof PayManaFromPoolAction payManaFromPoolAction) { + if (input instanceof InputPayMana manaInput) { + manaInput.useManaFromPool(payManaFromPoolAction.getSelectedColor()); return debugResult(action, true); } } else if (action instanceof PayCostAction) { - if (inp instanceof InputConfirm) { - inp.selectButtonOK(); + if (input instanceof InputConfirm) { + input.selectButtonOK(); return debugResult(action, true); } } else if (action instanceof ConfirmAction confirmAction) { - if (!confirmAction.isConfirmed() && inp instanceof InputPayMana manaInput + if (!confirmAction.isConfirmed() && input instanceof InputPayMana manaInput && manaInput.canCancelPaymentForMacro()) { - inp.selectButtonCancel(); + input.selectButtonCancel(); return debugResult(action, true); } - if (inp instanceof InputConfirm confirmInput && canReplayConfirm(confirmInput, confirmAction)) { + if (input instanceof InputConfirm confirmInput + && confirmAction.matchesPrompt(confirmInput.getCardViewForMacro(), + confirmInput.getMessageForMacro())) { if (confirmAction.isConfirmed()) { - inp.selectButtonOK(); + input.selectButtonOK(); } else { - inp.selectButtonCancel(); + input.selectButtonCancel(); } return debugResult(action, true); } - } else { - GameEntityView gev = action.getGameEntityView(); - if (gev instanceof CardView cardView) { - return debugResult(action, selectCard(cardView)); - } else if (gev instanceof PlayerView playerView) { - return debugResult(action, selectPlayer(playerView)); - } + } else if (action.getGameEntityView() instanceof CardView cardView) { + return debugResult(action, selectCard(cardView)); + } else if (action.getGameEntityView() instanceof PlayerView playerView) { + return debugResult(action, selectPlayer(playerView)); } return debugResult(action, false); } - private boolean canReplayConfirm(final InputConfirm input, final ConfirmAction action) { - final GameEntityView recordedView = action.getGameEntityView(); - if (!(recordedView instanceof CardView recordedCard)) { - return true; - } - final CardView inputCard = input.getCardViewForMacro(); - if (inputCard != null && recordedCard.getName().equals(inputCard.getName())) { - return true; - } - final String message = input.getMessageForMacro(); - return message != null && message.contains(recordedCard.getName()); - } - private boolean canAcceptImplicitTriggerConfirm(final InputConfirm input) { final String message = input.getMessageForMacro(); if (message == null || !message.startsWith(localizer.getMessage("lblUseTriggeredAbilityOf"))) { @@ -1445,10 +1394,6 @@ private boolean isEquivalentCard(final CardView original, final Card candidate) && hasSameController(original, candidate); } - private CardView selectedCardView(final PlayerAction action) { - return action instanceof SelectCardAction && action.getGameEntityView() instanceof CardView cardView ? cardView : null; - } - private boolean debugResult(final PlayerAction action, final boolean result) { if (DEBUG) { debug((result ? "accepted " : "rejected ") + action.describe()); From 79d5f4590c4fe54ebd2e6a8b26367b5fb00fec83 Mon Sep 17 00:00:00 2001 From: Madwand99 Date: Sat, 23 May 2026 09:54:49 -0700 Subject: [PATCH 6/7] Cleanup --- .../player/actions/PassPriorityAction.java | 7 +- .../player/RecordActionsMacroSystem.java | 79 +++++-------------- 2 files changed, 21 insertions(+), 65 deletions(-) diff --git a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java index 489af13c464..0805ee32c70 100644 --- a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java +++ b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java @@ -55,12 +55,7 @@ && isCombatPhase(currentPhase) } private static boolean isCombatPhase(final PhaseType phase) { - return phase == PhaseType.COMBAT_BEGIN - || phase == PhaseType.COMBAT_DECLARE_ATTACKERS - || phase == PhaseType.COMBAT_DECLARE_BLOCKERS - || phase == PhaseType.COMBAT_FIRST_STRIKE_DAMAGE - || phase == PhaseType.COMBAT_DAMAGE - || phase == PhaseType.COMBAT_END; + return phase.name().startsWith("COMBAT_"); } @Override diff --git a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java index 75caac8f62b..6d9d890e54d 100644 --- a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java +++ b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java @@ -247,7 +247,8 @@ private R consumeRememberedAction(final Class act playbackActions.remove(actionIndex); playbackRetries = 0; pendingStackOrderPriorityPasses = 0; - waitingForSelectionAfterStackOrder = action instanceof StackOrderAction && hasFutureSelectionAction(); + waitingForSelectionAfterStackOrder = action instanceof StackOrderAction + && findNextAction(SelectCardAction.class, SelectPlayerAction.class) >= 0; playerControllerHuman.getInputQueue().updateObservers(); notifyStatusListeners(); return result; @@ -289,17 +290,13 @@ private CardCollection takeCardsByName(final CardCollection available, final Lis @Override public String playbackText() { if (repeatIterations > 0) { - return playbackProgressText(repeatIteration, repeatIterations); + return repeatIteration + " / " + repeatIterations; } if (playbackActions.isEmpty()) { return null; } - return playbackProgressText(actions.size() - playbackActions.size(), actions.size()); - } - - private String playbackProgressText(final int current, final int total) { - return current + " / " + total; + return actions.size() - playbackActions.size() + " / " + actions.size(); } public boolean startRecording() { @@ -598,18 +595,20 @@ private int processNextAcceptedAction() { final int passPriority = findNextAction(PassPriorityAction.class); final int finishAttack = findNextActionBefore(passPriority, FinishTargetingAction.class); final int actionBoundary = finishAttack >= 0 ? finishAttack : passPriority; - final int playerAction = findNextPlayerSelectionBefore(actionBoundary); + final int playerAction = findNextActionBefore(actionBoundary, SelectPlayerAction.class); if (playerAction >= 0) { return processActionAt(playerAction); } - final int cardAction = findNextCardSelectionBefore(actionBoundary); + final int cardAction = findNextActionBefore(actionBoundary, SelectCardAction.class); final int cardResult = processActionAt(cardAction); if (cardResult >= 0) { return cardResult; } if (cardAction >= 0) { - return waitForPendingAttackInputAction(inp, cardAction); + return findPendingPlayerCard(cardAction) == null ? NO_ACTION_ACCEPTED + : waitForInput(inp, "waiting for postcombat main before " + + playbackActions.get(cardAction).describe()); } if (finishAttack >= 0) { return processActionAt(finishAttack); @@ -685,10 +684,10 @@ private int processNextAcceptedAction() { return waitForInput(inp); } else if (inp instanceof InputPassPriority) { final int passPriority = findNextAction(PassPriorityAction.class); - final int cardAction = findNextCardSelectionBefore(passPriority); + final int cardAction = findNextActionBefore(passPriority, SelectCardAction.class); final int stackOrderAction = findNextActionBefore(cardAction, StackOrderAction.class); if (stackOrderAction >= 0 && !isStackEmpty()) { - if (hasSimultaneousStackEntries()) { + if (playerControllerHuman.getGame().getStack().hasSimultaneousStackEntries()) { return WAIT_FOR_NEXT_INPUT; } if (pendingStackOrderPriorityPasses > 0) { @@ -713,7 +712,7 @@ private int processNextAcceptedAction() { return processActionAt(cardAction, () -> waitForPendingCardAction(inp, cardAction, stackOrderAction >= 0)); } - final int playerAction = findNextPlayerSelectionBefore(passPriority); + final int playerAction = findNextActionBefore(passPriority, SelectPlayerAction.class); if (playerAction >= 0) { return processActionAt(playerAction); } @@ -781,22 +780,15 @@ private int processAttackPassPriority(final int actionIndex, final Input input) return waitForInput(input); } final int result = processActionAt(actionIndex); - if (result >= 0) { - return result; - } - return waitForInput(input); + return result >= 0 ? result : waitForInput(input); } private int processNextListSelectionAction(final InputSelectEntitiesFromList selectInput) { for (int i = 0; i < playbackActions.size(); i++) { final PlayerAction action = playbackActions.get(i); - if (!action.isSelectionAction()) { - continue; - } - if (processAction(action)) { - return i; + if (action.isSelectionAction()) { + return processAction(action) ? i : finishListSelectionIfAny(selectInput); } - return finishListSelectionIfAny(selectInput); } return finishListSelectionIfAny(selectInput); } @@ -805,29 +797,17 @@ private int finishListSelectionIfAny(final InputSelectEntitiesFromList input) return input.getSelected().isEmpty() ? NO_ACTION_ACCEPTED : waitForInput(input); } - private boolean hasFutureSelectionAction() { - return findNextAction(SelectCardAction.class, SelectPlayerAction.class) >= 0; - } - private boolean isPriorityInput(final Input input) { return input instanceof InputPassPriority || input instanceof InputAttack; } - private boolean isObsoleteStackPass(final PlayerAction action) { - final PassPriorityAction passPriorityAction = action.asPassPriorityAction(); - return passPriorityAction != null && passPriorityAction.isObsoleteWhen(isStackEmpty()); - } - private boolean shouldSkipPassPriorityAction(final int actionIndex) { final PlayerAction action = playbackActions.get(actionIndex); - return isObsoleteStackPass(action) - || isStalePhasePass(action) - || isTrailingMainPhasePassBeforeNextIteration(actionIndex); - } - - private boolean isStalePhasePass(final PlayerAction action) { final PassPriorityAction passPriorityAction = action.asPassPriorityAction(); - return passPriorityAction != null && passPriorityAction.isStaleFor(getCurrentPhase()); + return passPriorityAction != null + && (passPriorityAction.isObsoleteWhen(isStackEmpty()) + || passPriorityAction.isStaleFor(getCurrentPhase())) + || isTrailingMainPhasePassBeforeNextIteration(actionIndex); } private boolean isTrailingMainPhasePassBeforeNextIteration(final int actionIndex) { @@ -848,10 +828,6 @@ private boolean isTrailingMainPhasePassBeforeNextIteration(final int actionIndex return !actions.isEmpty() && actions.get(0).asPassPriorityAction() == null; } - private boolean canReplayPassPriority(final PassPriorityAction action) { - return action.canReplay(isStackEmpty(), getCurrentPhase()); - } - private PhaseType getCurrentPhase() { return playerControllerHuman.getGame().getPhaseHandler().getPhase(); } @@ -860,10 +836,6 @@ private boolean isStackEmpty() { return playerControllerHuman.getGame().getStack().isEmpty(); } - private boolean hasSimultaneousStackEntries() { - return playerControllerHuman.getGame().getStack().hasSimultaneousStackEntries(); - } - private boolean waitForInterIterationStackClear() { if (isStackEmpty()) { playbackRetries = 0; @@ -914,13 +886,6 @@ private int waitForPendingCardAction(final Input input, final int actionIndex, f return NO_ACTION_ACCEPTED; } - private int waitForPendingAttackInputAction(final Input input, final int actionIndex) { - if (findPendingPlayerCard(actionIndex) == null) { - return NO_ACTION_ACCEPTED; - } - return waitForInput(input, "waiting for postcombat main before " + playbackActions.get(actionIndex).describe()); - } - private int waitForInput(final Input input, final String debugMessage) { debug(debugMessage); return waitForInput(input); @@ -1088,10 +1053,6 @@ private int findNextActionBefore(final int endIndex, final Class... actionCla return NO_ACTION_ACCEPTED; } - private int findNextCardSelectionBefore(final int endIndex) { return findNextActionBefore(endIndex, SelectCardAction.class); } - - private int findNextPlayerSelectionBefore(final int endIndex) { return findNextActionBefore(endIndex, SelectPlayerAction.class); } - private boolean noRemainingTargetSelectionsBefore(final int endIndex) { final int size = Math.min(endIndex, playbackActions.size()); for (int i = 0; i < size; i++) { @@ -1152,7 +1113,7 @@ public boolean processAction(PlayerAction action) { return debugResult(action, activateAbility(passPriorityInput, activateAbilityAction)); } } else if (action instanceof PassPriorityAction passPriorityAction) { - if (input instanceof InputPassPriority && canReplayPassPriority(passPriorityAction)) { + if (input instanceof InputPassPriority && passPriorityAction.canReplay(isStackEmpty(), getCurrentPhase())) { input.selectButtonOK(); return debugResult(action, true); } From 5b57374b9751800a1aaa393e59a2d102596988d9 Mon Sep 17 00:00:00 2001 From: Madwand99 Date: Sat, 23 May 2026 11:43:51 -0700 Subject: [PATCH 7/7] Separate out MacroActionReplayer class --- .../forge/player/MacroActionReplayer.java | 333 ++++++++++++++++++ .../player/RecordActionsMacroSystem.java | 294 +--------------- 2 files changed, 342 insertions(+), 285 deletions(-) create mode 100644 forge-gui/src/main/java/forge/player/MacroActionReplayer.java diff --git a/forge-gui/src/main/java/forge/player/MacroActionReplayer.java b/forge-gui/src/main/java/forge/player/MacroActionReplayer.java new file mode 100644 index 00000000000..9d911f63e1a --- /dev/null +++ b/forge-gui/src/main/java/forge/player/MacroActionReplayer.java @@ -0,0 +1,333 @@ +package forge.player; + +import forge.game.GameEntityView; +import forge.game.card.Card; +import forge.game.card.CardView; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.player.PlayerView; +import forge.game.player.actions.ActivateAbilityAction; +import forge.game.player.actions.ConfirmAction; +import forge.game.player.actions.FinishTargetingAction; +import forge.game.player.actions.PassPriorityAction; +import forge.game.player.actions.PayCostAction; +import forge.game.player.actions.PayManaFromPoolAction; +import forge.game.player.actions.PlayerAction; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; +import forge.gamemodes.match.input.Input; +import forge.gamemodes.match.input.InputAttack; +import forge.gamemodes.match.input.InputConfirm; +import forge.gamemodes.match.input.InputPassPriority; +import forge.gamemodes.match.input.InputPayMana; +import forge.gamemodes.match.input.InputSelectEntitiesFromList; +import forge.gamemodes.match.input.InputSelectTargets; +import forge.util.ITriggerEvent; + +import java.util.List; +import java.util.function.Consumer; + +class MacroActionReplayer { + private static final ITriggerEvent REPLAY_TRIGGER_EVENT = new DummyTriggerEvent(); + + private final PlayerControllerHuman playerControllerHuman; + private final Consumer debug; + + MacroActionReplayer(final PlayerControllerHuman playerControllerHuman, final Consumer debug) { + this.playerControllerHuman = playerControllerHuman; + this.debug = debug; + } + + boolean replay(final PlayerAction action) { + final Input input = playerControllerHuman.getInputProxy().getInput(); + if (action instanceof ActivateAbilityAction activateAbilityAction) { + return input instanceof InputPassPriority passPriorityInput + && activateAbility(passPriorityInput, activateAbilityAction); + } + if (action instanceof PassPriorityAction passPriorityAction) { + return replayPassPriority(input, passPriorityAction); + } + if (action instanceof FinishTargetingAction) { + return selectOkIf(input, input instanceof InputSelectTargets || input instanceof InputAttack); + } + if (action instanceof PayManaFromPoolAction payManaFromPoolAction) { + if (input instanceof InputPayMana manaInput) { + manaInput.useManaFromPool(payManaFromPoolAction.getSelectedColor()); + return true; + } + return false; + } + if (action instanceof PayCostAction) { + return selectOkIf(input, input instanceof InputConfirm); + } + if (action instanceof ConfirmAction confirmAction) { + return replayConfirm(input, confirmAction); + } + if (action.getGameEntityView() instanceof CardView cardView) { + return selectCard(cardView); + } + if (action.getGameEntityView() instanceof PlayerView playerView) { + return selectPlayer(playerView); + } + return false; + } + + private boolean replayPassPriority(final Input input, final PassPriorityAction action) { + return selectOkIf(input, (input instanceof InputPassPriority && action.canReplay(isStackEmpty(), getCurrentPhase())) + || (input instanceof InputAttack && action.canReplayDuringAttack(getCurrentPhase()))); + } + + private boolean replayConfirm(final Input input, final ConfirmAction action) { + if (!action.isConfirmed() && input instanceof InputPayMana manaInput + && manaInput.canCancelPaymentForMacro()) { + input.selectButtonCancel(); + return true; + } + if (input instanceof InputConfirm confirmInput + && action.matchesPrompt(confirmInput.getCardViewForMacro(), confirmInput.getMessageForMacro())) { + if (action.isConfirmed()) { + input.selectButtonOK(); + } else { + input.selectButtonCancel(); + } + return true; + } + return false; + } + + private boolean selectOkIf(final Input input, final boolean canSelect) { + if (canSelect) { + input.selectButtonOK(); + } + return canSelect; + } + + private PhaseType getCurrentPhase() { + return playerControllerHuman.getGame().getPhaseHandler().getPhase(); + } + + private boolean isStackEmpty() { + return playerControllerHuman.getGame().getStack().isEmpty(); + } + + Card findCard(final CardView cardView) { + if (cardView == null) { + return null; + } + final Card directCard = playerControllerHuman.getCard(cardView); + if (directCard != null) { + return directCard; + } + for (final Card card : playerControllerHuman.getGame().getCardsInGame()) { + if (isEquivalentCard(cardView, card)) { + return card; + } + } + return null; + } + + Card exactRecordedLandManaSource(final CardView recordedCard) { + final Card card = playerControllerHuman.getCard(recordedCard); + return isControlledByPlayer(card) && !card.isTapped() && card.isLand() ? card : null; + } + + boolean isControlledByPlayer(final Card card) { + return card != null && playerControllerHuman.getPlayer().equals(card.getController()); + } + + boolean selectManaSource(final Card card, final PlayerAction action) { + if (card == null || card.isTapped() || card.getManaAbilities().isEmpty() + || !selectCardForMacro(card)) { + return false; + } + debug.accept("using future mana source " + action.describe()); + return true; + } + + private boolean activateAbility(final InputPassPriority input, final ActivateAbilityAction action) { + final GameEntityView view = action.getGameEntityView(); + if (!(view instanceof CardView cardView)) { + return false; + } + + final Card card = findCard(cardView); + return card != null && activateAbility(input, action, card); + } + + private boolean activateAbility(final InputPassPriority input, final ActivateAbilityAction action, final Card card) { + for (final SpellAbility ability : card.getAllPossibleAbilities(playerControllerHuman.getPlayer(), true)) { + if (ability.toUnsuppressedString().equals(action.getAbilityDescription())) { + return input.selectAbility(ability); + } + } + return false; + } + + private boolean selectPlayer(final PlayerView playerView) { + final Input input = playerControllerHuman.getInputProxy().getInput(); + if (!(input instanceof InputSelectTargets targetInput)) { + return false; + } + final Player player = playerControllerHuman.getGame().getPlayer(playerView); + return player != null && targetInput.selectPlayerForMacro(player, REPLAY_TRIGGER_EVENT); + } + + private boolean selectCard(final CardView cardView) { + final Input input = playerControllerHuman.getInputProxy().getInput(); + if (input instanceof InputSelectEntitiesFromList selectInput) { + final Card choice = findListChoice(cardView, selectInput); + return selectCardForMacro(choice); + } + + final Card directCard = findCard(cardView); + if (input instanceof InputSelectTargets targetInput) { + final Card targetChoice = findTargetChoice(cardView, directCard, targetInput); + return targetChoice != null && targetInput.selectCardForMacro(targetChoice, REPLAY_TRIGGER_EVENT); + } + + if (input instanceof InputPassPriority passPriorityInput) { + if (directCard == null) { + return false; + } + final List abilities = directCard.getAllPossibleAbilities(playerControllerHuman.getPlayer(), true); + if (abilities.size() == 1) { + return passPriorityInput.selectAbility(abilities.get(0)); + } + return selectCardForMacro(directCard) && passPriorityInput.getChosenSa() != null; + } + if (input instanceof InputAttack attackInput && directCard != null + && attackInput.isDeclaredAttackerForMacro(directCard)) { + return false; + } + return selectCardForMacro(directCard); + } + + private boolean selectCardForMacro(final Card card) { + return card != null && playerControllerHuman.selectCard(card.getView(), null, REPLAY_TRIGGER_EVENT); + } + + private Card findTargetChoice(final CardView recordedChoice, final Card exactChoice, + final InputSelectTargets targetInput) { + final Card choice = findCardChoice(recordedChoice, exactChoice, targetInput.getValidCardsForMacro()); + debugNoMatch(choice, "target", recordedChoice, null); + return choice; + } + + private Card findListChoice(final CardView recordedChoice, final InputSelectEntitiesFromList selectInput) { + if (recordedChoice == null) { + return null; + } + final Card exactChoice = playerControllerHuman.getCard(recordedChoice); + final Card exactListChoice = findExactCardChoice(exactChoice, selectInput.getValidChoices()); + if (exactListChoice != null) { + return exactListChoice; + } + if (isRecordedBattlefieldLandChoice(recordedChoice, selectInput)) { + return null; + } + final Card choice = findCardChoice(recordedChoice, exactChoice, selectInput.getValidChoices()); + debugNoMatch(choice, "list", recordedChoice, selectInput.getValidChoices()); + return choice; + } + + private Card findCardChoice(final CardView recordedChoice, final Card exactChoice, final Iterable choices) { + final Card exactListChoice = findExactCardChoice(exactChoice, choices); + if (exactListChoice != null) { + return exactListChoice; + } + if (recordedChoice == null) { + return null; + } + final Card equivalent = findCardChoice(recordedChoice, choices, false); + return equivalent == null ? findCardChoice(recordedChoice, choices, true) : equivalent; + } + + private Card findExactCardChoice(final Card exactChoice, final Iterable choices) { + if (exactChoice == null) { + return null; + } + for (final Object choice : choices) { + if (choice == exactChoice) { + return exactChoice; + } + } + return null; + } + + private Card findCardChoice(final CardView recordedChoice, final Iterable choices, final boolean tokenMatch) { + for (final Object choice : choices) { + if (choice instanceof Card card + && (tokenMatch ? isEquivalentToken(recordedChoice, card) : isEquivalentCard(recordedChoice, card))) { + return card; + } + } + return null; + } + + private void debugNoMatch(final Card choice, final String choiceType, final CardView recordedChoice, + final Object choices) { + if (choice == null && recordedChoice != null) { + debug.accept("no " + choiceType + " match for " + recordedChoice + + (choices == null ? "" : " choices=" + choices)); + } + } + + private boolean isRecordedBattlefieldLandChoice(final CardView recordedChoice, + final InputSelectEntitiesFromList selectInput) { + if (exactRecordedLandManaSource(recordedChoice) == null) { + return false; + } + for (final Object validChoice : selectInput.getValidChoices()) { + if (validChoice instanceof Card card && card.isInZone(ZoneType.Battlefield)) { + return true; + } + } + return false; + } + + private boolean isEquivalentToken(final CardView original, final Card candidate) { + return isTokenLike(original) + && isTokenLike(candidate.getView()) + && hasSameController(original, candidate) + && normalizeTokenName(original.getName()).equals(normalizeTokenName(candidate.getView().getName())); + } + + private boolean isTokenLike(final CardView card) { + return card.isToken() || card.getName().endsWith(" Token"); + } + + private String normalizeTokenName(final String name) { + String normalized = name; + while (normalized.endsWith(" Token")) { + normalized = normalized.substring(0, normalized.length() - " Token".length()); + } + return normalized; + } + + private boolean hasSameController(final CardView original, final Card candidate) { + return original.getController() == null || original.getController().equals(candidate.getView().getController()); + } + + private boolean isEquivalentCard(final CardView original, final Card candidate) { + return original.getName().equals(candidate.getView().getName()) + && original.isToken() == candidate.isToken() + && hasSameController(original, candidate); + } + + private static class DummyTriggerEvent implements ITriggerEvent { + @Override + public int getButton() { + return 1; // Emulate left mouse button + } + + @Override + public int getX() { + return 0; + } + + @Override + public int getY() { + return 0; + } + } +} diff --git a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java index 6d9d890e54d..ed3dbc5c6ee 100644 --- a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java +++ b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java @@ -2,14 +2,10 @@ import com.google.common.collect.Lists; import forge.card.MagicColor; -import forge.game.GameEntity; import forge.game.GameEntityView; import forge.game.card.Card; import forge.game.card.CardCollection; -import forge.game.card.CardView; import forge.game.phase.PhaseType; -import forge.game.player.Player; -import forge.game.player.PlayerView; import forge.game.player.actions.ActivateAbilityAction; import forge.game.player.actions.ColorChoiceAction; import forge.game.player.actions.ConfirmAction; @@ -24,8 +20,6 @@ import forge.game.player.actions.SelectPlayerAction; import forge.game.player.actions.ScryAction; import forge.game.player.actions.StackOrderAction; -import forge.game.spellability.SpellAbility; -import forge.game.zone.ZoneType; import forge.gamemodes.match.input.Input; import forge.gamemodes.match.input.InputAttack; import forge.gamemodes.match.input.InputConfirm; @@ -36,7 +30,6 @@ import forge.gamemodes.match.input.InputSelectTargets; import forge.gui.FThreads; import forge.interfaces.IMacroSystem; -import forge.util.ITriggerEvent; import forge.util.Localizer; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -63,8 +56,8 @@ public class RecordActionsMacroSystem implements IMacroSystem { private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); private final PlayerControllerHuman playerControllerHuman; + private final MacroActionReplayer actionReplayer; private final Localizer localizer = Localizer.getInstance(); - private final ITriggerEvent replayTriggerEvent = new DummyTriggerEvent(); private final List actions = Lists.newArrayList(); private final List playbackActions = Lists.newArrayList(); @@ -81,6 +74,7 @@ public class RecordActionsMacroSystem implements IMacroSystem { public RecordActionsMacroSystem(final PlayerControllerHuman playerControllerHuman) { this.playerControllerHuman = playerControllerHuman; + this.actionReplayer = new MacroActionReplayer(playerControllerHuman, this::debug); } @Override @@ -921,8 +915,8 @@ private Card findPendingPlayerCard(final int actionIndex) { if (actionIndex < 0 || actionIndex >= playbackActions.size()) { return null; } - final Card card = findCard(playbackActions.get(actionIndex).getSelectedCardView()); - return isControlledByPlayer(card) ? card : null; + final Card card = actionReplayer.findCard(playbackActions.get(actionIndex).getSelectedCardView()); + return actionReplayer.isControlledByPlayer(card) ? card : null; } private int findNextAction(final Class... actionClasses) { @@ -984,7 +978,7 @@ private int activateLeadingRecordedManaSource() { final int size = findLeadingManaSourceSearchSize(); for (int i = 0; i < size; i++) { final PlayerAction action = playbackActions.get(i); - if (activateRecordedManaSource(findCard(action.getSelectedCardView()), action)) { + if (actionReplayer.selectManaSource(actionReplayer.findCard(action.getSelectedCardView()), action)) { return i; } } @@ -1005,7 +999,8 @@ private boolean activateFutureRecordedLandManaSource() { final int start = findLeadingManaSourceSearchSize(); for (int i = start; i < playbackActions.size(); i++) { final PlayerAction action = playbackActions.get(i); - if (activateRecordedManaSource(exactRecordedLandManaSource(action.getSelectedCardView()), action)) { + if (actionReplayer.selectManaSource( + actionReplayer.exactRecordedLandManaSource(action.getSelectedCardView()), action)) { return true; } } @@ -1015,31 +1010,13 @@ private boolean activateFutureRecordedLandManaSource() { private boolean hasFutureUsableRecordedLandSelection() { final int start = findLeadingManaSourceSearchSize(); for (int i = start; i < playbackActions.size(); i++) { - if (exactRecordedLandManaSource(playbackActions.get(i).getSelectedCardView()) != null) { + if (actionReplayer.exactRecordedLandManaSource(playbackActions.get(i).getSelectedCardView()) != null) { return true; } } return false; } - private Card exactRecordedLandManaSource(final CardView recordedCard) { - final Card card = playerControllerHuman.getCard(recordedCard); - return isControlledByPlayer(card) && !card.isTapped() && card.isLand() ? card : null; - } - - private boolean activateRecordedManaSource(final Card card, final PlayerAction action) { - if (card == null || card.isTapped() || card.getManaAbilities().isEmpty() - || !playerControllerHuman.selectCard(card.getView(), null, replayTriggerEvent)) { - return false; - } - debug("using future mana source " + action.describe()); - return true; - } - - private boolean isControlledByPlayer(final Card card) { - return card != null && playerControllerHuman.getPlayer().equals(card.getController()); - } - private int findNextActionBefore(final int endIndex, final Class... actionClasses) { final int size = endIndex < 0 ? playbackActions.size() : Math.min(endIndex, playbackActions.size()); for (int i = 0; i < size; i++) { @@ -1107,57 +1084,7 @@ public boolean processAction(PlayerAction action) { if (DEBUG) { debug("try " + action.describe()); } - final Input input = playerControllerHuman.getInputProxy().getInput(); - if (action instanceof ActivateAbilityAction activateAbilityAction) { - if (input instanceof InputPassPriority passPriorityInput) { - return debugResult(action, activateAbility(passPriorityInput, activateAbilityAction)); - } - } else if (action instanceof PassPriorityAction passPriorityAction) { - if (input instanceof InputPassPriority && passPriorityAction.canReplay(isStackEmpty(), getCurrentPhase())) { - input.selectButtonOK(); - return debugResult(action, true); - } - if (input instanceof InputAttack && passPriorityAction.canReplayDuringAttack(getCurrentPhase())) { - input.selectButtonOK(); - return debugResult(action, true); - } - } else if (action instanceof FinishTargetingAction) { - if (input instanceof InputSelectTargets || input instanceof InputAttack) { - input.selectButtonOK(); - return debugResult(action, true); - } - } else if (action instanceof PayManaFromPoolAction payManaFromPoolAction) { - if (input instanceof InputPayMana manaInput) { - manaInput.useManaFromPool(payManaFromPoolAction.getSelectedColor()); - return debugResult(action, true); - } - } else if (action instanceof PayCostAction) { - if (input instanceof InputConfirm) { - input.selectButtonOK(); - return debugResult(action, true); - } - } else if (action instanceof ConfirmAction confirmAction) { - if (!confirmAction.isConfirmed() && input instanceof InputPayMana manaInput - && manaInput.canCancelPaymentForMacro()) { - input.selectButtonCancel(); - return debugResult(action, true); - } - if (input instanceof InputConfirm confirmInput - && confirmAction.matchesPrompt(confirmInput.getCardViewForMacro(), - confirmInput.getMessageForMacro())) { - if (confirmAction.isConfirmed()) { - input.selectButtonOK(); - } else { - input.selectButtonCancel(); - } - return debugResult(action, true); - } - } else if (action.getGameEntityView() instanceof CardView cardView) { - return debugResult(action, selectCard(cardView)); - } else if (action.getGameEntityView() instanceof PlayerView playerView) { - return debugResult(action, selectPlayer(playerView)); - } - return debugResult(action, false); + return debugResult(action, actionReplayer.replay(action)); } private boolean canAcceptImplicitTriggerConfirm(final InputConfirm input) { @@ -1168,193 +1095,6 @@ private boolean canAcceptImplicitTriggerConfirm(final InputConfirm input) { return findNextAction(SelectCardAction.class, SelectPlayerAction.class) >= 0; } - private boolean activateAbility(final InputPassPriority input, final ActivateAbilityAction action) { - final GameEntityView view = action.getGameEntityView(); - if (!(view instanceof CardView cardView)) { - return false; - } - - final Card card = findCard(cardView); - return card != null && activateAbility(input, action, card); - } - - private boolean activateAbility(final InputPassPriority input, final ActivateAbilityAction action, final Card card) { - for (final SpellAbility ability : card.getAllPossibleAbilities(playerControllerHuman.getPlayer(), true)) { - if (ability.toUnsuppressedString().equals(action.getAbilityDescription())) { - return input.selectAbility(ability); - } - } - return false; - } - - private boolean selectPlayer(final PlayerView playerView) { - final Input inp = playerControllerHuman.getInputProxy().getInput(); - if (!(inp instanceof InputSelectTargets targetInput)) { - return false; - } - final Player player = playerControllerHuman.getGame().getPlayer(playerView); - return player != null && targetInput.selectPlayerForMacro(player, replayTriggerEvent); - } - - private boolean selectCard(final CardView cardView) { - final Input inp = playerControllerHuman.getInputProxy().getInput(); - if (inp instanceof InputSelectEntitiesFromList selectInput) { - final Card choice = findListChoice(cardView, selectInput); - return choice != null && playerControllerHuman.selectCard(choice.getView(), null, replayTriggerEvent); - } - - final Card directCard = findCard(cardView); - if (inp instanceof InputSelectTargets targetInput) { - final Card targetChoice = findTargetChoice(cardView, directCard, targetInput); - return targetChoice != null && targetInput.selectCardForMacro(targetChoice, replayTriggerEvent); - } - - if (inp instanceof InputPassPriority passPriorityInput) { - if (directCard == null) { - return false; - } - final List abilities = directCard.getAllPossibleAbilities(playerControllerHuman.getPlayer(), true); - if (abilities.size() == 1) { - return passPriorityInput.selectAbility(abilities.get(0)); - } - return playerControllerHuman.selectCard(directCard.getView(), null, replayTriggerEvent) - && passPriorityInput.getChosenSa() != null; - } - if (inp instanceof InputAttack attackInput && directCard != null - && attackInput.isDeclaredAttackerForMacro(directCard)) { - return false; - } - if (directCard != null && playerControllerHuman.selectCard(directCard.getView(), null, replayTriggerEvent)) { - return true; - } - - return false; - } - - private Card findTargetChoice(final CardView recordedChoice, final Card exactChoice, - final InputSelectTargets targetInput) { - final Card choice = findCardChoice(recordedChoice, exactChoice, targetInput.getValidCardsForMacro()); - debugNoMatch(choice, "target", recordedChoice, null); - return choice; - } - - private Card findListChoice(final CardView recordedChoice, final InputSelectEntitiesFromList selectInput) { - final Card exactChoice = playerControllerHuman.getCard(recordedChoice); - if (recordedChoice == null) { - return null; - } - final Card exactListChoice = findExactCardChoice(exactChoice, selectInput.getValidChoices()); - if (exactListChoice != null) { - return exactListChoice; - } - if (isRecordedBattlefieldLandChoice(recordedChoice, selectInput)) { - return null; - } - final Card choice = findCardChoice(recordedChoice, exactChoice, selectInput.getValidChoices()); - debugNoMatch(choice, "list", recordedChoice, selectInput.getValidChoices()); - return choice; - } - - private Card findCardChoice(final CardView recordedChoice, final Card exactChoice, final Iterable choices) { - final Card exactListChoice = findExactCardChoice(exactChoice, choices); - if (exactListChoice != null) { - return exactListChoice; - } - if (recordedChoice == null) { - return null; - } - final Card equivalent = findCardChoice(recordedChoice, choices, false); - return equivalent == null ? findCardChoice(recordedChoice, choices, true) : equivalent; - } - - private Card findExactCardChoice(final Card exactChoice, final Iterable choices) { - if (exactChoice == null) { - return null; - } - for (final Object choice : choices) { - if (choice == exactChoice) { - return exactChoice; - } - } - return null; - } - - private Card findCardChoice(final CardView recordedChoice, final Iterable choices, final boolean tokenMatch) { - for (final Object choice : choices) { - if (choice instanceof Card card - && (tokenMatch ? isEquivalentToken(recordedChoice, card) : isEquivalentCard(recordedChoice, card))) { - return card; - } - } - return null; - } - - private void debugNoMatch(final Card choice, final String choiceType, final CardView recordedChoice, - final Object choices) { - if (choice == null && recordedChoice != null) { - debug("no " + choiceType + " match for " + recordedChoice - + (choices == null ? "" : " choices=" + choices)); - } - } - - private boolean isRecordedBattlefieldLandChoice(final CardView recordedChoice, - final InputSelectEntitiesFromList selectInput) { - if (exactRecordedLandManaSource(recordedChoice) == null) { - return false; - } - for (final GameEntity validChoice : selectInput.getValidChoices()) { - if (validChoice instanceof Card card && card.isInZone(ZoneType.Battlefield)) { - return true; - } - } - return false; - } - - private boolean isEquivalentToken(final CardView original, final Card candidate) { - return isTokenLike(original) - && isTokenLike(candidate.getView()) - && hasSameController(original, candidate) - && normalizeTokenName(original.getName()).equals(normalizeTokenName(candidate.getView().getName())); - } - - private boolean isTokenLike(final CardView card) { - return card.isToken() || card.getName().endsWith(" Token"); - } - - private String normalizeTokenName(final String name) { - String normalized = name; - while (normalized.endsWith(" Token")) { - normalized = normalized.substring(0, normalized.length() - " Token".length()); - } - return normalized; - } - - private boolean hasSameController(final CardView original, final Card candidate) { - return original.getController() == null || original.getController().equals(candidate.getView().getController()); - } - - private Card findCard(final CardView cardView) { - if (cardView == null) { - return null; - } - final Card directCard = playerControllerHuman.getCard(cardView); - if (directCard != null) { - return directCard; - } - for (final Card card : playerControllerHuman.getGame().getCardsInGame()) { - if (isEquivalentCard(cardView, card)) { - return card; - } - } - return null; - } - - private boolean isEquivalentCard(final CardView original, final Card candidate) { - return original.getName().equals(candidate.getView().getName()) - && original.isToken() == candidate.isToken() - && hasSameController(original, candidate); - } - private boolean debugResult(final PlayerAction action, final boolean result) { if (DEBUG) { debug((result ? "accepted " : "rejected ") + action.describe()); @@ -1412,20 +1152,4 @@ private String describeActions(final List actionList) { return sb.toString(); } - private class DummyTriggerEvent implements ITriggerEvent { - @Override - public int getButton() { - return 1; // Emulate left mouse button - } - - @Override - public int getX() { - return 0; - } - - @Override - public int getY() { - return 0; - } - } }