Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions forge-game/src/main/java/forge/game/GameAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -2207,6 +2207,7 @@ public void revealTo(final CardCollectionView cards, final Iterable<Player> to,

final ZoneType zone = cards.getFirst().getZone().getZoneType();
final Player owner = cards.getFirst().getOwner();
logReveal(cards, zone, owner);
for (final Player p : to) {
p.getController().reveal(cards, zone, owner, messagePrefix, addSuffix);
}
Expand All @@ -2233,6 +2234,7 @@ public void reveal(CardCollectionView cards, ZoneType zt, Player cardOwner, bool
reveal(cards, zt, cardOwner, dontRevealToOwner, messagePrefix, true);
}
public void reveal(CardCollectionView cards, ZoneType zt, Player cardOwner, boolean dontRevealToOwner, String messagePrefix, boolean msgAddSuffix) {
logReveal(cards, zt, cardOwner);
for (Player p : game.getPlayers()) {
if (dontRevealToOwner && cardOwner == p) {
continue;
Expand All @@ -2241,6 +2243,18 @@ public void reveal(CardCollectionView cards, ZoneType zt, Player cardOwner, bool
}
}

/** Add a single game-log line naming the revealed cards and the zone they came from.
* Fired once here (not per viewer) so a hand reveal is one entry, not one-per-card or
* one-per-opponent. Gated for display by the REVEAL verbosity category. */
private void logReveal(final CardCollectionView cards, final ZoneType zone, final Player owner) {
if (cards.isEmpty()) {
return;
}
game.fireEvent(new GameEventAddLog(GameLogEntryType.REVEAL,
Localizer.getInstance().getMessage("lblRevealLogEntry",
Lang.joinHomogenous(cards), owner, zone.getTranslatedName().toLowerCase())));
}

public void revealUnplayableByAI(String title, Map<Player, Map<DeckSection, List<? extends PaperCard>>> unplayableCards) {
// Notify both players
for (Player p : game.getPlayers()) {
Expand Down
1 change: 1 addition & 0 deletions forge-game/src/main/java/forge/game/GameLogEntryType.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public enum GameLogEntryType {
ANTE("Ante"),
DRAFT("Draft"),
ZONE_CHANGE("Zone Change"),
REVEAL("Reveal"),
PLAYER_CONTROL("Player Control"),
DAMAGE("Damage"),
LIFE("Life"),
Expand Down
3 changes: 2 additions & 1 deletion forge-game/src/main/java/forge/game/GameLogVerbosity.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public enum GameLogVerbosity {
EnumSet.of(GameLogEntryType.GAME_OUTCOME, GameLogEntryType.MATCH_RESULTS,
GameLogEntryType.TURN, GameLogEntryType.MULLIGAN,
GameLogEntryType.ANTE, GameLogEntryType.DAMAGE,
GameLogEntryType.ZONE_CHANGE, GameLogEntryType.LAND,
GameLogEntryType.ZONE_CHANGE, GameLogEntryType.REVEAL,
GameLogEntryType.LAND,
GameLogEntryType.DISCARD, GameLogEntryType.COMBAT,
GameLogEntryType.STACK_ADD, GameLogEntryType.STACK_RESOLVE,
GameLogEntryType.LIFE)),
Expand Down
1 change: 1 addition & 0 deletions forge-gui/res/languages/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2124,6 +2124,7 @@ lblGainControl=gain control.
lblReturnToHand=return to hand.
lbldiscard=discard.
lblReveal=reveal
lblRevealLogEntry={0} revealed from {1}'s {2}
lblTap=tap
lblCurrentCard=Current Card
lblSelectNSpecifyTypeCardsToAction=Select %d {0} card(s) to {1}
Expand Down
15 changes: 13 additions & 2 deletions forge-gui/src/main/java/forge/gamemodes/match/YieldController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import forge.game.card.CardView;
import forge.game.combat.CombatView;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerView;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.StackItemView;
Expand Down Expand Up @@ -140,6 +141,11 @@ public synchronized void setAutoPassUntilEndOfTurn(boolean active) {
this.autoPassUntilEOT = active;
}
public synchronized void setMarker(PlayerView phaseOwner, PhaseType phase, boolean atOrPastAtClick) {
if (phaseOwner == null || phase == null) {
// Wire envelopes from an unhealthy client can land here; treat as a clear rather than store a junk marker.
clearMarker();
return;
}
autoPassUntilEOT = false;
autoPassUntilStackEmpty = false;
stackYieldRespectsInterrupts = false;
Expand Down Expand Up @@ -470,7 +476,9 @@ public void onSpellAbilityCast(SpellAbilityStackInstance si) {
if (!shouldEvaluateInterrupts()) return;
PlayerView local = owner.getLocalPlayerView();
if (local == null) return;
boolean isOpponent = !si.getActivatingPlayer().getView().equals(local);
// Triggered abilities and emblem-sourced spells can land here with a null activator.
Player activator = si.getActivatingPlayer();
boolean isOpponent = activator != null && !activator.getView().equals(local);

if (isOpponent && getBoolPref(FPref.YIELD_INTERRUPT_ON_OPPONENT_SPELL)) {
applyInterrupt();
Expand Down Expand Up @@ -501,7 +509,10 @@ public void onAttackersDeclared(CombatView combat) {
}
}

private boolean shouldEvaluateInterrupts() {
/** True when an event-driven interrupt should be considered: either an interruptible
* yield is active, or APINA + RESPECTS_INTERRUPTS are both on (so the next priority
* window's auto-pass can be blocked by autoPassInterrupted). */
public boolean shouldEvaluateInterrupts() {
if (isInterruptibleYieldActive()) return true;
return getBoolPref(FPref.YIELD_AUTO_PASS_NO_ACTIONS) && getBoolPref(FPref.YIELD_AUTO_PASS_RESPECTS_INTERRUPTS);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ public Void visit(final GameEventSpellAbilityCast event) {
private void evaluateYieldInterruptForSpellCast(GameEventSpellAbilityCast event) {
if (humanController == null) return;
YieldController yc = humanController.getYieldController();
if (!yc.isYieldActive()) return;
// isYieldActive() only covers explicit yields; APINA-with-respects-interrupts
// also wants to be told about casts so it can set autoPassInterrupted.
if (!yc.shouldEvaluateInterrupts()) return;
GameView gv = matchController.getGameView();
if (gv == null || gv.getGame() == null) return;
// Look up the actual SpellAbilityStackInstance by id (host-side; client gv.getGame() is null).
Expand Down Expand Up @@ -375,7 +377,8 @@ public Void visit(final GameEventBlockersDeclared event) {
public Void visit(final GameEventAttackersDeclared event) {
if (humanController != null) {
YieldController yc = humanController.getYieldController();
if (yc.isYieldActive()) {
// APINA-with-respects-interrupts wants the attackers signal too, not just explicit yields.
if (yc.shouldEvaluateInterrupts()) {
GameView gv = matchController.getGameView();
if (gv != null && gv.getCombat() != null) yc.onAttackersDeclared(gv.getCombat());
}
Expand Down
43 changes: 34 additions & 9 deletions forge-gui/src/main/java/forge/player/PlayerControllerHuman.java
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,12 @@ protected void reveal(final CardCollectionView cards, final ZoneType zone, final
message += " " + localizer.getMessage("lblPlayerZone", "{player's}", zone.getTranslatedName().toLowerCase());
}
final String fm = MessageUtil.formatMessage(message, getLocalPlayerView(), owner);
// While yielding with the reveal interrupt off, skip the modal the auto-pass would just
// plow through. The reveal is still recorded in the game log (GameAction logs it once,
// independent of the modal), so the information isn't lost.
if (shouldSuppressRevealModal()) {
return;
}
if (cards.isEmpty()) {
getGui().message(MessageUtil.formatMessage(localizer.getMessage("lblThereNoCardInPlayerZone", "{player's}", zone.getTranslatedName().toLowerCase()),
getLocalPlayerView(), owner), fm);
Expand Down Expand Up @@ -1890,19 +1896,38 @@ public void notifyOfValue(final SpellAbility sa, final GameObject realtedTarget,
final String message = MessageUtil.formatNotificationMessage(sa, player, realtedTarget, value);
if (sa != null && sa.isManaAbility()) {
getGame().fireEvent(new GameEventAddLog(GameLogEntryType.LAND, message));
} else if (sa != null && sa.getHostCard() != null && getGui().isLibgdxPort()) {
CardView cardView;
IPaperCard iPaperCard = sa.getHostCard().getPaperCard();
if (iPaperCard != null)
cardView = CardView.getCardForUi(iPaperCard);
else
cardView = sa.getHostCard().getView();
getGui().confirm(cardView, message, true, ImmutableList.of(localizer.getMessage("lblOK")));
} else {
getGui().message(message, sa == null || sa.getHostCard() == null ? "" : CardView.get(sa.getHostCard()).toString());
// notifyOfValue carries many non-reveal messages (coin flips, vote results,
// "Attack declaration invalid", ...), some important, so it is never suppressed.
if (sa != null && sa.getHostCard() != null && getGui().isLibgdxPort()) {
CardView cardView;
IPaperCard iPaperCard = sa.getHostCard().getPaperCard();
if (iPaperCard != null)
cardView = CardView.getCardForUi(iPaperCard);
else
cardView = sa.getHostCard().getView();
getGui().confirm(cardView, message, true, ImmutableList.of(localizer.getMessage("lblOK")));
} else {
getGui().message(message, sa == null || sa.getHostCard() == null ? "" : CardView.get(sa.getHostCard()).toString());
}
}
}

/**
* True when a card-reveal modal should be skipped while yielding:
* INTERRUPT_ON_REVEAL is off (if it were on, the reveal just interrupted, so
* the modal is meaningful) and {@link #mayAutoPass} confirms we'd auto-pass
* past it anyway (i.e. an explicit yield is active or APINA would fire — both
* of which already filter for autoPassInterrupted). Skipping is inherent to
* having the reveal interrupt off; there's no separate pref. Want the reveals?
* Turn on the reveal interrupt (with APINA-respects-interrupts). Only gates the
* reveal modal — the reveal is still written to the game log either way.
*/
private boolean shouldSuppressRevealModal() {
if (yieldController.getBoolPref(FPref.YIELD_INTERRUPT_ON_REVEAL)) return false;
return mayAutoPass();
}

/*
* (non-Javadoc)
*
Expand Down