diff --git a/forge-game/src/main/java/forge/game/GameView.java b/forge-game/src/main/java/forge/game/GameView.java index e8a35bd092d..26b527f0bb4 100644 --- a/forge-game/src/main/java/forge/game/GameView.java +++ b/forge-game/src/main/java/forge/game/GameView.java @@ -137,10 +137,6 @@ void updateStack(final MagicStack stack) { set(TrackableProperty.StormCount, stack.getSpellsCastThisTurn().size()); } - public boolean isFirstGameInMatch() { - return getNumPlayedGamesInMatch() == 0; - } - public int getNumPlayedGamesInMatch() { return get(TrackableProperty.NumPlayedGamesInMatch); } @@ -154,8 +150,6 @@ public boolean isMatchOver() { } public boolean isMulligan() { - if (get(TrackableProperty.Mulligan) == null) - return false; return get(TrackableProperty.Mulligan); } 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 23d17e9faf5..d414fae2253 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 @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicReference; - import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; import javax.swing.JOptionPane; @@ -67,7 +66,11 @@ import forge.game.spellability.StackItemView; import forge.game.zone.ZoneType; import forge.util.IHasForgeLog; +import forge.gamemodes.match.YieldController; +import forge.gamemodes.match.YieldMarker; +import forge.gamemodes.match.YieldUpdate; import forge.gamemodes.net.NetworkGuiGame; +import forge.interfaces.IGameController; import forge.gui.FNetOverlay; import forge.gui.FThreads; import forge.gui.GuiBase; @@ -129,6 +132,7 @@ import forge.view.FView; import forge.view.arcane.CardPanel; import forge.view.arcane.FloatingZone; + import net.miginfocom.layout.LinkHandler; import net.miginfocom.swing.MigLayout; @@ -180,7 +184,7 @@ public CMatchUI() { this.myDocs.put(EDocID.CARD_PICTURE, cDetailPicture.getCPicture().getView()); this.myDocs.put(EDocID.CARD_DETAIL, cDetailPicture.getCDetail().getView()); // only create an ante doc if playing for ante - if (isPreferenceEnabled(FPref.UI_ANTE)) { + if (FModel.getPreferences().getPrefBoolean(FPref.UI_ANTE)) { this.myDocs.put(EDocID.CARD_ANTES, cAntes.getView()); } else { this.myDocs.put(EDocID.CARD_ANTES, null); @@ -200,10 +204,6 @@ private void registerDocs() { } } - private static boolean isPreferenceEnabled(final ForgePreferences.FPref preferenceName) { - return FModel.getPreferences().getPrefBoolean(preferenceName); - } - FScreen getScreen() { return this.screen; } @@ -235,6 +235,35 @@ protected void afterDeltaApplied() { refreshAllViews(); } + @Override + public void refreshYieldUi(final PlayerView player) { + FThreads.invokeInEdtNowOrLater(() -> { + // Marker is rendered only on the local player's view of the targeted phase indicator. + PlayerView local = getCurrentPlayer(); + if (!player.equals(local)) { + return; + } + for (final VField f : getFieldViews()) { + for (PhaseLabel l : f.getPhaseIndicator().allLabels()) { + l.setYieldMarked(false); + } + } + IGameController controller = getGameController(local); + YieldMarker marker = controller != null ? controller.getYieldController().getAutoPassUntilMarker() : null; + if (marker == null) { + return; + } + VField markedField = getFieldViewFor(marker.getPhaseOwner()); + if (markedField == null) { + return; + } + PhaseLabel target = markedField.getPhaseIndicator().getLabelFor(marker.getPhase()); + if (target != null) { + target.setYieldMarked(true); + } + }); + } + private void refreshAllViews() { if (sortedPlayers != null) { FThreads.invokeInEdtNowOrLater(() -> { @@ -430,7 +459,7 @@ else if (selectedDocBeforeCombat != null) { //re-select doc that was selected be // Combat pairings changed — rebuild layout so grouping reflects them if (!"default".equals(FModel.getPreferences().getPref(FPref.UI_GROUP_PERMANENTS)) - || isPreferenceEnabled(FPref.UI_SEPARATE_COMBAT_STACKS)) { + || FModel.getPreferences().getPrefBoolean(FPref.UI_SEPARATE_COMBAT_STACKS)) { FThreads.invokeInEdtNowOrLater(() -> { for (final VField f : getFieldViews()) { f.getTabletop().doLayout(); @@ -1315,10 +1344,39 @@ private void actuateMatchPreferences() { final PhaseLabel label = pi.getLabelFor(phase); label.setEnabled(prefs.getPrefBoolean(keys[p - 1])); label.setOnToggled(() -> pushSkipPhaseToControllers(player, phase)); + label.setOnRightClick(() -> handleYieldMarkerToggle(label, player, phase)); } } - seedSkipPhaseCache(); + seedYieldStateOnHost(); + } + + private void handleYieldMarkerToggle(final PhaseLabel label, final PlayerView phaseOwner, final PhaseType phase) { + PlayerView local = getCurrentPlayer(); + if (local == null) { + return; + } + IGameController controller = getGameController(local); + if (controller == null) { + return; + } + YieldMarker existing = controller.getYieldController().getAutoPassUntilMarker(); + boolean clickedSameLabel = existing != null + && phaseOwner.equals(existing.getPhaseOwner()) + && phase == existing.getPhase(); + if (clickedSameLabel) { + controller.sendYieldUpdate(new YieldUpdate.ClearMarker(local)); + } else { + // Setting a marker implies we want to stop here — un-skip the cell + // so the marker can fire (skip-phase pref + marker would skip past). + label.setEnabled(true); + label.repaintOnlyThisLabel(); + boolean atOrPast = YieldController.isPriorityAtOrPastMarker(getGameView(), phaseOwner, phase); + controller.sendYieldUpdate(new YieldUpdate.SetMarker(phaseOwner, phase, atOrPast)); + // Pass current priority so the marker takes effect immediately. + controller.selectButtonOk(); + } + refreshYieldUi(local); } @Override diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/VAutoYields.java b/forge-gui-desktop/src/main/java/forge/screens/match/VAutoYields.java index 1863f9e8e46..7070c4e5915 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/VAutoYields.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/VAutoYields.java @@ -34,7 +34,7 @@ public VAutoYields(final CMatchUI matchUI) { setTitle(Localizer.getInstance().getMessage("lblAutoYields")); autoYields = new ArrayList<>(); - for (final String autoYield : matchUI.getGameController().getAutoYields()) { + for (final String autoYield : matchUI.getGameController().getYieldController().getAutoYields()) { autoYields.add(autoYield); } lstAutoYields = new FList<>(new AutoYieldsListModel()); diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/views/VPrompt.java b/forge-gui-desktop/src/main/java/forge/screens/match/views/VPrompt.java index 2fb0f440829..fa495026e6d 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/views/VPrompt.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/views/VPrompt.java @@ -65,37 +65,35 @@ public class VPrompt implements IVDoc { private final FScrollPane messageScroller = new FScrollPane(tarMessage, false, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); private final JLabel lblGames; - private CardView card = null ; + private CardView card = null; public void setCardView(final CardView card) { - this.card = card ; + this.card = card; } private KeyAdapter buttonKeyAdapter = new KeyAdapter() { @Override public void keyPressed(final KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { - if (btnCancel.isEnabled()) { - if (FModel.getPreferences().getPrefBoolean(FPref.UI_ALLOW_ESC_TO_END_TURN) || !btnCancel.getText().equals("End Turn")) { - btnCancel.doClick(); - } - } + if (e.getKeyCode() == KeyEvent.VK_ESCAPE + && btnCancel.isEnabled() + && (FModel.getPreferences().getPrefBoolean(FPref.UI_ALLOW_ESC_TO_END_TURN) + || !btnCancel.getText().equals(Localizer.getInstance().getMessage("lblEndTurn")))) { + btnCancel.doClick(); } } }; private final CPrompt controller; - //========= Constructor public VPrompt(final CPrompt controller) { this.controller = controller; lblGames = new FLabel.Builder() - .fontSize(12) - .fontStyle(Font.PLAIN) - .fontAlign(SwingConstants.CENTER) - .opaque() - .build(); + .fontSize(12) + .fontStyle(Font.PLAIN) + .fontAlign(SwingConstants.CENTER) + .opaque() + .build(); btnOK.addKeyListener(buttonKeyAdapter); btnCancel.addKeyListener(buttonKeyAdapter); @@ -107,15 +105,13 @@ public VPrompt(final CPrompt controller) { messageScroller.getViewport().getView().addMouseListener(new MouseAdapter() { @Override public void mouseEntered(final MouseEvent e) { - if ( card != null ) { - controller.getMatchUI().setCard(card); + if (card != null) { + controller.getMatchUI().setCard(card); } } }); } - //========== Overridden methods - /* (non-Javadoc) * @see forge.gui.framework.IVDoc#populate() */ diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/views/VStack.java b/forge-gui-desktop/src/main/java/forge/screens/match/views/VStack.java index 5f790982da5..521c48a1ab4 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/views/VStack.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/views/VStack.java @@ -18,6 +18,7 @@ package forge.screens.match.views; import java.awt.Color; +import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; @@ -27,6 +28,7 @@ import java.awt.image.BufferedImage; import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; @@ -35,7 +37,9 @@ import forge.CachedCardImage; import forge.game.GameView; import forge.game.card.CardView.CardStateView; +import forge.game.player.PlayerView; import forge.game.spellability.StackItemView; +import forge.gamemodes.match.YieldUpdate; import forge.gui.card.CardDetailUtil; import forge.gui.card.CardDetailUtil.DetailColors; import forge.gui.framework.DragCell; @@ -233,22 +237,30 @@ public void mouseClicked(final MouseEvent e) { } }); - if (item.isAbility()) { - addMouseListener(new FMouseAdapter() { - @Override - public void onLeftClick(final MouseEvent e) { - onClick(e); - } - @Override - public void onRightClick(final MouseEvent e) { - onClick(e); + addMouseListener(new FMouseAdapter() { + @Override + public void onLeftClick(final MouseEvent e) { + onClick(e); + } + @Override + public void onRightClick(final MouseEvent e) { + onClick(e); + } + private void onClick(final MouseEvent e) { + abilityMenu.setStackInstance(item); + boolean hasVisibleItem = false; + for (Component c : abilityMenu.getComponents()) { + if (c.isVisible()) { + hasVisibleItem = true; + break; + } } - private void onClick(final MouseEvent e) { - abilityMenu.setStackInstance(item); - abilityMenu.show(e.getComponent(), e.getX(), e.getY()); + if (!hasVisibleItem) { + return; } - }); - } + abilityMenu.show(e.getComponent(), e.getX(), e.getY()); + } + }); // TODO: A hacky workaround is currently used to make the game not leak the color information for Morph cards. final CardStateView curState = item.getSourceCard().getCurrentState(); @@ -286,6 +298,7 @@ private final class AbilityMenu extends JPopupMenu { private final JCheckBoxMenuItem jmiAutoYield; private final JCheckBoxMenuItem jmiAlwaysYes; private final JCheckBoxMenuItem jmiAlwaysNo; + private final JMenuItem jmiYieldToEntireStack; private StackItemView item; private Integer triggerID = 0; @@ -326,13 +339,24 @@ public AbilityMenu(){ } }); add(jmiAlwaysNo); + + jmiYieldToEntireStack = new JMenuItem(Localizer.getInstance().getMessage("lblYieldToEntireStack")); + jmiYieldToEntireStack.addActionListener(arg0 -> { + final PlayerView local = controller.getMatchUI().getCurrentPlayer(); + if (local == null) return; + controller.getMatchUI().getGameController().sendYieldUpdate(new YieldUpdate.StackYield(local, true)); + controller.getMatchUI().getGameController().passPriority(); + }); + add(jmiYieldToEntireStack); } public void setStackInstance(final StackItemView item0) { item = item0; triggerID = item.getSourceTrigger(); - jmiAutoYield.setSelected(controller.getMatchUI().getGameController().shouldAutoYield(item.getKey())); + jmiAutoYield.setVisible(item.isAbility()); + jmiAutoYield.setSelected(item.isAbility() + && controller.getMatchUI().getGameController().shouldAutoYield(item.getKey())); if (item.isOptionalTrigger() && controller.getMatchUI().isLocalPlayer(item.getActivatingPlayer())) { jmiAlwaysYes.setSelected(controller.getMatchUI().getGameController().shouldAlwaysAcceptTrigger(triggerID)); diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/special/PhaseIndicator.java b/forge-gui-desktop/src/main/java/forge/toolbox/special/PhaseIndicator.java index f723d27260a..0ae1a26cb5a 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/special/PhaseIndicator.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/special/PhaseIndicator.java @@ -1,192 +1,57 @@ package forge.toolbox.special; +import java.util.EnumMap; +import java.util.Map; + import javax.swing.JPanel; import forge.game.phase.PhaseType; import forge.util.Localizer; import net.miginfocom.swing.MigLayout; -/** - * TODO: Write javadoc for this type. - * - */ public class PhaseIndicator extends JPanel { private static final long serialVersionUID = -863730022835609252L; - - // Phase labels - private PhaseLabel lblUpkeep = new PhaseLabel("UP"); - private PhaseLabel lblDraw = new PhaseLabel("DR"); - private PhaseLabel lblMain1 = new PhaseLabel("M1"); - private PhaseLabel lblBeginCombat = new PhaseLabel("BC"); - private PhaseLabel lblDeclareAttackers = new PhaseLabel("DA"); - private PhaseLabel lblDeclareBlockers = new PhaseLabel("DB"); - private PhaseLabel lblFirstStrike = new PhaseLabel("FS"); - private PhaseLabel lblCombatDamage = new PhaseLabel("CD"); - private PhaseLabel lblEndCombat = new PhaseLabel("EC"); - private PhaseLabel lblMain2 = new PhaseLabel("M2"); - private PhaseLabel lblEndTurn = new PhaseLabel("ET"); - private PhaseLabel lblCleanup = new PhaseLabel("CL"); - - - public PhaseIndicator() { - this.setOpaque(false); - this.setLayout(new MigLayout("insets 0 0 1% 0, gap 0, wrap")); - populatePhase(); - } - - /** Adds phase indicator labels to phase area JPanel container. */ - private void populatePhase() { - final Localizer localizer = Localizer.getInstance(); - // Constraints string, set once - final String constraints = "w 94%!, h 7.2%, gaptop 1%, gapleft 3%"; - - lblUpkeep.setToolTipText(localizer.getMessage("htmlPhaseUpkeepTooltip")); - this.add(lblUpkeep, constraints); - - lblDraw.setToolTipText(localizer.getMessage("htmlPhaseDrawTooltip")); - this.add(lblDraw, constraints); - - lblMain1.setToolTipText(localizer.getMessage("htmlPhaseMain1Tooltip")); - this.add(lblMain1, constraints); - - lblBeginCombat.setToolTipText(localizer.getMessage("htmlPhaseBeginCombatTooltip")); - this.add(lblBeginCombat, constraints); - - lblDeclareAttackers.setToolTipText(localizer.getMessage("htmlPhaseDeclareAttackersTooltip")); - this.add(lblDeclareAttackers, constraints); - - lblDeclareBlockers.setToolTipText(localizer.getMessage("htmlPhaseDeclareBlockersTooltip")); - this.add(lblDeclareBlockers, constraints); - lblFirstStrike.setToolTipText(localizer.getMessage("htmlPhaseFirstStrikeDamageTooltip")); - this.add(lblFirstStrike, constraints); + private static final String CONSTRAINTS = "w 94%!, h 7.2%, gaptop 1%, gapleft 3%"; - lblCombatDamage.setToolTipText(localizer.getMessage("htmlPhaseCombatDamageTooltip")); - this.add(lblCombatDamage, constraints); + private final Map phaseLabels = new EnumMap<>(PhaseType.class); - lblEndCombat.setToolTipText(localizer.getMessage("htmlPhaseEndCombatTooltip")); - this.add(lblEndCombat, constraints); - - lblMain2.setToolTipText(localizer.getMessage("htmlPhaseMain2Tooltip")); - this.add(lblMain2, constraints); - - lblEndTurn.setToolTipText(localizer.getMessage("htmlPhaseEndTurnTooltip")); - this.add(lblEndTurn, constraints); - - lblCleanup.setToolTipText(localizer.getMessage("htmlPhaseCleanupTooltip")); - this.add(lblCleanup, constraints); - } - - - //========== Custom class handling - public PhaseLabel getLabelFor(final PhaseType s) { - switch (s) { - case UPKEEP: - return this.getLblUpkeep(); - case DRAW: - return this.getLblDraw(); - case MAIN1: - return this.getLblMain1(); - case COMBAT_BEGIN: - return this.getLblBeginCombat(); - case COMBAT_DECLARE_ATTACKERS: - return this.getLblDeclareAttackers(); - case COMBAT_DECLARE_BLOCKERS: - return this.getLblDeclareBlockers(); - case COMBAT_DAMAGE: - return this.getLblCombatDamage(); - case COMBAT_FIRST_STRIKE_DAMAGE: - return this.getLblFirstStrike(); - case COMBAT_END: - return this.getLblEndCombat(); - case MAIN2: - return this.getLblMain2(); - case END_OF_TURN: - return this.getLblEndTurn(); - case CLEANUP: - return this.getLblCleanup(); - default: - return null; - } - } - - /** - * Resets all phase buttons to "inactive", so highlight won't be drawn on - * them. "Enabled" state remains the same. - */ - public void resetPhaseButtons() { - getLblUpkeep().setActive(false); - getLblDraw().setActive(false); - getLblMain1().setActive(false); - getLblBeginCombat().setActive(false); - getLblDeclareAttackers().setActive(false); - getLblDeclareBlockers().setActive(false); - getLblFirstStrike().setActive(false); - getLblCombatDamage().setActive(false); - getLblEndCombat().setActive(false); - getLblMain2().setActive(false); - getLblEndTurn().setActive(false); - getLblCleanup().setActive(false); - } - - // Phases - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblUpkeep() { - return this.lblUpkeep; - } - - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblDraw() { - return this.lblDraw; - } - - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblMain1() { - return this.lblMain1; - } - - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblBeginCombat() { - return this.lblBeginCombat; - } - - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblDeclareAttackers() { - return this.lblDeclareAttackers; - } - - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblDeclareBlockers() { - return this.lblDeclareBlockers; - } - - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblCombatDamage() { - return this.lblCombatDamage; - } - - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblFirstStrike() { - return this.lblFirstStrike; + public PhaseIndicator() { + this.setOpaque(false); + this.setLayout(new MigLayout("insets 0 0 1% 0, gap 0, wrap")); + addPhaseLabel("UP", PhaseType.UPKEEP); + addPhaseLabel("DR", PhaseType.DRAW); + addPhaseLabel("M1", PhaseType.MAIN1); + addPhaseLabel("BC", PhaseType.COMBAT_BEGIN); + addPhaseLabel("DA", PhaseType.COMBAT_DECLARE_ATTACKERS); + addPhaseLabel("DB", PhaseType.COMBAT_DECLARE_BLOCKERS); + addPhaseLabel("FS", PhaseType.COMBAT_FIRST_STRIKE_DAMAGE); + addPhaseLabel("CD", PhaseType.COMBAT_DAMAGE); + addPhaseLabel("EC", PhaseType.COMBAT_END); + addPhaseLabel("M2", PhaseType.MAIN2); + addPhaseLabel("ET", PhaseType.END_OF_TURN); + addPhaseLabel("CL", PhaseType.CLEANUP); } - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblEndCombat() { - return this.lblEndCombat; + private void addPhaseLabel(String caption, PhaseType phaseType) { + PhaseLabel lbl = new PhaseLabel(caption, phaseType); + lbl.setToolTipText(Localizer.getInstance().getMessage("htmlPhaseTooltipFmt", phaseType.nameForUi)); + phaseLabels.put(phaseType, lbl); + add(lbl, CONSTRAINTS); } - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblMain2() { - return this.lblMain2; + public PhaseLabel getLabelFor(final PhaseType phaseType) { + return phaseLabels.get(phaseType); } - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblEndTurn() { - return this.lblEndTurn; + public Iterable allLabels() { + return phaseLabels.values(); } - /** @return {@link javax.swing.JLabel} */ - public PhaseLabel getLblCleanup() { - return this.lblCleanup; + /** Resets all phase buttons to "inactive". "Enabled" state is preserved. */ + public void resetPhaseButtons() { + for (PhaseLabel lbl : phaseLabels.values()) { + lbl.setActive(false); + } } -} \ No newline at end of file +} diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/special/PhaseLabel.java b/forge-gui-desktop/src/main/java/forge/toolbox/special/PhaseLabel.java index 28f44c3de31..4c6fe7af041 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/special/PhaseLabel.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/special/PhaseLabel.java @@ -1,13 +1,18 @@ package forge.toolbox.special; +import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JLabel; import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import forge.game.phase.PhaseType; import forge.toolbox.FSkin; /** @@ -17,10 +22,16 @@ */ @SuppressWarnings("serial") public class PhaseLabel extends JLabel { + /** Tint used when a yield marker is targeted at this (player, phase) cell. */ + private static final Color YIELD_MARKER_COLOR = new Color(0xFFA528); + + private final PhaseType phaseType; private boolean enabled = true; private boolean active = false; private boolean hover = false; + private boolean yieldMarked = false; private Runnable onToggled; + private Runnable onRightClick; /** @@ -28,17 +39,24 @@ public class PhaseLabel extends JLabel { * PhaseLabel has "skip" and "active" states, meaning * "this phase is (not) skipped" and "this is the current phase". * - * @param txt - *   Label text + * @param txt   Label text + * @param phaseType0   The PhaseType this label represents (may be null for header/spacer labels) */ - public PhaseLabel(final String txt) { + public PhaseLabel(final String txt, final PhaseType phaseType0) { super(txt); + this.phaseType = phaseType0; this.setHorizontalTextPosition(SwingConstants.CENTER); this.setHorizontalAlignment(SwingConstants.CENTER); this.addMouseListener(new MouseAdapter() { @Override public void mousePressed(final MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + if (PhaseLabel.this.onRightClick != null) { + PhaseLabel.this.onRightClick.run(); + } + return; + } PhaseLabel.this.enabled = !PhaseLabel.this.enabled; if (PhaseLabel.this.onToggled != null) { PhaseLabel.this.onToggled.run(); @@ -59,11 +77,25 @@ public void mouseExited(final MouseEvent e) { }); } + public PhaseType getPhaseType() { + return phaseType; + } + + public boolean isYieldMarked() { + return yieldMarked; + } + + public void setYieldMarked(final boolean b) { + if (this.yieldMarked != b) { + this.yieldMarked = b; + repaintOnlyThisLabel(); + } + } + /** * Determines whether play pauses at this phase or not. - * - * @param b - *   boolean, true if play pauses + * + * @param b   boolean, true if play pauses */ @Override public void setEnabled(final boolean b) { @@ -84,11 +116,15 @@ public void setOnToggled(final Runnable r) { this.onToggled = r; } + /** Fires when the user right-clicks this label. */ + public void setOnRightClick(final Runnable r) { + this.onRightClick = r; + } + /** * Makes this phase the current phase (or not). - * - * @param b - *   boolean, true if phase is current + * + * @param b   boolean, true if phase is current */ public void setActive(final boolean b) { this.active = b; @@ -97,7 +133,7 @@ public void setActive(final boolean b) { /** * Determines if this phase is the current phase (or not). - * + * * @return boolean */ public boolean getActive() { @@ -110,37 +146,61 @@ public void repaintOnlyThisLabel() { repaint(0, 0, d.width, d.height); } - /* - * (non-Javadoc) - * - * @see javax.swing.JComponent#paintComponent(java.awt.Graphics) - */ @Override public void paintComponent(final Graphics g) { final int w = this.getWidth(); final int h = this.getHeight(); - FSkin.SkinColor c; - // Set color according to skip or active or hover state of label + // Precedence: hover > yieldMarked > active/enabled combinations. if (this.hover) { - c = FSkin.getColor(FSkin.Colors.CLR_HOVER); + FSkin.setGraphicsColor(g, FSkin.getColor(FSkin.Colors.CLR_HOVER)); + g.fillRoundRect(1, 1, w - 2, h - 2, 5, 5); + } + else if (this.yieldMarked) { + g.setColor(YIELD_MARKER_COLOR); + g.fillRoundRect(1, 1, w - 2, h - 2, 5, 5); } else if (this.active && this.enabled) { - c = FSkin.getColor(FSkin.Colors.CLR_PHASE_ACTIVE_ENABLED); + FSkin.setGraphicsColor(g, FSkin.getColor(FSkin.Colors.CLR_PHASE_ACTIVE_ENABLED)); + g.fillRoundRect(1, 1, w - 2, h - 2, 5, 5); } else if (!this.active && this.enabled) { - c = FSkin.getColor(FSkin.Colors.CLR_PHASE_INACTIVE_ENABLED); + FSkin.setGraphicsColor(g, FSkin.getColor(FSkin.Colors.CLR_PHASE_INACTIVE_ENABLED)); + g.fillRoundRect(1, 1, w - 2, h - 2, 5, 5); } else if (this.active && !this.enabled) { - c = FSkin.getColor(FSkin.Colors.CLR_PHASE_ACTIVE_DISABLED); + FSkin.setGraphicsColor(g, FSkin.getColor(FSkin.Colors.CLR_PHASE_ACTIVE_DISABLED)); + g.fillRoundRect(1, 1, w - 2, h - 2, 5, 5); } else { - c = FSkin.getColor(FSkin.Colors.CLR_PHASE_INACTIVE_DISABLED); + FSkin.setGraphicsColor(g, FSkin.getColor(FSkin.Colors.CLR_PHASE_INACTIVE_DISABLED)); + g.fillRoundRect(1, 1, w - 2, h - 2, 5, 5); } - // Center vertically and horizontally. Show border if active. - FSkin.setGraphicsColor(g, c); - g.fillRoundRect(1, 1, w - 2, h - 2, 5, 5); - super.paintComponent(g); + if (this.yieldMarked) { + drawChevron(g, w, h); + } else { + super.paintComponent(g); + } + } + + private void drawChevron(final Graphics g, final int w, final int h) { + Graphics2D g2 = (Graphics2D) g.create(); + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setColor(Color.BLACK); + // Two back-to-back triangles — total width is `size`, total height is `size`. + int size = Math.max(6, (int) (h * 0.55)); + int x = (w - size) / 2; + int y = (h - size) / 2; + int[] xs1 = {x, x + size / 2, x}; + int[] ys1 = {y, y + size / 2, y + size}; + g2.fillPolygon(xs1, ys1, 3); + int[] xs2 = {x + size / 2, x + size, x + size / 2}; + int[] ys2 = {y, y + size / 2, y + size}; + g2.fillPolygon(xs2, ys2, 3); + } finally { + g2.dispose(); + } } -} \ No newline at end of file +} diff --git a/forge-gui-desktop/src/main/java/forge/view/FDialog.java b/forge-gui-desktop/src/main/java/forge/view/FDialog.java index cdf9e665e1a..d727c802178 100644 --- a/forge-gui-desktop/src/main/java/forge/view/FDialog.java +++ b/forge-gui-desktop/src/main/java/forge/view/FDialog.java @@ -130,12 +130,13 @@ public void componentResized(final ComponentEvent e) { //must update shape whene //Make Escape key close dialog if allowed @Override public boolean dispatchKeyEvent(final KeyEvent e) { - if (e.getID() == KeyEvent.KEY_PRESSED) { - if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { - final WindowEvent wev = new WindowEvent(this, WindowEvent.WINDOW_CLOSING); - Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(wev); - return true; - } + if (e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_ESCAPE) { + // Non-modal floats (e.g. the chat panel) shouldn't swallow ESC unless the user + // has actually focused them — otherwise pressing ESC anywhere in the app closes + // any open non-modal instead of running the focused window's own ESC handler. + if (!isModal() && !isFocused()) return false; + Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING)); + return true; } return false; } diff --git a/forge-gui-mobile/src/forge/screens/match/MatchController.java b/forge-gui-mobile/src/forge/screens/match/MatchController.java index 68f98d71958..9480eb49608 100644 --- a/forge-gui-mobile/src/forge/screens/match/MatchController.java +++ b/forge-gui-mobile/src/forge/screens/match/MatchController.java @@ -38,8 +38,12 @@ import forge.game.player.PlayerView; import forge.game.spellability.SpellAbilityView; import forge.game.zone.ZoneType; +import forge.gamemodes.match.YieldController; +import forge.gamemodes.match.YieldMarker; +import forge.gamemodes.match.YieldUpdate; import forge.gamemodes.net.NetworkGuiGame; import forge.gamemodes.match.HostedMatch; +import forge.interfaces.IGameController; import forge.gui.FThreads; import forge.gui.GuiBase; import forge.gui.util.SGuiChoose; @@ -571,10 +575,37 @@ private static void actuateMatchPreferences() { final VPhaseIndicator.PhaseLabel label = pi.getLabel(phase); label.setStopAtPhase(prefs.getPrefBoolean(keys[p - 1])); label.setOnToggled(() -> instance.pushSkipPhaseToControllers(player, phase)); + label.setOnLongPress(() -> instance.handleYieldMarkerToggle(label, player, phase)); } } - instance.seedSkipPhaseCache(); + instance.seedYieldStateOnHost(); + } + + private void handleYieldMarkerToggle(final VPhaseIndicator.PhaseLabel label, final PlayerView phaseOwner, final PhaseType phase) { + PlayerView local = getCurrentPlayer(); + if (local == null) { + return; + } + IGameController controller = getGameController(local); + if (controller == null) { + return; + } + YieldMarker existing = controller.getYieldController().getAutoPassUntilMarker(); + boolean clickedSameLabel = existing != null + && phaseOwner.equals(existing.getPhaseOwner()) + && phase == existing.getPhase(); + if (clickedSameLabel) { + controller.sendYieldUpdate(new YieldUpdate.ClearMarker(local)); + } else { + // Setting a marker implies we want to stop here — un-skip the cell so the marker can fire. + label.setStopAtPhase(true); + boolean atOrPast = YieldController.isPriorityAtOrPastMarker(getGameView(), phaseOwner, phase); + controller.sendYieldUpdate(new YieldUpdate.SetMarker(phaseOwner, phase, atOrPast)); + // Pass current priority so the marker takes effect immediately. + controller.selectButtonOk(); + } + refreshYieldUi(local); } public static void writeMatchPreferences() { @@ -695,6 +726,42 @@ public boolean isUiSetToSkipPhase(final PlayerView playerTurn, final PhaseType p return !view.stopAtPhase(playerTurn, phase); } + @Override + public void refreshYieldUi(final PlayerView player) { + FThreads.invokeInEdtNowOrLater(() -> { + if (view == null) { + return; + } + // Marker only rendered for the local player's view. + PlayerView local = getCurrentPlayer(); + if (!player.equals(local)) { + return; + } + for (final VPlayerPanel panel : view.getPlayerPanelsList()) { + for (VPhaseIndicator.PhaseLabel l : panel.getPhaseIndicator().allLabels()) { + l.setYieldMarked(false); + } + } + IGameController controller = getGameController(local); + YieldMarker marker = controller != null ? controller.getYieldController().getAutoPassUntilMarker() : null; + if (marker == null) { + return; + } + VPlayerPanel markedPanel = view.getPlayerPanel(marker.getPhaseOwner()); + if (markedPanel == null) { + return; + } + VPhaseIndicator markedPi = markedPanel.getPhaseIndicator(); + if (markedPi == null) { + return; + } + VPhaseIndicator.PhaseLabel target = markedPi.getLabel(marker.getPhase()); + if (target != null) { + target.setYieldMarked(true); + } + }); + } + public static HostedMatch hostMatch() { hostedMatch = new HostedMatch(); return hostedMatch; diff --git a/forge-gui-mobile/src/forge/screens/match/MatchScreen.java b/forge-gui-mobile/src/forge/screens/match/MatchScreen.java index 72b99d141cd..f92530c42e7 100644 --- a/forge-gui-mobile/src/forge/screens/match/MatchScreen.java +++ b/forge-gui-mobile/src/forge/screens/match/MatchScreen.java @@ -645,13 +645,11 @@ public boolean keyDown(int keyCode) { return true; } return getActivePrompt().getBtnCancel().trigger(); //trigger Cancel if can't trigger OK - case Keys.ESCAPE: - if (!FModel.getPreferences().getPrefBoolean(FPref.UI_ALLOW_ESC_TO_END_TURN) && !Forge.hasGamepad()) {//bypass check - if (getActivePrompt().getBtnCancel().getText().equals(Forge.getLocalizer().getMessage("lblEndTurn"))) { - return false; - } - } - return getActivePrompt().getBtnCancel().trigger(); //otherwise trigger Cancel + case Keys.ESCAPE: { + boolean cancelEligible = FModel.getPreferences().getPrefBoolean(FPref.UI_ALLOW_ESC_TO_END_TURN) || Forge.hasGamepad() + || !getActivePrompt().getBtnCancel().getText().equals(Forge.getLocalizer().getMessage("lblEndTurn")); + return cancelEligible && getActivePrompt().getBtnCancel().trigger(); + } case Keys.BACK: return true; //suppress Back button so it's not bumped when trying to press OK or Cancel buttons case Keys.A: //alpha strike on Ctrl+A on Android, A when running on desktop diff --git a/forge-gui-mobile/src/forge/screens/match/views/VAutoYields.java b/forge-gui-mobile/src/forge/screens/match/views/VAutoYields.java index f4bda9b8938..6d69c04b8ce 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VAutoYields.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VAutoYields.java @@ -18,7 +18,7 @@ public class VAutoYields extends FDialog { public VAutoYields() { super(Forge.getLocalizer().getMessage("lblAutoYields"), 2); List autoYields = new ArrayList<>(); - for (String autoYield : MatchController.instance.getGameController().getAutoYields()) { + for (String autoYield : MatchController.instance.getGameController().getYieldController().getAutoYields()) { autoYields.add(autoYield); } lstAutoYields = add(new FChoiceList(autoYields) { diff --git a/forge-gui-mobile/src/forge/screens/match/views/VPhaseIndicator.java b/forge-gui-mobile/src/forge/screens/match/views/VPhaseIndicator.java index 711a62f6833..9c166e6632b 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VPhaseIndicator.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VPhaseIndicator.java @@ -22,6 +22,8 @@ public class VPhaseIndicator extends FContainer { public static final float PADDING_X = Utils.scale(1); public static final float PADDING_Y = Utils.scale(2); + private static final Color YIELD_MARKER_COLOR = new Color(0xFFA528FF); + private final Map phaseLabels = new HashMap<>(); private FSkinFont font; @@ -48,6 +50,10 @@ public PhaseLabel getLabel(PhaseType phaseType) { return phaseLabels.get(phaseType); } + public Iterable allLabels() { + return phaseLabels.values(); + } + public void resetPhaseButtons() { for (PhaseLabel lbl : phaseLabels.values()) { lbl.setActive(false); @@ -86,7 +92,7 @@ protected void doLayout(float width, float height) { float x = 0; float w = width / phaseLabels.size(); float h = height; - + for (FDisplayObject lbl : getChildren()) { lbl.setBounds(x, 0, w, h); x += w; @@ -110,7 +116,9 @@ public class PhaseLabel extends FDisplayObject { private final PhaseType phaseType; private boolean stopAtPhase = false; private boolean active = false; + private boolean yieldMarked = false; private Runnable onToggled; + private Runnable onLongPress; public PhaseLabel(String caption0, PhaseType phaseType0) { caption = caption0; @@ -135,11 +143,23 @@ public void setStopAtPhase(boolean stopAtPhase0) { stopAtPhase = stopAtPhase0; } + public boolean isYieldMarked() { + return yieldMarked; + } + public void setYieldMarked(boolean v) { + this.yieldMarked = v; + } + /** Fires after the user toggles this label by tapping. */ public void setOnToggled(Runnable r) { onToggled = r; } + /** Fires when the user long-presses this label. */ + public void setOnLongPress(Runnable r) { + onLongPress = r; + } + @Override public boolean tap(float x, float y, int count) { stopAtPhase = !stopAtPhase; @@ -147,6 +167,15 @@ public boolean tap(float x, float y, int count) { return true; } + @Override + public boolean longPress(float x, float y) { + if (onLongPress == null) { + return false; + } + onLongPress.run(); + return true; + } + @Override public void draw(final Graphics g) { float x = PADDING_X; @@ -154,21 +183,36 @@ public void draw(final Graphics g) { float h = getHeight(); //determine back color according to skip or active state of label - FSkinColor backColor; - if (active && stopAtPhase) { - backColor = Forge.isMobileAdventureMode ? FSkinColor.get(Colors.ADV_CLR_PHASE_ACTIVE_ENABLED) : FSkinColor.get(Colors.CLR_PHASE_ACTIVE_ENABLED); - } - else if (!active && stopAtPhase) { - backColor = Forge.isMobileAdventureMode ? FSkinColor.get(Colors.ADV_CLR_PHASE_INACTIVE_ENABLED) : FSkinColor.get(Colors.CLR_PHASE_INACTIVE_ENABLED); - } - else if (active && !stopAtPhase) { - backColor = Forge.isMobileAdventureMode ? FSkinColor.get(Colors.ADV_CLR_PHASE_ACTIVE_DISABLED) : FSkinColor.get(Colors.CLR_PHASE_ACTIVE_DISABLED); - } - else { - backColor = Forge.isMobileAdventureMode ? FSkinColor.get(Colors.ADV_CLR_PHASE_INACTIVE_DISABLED) : FSkinColor.get(Colors.CLR_PHASE_INACTIVE_DISABLED); + if (yieldMarked) { + g.fillRect(YIELD_MARKER_COLOR, x, 0, w, h); + drawChevron(g, x, w, h); + // Skip the caption when marked — chevron replaces the phase abbreviation. + } else { + FSkinColor backColor; + if (active && stopAtPhase) { + backColor = Forge.isMobileAdventureMode ? FSkinColor.get(Colors.ADV_CLR_PHASE_ACTIVE_ENABLED) : FSkinColor.get(Colors.CLR_PHASE_ACTIVE_ENABLED); + } + else if (!active && stopAtPhase) { + backColor = Forge.isMobileAdventureMode ? FSkinColor.get(Colors.ADV_CLR_PHASE_INACTIVE_ENABLED) : FSkinColor.get(Colors.CLR_PHASE_INACTIVE_ENABLED); + } + else if (active && !stopAtPhase) { + backColor = Forge.isMobileAdventureMode ? FSkinColor.get(Colors.ADV_CLR_PHASE_ACTIVE_DISABLED) : FSkinColor.get(Colors.CLR_PHASE_ACTIVE_DISABLED); + } + else { + backColor = Forge.isMobileAdventureMode ? FSkinColor.get(Colors.ADV_CLR_PHASE_INACTIVE_DISABLED) : FSkinColor.get(Colors.CLR_PHASE_INACTIVE_DISABLED); + } + g.fillRect(isHovered() ? backColor.brighter() : backColor, x, 0, w, h); + g.drawText(caption, isHovered() && font.canIncrease() ? font.increase() : font, Color.BLACK, x, 0, w, h, false, Align.center, true); } - g.fillRect(isHovered() ? backColor.brighter() : backColor, x, 0, w, h); - g.drawText(caption, isHovered() && font.canIncrease() ? font.increase() : font, Color.BLACK, x, 0, w, h, false, Align.center, true); + } + + private void drawChevron(final Graphics g, float x, float w, float h) { + // Two back-to-back triangles centered in the cell + float size = Math.max(Utils.scale(6f), h * 0.55f); + float cx = x + (w - size) / 2f; + float cy = (h - size) / 2f; + g.fillTriangle(Color.BLACK, cx, cy, cx + size / 2f, cy + size / 2f, cx, cy + size); + g.fillTriangle(Color.BLACK, cx + size / 2f, cy, cx + size, cy + size / 2f, cx + size / 2f, cy + size); } } } diff --git a/forge-gui-mobile/src/forge/screens/match/views/VPrompt.java b/forge-gui-mobile/src/forge/screens/match/views/VPrompt.java index 1c7e0897f6c..2d798ef2ca2 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VPrompt.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VPrompt.java @@ -40,10 +40,10 @@ public static FSkinColor getForeColor() { private final FButton btnOk, btnCancel; private final MessageLabel lblMessage; private String message; - private CardView card = null ; + private CardView card = null; public void setCardView(final CardView card) { - this.card = card ; + this.card = card; } public VPrompt(String okText, String cancelText, FEventHandler okCommand, FEventHandler cancelCommand) { @@ -121,7 +121,7 @@ public boolean tap(float x, float y, int count) { @Override public boolean fling(float x, float y) { - if ( card != null ) { + if (card != null) { CardZoom.show(card); } return true; @@ -129,7 +129,7 @@ public boolean fling(float x, float y) { @Override public boolean longPress(float x, float y) { - if ( card != null ) { + if (card != null) { CardZoom.show(card); } return true; diff --git a/forge-gui-mobile/src/forge/screens/match/views/VStack.java b/forge-gui-mobile/src/forge/screens/match/views/VStack.java index db97402fbf5..bfc37d27b74 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VStack.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VStack.java @@ -13,6 +13,7 @@ import forge.Graphics; import forge.assets.FSkinColor; import forge.assets.FSkinFont; +import forge.assets.FSkinImage; import forge.assets.TextRenderer; import forge.card.CardRenderer; import forge.card.CardRenderer.CardStackPosition; @@ -22,6 +23,7 @@ import forge.game.player.PlayerView; import forge.game.spellability.StackItemView; import forge.game.zone.ZoneType; +import forge.gamemodes.match.YieldUpdate; import forge.gui.card.CardDetailUtil; import forge.gui.card.CardDetailUtil.DetailColors; import forge.interfaces.IGameController; @@ -284,10 +286,10 @@ public boolean tap(float x, float y, int count) { final IGameController controller = MatchController.instance.getGameController(); final PlayerView player = MatchController.instance.getCurrentPlayer(); if (player != null) { //don't show menu if tapping on art - if (stackInstance.isAbility()) { - FPopupMenu menu = new FPopupMenu() { - @Override - protected void buildMenu() { + FPopupMenu menu = new FPopupMenu() { + @Override + protected void buildMenu() { + if (stackInstance.isAbility()) { final String key = stackInstance.getKey(); final boolean autoYield = controller.shouldAutoYield(key); addItem(new FCheckBoxMenuItem(Forge.getLocalizer().getMessage("cbpAutoYieldMode"), autoYield, @@ -323,13 +325,19 @@ protected void buildMenu() { } })); } - addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblZoomOrDetails"), e -> CardZoom.show(stackInstance.getSourceCard()))); } - }; + addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblYieldToEntireStack"), + Forge.hdbuttons ? FSkinImage.HDYIELD : FSkinImage.WARNING, + e -> { + controller.sendYieldUpdate(new YieldUpdate.StackYield(player, true)); + controller.passPriority(); + })); + addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblZoomOrDetails"), e -> CardZoom.show(stackInstance.getSourceCard()))); + } + }; - menu.show(this, x, y); - return true; - } + menu.show(this, x, y); + return true; } CardZoom.show(stackInstance.getSourceCard()); return true; diff --git a/forge-gui/res/cardsfolder/c/commit_memory.txt b/forge-gui/res/cardsfolder/c/commit_memory.txt index 5cd6e55e4f0..3bf552eac69 100644 --- a/forge-gui/res/cardsfolder/c/commit_memory.txt +++ b/forge-gui/res/cardsfolder/c/commit_memory.txt @@ -12,6 +12,6 @@ Name:Memory ManaCost:4 U U Types:Sorcery K:Aftermath -A:SP$ ChangeZoneAll | ChangeType$ Card | Origin$ Hand,Graveyard | Destination$ Library | Shuffle$ True | SubAbility$ DBDraw | UseAllOriginZones$ True | AILogic$ TimeTwister | SpellDescription$ Each player shuffles their graveyard and hand into their library, then draws seven cards. +A:SP$ ChangeZoneAll | ChangeType$ Card | Origin$ Hand,Graveyard | Destination$ Library | Shuffle$ True | SubAbility$ DBDraw | UseAllOriginZones$ True | AILogic$ Timetwister | SpellDescription$ Each player shuffles their graveyard and hand into their library, then draws seven cards. SVar:DBDraw:DB$ Draw | NumCards$ 7 | Defined$ Player | StackDescription$ None Oracle:Aftermath (Cast this spell only from your graveyard. Then exile it.)\nEach player shuffles their hand and graveyard into their library, then draws seven cards. diff --git a/forge-gui/res/cardsfolder/e/echo_of_eons.txt b/forge-gui/res/cardsfolder/e/echo_of_eons.txt index e769eae85f0..5a0f3f543a0 100644 --- a/forge-gui/res/cardsfolder/e/echo_of_eons.txt +++ b/forge-gui/res/cardsfolder/e/echo_of_eons.txt @@ -1,7 +1,7 @@ Name:Echo of Eons ManaCost:4 U U Types:Sorcery -A:SP$ ChangeZoneAll | ChangeType$ Card | Origin$ Hand,Graveyard | Destination$ Library | Shuffle$ True | SubAbility$ DBDraw | UseAllOriginZones$ True | AILogic$ TimeTwister | SpellDescription$ Each player shuffles their graveyard and hand into their library, then draws seven cards. +A:SP$ ChangeZoneAll | ChangeType$ Card | Origin$ Hand,Graveyard | Destination$ Library | Shuffle$ True | SubAbility$ DBDraw | UseAllOriginZones$ True | AILogic$ Timetwister | SpellDescription$ Each player shuffles their graveyard and hand into their library, then draws seven cards. SVar:DBDraw:DB$ Draw | NumCards$ 7 | Defined$ Player K:Flashback:2 U Oracle:Each player shuffles their hand and graveyard into their library, then draws seven cards.\nFlashback {2}{U} (You may cast this card from your graveyard for its flashback cost. Then exile it.) diff --git a/forge-gui/res/cardsfolder/t/timetwister.txt b/forge-gui/res/cardsfolder/t/timetwister.txt index 2f4ca6b4aa4..b9bb4f80feb 100644 --- a/forge-gui/res/cardsfolder/t/timetwister.txt +++ b/forge-gui/res/cardsfolder/t/timetwister.txt @@ -1,6 +1,6 @@ Name:Timetwister ManaCost:2 U Types:Sorcery -A:SP$ ChangeZoneAll | ChangeType$ Card | Origin$ Hand,Graveyard | Destination$ Library | Shuffle$ True | SubAbility$ DBDraw | UseAllOriginZones$ True | AILogic$ TimeTwister | SpellDescription$ Each player shuffles their graveyard and hand into their library, then draws seven cards. +A:SP$ ChangeZoneAll | ChangeType$ Card | Origin$ Hand,Graveyard | Destination$ Library | Shuffle$ True | SubAbility$ DBDraw | UseAllOriginZones$ True | AILogic$ Timetwister | SpellDescription$ Each player shuffles their graveyard and hand into their library, then draws seven cards. SVar:DBDraw:DB$ Draw | NumCards$ 7 | Defined$ Player Oracle:Each player shuffles their hand and graveyard into their library, then draws seven cards. (Then put Timetwister into its owner's graveyard.) diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index 02f61675340..d0fd9a98970 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -2943,20 +2943,6 @@ lblAllTokens=Alle Spielsteine #StartRenderer.java lblClickToAddTargetToFavorites=Klicke um {0} zu deinen Favoriten hinzuzufügen lblClickToRemoveTargetToFavorites=Klicke um {0} aus deinen Favoriten zu entfernen -#PhaseIndicator.java -#translate html*** please keep HTML Tags -htmlPhaseUpkeepTooltip=Phase: Versorgung
Klicke zum Umschalten. -htmlPhaseDrawTooltip=Phase: Karte ziehen
Klicke zum Umschalten. -htmlPhaseMain1Tooltip=Phase: Hauptphase 1
Klicke zum Umschalten. -htmlPhaseBeginCombatTooltip=Phase: Vor dem Kampf
Klicke zum Umschalten. -htmlPhaseDeclareAttackersTooltip=Phase: Angeifer deklarieren
Klicke zum Umschalten. -htmlPhaseDeclareBlockersTooltip=Phase: Blocker deklarieren
Klicke zum Umschalten. -htmlPhaseFirstStrikeDamageTooltip=Phase: Erstschlagschaden
Klicke zum Umschalten. -htmlPhaseCombatDamageTooltip=Phase: Kampfschaden
Klicke zum Umschalten. -htmlPhaseEndCombatTooltip=Phase: Ende des Kampfes
Klicke zum Umschalten. -htmlPhaseMain2Tooltip=Phase: Hauptphase 2
Klicke zum Umschalten. -htmlPhaseEndTurnTooltip=Phase: Ende des Zuges
Klicke zum Umschalten. -htmlPhaseCleanupTooltip=Phase: Aufräumen
Klicke zum Umschalten. #GuiChoose.java lblSideboardForPlayer=Sideboard für {0} lblOtherInteger=Andere... diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 928f3503f25..57858557500 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -1623,6 +1623,9 @@ lblCloseGameSpectator=This will close this game and you will not be able to resu lblCloseGame=Close Game? lblWaitingForOpponent=Waiting for opponent... lblYieldingUntilEndOfTurn=Yielding until end of turn.\nYou may cancel this yield to take an action. +lblYieldingUntilPhaseFmt=Yielding until {0}''s {1}.\nYou may cancel this yield to take an action. +lblYieldingUntilStackClears=Yielding until stack clears.\nYou may cancel this yield to take an action. +lblYieldToEntireStack=Yield to entire stack lblStopWatching=Stop Watching lblEnterNumberBetweenMinAndMax=Enter a number between {0} and {1}: lblEnterNumberGreaterThanOrEqualsToMin=Enter a number greater than or equal to {0}: @@ -3120,18 +3123,7 @@ lblClickToAddTargetToFavorites=Click to add {0} to your favorites lblClickToRemoveTargetToFavorites=Click to remove {0} from your favorites #PhaseIndicator.java #translate html*** please keep HTML Tags -htmlPhaseUpkeepTooltip=Phase: Upkeep
Click to toggle. -htmlPhaseDrawTooltip=Phase: Draw
Click to toggle. -htmlPhaseMain1Tooltip=Phase: Main 1
Click to toggle. -htmlPhaseBeginCombatTooltip=Phase: Begin Combat
Click to toggle. -htmlPhaseDeclareAttackersTooltip=Phase: Declare Attackers
Click to toggle. -htmlPhaseDeclareBlockersTooltip=Phase: Declare Blockers
Click to toggle. -htmlPhaseFirstStrikeDamageTooltip=Phase: First Strike Damage
Click to toggle. -htmlPhaseCombatDamageTooltip=Phase: Combat Damage
Click to toggle. -htmlPhaseEndCombatTooltip=Phase: End Combat
Click to toggle. -htmlPhaseMain2Tooltip=Phase: Main 2
Click to toggle. -htmlPhaseEndTurnTooltip=Phase: End Turn
Click to toggle. -htmlPhaseCleanupTooltip=Phase: Cleanup
Click to toggle. +htmlPhaseTooltipFmt=Phase: {0}
Click to toggle. Right-click to fast-forward. #GuiChoose.java lblSideboardForPlayer=Sideboard for {0} lblOtherInteger=Other... diff --git a/forge-gui/res/languages/es-ES.properties b/forge-gui/res/languages/es-ES.properties index 6111e1f475c..874e1fc2d1d 100644 --- a/forge-gui/res/languages/es-ES.properties +++ b/forge-gui/res/languages/es-ES.properties @@ -2934,20 +2934,6 @@ lblAllTokens=Todas las fichas #StartRenderer.java lblClickToAddTargetToFavorites=Clic para añadir {0} a tus favoritos lblClickToRemoveTargetToFavorites=Clic para eliminar {0} de tus favoritos -#PhaseIndicator.java -#translate html*** please keep HTML Tags -htmlPhaseUpkeepTooltip=Fase: Mantenimiento
Clic para alternar. -htmlPhaseDrawTooltip=Fase: Robar
Clic para alternar. -htmlPhaseMain1Tooltip=Fase: Principal 1
Clic para alternar. -htmlPhaseBeginCombatTooltip=Fase: Inicio del Combate
Clic para alternar. -htmlPhaseDeclareAttackersTooltip=Fase: Declarar Atacantes
Clic para alternar. -htmlPhaseDeclareBlockersTooltip=Fase: Declarar Bloqueadoras
Clic para alternar. -htmlPhaseFirstStrikeDamageTooltip=Fase: Daño Dañar Primero
Clic para alternar. -htmlPhaseCombatDamageTooltip=Fase: Daño de Combate
Clic para alternar. -htmlPhaseEndCombatTooltip=Fase: Final del Combate
Clic para alternar. -htmlPhaseMain2Tooltip=Fase: Principal 2
Clic para alternar. -htmlPhaseEndTurnTooltip=Fase: Fin del Turno
Clic para alternar. -htmlPhaseCleanupTooltip=Fase: Limpieza
Clic para alternar. #GuiChoose.java lblSideboardForPlayer=Banquillo para {0} lblOtherInteger=Otro... diff --git a/forge-gui/res/languages/fr-FR.properties b/forge-gui/res/languages/fr-FR.properties index 0f2725cec34..47ca536dd7c 100644 --- a/forge-gui/res/languages/fr-FR.properties +++ b/forge-gui/res/languages/fr-FR.properties @@ -2928,20 +2928,6 @@ lblAllTokens=Tous les jetons #StartRenderer.java lblClickToAddTargetToFavorites=Cliquez pour ajouter {0} à vos favoris lblClickToRemoveTargetToFavorites=Cliquez pour supprimer {0} de vos favoris -#PhaseIndicator.java -#translate html*** please keep HTML Tags -htmlPhaseUpkeepTooltip=Phase : Entretien
Cliquez pour basculer. -htmlPhaseDrawTooltip=Phase : Dessiner
Cliquez pour basculer. -htmlPhaseMain1Tooltip=Phase : Principal 1
Cliquez pour basculer. -htmlPhaseBeginCombatTooltip=Phase : commencer le combat
Cliquez pour basculer. -htmlPhaseDeclareAttackersTooltip=Phase : Déclarer les attaquants
Cliquez pour basculer. -htmlPhaseDeclareBlockersTooltip=Phase : Déclarer les bloqueurs
Cliquez pour basculer. -htmlPhaseFirstStrikeDamageTooltip=Phase : Dégâts de la première frappe
Cliquez pour basculer. -htmlPhaseCombatDamageTooltip=Phase : Dégâts de combat
Cliquez pour basculer. -htmlPhaseEndCombatTooltip=Phase : Fin du combat
Cliquez pour basculer. -htmlPhaseMain2Tooltip=Phase : Principal 2
Cliquez pour basculer. -htmlPhaseEndTurnTooltip=Phase : fin de virage
Cliquez pour basculer. -htmlPhaseCleanupTooltip=Phase : Nettoyage
Cliquez pour basculer. #GuiChoose.java lblSideboardForPlayer=Sideboard pour {0} lblOtherInteger=Autre... diff --git a/forge-gui/res/languages/it-IT.properties b/forge-gui/res/languages/it-IT.properties index a6fb937f4fa..005da955264 100644 --- a/forge-gui/res/languages/it-IT.properties +++ b/forge-gui/res/languages/it-IT.properties @@ -2926,20 +2926,6 @@ lblAllTokens=Tutti i segnalini #StartRenderer.java lblClickToAddTargetToFavorites=Clicca per aggiungere {0} ai preferiti lblClickToRemoveTargetToFavorites=Clicca per rimuovere {0} ai preferiti -#PhaseIndicator.java -#translate html*** please keep HTML Tags -htmlPhaseUpkeepTooltip=Fase: Mantenimento
Clic per (dis)attivare. -htmlPhaseDrawTooltip=Fase: Acquisizione
Clic per (dis)attivare. -htmlPhaseMain1Tooltip=Fase: Principale 1
Clic per (dis)attivare. -htmlPhaseBeginCombatTooltip=Fase: Inizio combattimento
Clic per (dis)attivare. -htmlPhaseDeclareAttackersTooltip=Fase: Dichiara attaccanti
Clic per (dis)attivare. -htmlPhaseDeclareBlockersTooltip=Fase: Dichiara bloccanti
Clic per (dis)attivare. -htmlPhaseFirstStrikeDamageTooltip=Fase: Danno attacco improvviso
Clic per (dis)attivare. -htmlPhaseCombatDamageTooltip=Fase: Danno da combattimento
Clic per (dis)attivare. -htmlPhaseEndCombatTooltip=Fase: Fine combattimento
Clic per (dis)attivare. -htmlPhaseMain2Tooltip=Fase: Principale 2
Clic per (dis)attivare. -htmlPhaseEndTurnTooltip=Fase: Finale
Clic per (dis)attivare. -htmlPhaseCleanupTooltip=Fase: Cancellazione
Clic per (dis)attivare. #GuiChoose.java lblSideboardForPlayer=Sideboard per {0} lblOtherInteger=Altro... diff --git a/forge-gui/res/languages/ja-JP.properties b/forge-gui/res/languages/ja-JP.properties index 1da5d2b9a93..ffeded71ab6 100644 --- a/forge-gui/res/languages/ja-JP.properties +++ b/forge-gui/res/languages/ja-JP.properties @@ -2922,20 +2922,6 @@ lblAllTokens=全てのトークン #StartRenderer.java lblClickToAddTargetToFavorites={0}をクリックしてお気に入りに登録します lblClickToRemoveTargetToFavorites={0}をクリックしてお気に入りから外します -#PhaseIndicator.java -#translate html*** please keep HTML Tags -htmlPhaseUpkeepTooltip=アップキープ・ステップ
クリックで切り替え。 -htmlPhaseDrawTooltip=ドロー・ステップ
クリックで切り替え。 -htmlPhaseMain1Tooltip=戦闘前メイン・フェイズ
クリックで切り替え。 -htmlPhaseBeginCombatTooltip=戦闘開始ステップ
クリックで切り替え。 -htmlPhaseDeclareAttackersTooltip=攻撃クリーチャー指定ステップ
クリックで切り替え。 -htmlPhaseDeclareBlockersTooltip=ブロック・クリーチャー指定ステップ
クリックで切り替え。 -htmlPhaseFirstStrikeDamageTooltip=先制戦闘ダメージ・ステップ
クリックで切り替え。 -htmlPhaseCombatDamageTooltip=通常戦闘ダメージ・ステップ
クリックで切り替え。 -htmlPhaseEndCombatTooltip=戦闘終了ステップ
クリックで切り替え。 -htmlPhaseMain2Tooltip=戦闘後メイン・フェイズ
クリックで切り替え。 -htmlPhaseEndTurnTooltip=終了ステップ
クリックで切り替え。 -htmlPhaseCleanupTooltip=クリンナップ・ステップ
クリックで切り替え。 #GuiChoose.java lblSideboardForPlayer={0}のサイドボード lblOtherInteger=他... diff --git a/forge-gui/res/languages/ko-KR.properties b/forge-gui/res/languages/ko-KR.properties index 21361f43bb9..144c24f2661 100644 --- a/forge-gui/res/languages/ko-KR.properties +++ b/forge-gui/res/languages/ko-KR.properties @@ -3019,20 +3019,6 @@ lblAllTokens=모든 토큰 #StartRenderer.java lblClickToAddTargetToFavorites={0}를 클릭하여 즐겨찾기에 추가합니다 lblClickToRemoveTargetToFavorites={0}를 클릭하여 즐겨찾기에서 제거합니다 -#PhaseIndicator.java -#translate html*** please keep HTML Tags -htmlPhaseUpkeepTooltip=업케이프 단계
클릭으로 전환 -htmlPhaseDrawTooltip=드로우 단계
클릭으로 전환 -htmlPhaseMain1Tooltip=전투 전 메인 페이즈
클릭으로 전환 -htmlPhaseBeginCombatTooltip=전투 시작 단계
클릭으로 전환 -htmlPhaseDeclareAttackersTooltip=공격 생물 지정 단계
클릭으로 전환 -htmlPhaseDeclareBlockersTooltip=블록 생물 지정 단계
클릭으로 전환 -htmlPhaseFirstStrikeDamageTooltip=선제 전투 피해 단계
클릭으로 전환 -htmlPhaseCombatDamageTooltip=일반 전투 피해 단계
클릭으로 전환 -htmlPhaseEndCombatTooltip=전투 종료 단계
클릭으로 전환 -htmlPhaseMain2Tooltip=전투 후 메인 페이즈
클릭으로 전환 -htmlPhaseEndTurnTooltip=종료 단계
클릭으로 전환 -htmlPhaseCleanupTooltip=정리 단계
클릭으로 전환 #GuiChoose.java lblSideboardForPlayer={0}의 사이드보드 lblOtherInteger=기타... diff --git a/forge-gui/res/languages/pt-BR.properties b/forge-gui/res/languages/pt-BR.properties index 583aa202520..7f600ce6343 100644 --- a/forge-gui/res/languages/pt-BR.properties +++ b/forge-gui/res/languages/pt-BR.properties @@ -2988,20 +2988,6 @@ lblAllTokens=Todas as Fichas #StartRenderer.java lblClickToAddTargetToFavorites=Clique para adicionar {0} a seus favoritos lblClickToRemoveTargetToFavorites=Clique para remover {0} de seus favoritos -#PhaseIndicator.java -#translate html*** please keep HTML Tags -htmlPhaseUpkeepTooltip=Etapa\: Manutenção
Clique p/ alternar. -htmlPhaseDrawTooltip=Etapa\: Compra
Clique p/ alternar. -htmlPhaseMain1Tooltip=Etapa\: 1ª Principal
Clique p/ alternar. -htmlPhaseBeginCombatTooltip=Etapa\: Início de Combate
Clique p/ alternar. -htmlPhaseDeclareAttackersTooltip=Etapa\: Declaração de Atacantes
Clique p/ alternar. -htmlPhaseDeclareBlockersTooltip=Etapa\: Declaração de Bloqueadores
Clique p/ alternar. -htmlPhaseFirstStrikeDamageTooltip=Etapa\: Dano de Iniciativa
Clique p/ alternar. -htmlPhaseCombatDamageTooltip=Etapa\: Dano de Combate
Clique p/ alternar. -htmlPhaseEndCombatTooltip=Etapa\: Fim de Combate
Clique p/ alternar. -htmlPhaseMain2Tooltip=Etapa\: 2ª Principal
Clique p/ alternar. -htmlPhaseEndTurnTooltip=Etapa\: Fim do Turno
Clique p/ alternar. -htmlPhaseCleanupTooltip=Etapa\: Limpeza
Clique p/ alternar. #GuiChoose.java lblSideboardForPlayer=Sideboard para {0} lblOtherInteger=Outro... diff --git a/forge-gui/res/languages/zh-CN.properties b/forge-gui/res/languages/zh-CN.properties index 26076c1a936..cacf69eb512 100644 --- a/forge-gui/res/languages/zh-CN.properties +++ b/forge-gui/res/languages/zh-CN.properties @@ -2912,20 +2912,6 @@ lblAllTokens=所有衍生物 #StartRenderer.java lblClickToAddTargetToFavorites=点击以将{0}添加到你的收藏夹 lblClickToRemoveTargetToFavorites=点击以将{0}从你的收藏夹移除 -#PhaseIndicator.java -#translate html*** please keep HTML Tags -htmlPhaseUpkeepTooltip=阶段:维持
单击切换是否自动让过。 -htmlPhaseDrawTooltip=阶段:抓牌
单击切换是否自动让过。 -htmlPhaseMain1Tooltip=阶段:主1
单击切换是否自动让过。 -htmlPhaseBeginCombatTooltip=阶段:战斗开始
单击切换是否自动让过。 -htmlPhaseDeclareAttackersTooltip=阶段:宣告攻击者
单击切换是否自动让过。 -htmlPhaseDeclareBlockersTooltip=阶段:宣告阻挡者
单击切换是否自动让过。 -htmlPhaseFirstStrikeDamageTooltip=阶段:先攻伤害
单击切换是否自动让过。 -htmlPhaseCombatDamageTooltip=阶段:战斗伤害
单击切换是否自动让过。 -htmlPhaseEndCombatTooltip=阶段:战斗结束
单击切换是否自动让过。 -htmlPhaseMain2Tooltip=阶段:主2
单击切换是否自动让过。 -htmlPhaseEndTurnTooltip=阶段:结束回合
单击切换是否自动让过。 -htmlPhaseCleanupTooltip=阶段:清除
单击切换是否自动让过。 #GuiChoose.java lblSideboardForPlayer=为{0}换备 lblOtherInteger=其他数 diff --git a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java index 11a7f1ebfb6..2bc7f4fb5df 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java @@ -1,6 +1,7 @@ package forge.gamemodes.match; import com.google.common.collect.*; + import forge.game.GameEntityView; import forge.game.GameEndReason; import forge.game.GameLog; @@ -25,6 +26,7 @@ import forge.trackable.TrackableTypes; import forge.util.FSerializableFunction; import forge.util.Localizer; + import org.apache.commons.lang3.StringUtils; import java.io.Serializable; @@ -170,7 +172,6 @@ public void setGameController(PlayerView player, final IGameController gameContr gameControllers.put(player, originalGameControllers.get(player)); } else { gameControllers.remove(player); - autoPassUntilEndOfTurn.remove(player); final PlayerView currentPlayer = getCurrentPlayer(); if (player.equals(currentPlayer)) { // set current player to a value known to be legal @@ -441,36 +442,6 @@ public String getConcedeCaption() { // Auto-yield and other input-related code - private final Set autoPassUntilEndOfTurn = Sets.newHashSet(); - - /** - * Automatically pass priority until reaching the Cleanup phase of the - * current turn. - */ - @Override - public final void autoPassUntilEndOfTurn(final PlayerView player) { - autoPassUntilEndOfTurn.add(player); - updateAutoPassPrompt(); - } - - @Override - public final void autoPassCancel(final PlayerView player) { - if (!autoPassUntilEndOfTurn.remove(player)) { - return; - } - - //prevent prompt getting stuck on yielding message while actually waiting for next input opportunity - final PlayerView playerView = getCurrentPlayer(); - showPromptMessage(playerView, ""); - updateButtons(playerView, false, false, false); - awaitNextInput(); - } - - @Override - public final boolean mayAutoPass(final PlayerView player) { - return autoPassUntilEndOfTurn.contains(player); - } - private Timer awaitNextInputTimer; private TimerTask awaitNextInputTask; @@ -508,8 +479,42 @@ private void checkAwaitNextInputTimer() { } protected final void updatePromptForAwait(final PlayerView playerView) { - showPromptMessage(playerView, Localizer.getInstance().getMessage("lblWaitingForOpponent")); - updateButtons(playerView, false, false, false); + // Append "Waiting for opponent..." below the yield prompt so the user keeps the + // cancel-yield UI during opponent turns instead of losing it to the await prompt. + String waiting = Localizer.getInstance().getMessage("lblWaitingForOpponent"); + String yieldMsg = currentYieldMessage(); + if (yieldMsg != null) { + cancelAwaitNextInput(); + showPromptMessage(playerView, yieldMsg + "\n\n" + waiting); + updateButtons(playerView, false, true, false); + } else { + showPromptMessage(playerView, waiting); + updateButtons(playerView, false, false, false); + } + } + + private String currentYieldMessage() { + YieldController yielding = null; + for (IGameController c : gameControllers.values()) { + YieldController yc = c.getYieldController(); + if (yc != null && yc.shouldAutoYield()) { + yielding = yc; + break; + } + } + if (yielding == null) return null; + Localizer loc = Localizer.getInstance(); + if (yielding.getAutoPassUntilMarker() != null) { + YieldMarker m = yielding.getAutoPassUntilMarker(); + return loc.getMessage("lblYieldingUntilPhaseFmt", m.getPhaseOwner().getName(), m.getPhase().nameForUi); + } + if (yielding.autoPassUntilStackEmpty()) { + return loc.getMessage("lblYieldingUntilStackClears"); + } + if (yielding.autoPassUntilEndOfTurn()) { + return loc.getMessage("lblYieldingUntilEndOfTurn"); + } + return null; } @Override @@ -546,7 +551,9 @@ private void updateWaitingDisplay(final PlayerView forPlayer, final String waiti } else { timeStr = String.format("%d:%02d", elapsedSec / 60, elapsedSec % 60); } - showPromptMessageNoCancel(forPlayer, Localizer.getInstance().getMessage("lblWaitingForPlayer", waitingForPlayerName) + " (" + timeStr + ")"); + String waiting = Localizer.getInstance().getMessage("lblWaitingForPlayer", waitingForPlayerName) + " (" + timeStr + ")"; + String yieldMsg = currentYieldMessage(); + showPromptMessageNoCancel(forPlayer, yieldMsg != null ? yieldMsg + "\n\n" + waiting : waiting); } protected void cancelWaitingTimer() { @@ -604,13 +611,27 @@ public final void cancelAwaitNextInput() { @Override public final void updateAutoPassPrompt() { - if (!autoPassUntilEndOfTurn.isEmpty()) { - //allow user to cancel auto-pass - cancelAwaitNextInput(); //don't overwrite prompt with awaiting opponent - showPromptMessage(getCurrentPlayer(), Localizer.getInstance().getMessage("lblYieldingUntilEndOfTurn")); - updateButtons(getCurrentPlayer(), false, true, false); + String message = currentYieldMessage(); + if (message == null) return; + cancelAwaitNextInput(); + showPromptMessage(getCurrentPlayer(), message); + updateButtons(getCurrentPlayer(), false, true, false); + if (GuiBase.isNetPlay(this)) { + showWaitingTimer(getCurrentPlayer(), findWaitingForPlayerName(getCurrentPlayer())); } } + + @Override + public void applyYieldUpdate(YieldUpdate update) { + PlayerView pv; + if (update instanceof YieldUpdate.ClearMarker u) pv = u.player(); + else if (update instanceof YieldUpdate.StackYield u) pv = u.player(); + else return; + IGameController c = getGameController(pv); + if (c != null) c.applyYieldUpdate(update); + refreshYieldUi(pv); + } + // End auto-yield/input code /** @@ -839,4 +860,5 @@ public void updateDependencies() { public void applyDelta(DeltaPacket packet) { // No-op for local games - network implementation is in NetworkGuiGame } + } diff --git a/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java b/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java index adaf086b601..1f1b198a92b 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java @@ -387,7 +387,7 @@ public void endCurrentGame() { ngg.shutdownForwarder(); } humanController.getGui().setGameSpeed(PlaybackSpeed.NORMAL); - humanController.clearAutoYields(); + humanController.getYieldController().clearAutoYields(); if (humanCount > 0) //conceded humanController.getGui().afterGameEnd(); @@ -427,7 +427,7 @@ public Void visit(final UiEventBlockerAssigned event) { humanController.getGui().updateSingleCard(event.blocker()); final PlayerView p = humanController.getPlayer().getView(); if (event.attackerBeingBlocked() != null && event.attackerBeingBlocked().getController().equals(p)) { - humanController.getGui().autoPassCancel(p); + humanController.autoPassCancel(); } } return null; diff --git a/forge-gui/src/main/java/forge/gamemodes/match/YieldController.java b/forge-gui/src/main/java/forge/gamemodes/match/YieldController.java new file mode 100644 index 00000000000..9cc26533bb8 --- /dev/null +++ b/forge-gui/src/main/java/forge/gamemodes/match/YieldController.java @@ -0,0 +1,263 @@ +package forge.gamemodes.match; + +import forge.game.GameView; +import forge.game.phase.PhaseType; +import forge.game.player.PlayerView; +import forge.localinstance.properties.ForgeConstants; +import forge.localinstance.properties.ForgePreferences.FPref; +import forge.model.FModel; +import forge.player.AutoYieldStore; +import forge.player.LobbyPlayerHuman; +import forge.player.PersistentYieldStore; +import forge.player.PlayerControllerHuman; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + *

Auto-yield state and trigger decisions live in a single {@link AutoYieldStore} + * resolved by {@link #activeStore()}: + *

    + *
  • Host PCH for a local player → the {@link LobbyPlayerHuman}'s persistent store + * (tier-aware via FPref).
  • + *
  • Host PCH for a remote player → a per-game {@link #localStore} cache populated + * by client wire messages.
  • + *
  • {@link forge.gamemodes.net.client.NetGameController}'s controller (owner == null) + * → its session-lifetime {@link #localStore}, tier-aware via the local user's FPref.
  • + *
+ */ +public class YieldController { + + private final PlayerControllerHuman owner; + + private boolean autoPassUntilEOT; + private boolean autoPassUntilStackEmpty; + private YieldMarker autoPassUntilMarker; + + /** Priority has passed through any non-target phase since marker activation. */ + private boolean hasLeftMarker; + /** Marker was set while priority was already at its target; require a full cycle to fire. */ + private boolean activationOnMarker; + + private final AutoYieldStore localStore = new AutoYieldStore(); + private final Map> skipPhases = new HashMap<>(); + + public YieldController(PlayerControllerHuman owner) { + this.owner = owner; + } + + public boolean isSkippingPhase(PlayerView turnPlayer, PhaseType phase) { + EnumSet set = skipPhases.get(turnPlayer); + return set != null && set.contains(phase); + } + public void setSkipPhase(PlayerView turnPlayer, PhaseType phase, boolean skip) { + if (turnPlayer == null || phase == null) return; + EnumSet set = skipPhases.computeIfAbsent(turnPlayer, k -> EnumSet.noneOf(PhaseType.class)); + if (skip) set.add(phase); + else set.remove(phase); + } + + public boolean autoPassUntilStackEmpty() { + return autoPassUntilStackEmpty; + } + public boolean autoPassUntilEndOfTurn() { + return autoPassUntilEOT; + } + public YieldMarker getAutoPassUntilMarker() { + return autoPassUntilMarker; + } + + public void setAutoPassUntilStackEmpty(boolean active) { + if (active) autoPassUntilEOT = false; + this.autoPassUntilStackEmpty = active; + } + public void setAutoPassUntilEOTWithoutInterruptions(boolean active) { + this.autoPassUntilEOT = active; + } + + // setMarker/clearMarker are mutated from EDT (right-click), Netty (wire receive), and game thread. + public synchronized void setMarker(PlayerView phaseOwner, PhaseType phase, boolean atOrPastAtClick) { + autoPassUntilEOT = false; + if (phaseOwner == null || phase == null) { + clearMarker(); + return; + } + autoPassUntilMarker = new YieldMarker(phaseOwner, phase); + // Activating at-or-past target on the owner's current turn must wait for next turn's + // occurrence; otherwise pastTarget would fire and clear the marker on the same turn. + hasLeftMarker = !atOrPastAtClick; + activationOnMarker = atOrPastAtClick; + } + public synchronized void clearMarker() { + autoPassUntilMarker = null; + hasLeftMarker = false; + activationOnMarker = false; + } + + /** Click-site helper: true when priority is at or past {@code phase} on {@code phaseOwner}'s current turn. */ + public static boolean isPriorityAtOrPastMarker(GameView gv, PlayerView phaseOwner, PhaseType phase) { + PhaseType currentPhase = gv.getPhase(); + if (!phaseOwner.equals(gv.getPlayerTurn())) return false; + if (currentPhase == null) return false; + return currentPhase == phase || currentPhase.isAfter(phase); + } + + public boolean shouldAutoYield() { + if (autoPassUntilEOT) return true; + GameView gv = owner != null && owner.getGui() != null ? owner.getGui().getGameView() : null; + if (autoPassUntilStackEmpty) { + if (gv != null && gv.peekStack() != null) return true; + autoPassUntilStackEmpty = false; + } + if (autoPassUntilMarker != null && gv != null) { + PlayerView turnPlayer = gv.getPlayerTurn(); + PhaseType currentPhase = gv.getPhase(); + boolean inMarkerOwnerTurn = autoPassUntilMarker.getPhaseOwner().equals(turnPlayer); + boolean atTarget = inMarkerOwnerTurn && currentPhase == autoPassUntilMarker.getPhase(); + boolean pastTarget = inMarkerOwnerTurn && currentPhase != null && currentPhase.isAfter(autoPassUntilMarker.getPhase()); + if (hasLeftMarker && (atTarget || (!activationOnMarker && pastTarget))) { + clearMarker(); + PlayerView local = owner.getLocalPlayerView(); + if (local != null) owner.getGui().applyYieldUpdate(new YieldUpdate.ClearMarker(local)); + } + if (!atTarget && !hasLeftMarker) hasLeftMarker = true; + } + return autoPassUntilMarker != null; + } + + // ---- Auto-yield (per-card/ability) and trigger decisions ---- + + public boolean shouldAutoYield(String key) { + AutoYieldStore store = activeStore(); + if (store.isDisabled()) return false; + if (!tierAware()) { + // Cache: keys stored at storageKey shape (full or stripped) + return store.shouldYield(AutoYieldStore.Tier.GAME, key) + || store.shouldYield(AutoYieldStore.Tier.GAME, AutoYieldStore.abilitySuffix(key)); + } + if (activeModeIsInstall()) { + return PersistentYieldStore.get().contains(AutoYieldStore.abilitySuffix(key)); + } + AutoYieldStore.Tier tier = activeTier(); + boolean abilityScope = tier != AutoYieldStore.Tier.GAME; + String storageKey = abilityScope ? AutoYieldStore.abilitySuffix(key) : key; + return store.shouldYield(tier, storageKey); + } + + /** Tier-aware user-initiated set. Returns the storage key (stripped if ability-scope) for wire propagation. */ + public String setShouldAutoYield(String key, boolean autoYield, boolean abilityScope) { + String storageKey = abilityScope ? AutoYieldStore.abilitySuffix(key) : key; + if (activeModeIsInstall()) { + PersistentYieldStore.get().setYield(storageKey, autoYield); + } else { + activeStore().setYield(activeTier(), storageKey, autoYield); + } + return storageKey; + } + + /** + * Cache mode (host PCH for remote, or NetGameController) → {@link #localStore}. + * Tier-aware mode (host PCH for local player) → LobbyPlayer's persistent store. + */ + private AutoYieldStore activeStore() { + if (owner != null && !owner.isRemoteClient()) { + return ((LobbyPlayerHuman) owner.getLobbyPlayer()).getYieldStore(); + } + return localStore; + } + + /** True if FPref tier/install logic applies (local user context). False for the host's remote-cache mode. */ + private boolean tierAware() { + return owner == null || !owner.isRemoteClient(); + } + + private static boolean activeModeIsInstall() { + return ForgeConstants.AUTO_YIELD_PER_ABILITY_INSTALL.equals(FModel.getPreferences().getPref(FPref.UI_AUTO_YIELD_MODE)); + } + + private static AutoYieldStore.Tier activeTier() { + String mode = FModel.getPreferences().getPref(FPref.UI_AUTO_YIELD_MODE); + if (ForgeConstants.AUTO_YIELD_PER_CARD.equals(mode)) return AutoYieldStore.Tier.GAME; + if (ForgeConstants.AUTO_YIELD_PER_ABILITY_SESSION.equals(mode)) return AutoYieldStore.Tier.SESSION; + return AutoYieldStore.Tier.MATCH; + } + + /** Cache-mode write of a wire-received update. Storage key is already at the right shape. */ + public void applyAutoYieldFromWire(String storageKey, boolean active) { + activeStore().setYield(AutoYieldStore.Tier.GAME, storageKey, active); + } + + public Iterable getAutoYields() { + AutoYieldStore store = activeStore(); + if (!tierAware()) return store.getYields(AutoYieldStore.Tier.GAME); + if (activeModeIsInstall()) return PersistentYieldStore.get().getYields(); + return store.getYields(activeTier()); + } + + public void clearAutoYields() { + if (!tierAware()) { + localStore.clear(); + return; + } + boolean matchOver = owner.getGame() == null || owner.getGame().getView().isMatchOver(); + activeStore().onGameEnd(matchOver); + } + + public boolean getDisableAutoYields() { + return activeStore().isDisabled(); + } + public void setDisableAutoYields(boolean disable) { + activeStore().setDisabled(disable); + } + + public boolean shouldAlwaysAcceptTrigger(int trigger) { + return activeStore().getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.ACCEPT; + } + public boolean shouldAlwaysDeclineTrigger(int trigger) { + return activeStore().getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.DECLINE; + } + + public void setAlwaysAcceptTrigger(int trigger) { + activeStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ACCEPT); + } + public void setAlwaysDeclineTrigger(int trigger) { + activeStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.DECLINE); + } + public void setAlwaysAskTrigger(int trigger) { + activeStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ASK); + } + + public void setTriggerDecision(int trigger, AutoYieldStore.TriggerDecision decision) { + activeStore().setTriggerDecision(trigger, decision); + } + + /** Build the seed payload from this controller's authoritative store. */ + public YieldStateSnapshot buildClientSnapshot(Map> skipPhases) { + Set cardYields = new HashSet<>(); + Set abilityYields = new HashSet<>(); + boolean abilityScope = activeModeIsInstall() || activeTier() != AutoYieldStore.Tier.GAME; + for (String key : getAutoYields()) { + if (abilityScope) abilityYields.add(key); + else cardYields.add(key); + } + // Trigger decisions are per-game; deltas flow during play. + Map triggers = new HashMap<>(); + return new YieldStateSnapshot(cardYields, abilityYields, triggers, getDisableAutoYields(), skipPhases); + } + + /** Atomic seed of client-persistent state at game start or reconnection. Cache mode only. */ + public void applyClientSeed(YieldStateSnapshot snap) { + localStore.clear(); + for (String k : snap.cardYields()) localStore.setYield(AutoYieldStore.Tier.GAME, k, true); + for (String k : snap.abilityYields()) localStore.setYield(AutoYieldStore.Tier.GAME, k, true); + for (Map.Entry e : snap.triggerDecisions().entrySet()) { + localStore.setTriggerDecision(e.getKey(), e.getValue()); + } + localStore.setDisabled(snap.autoYieldsDisabled()); + skipPhases.clear(); + skipPhases.putAll(snap.skipPhases()); + } +} diff --git a/forge-gui/src/main/java/forge/gamemodes/match/YieldMarker.java b/forge-gui/src/main/java/forge/gamemodes/match/YieldMarker.java new file mode 100644 index 00000000000..4e0821d6443 --- /dev/null +++ b/forge-gui/src/main/java/forge/gamemodes/match/YieldMarker.java @@ -0,0 +1,35 @@ +package forge.gamemodes.match; + +import forge.game.phase.PhaseType; +import forge.game.player.PlayerView; + +import java.io.Serializable; +import java.util.Objects; + +/** Immutable (phaseOwner, phase) target of a yield-until-phase intent. */ +public final class YieldMarker implements Serializable { + private static final long serialVersionUID = 1L; + + private final PlayerView phaseOwner; + private final PhaseType phase; + + public YieldMarker(PlayerView phaseOwner, PhaseType phase) { + this.phaseOwner = Objects.requireNonNull(phaseOwner); + this.phase = Objects.requireNonNull(phase); + } + + public PlayerView getPhaseOwner() { return phaseOwner; } + public PhaseType getPhase() { return phase; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof YieldMarker)) return false; + YieldMarker other = (YieldMarker) o; + return phaseOwner.equals(other.phaseOwner) && phase == other.phase; + } + @Override + public int hashCode() { return Objects.hash(phaseOwner, phase); } + @Override + public String toString() { return phaseOwner + "@" + phase; } +} diff --git a/forge-gui/src/main/java/forge/gamemodes/match/YieldStateSnapshot.java b/forge-gui/src/main/java/forge/gamemodes/match/YieldStateSnapshot.java new file mode 100644 index 00000000000..0e4ef02e470 --- /dev/null +++ b/forge-gui/src/main/java/forge/gamemodes/match/YieldStateSnapshot.java @@ -0,0 +1,23 @@ +package forge.gamemodes.match; + +import forge.game.phase.PhaseType; +import forge.game.player.PlayerView; +import forge.player.AutoYieldStore; + +import java.io.Serializable; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +/** + * Atomic snapshot of a client's persistent yield state, transmitted at + * game start (and on reconnection) so the host's PCH proxy is fully + * seeded in one wire message instead of N. + */ +public record YieldStateSnapshot( + Set cardYields, + Set abilityYields, + Map triggerDecisions, + boolean autoYieldsDisabled, + Map> skipPhases +) implements Serializable {} diff --git a/forge-gui/src/main/java/forge/gamemodes/match/YieldUpdate.java b/forge-gui/src/main/java/forge/gamemodes/match/YieldUpdate.java new file mode 100644 index 00000000000..09096e6cdcc --- /dev/null +++ b/forge-gui/src/main/java/forge/gamemodes/match/YieldUpdate.java @@ -0,0 +1,38 @@ +package forge.gamemodes.match; + +import forge.game.phase.PhaseType; +import forge.game.player.PlayerView; +import forge.player.AutoYieldStore; + +import java.io.Serializable; + +/** + * Unified envelope for all yield-related sync between client and host. + * + * Receiver dispatches via exhaustive switch in PlayerControllerHuman + * (host-side) and NetworkGuiGame (client-side). + */ +public sealed interface YieldUpdate extends Serializable + permits YieldUpdate.SetMarker, + YieldUpdate.ClearMarker, + YieldUpdate.StackYield, + YieldUpdate.TriggerDecision, + YieldUpdate.CardAutoYield, + YieldUpdate.SkipPhase, + YieldUpdate.SeedFromClient { + + /** {@code atOrPastAtClick}: priority was at-or-past target on owner's turn when the user clicked — computed by the UI so client cache and host PCH initialize identically. */ + record SetMarker(PlayerView phaseOwner, PhaseType phase, boolean atOrPastAtClick) implements YieldUpdate {} + + record ClearMarker(PlayerView player) implements YieldUpdate {} + + record StackYield(PlayerView player, boolean active) implements YieldUpdate {} + + record TriggerDecision(int trigId, AutoYieldStore.TriggerDecision decision) implements YieldUpdate {} + + record CardAutoYield(String cardKey, boolean active, boolean abilityScope) implements YieldUpdate {} + + record SkipPhase(PlayerView turnPlayer, PhaseType phase, boolean skip) implements YieldUpdate {} + + record SeedFromClient(YieldStateSnapshot snapshot) implements YieldUpdate {} +} diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java index be1a4600bcb..2b02c874848 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputLockUI.java @@ -4,6 +4,8 @@ import forge.game.player.Player; import forge.game.player.PlayerView; import forge.game.spellability.SpellAbility; +import forge.gamemodes.match.YieldController; +import forge.gamemodes.match.YieldUpdate; import forge.gui.FThreads; import forge.player.PlayerControllerHuman; import forge.util.ITriggerEvent; @@ -58,8 +60,12 @@ public void run() { private final Runnable showMessageFromEdt = new Runnable() { @Override public void run() { - controller.getGui().updateButtons(InputLockUI.this.getOwner(), "", "", false, false, false); - showMessage(Localizer.getInstance().getMessage("lblWaitingforActions")); + if (controller.mayAutoPass()) { + controller.getGui().updateAutoPassPrompt(); + } else { + controller.getGui().updateButtons(InputLockUI.this.getOwner(), "", "", false, false, false); + showMessage(Localizer.getInstance().getMessage("lblWaitingforActions")); + } } }; @@ -87,9 +93,21 @@ public void selectButtonOK() { } @Override public void selectButtonCancel() { - //cancel auto pass for all players - for (final Player player : controller.getGame().getPlayers()) { - player.getController().autoPassCancel(); + // autoPassCancel first: its mayAutoPass gate needs at least one mode still active to do UI updates. + controller.autoPassCancel(); + YieldController yc = controller.getYieldController(); + PlayerView pv = controller.getLocalPlayerView(); + if (yc.getAutoPassUntilMarker() != null) { + yc.clearMarker(); + if (controller.getGui() != null) { + controller.getGui().applyYieldUpdate(new YieldUpdate.ClearMarker(pv)); + } + } + if (yc.autoPassUntilStackEmpty()) { + yc.setAutoPassUntilStackEmpty(false); + if (controller.getGui() != null) { + controller.getGui().applyYieldUpdate(new YieldUpdate.StackYield(pv, false)); + } } } diff --git a/forge-gui/src/main/java/forge/gamemodes/net/NetworkGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/net/NetworkGuiGame.java index e0549bcddd7..d8842f6033e 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/NetworkGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/NetworkGuiGame.java @@ -10,6 +10,9 @@ import forge.game.zone.ZoneType; import forge.gamemodes.match.AbstractGuiGame; import forge.gamemodes.net.client.NetGameController; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; import forge.interfaces.IGameController; import forge.trackable.Tracker; import forge.trackable.TrackableCollection; @@ -616,16 +619,28 @@ protected final void pushSkipPhaseToControllers(final PlayerView player, final P } } - protected final void seedSkipPhaseCache() { + /** + * Replace the host's persistent yield state for each controlled player + * in one atomic message: auto-yields and trigger-disabled flag from the + * AutoYieldStore, skip-phase prefs from PhaseLabel state. Per-key edits + * during play flow as individual YieldUpdate deltas. + */ + protected final void seedYieldStateOnHost() { + Map> skipPhases = new HashMap<>(); + for (PlayerView p : getGameView().getPlayers()) { + EnumSet set = EnumSet.noneOf(PhaseType.class); + for (PhaseType ph : PhaseType.values()) { + if (isUiSetToSkipPhase(p, ph)) { + set.add(ph); + } + } + if (!set.isEmpty()) { + skipPhases.put(p, set); + } + } for (final IGameController c : getOriginalGameControllers()) { if (c instanceof NetGameController nc) { - for (PlayerView p : getGameView().getPlayers()) { - for (PhaseType ph : PhaseType.values()) { - if (isUiSetToSkipPhase(p, ph)) { - nc.setUiShouldSkipPhase(p, ph, Boolean.TRUE); - } - } - } + nc.seedYieldStateOnHost(skipPhases); } } } diff --git a/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java b/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java index 2aac7bd9f63..44f3146dbcc 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java @@ -5,11 +5,11 @@ import forge.game.GameView; import forge.game.card.CardView; -import forge.game.phase.PhaseType; import forge.game.player.DelayedReveal; import forge.game.player.PlayerView; import forge.game.spellability.SpellAbilityView; import forge.gamemodes.match.NextGameDecision; +import forge.gamemodes.match.YieldUpdate; import forge.gui.GuiBase; import forge.gui.interfaces.IGuiGame; import forge.interfaces.IGameController; @@ -74,6 +74,8 @@ public enum ProtocolMethod implements IHasForgeLog { showWaitingTimer (Mode.SERVER, Void.TYPE, PlayerView.class, String.class), setHighlighted (Mode.SERVER, Void.TYPE, GameEntityView.class, Boolean.TYPE), applyDelta (Mode.SERVER, Void.TYPE, DeltaPacket.class), + /** Server→client push of authoritative yield-state changes. */ + applyYieldUpdate (Mode.SERVER, Void.TYPE, YieldUpdate.class), // Client -> Server // Note: these should all return void, to avoid awkward situations in @@ -94,11 +96,7 @@ public enum ProtocolMethod implements IHasForgeLog { alphaStrike (Mode.CLIENT, Void.TYPE), reorderHand (Mode.CLIENT, Void.TYPE, CardView.class, Integer.TYPE), requestResync (Mode.CLIENT, Void.TYPE), - setShouldAutoYield (Mode.CLIENT, Void.TYPE, String.class, Boolean.TYPE, Boolean.TYPE), - setShouldAlwaysAcceptTrigger (Mode.CLIENT, Void.TYPE, Integer.TYPE), - setShouldAlwaysDeclineTrigger (Mode.CLIENT, Void.TYPE, Integer.TYPE), - setShouldAlwaysAskTrigger (Mode.CLIENT, Void.TYPE, Integer.TYPE), - setUiShouldSkipPhase (Mode.CLIENT, Void.TYPE, PlayerView.class, PhaseType.class, Boolean.TYPE); + sendYieldUpdate (Mode.CLIENT, Void.TYPE, YieldUpdate.class); private enum Mode { SERVER(IGuiGame.class), diff --git a/forge-gui/src/main/java/forge/gamemodes/net/client/FGameClient.java b/forge-gui/src/main/java/forge/gamemodes/net/client/FGameClient.java index 28b91d5cdeb..a6f0d4c0ef4 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/client/FGameClient.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/client/FGameClient.java @@ -168,7 +168,6 @@ void setGameControllers(final Iterable myPlayers) { for (final PlayerView p : myPlayers) { NetGameController controller = new NetGameController(this); clientGui.setOriginalGameController(p, controller); - controller.replayActiveYields(); } } diff --git a/forge-gui/src/main/java/forge/gamemodes/net/client/NetGameController.java b/forge-gui/src/main/java/forge/gamemodes/net/client/NetGameController.java index cabab229155..62978501ed5 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/client/NetGameController.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/client/NetGameController.java @@ -6,30 +6,36 @@ import forge.game.player.actions.PlayerAction; import forge.game.spellability.SpellAbilityView; import forge.gamemodes.match.NextGameDecision; +import forge.gamemodes.match.YieldController; +import forge.gamemodes.match.YieldUpdate; import forge.gamemodes.net.GameProtocolSender; import forge.gamemodes.net.ProtocolMethod; import forge.interfaces.IDevModeCheats; import forge.interfaces.IGameController; import forge.interfaces.IMacroSystem; -import forge.localinstance.properties.ForgeConstants; -import forge.localinstance.properties.ForgePreferences; -import forge.model.FModel; import forge.player.AutoYieldStore; -import forge.player.PersistentYieldStore; import forge.util.ITriggerEvent; +import java.util.EnumSet; import java.util.List; +import java.util.Map; public class NetGameController implements IGameController { private final GameProtocolSender sender; - private final AutoYieldStore yieldStore = new AutoYieldStore(); + /** Source of truth for this client's yield state (auto-yields, trigger decisions, markers, skip-phase, etc.). */ + private final YieldController yieldController = new YieldController(null); public NetGameController(final IToServer server) { sender = new GameProtocolSender(server); } + @Override + public YieldController getYieldController() { + return yieldController; + } + private void send(final ProtocolMethod method, final Object... args) { sender.send(method, args); } @@ -135,100 +141,81 @@ public void requestResync() { send(ProtocolMethod.requestResync); } - private boolean activeModeIsInstall() { - return ForgeConstants.AUTO_YIELD_PER_ABILITY_INSTALL.equals( - FModel.getPreferences().getPref(ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); - } - - private boolean activeModeIsAbilityScope() { - return !ForgeConstants.AUTO_YIELD_PER_CARD.equals( - FModel.getPreferences().getPref(ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); - } - - private AutoYieldStore.Tier activeTier() { - String mode = FModel.getPreferences().getPref(ForgePreferences.FPref.UI_AUTO_YIELD_MODE); - if (ForgeConstants.AUTO_YIELD_PER_CARD.equals(mode)) return AutoYieldStore.Tier.GAME; - if (ForgeConstants.AUTO_YIELD_PER_ABILITY_SESSION.equals(mode)) return AutoYieldStore.Tier.SESSION; - return AutoYieldStore.Tier.MATCH; - } - @Override public boolean shouldAutoYield(final String key) { - if (yieldStore.isDisabled()) return false; - if (activeModeIsInstall()) { - return PersistentYieldStore.get().contains(AutoYieldStore.abilitySuffix(key)); - } - String storageKey = activeModeIsAbilityScope() ? AutoYieldStore.abilitySuffix(key) : key; - return yieldStore.shouldYield(activeTier(), storageKey); + return yieldController.shouldAutoYield(key); } @Override public void setShouldAutoYield(final String key, final boolean autoYield, final boolean isAbilityScope) { - String storageKey = isAbilityScope ? AutoYieldStore.abilitySuffix(key) : key; - if (activeModeIsInstall()) { - PersistentYieldStore.get().setYield(storageKey, autoYield); - } else { - yieldStore.setYield(activeTier(), storageKey, autoYield); - } - send(ProtocolMethod.setShouldAutoYield, storageKey, autoYield, isAbilityScope); - } - - @Override - public Iterable getAutoYields() { - return activeModeIsInstall() - ? PersistentYieldStore.get().getYields() - : yieldStore.getYields(activeTier()); + String storageKey = yieldController.setShouldAutoYield(key, autoYield, isAbilityScope); + send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.CardAutoYield(storageKey, autoYield, isAbilityScope)); } @Override - public void clearAutoYields() { - // No-op locally: tier lifecycle is driven separately. Server-side mirror is cleared by HostedMatch. - } - - @Override - public boolean getDisableAutoYields() { return yieldStore.isDisabled(); } - + public boolean getDisableAutoYields() { return yieldController.getDisableAutoYields(); } @Override - public void setDisableAutoYields(final boolean disable) { yieldStore.setDisabled(disable); } + public void setDisableAutoYields(final boolean disable) { yieldController.setDisableAutoYields(disable); } @Override public boolean shouldAlwaysAcceptTrigger(final int trigger) { - return yieldStore.getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.ACCEPT; + return yieldController.shouldAlwaysAcceptTrigger(trigger); } - @Override public boolean shouldAlwaysDeclineTrigger(final int trigger) { - return yieldStore.getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.DECLINE; + return yieldController.shouldAlwaysDeclineTrigger(trigger); } @Override public void setShouldAlwaysAcceptTrigger(final int trigger) { - yieldStore.setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ACCEPT); - send(ProtocolMethod.setShouldAlwaysAcceptTrigger, trigger); + yieldController.setAlwaysAcceptTrigger(trigger); + send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.TriggerDecision(trigger, AutoYieldStore.TriggerDecision.ACCEPT)); } - @Override public void setShouldAlwaysDeclineTrigger(final int trigger) { - yieldStore.setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.DECLINE); - send(ProtocolMethod.setShouldAlwaysDeclineTrigger, trigger); + yieldController.setAlwaysDeclineTrigger(trigger); + send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.TriggerDecision(trigger, AutoYieldStore.TriggerDecision.DECLINE)); } - @Override public void setShouldAlwaysAskTrigger(final int trigger) { - yieldStore.setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ASK); - send(ProtocolMethod.setShouldAlwaysAskTrigger, trigger); + yieldController.setAlwaysAskTrigger(trigger); + send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.TriggerDecision(trigger, AutoYieldStore.TriggerDecision.ASK)); } - public void replayActiveYields() { - boolean abilityScope = activeModeIsAbilityScope(); - for (String key : getAutoYields()) { - send(ProtocolMethod.setShouldAutoYield, key, Boolean.TRUE, abilityScope); + public void setUiShouldSkipPhase(final PlayerView turnPlayer, final PhaseType phase, final boolean shouldSkip) { + send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.SkipPhase(turnPlayer, phase, shouldSkip)); + } + + @Override + public void applyYieldUpdate(final YieldUpdate update) { + // Local self-apply for marker/stack-yield user actions that route through + // sendYieldUpdate. Other cases dispatch via dedicated setters above. + if (update instanceof YieldUpdate.SetMarker u) { + yieldController.setMarker(u.phaseOwner(), u.phase(), u.atOrPastAtClick()); + } else if (update instanceof YieldUpdate.ClearMarker) { + yieldController.clearMarker(); + } else if (update instanceof YieldUpdate.StackYield u) { + yieldController.setAutoPassUntilStackEmpty(u.active()); } } + /** + * Remote client's outbound path for user-initiated yield actions (right-click + * marker, ESC, "yield to entire stack" menu): mutate local cache for immediate + * UI response and ship to host. + */ @Override - public void setUiShouldSkipPhase(final PlayerView turnPlayer, final PhaseType phase, final boolean shouldSkip) { - send(ProtocolMethod.setUiShouldSkipPhase, turnPlayer, phase, shouldSkip); + public void sendYieldUpdate(final YieldUpdate update) { + applyYieldUpdate(update); + send(ProtocolMethod.sendYieldUpdate, update); + } + + /** + * Build a YieldStateSnapshot from the local persistent yield state plus the + * GUI-loaded skip-phase prefs and ship it to the host in one wire message. + */ + public void seedYieldStateOnHost(Map> skipPhases) { + send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.SeedFromClient(yieldController.buildClientSnapshot(skipPhases))); } private IMacroSystem macros; diff --git a/forge-gui/src/main/java/forge/gamemodes/net/server/RemoteClientGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/net/server/RemoteClientGuiGame.java index f9193b422e2..2f373fedcec 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/server/RemoteClientGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/server/RemoteClientGuiGame.java @@ -13,6 +13,7 @@ import forge.game.player.PlayerView; import forge.game.spellability.SpellAbilityView; import forge.game.zone.ZoneType; +import forge.gamemodes.match.YieldUpdate; import forge.gamemodes.net.NetworkGuiGame; import forge.gamemodes.net.DeltaPacket; import forge.gamemodes.net.GameProtocolSender; @@ -275,6 +276,11 @@ public void showPromptMessage(final PlayerView playerView, final String message) send(ProtocolMethod.showPromptMessage, playerView, message); } + @Override + public void applyYieldUpdate(final YieldUpdate update) { + send(ProtocolMethod.applyYieldUpdate, update); + } + @Override public void showCardPromptMessage(final PlayerView playerView, final String message, final CardView card) { updateGameView(); diff --git a/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java b/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java index 24a1f89f3e1..514917f447b 100644 --- a/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java +++ b/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java @@ -15,6 +15,7 @@ import forge.game.player.PlayerView; import forge.game.spellability.SpellAbilityView; import forge.game.zone.ZoneType; +import forge.gamemodes.match.YieldUpdate; import forge.gamemodes.match.input.InputConfirm; import forge.gamemodes.net.DeltaPacket; import forge.gui.control.PlaybackSpeed; @@ -261,9 +262,6 @@ default List many(final String title, final String topCaption, final int boolean isUiSetToSkipPhase(PlayerView playerTurn, PhaseType phase); - void autoPassUntilEndOfTurn(PlayerView player); - boolean mayAutoPass(PlayerView player); - void autoPassCancel(PlayerView player); void updateAutoPassPrompt(); void setCurrentPlayer(PlayerView player); @@ -274,6 +272,12 @@ default List many(final String title, final String topCaption, final int */ void applyDelta(DeltaPacket packet); + /** Repaint marker chevron / stack-yield UI for the given player. */ + default void refreshYieldUi(PlayerView player) {} + + /** Apply an authoritative yield-state change. {@link forge.gamemodes.match.AbstractGuiGame} routes to the local {@link forge.interfaces.IGameController}; {@link forge.gamemodes.net.server.RemoteClientGuiGame} forwards over the wire. */ + void applyYieldUpdate(YieldUpdate update); + /** Returns true if this game instance is a network game. */ boolean isNetGame(); void setNetGame(); diff --git a/forge-gui/src/main/java/forge/interfaces/IGameController.java b/forge-gui/src/main/java/forge/interfaces/IGameController.java index b08517d37d9..d10bb1032d8 100644 --- a/forge-gui/src/main/java/forge/interfaces/IGameController.java +++ b/forge-gui/src/main/java/forge/interfaces/IGameController.java @@ -3,10 +3,11 @@ import java.util.List; import forge.game.card.CardView; -import forge.game.phase.PhaseType; import forge.game.player.PlayerView; import forge.game.spellability.SpellAbilityView; import forge.gamemodes.match.NextGameDecision; +import forge.gamemodes.match.YieldController; +import forge.gamemodes.match.YieldUpdate; import forge.util.ITriggerEvent; public interface IGameController { @@ -25,10 +26,6 @@ public interface IGameController { void selectButtonCancel(); - void passPriority(); - - void passPriorityUntilEndOfTurn(); - void selectPlayer(PlayerView playerView, ITriggerEvent triggerEvent); boolean selectCard(CardView cardView, List otherCardViewsToSelect, ITriggerEvent triggerEvent); @@ -53,7 +50,10 @@ public interface IGameController { */ void requestResync(); - // --- Auto-yield preferences (per-player) --- + void passPriority(); + void passPriorityUntilEndOfTurn(); + + // Auto-yield preferences boolean shouldAutoYield(String key); /** * @param isAbilityScope true if {@code key} is an ability suffix (Per Ability * modes); @@ -61,17 +61,26 @@ public interface IGameController { * route storage by this flag instead of consulting the host's own UI_AUTO_YIELD_MODE. */ void setShouldAutoYield(String key, boolean autoYield, boolean isAbilityScope); - Iterable getAutoYields(); - void clearAutoYields(); boolean getDisableAutoYields(); void setDisableAutoYields(boolean disable); - // --- Trigger accept/decline preferences (per-player) --- + // Trigger accept/decline preferences boolean shouldAlwaysAcceptTrigger(int trigger); boolean shouldAlwaysDeclineTrigger(int trigger); void setShouldAlwaysAcceptTrigger(int trigger); void setShouldAlwaysDeclineTrigger(int trigger); void setShouldAlwaysAskTrigger(int trigger); - void setUiShouldSkipPhase(PlayerView turnPlayer, PhaseType phase, boolean shouldSkip); + /** Apply a unified yield update envelope to this controller's YieldController. */ + void applyYieldUpdate(YieldUpdate update); + + /** + * Wire entry for client->host yield update; receivers route to applyYieldUpdate. + * Named to match the {@code sendYieldUpdate} ProtocolMethod (lookup is by name). + */ + default void sendYieldUpdate(YieldUpdate update) { + applyYieldUpdate(update); + } + + YieldController getYieldController(); } diff --git a/forge-gui/src/main/java/forge/player/AutoYieldStore.java b/forge-gui/src/main/java/forge/player/AutoYieldStore.java index 28ad9f2ff0d..fce4832a2e4 100644 --- a/forge-gui/src/main/java/forge/player/AutoYieldStore.java +++ b/forge-gui/src/main/java/forge/player/AutoYieldStore.java @@ -29,14 +29,13 @@ public void setYield(Tier tier, String key, boolean autoYield) { } public Iterable getYields(Tier tier) { return yieldsByTier.get(tier); } + public boolean isDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } public TriggerDecision getTriggerDecision(int triggerId) { - TriggerDecision d = triggerDecisions.get(triggerId); - return d == null ? TriggerDecision.ASK : d; + return triggerDecisions.getOrDefault(triggerId, TriggerDecision.ASK); } - public void setTriggerDecision(int triggerId, TriggerDecision decision) { if (decision == TriggerDecision.ASK) triggerDecisions.remove(triggerId); else triggerDecisions.put(triggerId, decision); @@ -50,6 +49,13 @@ public void onGameEnd(boolean matchOver) { } } + /** Wipe all yields, trigger decisions, and the disabled flag — used to reseed the cache from a client snapshot. */ + public void clear() { + for (Set set : yieldsByTier.values()) set.clear(); + triggerDecisions.clear(); + disabled = false; + } + /** Strips the "Card (id=N): " prefix to derive the ability-scope key, or returns the input unchanged. */ public static String abilitySuffix(String rawKey) { return rawKey.contains("): ") ? rawKey.substring(rawKey.indexOf("): ") + 3) : rawKey; diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 3d4a9186cc9..14c8e631eb5 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -48,6 +48,8 @@ import forge.game.zone.Zone; import forge.game.zone.ZoneType; import forge.gamemodes.match.NextGameDecision; +import forge.gamemodes.match.YieldController; +import forge.gamemodes.match.YieldUpdate; import forge.gamemodes.match.input.*; import forge.util.IHasForgeLog; import forge.gui.FThreads; @@ -100,16 +102,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont private IGuiGame gui; - // Inlined server-side mirror used only when this controller serves a remote network - // player (gui instanceof RemoteClientGuiGame). Two scope buckets so the host doesn't - // need to know the client's UI_AUTO_YIELD_MODE: the client tells us which bucket via - // the isAbilityScope flag on setShouldAutoYield. - private final Set remoteCardYields = Sets.newHashSet(); - private final Set remoteAbilityYields = Sets.newHashSet(); - private final Map remoteTriggerDecisions = Maps.newTreeMap(); - private boolean remoteAutoYieldsDisabled; - // Empty/missing entry returns false (don't skip), matching a fresh UI's conservative default - private final Map> remoteSkipPhases = Maps.newHashMap(); + private final YieldController yieldController = new YieldController(this); protected final InputQueue inputQueue; protected final InputProxy inputProxy; @@ -139,6 +132,10 @@ public final void setGui(final IGuiGame gui) { this.gui = gui; } + public YieldController getYieldController() { + return yieldController; + } + public final InputQueue getInputQueue() { return inputQueue; } @@ -1529,8 +1526,7 @@ public List chooseSpellAbilityToPlay() { } if (stack.isEmpty()) { - if (isUiSetToSkipPhase(getGame().getPhaseHandler().getPlayerTurn().getView(), - getGame().getPhaseHandler().getPhase())) { + if (isUiSetToSkipPhase(getGame().getPhaseHandler().getPlayerTurn().getView(), getGame().getPhaseHandler().getPhase())) { netLog.trace("Returning null (skipPhase) for player {}", player.getName()); return null; // avoid prompt for input if stack is empty and // player is set to skip the current phase @@ -2369,7 +2365,7 @@ public boolean tryUndoLastAction() { // ensure prompt updated if needed currentInput.showMessageInitial(); } - if (gui.isNetGame()) { + if (getGui().isNetGame()) { // Flush events to remote clients — the undo modifies game state // (untaps lands, etc.) after the prompt is shown, and without this // the updated state sits in the forwarder buffer until the next action. @@ -3345,23 +3341,6 @@ public void concede() { } } - public boolean mayAutoPass() { - return getGui().mayAutoPass(getLocalPlayerView()); - } - - public void autoPassUntilEndOfTurn() { - getGui().autoPassUntilEndOfTurn(getLocalPlayerView()); - } - - @Override - public void autoPassCancel() { - if (getGui() == null) { - return; - } - - getGui().autoPassCancel(getLocalPlayerView()); - } - @Override public void awaitNextInput() { getGui().awaitNextInput(); @@ -3479,140 +3458,110 @@ public void requestResync() { // No-op for local games - resync is only used for network play } - private boolean isRemoteClient() { + public boolean isRemoteClient() { return gui instanceof forge.gamemodes.net.server.RemoteClientGuiGame; } - private AutoYieldStore localStore() { - return ((LobbyPlayerHuman) getLobbyPlayer()).getYieldStore(); - } - - private boolean activeModeIsInstall() { - return ForgeConstants.AUTO_YIELD_PER_ABILITY_INSTALL.equals(FModel.getPreferences().getPref(FPref.UI_AUTO_YIELD_MODE)); - } - - private AutoYieldStore.Tier activeTier() { - String mode = FModel.getPreferences().getPref(FPref.UI_AUTO_YIELD_MODE); - if (ForgeConstants.AUTO_YIELD_PER_CARD.equals(mode)) return AutoYieldStore.Tier.GAME; - if (ForgeConstants.AUTO_YIELD_PER_ABILITY_SESSION.equals(mode)) return AutoYieldStore.Tier.SESSION; - return AutoYieldStore.Tier.MATCH; + public boolean mayAutoPass() { + return yieldController.shouldAutoYield(); } - @Override - public boolean shouldAutoYield(final String key) { - if (isRemoteClient()) { - if (remoteAutoYieldsDisabled) return false; - if (remoteCardYields.contains(key)) return true; - return remoteAbilityYields.contains(AutoYieldStore.abilitySuffix(key)); - } - if (localStore().isDisabled()) return false; - if (activeModeIsInstall()) { - return PersistentYieldStore.get().contains(AutoYieldStore.abilitySuffix(key)); - } - boolean abilityScope = activeTier() != AutoYieldStore.Tier.GAME; - String storageKey = abilityScope ? AutoYieldStore.abilitySuffix(key) : key; - return localStore().shouldYield(activeTier(), storageKey); + public void autoPassUntilEndOfTurn() { + yieldController.setAutoPassUntilEOTWithoutInterruptions(true); + getGui().updateAutoPassPrompt(); } @Override - public void setShouldAutoYield(final String key, final boolean autoYield, final boolean isAbilityScope) { - if (isRemoteClient()) { - Set bucket = isAbilityScope ? remoteAbilityYields : remoteCardYields; - if (autoYield) bucket.add(key); - else bucket.remove(key); - return; - } - String storageKey = isAbilityScope ? AutoYieldStore.abilitySuffix(key) : key; - if (activeModeIsInstall()) { - PersistentYieldStore.get().setYield(storageKey, autoYield); + public void autoPassCancel() { + if (!mayAutoPass()) { return; } - localStore().setYield(activeTier(), storageKey, autoYield); + yieldController.setAutoPassUntilEOTWithoutInterruptions(false); + PlayerView playerView = getLocalPlayerView(); + getGui().showPromptMessage(playerView, ""); + getGui().updateButtons(playerView, false, false, false); + getGui().awaitNextInput(); } @Override - public Iterable getAutoYields() { - if (isRemoteClient()) { - return Iterables.concat(remoteCardYields, remoteAbilityYields); - } - if (activeModeIsInstall()) return PersistentYieldStore.get().getYields(); - return localStore().getYields(activeTier()); + public boolean shouldAutoYield(final String key) { + return yieldController.shouldAutoYield(key); } - @Override - public void clearAutoYields() { - if (isRemoteClient()) { - remoteCardYields.clear(); - remoteAbilityYields.clear(); - remoteTriggerDecisions.clear(); - return; - } - localStore().onGameEnd(getGame() == null || getGame().getView().isMatchOver()); + public void setShouldAutoYield(final String key, final boolean autoYield, final boolean isAbilityScope) { + yieldController.setShouldAutoYield(key, autoYield, isAbilityScope); } @Override public boolean getDisableAutoYields() { - return isRemoteClient() ? remoteAutoYieldsDisabled : localStore().isDisabled(); + return yieldController.getDisableAutoYields(); } - @Override public void setDisableAutoYields(final boolean disable) { - if (isRemoteClient()) remoteAutoYieldsDisabled = disable; - else localStore().setDisabled(disable); + yieldController.setDisableAutoYields(disable); } @Override public boolean shouldAlwaysAcceptTrigger(final int trigger) { - if (isRemoteClient()) return Boolean.TRUE.equals(remoteTriggerDecisions.get(trigger)); - return localStore().getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.ACCEPT; + return yieldController.shouldAlwaysAcceptTrigger(trigger); } - @Override public boolean shouldAlwaysDeclineTrigger(final int trigger) { - if (isRemoteClient()) return Boolean.FALSE.equals(remoteTriggerDecisions.get(trigger)); - return localStore().getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.DECLINE; + return yieldController.shouldAlwaysDeclineTrigger(trigger); } @Override public void setShouldAlwaysAcceptTrigger(final int trigger) { - if (isRemoteClient()) remoteTriggerDecisions.put(trigger, Boolean.TRUE); - else localStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ACCEPT); + yieldController.setAlwaysAcceptTrigger(trigger); if (isPromptingForTrigger(trigger)) selectButtonOk(); } - @Override public void setShouldAlwaysDeclineTrigger(final int trigger) { - if (isRemoteClient()) remoteTriggerDecisions.put(trigger, Boolean.FALSE); - else localStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.DECLINE); + yieldController.setAlwaysDeclineTrigger(trigger); if (isPromptingForTrigger(trigger)) selectButtonCancel(); } - + @Override + public void setShouldAlwaysAskTrigger(final int trigger) { + yieldController.setAlwaysAskTrigger(trigger); + } private boolean isPromptingForTrigger(final int trigger) { if (!(inputQueue.getInput() instanceof InputConfirm)) return false; final SpellAbilityStackInstance top = getGame().getStack().peek(); return top != null && top.isStateTrigger(trigger); } - @Override - public void setShouldAlwaysAskTrigger(final int trigger) { - if (isRemoteClient()) remoteTriggerDecisions.remove(trigger); - else localStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ASK); - } - public boolean isUiSetToSkipPhase(final PlayerView turnPlayer, final PhaseType phase) { if (isRemoteClient()) { - EnumSet set = remoteSkipPhases.get(turnPlayer); - return set != null && set.contains(phase); + return yieldController.isSkippingPhase(turnPlayer, phase); } return getGui().isUiSetToSkipPhase(turnPlayer, phase); } @Override - public void setUiShouldSkipPhase(final PlayerView turnPlayer, final PhaseType phase, final boolean shouldSkip) { - if (!isRemoteClient()) return; - EnumSet set = remoteSkipPhases.computeIfAbsent(turnPlayer, - k -> EnumSet.noneOf(PhaseType.class)); - if (shouldSkip) set.add(phase); - else set.remove(phase); + public void applyYieldUpdate(final YieldUpdate update) { + boolean activatedYield = false; + if (update instanceof YieldUpdate.SetMarker u) { + yieldController.setMarker(u.phaseOwner(), u.phase(), u.atOrPastAtClick()); + activatedYield = true; + } else if (update instanceof YieldUpdate.ClearMarker) { + yieldController.clearMarker(); + } else if (update instanceof YieldUpdate.StackYield u) { + yieldController.setAutoPassUntilStackEmpty(u.active()); + activatedYield = u.active(); + } else if (update instanceof YieldUpdate.TriggerDecision u) { + yieldController.setTriggerDecision(u.trigId(), u.decision()); + } else if (update instanceof YieldUpdate.CardAutoYield u) { + yieldController.applyAutoYieldFromWire(u.cardKey(), u.active()); + } else if (update instanceof YieldUpdate.SkipPhase u) { + yieldController.setSkipPhase(u.turnPlayer(), u.phase(), u.skip()); + } else if (update instanceof YieldUpdate.SeedFromClient u) { + yieldController.applyClientSeed(u.snapshot()); + } + if (activatedYield) { + // Switch the cancel button + prompt to "Yielding until X" so the user can disarm. + // Otherwise the previous InputPassPriority "End Turn" label would persist and ESC + // would skip the click on the client. + getGui().updateAutoPassPrompt(); + } } }