Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9e65d29
Add experimental yield system for reduced multiplayer micromanagement
MostCromulent Jan 28, 2026
b47d81a
Add preferences GUI toggle for experimental yield options
MostCromulent Jan 28, 2026
5f4d7c8
Fix experimental yield system bugs and improve UX
MostCromulent Jan 28, 2026
284c10a
Fix multiplayer yield issues and simplify yield logic
MostCromulent Jan 28, 2026
f5f7287
Add PR documentation for experimental yield system
MostCromulent Jan 28, 2026
17b8822
Improve yield system integration and fix End Turn behavior
MostCromulent Jan 28, 2026
35a7c3f
Add authorship section to PR documentation
MostCromulent Jan 28, 2026
34fc365
Add new yield modes and F-key hotkeys for yield system
MostCromulent Jan 29, 2026
9595faf
Fix yield timing for End Step and Your Turn buttons
MostCromulent Jan 29, 2026
2eb6bc0
Add auto-suppress for declined suggestions and bug fixes
MostCromulent Jan 29, 2026
43b3898
Add mass removal interrupt option for yield system
MostCromulent Jan 29, 2026
59086a6
Add Yield Until Next Phase mode with dynamic hotkey display
MostCromulent Jan 29, 2026
57466cd
Comprehensively update DOCUMENTATION.md for accuracy and completeness
claude Jan 29, 2026
b100b93
Merge pull request #12 from MostCromulent/claude/update-documentation…
MostCromulent Jan 29, 2026
aeed733
Remove redundant PR documentation file
MostCromulent Jan 30, 2026
e1104e6
Fix targeting interrupt not detecting sub-ability targets
MostCromulent Jan 30, 2026
85dbd6a
Remove duplicate hasAvailableActions function
MostCromulent Jan 30, 2026
9211d97
Fix yield system for multiplayer non-host players
MostCromulent Jan 30, 2026
1f9e92e
Network-safe smart suggestions and yield button fixes
MostCromulent Jan 30, 2026
c21416e
Fix yield sync recursion and smart suggestion mana checking
MostCromulent Jan 31, 2026
6ddc7f6
Fix yield panel disappearing on layout refresh and improve 2-player l…
MostCromulent Jan 31, 2026
4b8c1b1
Add Expanded Yield Options wiki documentation
MostCromulent Jan 31, 2026
98f458b
Refactor PlayerView lookup to reuse existing method
MostCromulent Jan 31, 2026
b7442b0
Shift yield hotkeys from F1-F6 to F2-F7 to avoid F1=Help conflict
MostCromulent Jan 31, 2026
47947e3
Disable smart yield suggestions on mobile GUI
MostCromulent Jan 31, 2026
b586166
Add toggle behavior for yield buttons
MostCromulent Jan 31, 2026
bf9910b
Consolidate yield state tracking into YieldState class
MostCromulent Jan 31, 2026
cbcd373
Make hasAvailableActions computation conditional on experimental yields
MostCromulent Feb 1, 2026
9db3049
Add toggles for suggestion suppression behavior
MostCromulent Feb 1, 2026
925c37c
Remove DOCUMENTATION.md (moved to NetworkPlay/dev)
MostCromulent Feb 1, 2026
8cdeacd
Move yield computation to PlayerView and query controller for prefere…
MostCromulent Feb 1, 2026
c5b154d
Add preference guard to updateWillLoseManaAtEndOfPhase
MostCromulent Feb 1, 2026
11e15be
Remove right-click yield menu and simplify yield button layout
MostCromulent Feb 7, 2026
8057365
Clean up yield rework: remove dead code and fix shortcut filter
MostCromulent Feb 7, 2026
c7074ed
Merge branch 'master' into YieldRework
MostCromulent Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions docs/Expanded-Yield-Options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Expanded Yield Options

The standard priority system in Forge can involve dozens of priority passes every turn. This can cause frustration, particularly in multiplayer Magic games like Commander, where one player's delay responding to priority can slow down the game for everybody else.

**Expanded Yield Options** is an experimental feature that significantly expands the legacy Forge auto-pass system through:

- giving players the ability to automatically yield priority until specific game conditions are met, without needing to respond to priority passes in the meantime.
- configurable yield interrupt conditions, so you'll always get control back when something important happens (e.g. you are attacked or targeted by a spell).
- smart suggestions for you to enable yield if there are no useful actions you can take (e.g. it is another player's turn and you have no mana or playable cards).

These features are highly configurable through the in-game menu, and can be set up to suit your own gameplay preferences.


**Note:** This feature is disabled by default and must be explicitly enabled in preferences.

## How to Enable:

1. In the Forge main menu open Gameplay Settings > Preferences.
2. Under the Gameplay section, click **Experimental: Enable expanded yield options**.
4. Restart the game to take effect.

## Once enabled:
- **Yield Options** will appear as a dockable panel inside the match UI (by default this is a tab in the same panel as prompt). This panel can be re-arranged within the layout at your convenience.
- The Yield Options submenu appears in: Forge > Game > Yield Options.
- Keyboard shortcuts for different yield modes become active.
- Smart suggestions begin appearing in the prompt area (if enabled).

## Yield Modes

The Yield Options panel and keyboard shortcuts provide the following yield modes:

| Mode | Description | Ends When | Default Hotkey |
|------|-------------|-----------|----------------|
| **Next Phase** | Auto-pass until phase changes | Any phase transition | F2 |
| **Until Combat** | Auto-pass until combat begins | Next COMBAT_BEGIN phase | F3 |
| **Until End Step** | Auto-pass until end step | Next END_OF_TURN phase | F4 |
| **Until Next Turn** | Auto-pass until next turn | Turn number changes | F5 |
| **Until Your Next Turn** | Auto-pass until you become active player | Your turn starts (3+ player games only) | F6 |
| **Until Stack Clears** | Auto-pass while stack has items | Stack becomes empty | F7 |

If you engage a yield mode, the button for that mode will be highlighted in the Yield Options panel to signify the yield is active. The prompt area will also describe what event you are yielding to.

A yield can be cancelled at any time by pressing the ESC key, or by clicking the highlighted yield button again (toggle behavior). You will then be given priority passes as normal.

Yield buttons are disabled during pre-game, mulligan and cleanup/discard phases.

If enabled in the Yield Options menu, you can also right-click the "End Turn" button in the prompt area to select yield options.

All keyboard shortcuts above can be configured in the Preferences menu.

## Interrupt Conditions

Yield modes automatically cancel when important game events occur. Each interrupt can be individually configured in Forge > Game > Yield Options > Interrupt Settings.

| Interrupt | Default | Description |
|-----------|---------|---------------------------------------------------------------------------------------|
| **Attackers declared against you** | ON | Triggers when creatures attack you specifically (not when other players are attacked) |
| **You can declare blockers** | ON | Triggers when creatures are attacking you |
| **You or your permanents targeted** | ON | Triggers when a spell/ability targets you or something you control |
| **Mass removal spell cast** | ON | Triggers when opponent casts a board wipe or mass removal spell. |
| **Opponent casts any spell** | OFF | Triggers on spells and activated abilities (not triggered abilities) |
| **Combat begins** | OFF | Triggers at start of any combat phase |
| **Cards revealed or choices made** | OFF | Triggers when opponent reveal dialogs and choices are made. |

**Multiplayer Note:** Attack and blocker interrupts are scoped to you specifically. If Player A attacks Player B, your yield will NOT be interrupted.

## Smart Yield Suggestions

When enabled, the system detects situations where you likely cannot take action and prompts you with a yield suggestion. Suggestions appear in the prompt area with Accept/Decline buttons.

| Suggestion | When It Appears | Suggested Mode |
|------------|-----------------|----------------|
| **Cannot respond to stack** | You have no instant-speed responses available | Until Stack Clears |
| **No mana available** | You have cards but no untapped mana sources (not your turn) | Default yield mode |
| **No actions available** | No playable cards or activatable abilities (not your turn, stack empty) | Default yield mode |

**Suggestion Behavior:**
- Each suggestion type can be individually enabled/disabled in preferences
- Suggestions will not appear if you're already yielding
- Declining a suggestion suppresses that kind of suggestion until the next turn (i.e. this stops you repeatedly recieving the same prompt).
- Clicking a yield button while a suggestion is showing activates the clicked yield mode instead of the suggested one.
- **On your own turn:** By default, the "no mana" and "no actions" suggestions are suppressed on your own turn since you typically want to take actions during your turn. This can be disabled in Game > Yield Options > Automatic Suggestions > "Suppress On Own Turn". Note: Suggestions are always suppressed on your first turn regardless of this setting, since you won't have any lands or mana yet.
- **After a yield ends:** By default, suggestions are suppressed for one priority pass when a yield expires or is interrupted. This gives you time to assess the game state before deciding whether to re-yield. The system assumes you may want to take an action at the moment the yield ends. This behavior can be disabled in Game > Yield Options > Automatic Suggestions > "Suppress After Yield Ends".



## Troubleshooting

### Yield doesn't activate when clicking button
- Verify **Experimental Yield Options** is set to `true` in preferences
- Restart Forge after changing the preference
- Yield buttons are disabled during mulligan, pre-game, and cleanup phases

### Yield clears unexpectedly
- Check interrupt settings in Forge > Game > Yield Options > Interrupt Settings
- If being attacked or targeted, yield will clear (if those interrupts are enabled)
- Yield modes automatically clear when their end condition is met

### Smart suggestions not appearing
- Verify individual suggestion preferences are enabled
- Suggestions don't appear if you're already yielding
- If you declined a suggestion, it won't appear again until next turn
- Suggestions only appear when experimental yields are enabled

### Network play notes
- All players (host and clients) must have enabled Expanded Yield Options for the system to work in network multiplayer.
- Each client manages its own yield state - yield preferences are not synchronized.
- Yield state cannot cause desync; the network layer only sees standard priority pass messages.

## Bugs and suggestions?

Please feel free to provide feedback and bug reports in the Discord.
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Network FAQ](Network-FAQ.md)
- [Network Extra](Networking-Extras.md)
- [Advanced search](Advanced-Search.md)
- [Expanded Yield Options](Expanded-Yield-Options.md)

- Adventure Mode

Expand Down
6 changes: 6 additions & 0 deletions forge-game/src/main/java/forge/game/phase/PhaseHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,12 @@ else if (!game.getStack().hasSimultaneousStackEntries()) {
for (final Player p : game.getPlayers()) {
p.setHasPriority(getPriorityPlayer() == p);
}

// Update available actions for yield suggestions (per-player, based on controller preference)
Player priorityPlayer = getPriorityPlayer();
if (priorityPlayer != null && priorityPlayer.getController().shouldTrackAvailableActions()) {
priorityPlayer.getView().updateHasAvailableActions(priorityPlayer);
}
}

private boolean checkStateBasedEffects() {
Expand Down
3 changes: 3 additions & 0 deletions forge-game/src/main/java/forge/game/player/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -1759,6 +1759,9 @@ public final ManaPool getManaPool() {
}
public void updateManaForView() {
view.updateMana(this);
if (getController().shouldTrackAvailableActions()) {
view.updateWillLoseManaAtEndOfPhase(this);
}
}

public final int getNumPowerSurgeLands() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ public boolean isAI() {
return false;
}

/**
* Whether to compute available actions for yield suggestions.
* Returns false by default (AI players, test controllers).
* Human players override to check preferences.
*/
public boolean shouldTrackAvailableActions() {
return false;
}

public Game getGame() { return gameView.getGame(); }
public Match getMatch() { return gameView.getMatch(); }
public Player getPlayer() { return player; }
Expand Down
93 changes: 93 additions & 0 deletions forge-game/src/main/java/forge/game/player/PlayerView.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import forge.game.card.Card;
import forge.game.card.CardView;
import forge.game.card.CounterType;
import forge.game.spellability.SpellAbility;
import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType;
import forge.trackable.TrackableCollection;
Expand Down Expand Up @@ -556,6 +557,98 @@ void updateMana(Player p) {
set(TrackableProperty.Mana, mana);
}

public boolean hasAvailableActions() {
Boolean val = get(TrackableProperty.HasAvailableActions);
return val != null && val;
}

/**
* Check if this player has any available actions (playable spells/abilities).
* Used for smart yield suggestions in network play.
*
* Note: This uses a heuristic for mana checking since CostPartMana.canPay()
* always returns true. We estimate available mana from floating mana plus
* untapped mana sources and compare to spell CMCs.
*/
public void updateHasAvailableActions(Player p) {
// Estimate available mana: floating mana + untapped mana-producing permanents
int availableMana = p.getManaPool().totalMana();
for (Card card : p.getCardsIn(ZoneType.Battlefield)) {
if (!card.isTapped() && !card.getManaAbilities().isEmpty()) {
// Count each untapped mana source as ~1 mana (simplified estimate)
availableMana++;
}
}

// Check hand for playable spells that we can afford
for (Card card : p.getCardsIn(ZoneType.Hand)) {
for (SpellAbility sa : card.getAllPossibleAbilities(p, true)) {
// Check if this is a spell we could potentially afford
if (sa.isSpell()) {
int cmc = sa.getPayCosts().getTotalMana().getCMC();
if (cmc <= availableMana) {
set(TrackableProperty.HasAvailableActions, true);
return;
}
} else if (sa.isLandAbility()) {
// Land abilities are already filtered by canPlay() for timing
set(TrackableProperty.HasAvailableActions, true);
return;
}
}
}

// Check battlefield for non-mana activated abilities we can afford
for (Card card : p.getCardsIn(ZoneType.Battlefield)) {
for (SpellAbility sa : card.getAllPossibleAbilities(p, true)) {
if (!sa.isManaAbility()) {
// Check if we can afford the activation cost
int activationCost = 0;
if (sa.getPayCosts() != null && sa.getPayCosts().hasManaCost()) {
activationCost = sa.getPayCosts().getTotalMana().getCMC();
}
if (activationCost <= availableMana) {
set(TrackableProperty.HasAvailableActions, true);
return;
}
}
}
}

set(TrackableProperty.HasAvailableActions, false);
}

/**
* Check if player has any mana available (floating or from untapped lands).
* Used by yield suggestion system to determine if player can cast spells.
*/
public boolean hasManaAvailable() {
// Check floating mana
for (byte manaType : ManaAtom.MANATYPES) {
if (getMana(manaType) > 0) return true;
}

// Check for untapped lands
FCollectionView<CardView> battlefield = getBattlefield();
if (battlefield != null) {
for (CardView cv : battlefield) {
if (!cv.isTapped() && cv.getCurrentState().isLand()) {
return true;
}
}
}

return false;
}

public boolean willLoseManaAtEndOfPhase() {
Boolean val = get(TrackableProperty.WillLoseManaAtEndOfPhase);
return val != null && val;
}
void updateWillLoseManaAtEndOfPhase(Player p) {
set(TrackableProperty.WillLoseManaAtEndOfPhase, p.getManaPool().willManaBeLostAtEndOfPhase());
}

private List<String> getDetailsList() {
final List<String> details = Lists.newArrayListWithCapacity(8);
details.add(Localizer.getInstance().getMessage("lblLifeHas", String.valueOf(getLife())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public StackItemView(SpellAbilityStackInstance si) {
updateOptionalTrigger(si);
updateSubInstance(si);
updateOptionalCost(si);
updateApiType(si);
}

public String getKey() {
Expand Down Expand Up @@ -97,6 +98,14 @@ void updateOptionalCost(SpellAbilityStackInstance si) {
set(TrackableProperty.OptionalCosts, OptionalCostString);
}

public String getApiType() {
return get(TrackableProperty.ApiType);
}
void updateApiType(SpellAbilityStackInstance si) {
SpellAbility sa = si.getSpellAbility();
set(TrackableProperty.ApiType, sa != null && sa.getApi() != null ? sa.getApi().name() : null);
}

public int getSourceTrigger() {
return get(TrackableProperty.SourceTrigger);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ public enum TrackableProperty {
HasDelirium(TrackableTypes.BooleanType),
AvatarLifeDifference(TrackableTypes.IntegerType, FreezeMode.IgnoresFreeze),
HasLost(TrackableTypes.BooleanType),
HasAvailableActions(TrackableTypes.BooleanType),
WillLoseManaAtEndOfPhase(TrackableTypes.BooleanType),

//SpellAbility
HostCard(TrackableTypes.CardViewType),
Expand Down Expand Up @@ -275,6 +277,7 @@ public enum TrackableProperty {
Ability(TrackableTypes.BooleanType),
OptionalTrigger(TrackableTypes.BooleanType),
OptionalCosts(TrackableTypes.StringType),
ApiType(TrackableTypes.StringType),

//Combat
AttackersWithDefenders(TrackableTypes.GenericMapType, FreezeMode.IgnoresFreeze),
Expand Down
Loading