diff --git a/.gitignore b/.gitignore index c0af41debcf..45e4e6ea42f 100644 --- a/.gitignore +++ b/.gitignore @@ -96,4 +96,4 @@ __pycache__ *.pyc # Ignore Claude configuration -.claude \ No newline at end of file +.claude diff --git a/forge-core/src/main/java/forge/deck/DeckBase.java b/forge-core/src/main/java/forge/deck/DeckBase.java index 12eb7544b06..5ce3fb08bed 100644 --- a/forge-core/src/main/java/forge/deck/DeckBase.java +++ b/forge-core/src/main/java/forge/deck/DeckBase.java @@ -30,6 +30,7 @@ public abstract class DeckBase implements Serializable, Comparable, In private String name; private transient String directory; private String comment = null; + private DeckFormat deckFormat = DeckFormat.Constructed; /** * Instantiates a new deck base. @@ -116,6 +117,14 @@ public String getComment() { return comment; } + public DeckFormat getDeckFormat() { + return deckFormat; + } + + public void setDeckFormat(final DeckFormat deckFormat0) { + deckFormat = deckFormat0 == null ? DeckFormat.Constructed : deckFormat0; + } + /** * New instance. * @@ -132,6 +141,7 @@ public String getComment() { protected void cloneFieldsTo(final DeckBase clone) { clone.directory = directory; clone.comment = comment; + clone.deckFormat = deckFormat; } /** diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index 360078df8c1..29a10036ecf 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -42,6 +42,8 @@ public enum DeckFormat { // Main board: allowed size SB: restriction Max distinct non-basic cards Constructed ( Range.of(60, Integer.MAX_VALUE), Range.of(0, 15), 4), + /** DanDan decks do not use the default 4-of restriction. */ + DanDan ( Range.of(60, Integer.MAX_VALUE), Range.of(0, 15), Integer.MAX_VALUE), QuestDeck ( Range.of(40, Integer.MAX_VALUE), Range.of(0, 15), 4), Limited ( Range.of(40, Integer.MAX_VALUE), null, Integer.MAX_VALUE) { @Override diff --git a/forge-core/src/main/java/forge/deck/io/DeckSerializer.java b/forge-core/src/main/java/forge/deck/io/DeckSerializer.java index b591dcde9f6..b4436fc3b10 100644 --- a/forge-core/src/main/java/forge/deck/io/DeckSerializer.java +++ b/forge-core/src/main/java/forge/deck/io/DeckSerializer.java @@ -45,6 +45,7 @@ private static List serializeDeck(Deck d) { out.add(TextUtil.enclosedBracket("metadata")); out.add(TextUtil.concatNoSpace(DeckFileHeader.NAME,"=", d.getName().replaceAll("\n", ""))); + out.add(TextUtil.concatNoSpace(DeckFileHeader.DECK_TYPE, "=", d.getDeckFormat().name())); // these are optional if (d.getComment() != null) { out.add(TextUtil.concatNoSpace(DeckFileHeader.COMMENT,"=", d.getComment().replaceAll("\n", ""))); @@ -99,6 +100,7 @@ public static Deck fromSections(final Map> sections) { } Deck d = new Deck(dh.getName()); + d.setDeckFormat(dh.getDeckType()); d.setComment(dh.getComment()); d.setAiHints(dh.getAiHints()); d.getTags().addAll(dh.getTags()); diff --git a/forge-game/src/main/java/forge/game/DanDanViewZones.java b/forge-game/src/main/java/forge/game/DanDanViewZones.java new file mode 100644 index 00000000000..5e873be758c --- /dev/null +++ b/forge-game/src/main/java/forge/game/DanDanViewZones.java @@ -0,0 +1,119 @@ +package forge.game; + +import java.util.HashSet; + +import forge.card.CardType; +import forge.game.card.Card; +import forge.game.card.CardView; +import forge.game.player.Player; +import forge.game.player.PlayerView; +import forge.game.zone.PlayerZone; +import forge.game.zone.ZoneType; +import forge.trackable.TrackableProperty; +import forge.util.collect.FCollectionView; + +/** + * Canonical {@link PlayerView} source for DanDan shared {@link ZoneType#Library} and + * {@link ZoneType#Graveyard}: each seat may carry its own trackable copy, but UI and verbose tooling + * should use the first registered player's lists so order and counts match the engine and sim. + */ +public final class DanDanViewZones { + + private DanDanViewZones() { + } + + /** Prefer live {@link Game} rules when present so UI matches sim even if trackable {@link GameType} lags. */ + public static boolean isDanDan(final GameView gameView) { + if (gameView == null) { + return false; + } + final Game g = gameView.getGame(); + if (g != null) { + if (g.getRules() != null && g.getRules().isDanDan()) { + return true; + } + } + final Match match = gameView.getMatch(); + if (match != null) { + if (match.getRules() != null && match.getRules().isDanDan()) { + return true; + } + } + return gameView.getGameType() == GameType.DanDan; + } + + /** + * Card list to show for {@code player} in {@code zone}. For DanDan library/graveyard, returns the + * first player's list. + */ + public static FCollectionView cardsForZoneDisplay(final GameView gameView, final PlayerView player, + final ZoneType zone) { + if (gameView != null && isDanDan(gameView) + && (zone == ZoneType.Library || zone == ZoneType.Graveyard)) { + // Prefer live model zone order when available (desktop local games). + final Game g = gameView.getGame(); + if (g != null && !g.getPlayers().isEmpty()) { + final Player canonical = g.getPlayers().get(0); + final PlayerZone sharedZone = canonical.getZone(zone); + if (sharedZone != null) { + final Iterable liveCards = sharedZone.getCards(false); + return CardView.getCollection(liveCards); + } + } + + final FCollectionView players = gameView.getPlayers(); + if (players != null && !players.isEmpty()) { + final FCollectionView shared = players.get(0).getCards(zone); + if (shared != null) { + return shared; + } + } + } + return player == null ? null : player.getCards(zone); + } + + /** Count aligned with {@link #cardsForZoneDisplay}. */ + public static int zoneCountForDisplay(final GameView gameView, final PlayerView player, final ZoneType zone) { + final FCollectionView cards = cardsForZoneDisplay(gameView, player, zone); + return cards == null ? 0 : cards.size(); + } + + /** + * Distinct card types in graveyard for UI (e.g. tooltips, delirium tint); uses canonical list in DanDan. + */ + public static int graveyardTypeCountForDisplay(final GameView gameView, final PlayerView player) { + if (gameView != null && isDanDan(gameView)) { + final FCollectionView cards = cardsForZoneDisplay(gameView, player, ZoneType.Graveyard); + if (cards == null) { + return 0; + } + final HashSet types = new HashSet<>(); + for (final CardView c : cards) { + types.addAll(c.getCurrentState().getType().getCoreTypes()); + } + return types.size(); + } + return player == null ? 0 : player.getZoneTypes(TrackableProperty.Graveyard); + } + + /** Delirium highlight in zone tabs; uses canonical graveyard in DanDan. */ + public static boolean hasDeliriumForDisplay(final GameView gameView, final PlayerView player) { + if (player == null) { + return false; + } + return graveyardTypeCountForDisplay(gameView, player) >= 4; + } + + private static String shortIdHash(final Iterable cards) { + if (cards == null) { + return "null"; + } + int count = 0; + int hash = 1; + for (final CardView c : cards) { + count++; + hash = 31 * hash + (c == null ? 0 : c.getId()); + } + return count + ":" + Integer.toHexString(hash); + } +} diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index d9808f36ded..57ecae4a51f 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -651,6 +651,19 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer copied.clearControllers(); } + if (zoneTo.is(ZoneType.Hand)) { + final GameRules rules = game.getRules(); + if (rules != null && rules.isDanDan()) { + final Player handPlayer = zoneTo.getPlayer(); + if (copied.getOwner() != handPlayer) { + copied.setOwner(handPlayer); + } + if (copied.getController() != handPlayer) { + copied.setController(handPlayer, game.getNextTimestamp()); + } + } + } + return copied; } @@ -854,6 +867,10 @@ public final Card moveToHand(final Card c, SpellAbility cause, Map params) { + final PlayerZone hand = recipient != null ? recipient.getZone(ZoneType.Hand) : c.getOwner().getZone(ZoneType.Hand); + return moveTo(hand, c, cause, params); + } public final Card moveToPlay(final Card c, SpellAbility cause, Map params) { return moveToPlay(c, c.getController(), cause, params); @@ -881,7 +898,15 @@ public final Card moveToLibrary(Card c, int libPosition, SpellAbility cause) { return moveToLibrary(c, libPosition, cause, null); } public final Card moveToLibrary(Card c, int libPosition, SpellAbility cause, Map params) { - final PlayerZone library = c.getOwner().getZone(ZoneType.Library); + final PlayerZone library; + final GameRules rules = game.getRules(); + final boolean isDanDan = rules != null && rules.isDanDan(); + if (isDanDan && !game.getPlayers().isEmpty()) { + // DanDan uses one shared library zone for all players. + library = game.getPlayers().get(0).getZone(ZoneType.Library); + } else { + library = c.getOwner().getZone(ZoneType.Library); + } if (libPosition == -1 || libPosition > library.size()) { libPosition = library.size(); } diff --git a/forge-game/src/main/java/forge/game/GameRules.java b/forge-game/src/main/java/forge/game/GameRules.java index 6ed988583ef..54028d37ed0 100644 --- a/forge-game/src/main/java/forge/game/GameRules.java +++ b/forge-game/src/main/java/forge/game/GameRules.java @@ -1,5 +1,8 @@ package forge.game; +import forge.game.zone.Zone; +import forge.game.zone.ZoneType; + import java.util.EnumSet; import java.util.Set; @@ -121,11 +124,43 @@ public boolean hasAppliedVariant(final GameType variant) { return appliedVariants.contains(variant); } + public boolean isTypeOrVariant(final GameType type) { + return gameType == type || hasAppliedVariant(type); + } + + public boolean isDanDan() { + return isTypeOrVariant(GameType.DanDan); + } + + /** + * When true, card-property and related activation logic should not fail strictly on + * per-player controller or ownership for the given zone. Callers pass the card's + * current zone or last-known zone as appropriate. + *

+ * Today this applies to DanDan's shared graveyard only; additional variants or zones + * can be folded into the implementation without changing the method name. + *

+ * + * @param zoneType the zone type to evaluate, or null (treated as not relaxed) + * @return whether relaxed controller/ownership checks apply for card properties + */ + public boolean relaxesControllerOwnershipForCardProperties(final ZoneType zoneType) { + return isDanDan() && zoneType == ZoneType.Graveyard; + } + + /** + * @param zone the zone to evaluate, or null (treated as not relaxed) + * @see #relaxesControllerOwnershipForCardProperties(ZoneType) + */ + public boolean relaxesControllerOwnershipForCardProperties(final Zone zone) { + return zone != null && relaxesControllerOwnershipForCardProperties(zone.getZoneType()); + } + public boolean hasCommander() { - return appliedVariants.contains(GameType.Commander) - || appliedVariants.contains(GameType.Oathbreaker) - || appliedVariants.contains(GameType.TinyLeaders) - || appliedVariants.contains(GameType.Brawl); + return isTypeOrVariant(GameType.Commander) + || isTypeOrVariant(GameType.Oathbreaker) + || isTypeOrVariant(GameType.TinyLeaders) + || isTypeOrVariant(GameType.Brawl); } public boolean useGrayText() { diff --git a/forge-game/src/main/java/forge/game/GameType.java b/forge-game/src/main/java/forge/game/GameType.java index 0c400403954..c1b01d67d05 100644 --- a/forge-game/src/main/java/forge/game/GameType.java +++ b/forge-game/src/main/java/forge/game/GameType.java @@ -31,6 +31,7 @@ public enum GameType { AdventureEvent (DeckFormat.Limited, true, true, true, "lblAdventure", ""), Puzzle (DeckFormat.Puzzle, false, false, false, "lblPuzzle", "lblPuzzleDesc"), Constructed (DeckFormat.Constructed, false, true, true, "lblConstructed", ""), + DanDan (DeckFormat.DanDan, false, true, true, "lblDanDan", "lblDanDanDesc"), DeckManager (DeckFormat.Constructed, false, true, true, "lblDeckManager", ""), Vanguard (DeckFormat.Vanguard, true, true, true, "lblVanguard", "lblVanguardDesc"), Commander (DeckFormat.Commander, false, false, false, "lblCommander", "lblCommanderDesc"), @@ -158,7 +159,7 @@ public EnumSet getSupplimentalDeckSections() { return EnumSet.noneOf(DeckSection.class); //Already an extra deck, like a dedicated Scheme or Planar deck. if(deckFormat == DeckFormat.Limited) return EnumSet.of(DeckSection.Conspiracy, DeckSection.Contraptions, DeckSection.Attractions); - if(this == Constructed || this == Commander) + if(this == Constructed || this == Commander || this == DanDan) return EnumSet.of(DeckSection.Avatar, DeckSection.Schemes, DeckSection.Planes, DeckSection.Conspiracy, DeckSection.Attractions, DeckSection.Contraptions); return EnumSet.of(DeckSection.Attractions, DeckSection.Contraptions); diff --git a/forge-game/src/main/java/forge/game/Match.java b/forge-game/src/main/java/forge/game/Match.java index a1a7b7e4451..cbede2c53a0 100644 --- a/forge-game/src/main/java/forge/game/Match.java +++ b/forge-game/src/main/java/forge/game/Match.java @@ -229,9 +229,14 @@ private void prepareAllZones(final Game game) { final FCollectionView players = game.getPlayers(); final List playersConditions = game.getMatch().getPlayers(); + final boolean isDanDan = rules.isDanDan() && !players.isEmpty(); + final Player sharedDanDanPlayer = isDanDan ? players.get(0) : null; + final RegisteredPlayer sharedDanDanCondition = isDanDan && !playersConditions.isEmpty() ? playersConditions.get(0) : null; boolean isFirstGame = gameOutcomes.isEmpty(); - boolean canSideBoard = !isFirstGame && rules.getGameType().isSideboardingAllowed(); + // DanDan has a shared library/graveyard but no "between-games" sideboarding. + // Prevent sideboarding logic from ever running for DanDan matches. + boolean canSideBoard = !isFirstGame && rules.getGameType().isSideboardingAllowed() && !isDanDan; // Only allow this if feature flag is on AND for certain match types boolean sideboardForAIs = rules.getSideboardForAI() && rules.getGameType().getDeckFormat().equals(DeckFormat.Constructed); @@ -250,6 +255,9 @@ private void prepareAllZones(final Game game) { for (int i = 0; i < playersConditions.size(); i++) { final Player player = players.get(i); final RegisteredPlayer psc = playersConditions.get(i); + if (isDanDan && i > 0) { + psc.useSharedDeckFrom(sharedDanDanCondition); + } PlayerController person = player.getController(); if (canSideBoard) { @@ -313,7 +321,12 @@ private void prepareAllZones(final Game game) { } } - preparePlayerZone(player, ZoneType.Library, myDeck.getLeft().getMain(), psc.useRandomFoil()); + if (!isDanDan || i == 0) { + preparePlayerZone(player, ZoneType.Library, myDeck.getLeft().getMain(), psc.useRandomFoil()); + } else { + player.useSharedZoneFrom(sharedDanDanPlayer, ZoneType.Library); + player.useSharedZoneFrom(sharedDanDanPlayer, ZoneType.Graveyard); + } if (myDeck.getLeft().has(DeckSection.Sideboard)) { preparePlayerZone(player, ZoneType.Sideboard, myDeck.getLeft().get(DeckSection.Sideboard), psc.useRandomFoil()); @@ -322,7 +335,10 @@ private void prepareAllZones(final Game game) { player.initVariantsZones(psc); - player.shuffle(null); + //Necessary to prevent duplicating shuffle events (and triggers to shuffle listeners) in DanDan setup + if (!isDanDan || i == 0) { + player.shuffle(null); + } if (isFirstGame) { Map> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck.getLeft()); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java index 93d326957c9..0d5f9f16188 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java @@ -667,7 +667,26 @@ private void changeKnownOriginResolve(final SpellAbility sa) { CardFactoryUtil.setFaceDownState(gameCard, sa); } - movedCard = game.getAction().moveTo(gameCard.getController().getZone(destination), gameCard, sa, moveParams); + // Search effects should default to the searching player's battlefield. + // Avoid using hidden-zone controller state directly as it can be stale/leaked. + Player battlefieldRecipient = activator; + if (isDanDanLibraryToBattlefieldSearch(sa, origin, destination)) { + battlefieldRecipient = sa.getActivatingPlayer(); + if (gameCard.getOwner() != battlefieldRecipient) { + gameCard.setOwner(battlefieldRecipient); + } + if (gameCard.getController() != battlefieldRecipient) { + gameCard.runChangeControllerCommands(); + gameCard.setController(battlefieldRecipient, game.getNextTimestamp()); + } + } else if (!sa.hasParam("GainControl") && shouldUseDanDanSelfRecipientHeuristic(sa)) { + battlefieldRecipient = activator; + if (battlefieldRecipient != gameCard.getController()) { + gameCard.runChangeControllerCommands(); + } + gameCard.setController(battlefieldRecipient, game.getNextTimestamp()); + } + movedCard = game.getAction().moveTo(battlefieldRecipient.getZone(destination), gameCard, sa, moveParams); // below stuff only if it changed zones if (movedCard.getZone().equals(originZone)) { continue; @@ -729,7 +748,11 @@ private void changeKnownOriginResolve(final SpellAbility sa) { handleExiledWith(gameCard, sa); } - movedCard = game.getAction().moveTo(destination, gameCard, libPos, sa, moveParams); + if (destination.equals(ZoneType.Hand)) { + movedCard = game.getAction().moveToHand(gameCard, resolveHandRecipientForDanDan(sa, activator, gameCard), sa, moveParams); + } else { + movedCard = game.getAction().moveTo(destination, gameCard, libPos, sa, moveParams); + } if (destination.equals(ZoneType.Exile) && lastStateBattlefield.contains(gameCard) && hostCard.equals(gameCard)) { // support Parallax Wave returning itself @@ -1391,7 +1414,26 @@ else if (c.isAura()) { // When it should enter the battlefield attached to an il c.turnFaceDown(true); CardFactoryUtil.setFaceDownState(c, sa); } - movedCard = game.getAction().moveToPlay(c, c.getController(), sa, moveParams); + // Search effects should default to the searching player's battlefield. + // Avoid using hidden-zone controller state directly as it can be stale/leaked. + Player battlefieldRecipient = player; + if (isDanDanLibraryToBattlefieldSearch(sa, origin, destination)) { + battlefieldRecipient = sa.getActivatingPlayer(); + if (c.getOwner() != battlefieldRecipient) { + c.setOwner(battlefieldRecipient); + } + if (c.getController() != battlefieldRecipient) { + c.runChangeControllerCommands(); + c.setController(battlefieldRecipient, game.getNextTimestamp()); + } + } else if (!sa.hasParam("GainControl") && shouldUseDanDanSelfRecipientHeuristic(sa)) { + battlefieldRecipient = sa.getActivatingPlayer(); + if (battlefieldRecipient != c.getController()) { + c.runChangeControllerCommands(); + } + c.setController(battlefieldRecipient, game.getNextTimestamp()); + } + movedCard = game.getAction().moveToPlay(c, battlefieldRecipient, sa, moveParams); if (sa.hasParam("AttachAfter") && movedCard.isAttachment() && movedCard.isInPlay()) { CardCollection list = AbilityUtils.getDefinedCards(source, sa.getParam("AttachAfter"), sa); @@ -1432,7 +1474,11 @@ else if (destination.equals(ZoneType.Exile)) { } } else { - movedCard = game.getAction().moveTo(destination, c, 0, sa, moveParams); + if (destination.equals(ZoneType.Hand)) { + movedCard = game.getAction().moveToHand(c, resolveHandRecipientForDanDan(sa, sa.getActivatingPlayer(), c), sa, moveParams); + } else { + movedCard = game.getAction().moveTo(destination, c, 0, sa, moveParams); + } } movedCards.add(movedCard); @@ -1573,6 +1619,47 @@ private static boolean allowMultiSelect(Player decider, SpellAbility sa) { && !sa.hasParam("WithTotalCardTypes"); } + private static Player resolveHandRecipientForDanDan(final SpellAbility sa, final Player activator, final Card card) { + if (shouldUseDanDanSelfRecipientHeuristic(sa)) { + return activator; + } + return card.getOwner(); + } + + private static boolean isDanDanLibraryToBattlefieldSearch(final SpellAbility sa, final List origin, final ZoneType destination) { + final Card host = sa.getHostCard(); + final GameRules rules = host != null ? host.getGame().getRules() : null; + return rules != null && rules.isDanDan() + && !sa.hasParam("GainControl") + && destination == ZoneType.Battlefield + && origin.contains(ZoneType.Library); + } + + private static boolean shouldUseDanDanSelfRecipientHeuristic(final SpellAbility sa) { + final Card host = sa.getHostCard(); + final GameRules rules = host != null ? host.getGame().getRules() : null; + if (rules == null || !rules.isDanDan()) { + return false; + } + if (hasRecipientIntentHint(sa.getParam("ChangeType")) + || hasRecipientIntentHint(sa.getParam("Defined")) + || hasRecipientIntentHint(sa.getParam("DefinedPlayer"))) { + return true; + } + if (sa.usesTargeting()) { + for (final String vt : sa.getTargetRestrictions().getValidTgts()) { + if (hasRecipientIntentHint(vt)) { + return true; + } + } + } + return false; + } + + private static boolean hasRecipientIntentHint(final String param) { + return param != null && (param.contains("YouOwn") || param.contains("YouCtrl")); + } + /** *

* removeFromStack. diff --git a/forge-game/src/main/java/forge/game/ability/effects/ExploreEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ExploreEffect.java index 40f07049c00..22345c86f75 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ExploreEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ExploreEffect.java @@ -74,7 +74,7 @@ public void resolve(SpellAbility sa) { Localizer.getInstance().getMessage("lblRevealedForExplore") + " - "); final Card r = top.getFirst(); if (r.isLand()) { - game.getAction().moveTo(ZoneType.Hand, r, sa, moveParams); + game.getAction().moveToHand(r, pl, sa, moveParams); revealedLand = true; } else { Map params = Maps.newHashMap(); diff --git a/forge-game/src/main/java/forge/game/ability/effects/SeekEffect.java b/forge-game/src/main/java/forge/game/ability/effects/SeekEffect.java index 24623a91fa6..a1f27d98c9a 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/SeekEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/SeekEffect.java @@ -76,7 +76,7 @@ public void resolve(SpellAbility sa) { Map moveParams = AbilityKey.newMap(); moveParams.put(AbilityKey.LastStateBattlefield, lastStateBattlefield); moveParams.put(AbilityKey.LastStateGraveyard, lastStateGraveyard); - Card movedCard = game.getAction().moveToHand(c, sa, moveParams); + Card movedCard = game.getAction().moveToHand(c, seeker, sa, moveParams); ZoneType resultZone = movedCard.getZone().getZoneType(); if (!resultZone.equals(ZoneType.Library)) { // as long as it moved we add to triggerList triggerList.put(ZoneType.Library, movedCard.getZone().getZoneType(), movedCard); diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index e30a771f74b..efa9a24e6b1 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -41,6 +41,8 @@ public class CardProperty { public static boolean cardHasProperty(Card card, String property, Player sourceController, Card source, CardTraitBase spellAbility) { final Game game = card.getGame(); final Combat combat = game.getCombat(); + final boolean useRelaxedOwnershipForCardProperties = game != null + && game.getRules().relaxesControllerOwnershipForCardProperties(card.getZone()); // lki can't be null but it does return this final Card lki = game.getChangeZoneLKIInfo(card); final Player controller = lki.getController(); @@ -174,19 +176,19 @@ public static boolean cardHasProperty(Card card, String property, Player sourceC return false; } } else if (property.startsWith("YouCtrl")) { - if (!controller.equals(sourceController)) { + if (!controller.equals(sourceController) && !useRelaxedOwnershipForCardProperties) { return false; } } else if (property.startsWith("YourTeamCtrl")) { - if (controller.getTeam() != sourceController.getTeam()) { + if (controller.getTeam() != sourceController.getTeam() && !useRelaxedOwnershipForCardProperties) { return false; } } else if (property.startsWith("YouDontCtrl")) { - if (controller.equals(sourceController)) { + if (controller.equals(sourceController) && !useRelaxedOwnershipForCardProperties) { return false; } } else if (property.startsWith("OppCtrl")) { - if (!controller.getOpponents().contains(sourceController)) { + if (!controller.getOpponents().contains(sourceController) && !useRelaxedOwnershipForCardProperties) { return false; } } else if (property.startsWith("ChosenCtrl")) { @@ -283,24 +285,25 @@ public static boolean cardHasProperty(Card card, String property, Player sourceC return false; } } else if (property.startsWith("YouOwn")) { - if (!card.getOwner().equals(sourceController)) { + if (!card.getOwner().equals(sourceController) && !useRelaxedOwnershipForCardProperties) { return false; } } else if (property.startsWith("YouDontOwn")) { - if (card.getOwner().equals(sourceController)) { + if (card.getOwner().equals(sourceController) && !useRelaxedOwnershipForCardProperties) { return false; } } else if (property.startsWith("OppOwn")) { - if (!card.getOwner().getOpponents().contains(sourceController)) { + if (!card.getOwner().getOpponents().contains(sourceController) && !useRelaxedOwnershipForCardProperties) { return false; } } else if (property.equals("TargetedPlayerOwn")) { - if (!AbilityUtils.getDefinedPlayers(source, "TargetedPlayer", spellAbility).contains(card.getOwner())) { + if (!AbilityUtils.getDefinedPlayers(source, "TargetedPlayer", spellAbility).contains(card.getOwner()) + && !useRelaxedOwnershipForCardProperties) { return false; } } else if (property.startsWith("OwnedBy")) { final String valid = property.substring(8); - if (!card.getOwner().isValid(valid, sourceController, source, spellAbility)) { + if (!card.getOwner().isValid(valid, sourceController, source, spellAbility) && !useRelaxedOwnershipForCardProperties) { final List lp = AbilityUtils.getDefinedPlayers(source, valid, spellAbility); if (!lp.contains(card.getOwner())) { return false; @@ -579,13 +582,13 @@ public static boolean cardHasProperty(Card card, String property, Player sourceC return false; } } else if (property.startsWith("TopGraveyardCreature")) { - CardCollection cards = CardLists.filter(card.getOwner().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES); + CardCollection cards = CardLists.filter(graveyardOrderViewForProperty(game, sourceController, card), CardPredicates.CREATURES); Collections.reverse(cards); if (cards.isEmpty() || !card.equals(cards.get(0))) { return false; } } else if (property.startsWith("TopGraveyard")) { - final CardCollection cards = new CardCollection(card.getOwner().getCardsIn(ZoneType.Graveyard)); + final CardCollection cards = new CardCollection(graveyardOrderViewForProperty(game, sourceController, card)); Collections.reverse(cards); if (property.substring(12).matches("[0-9][0-9]?")) { int n = Integer.parseInt(property.substring(12)); @@ -603,7 +606,7 @@ public static boolean cardHasProperty(Card card, String property, Player sourceC } } } else if (property.startsWith("BottomGraveyard")) { - final CardCollectionView cards = card.getOwner().getCardsIn(ZoneType.Graveyard); + final CardCollectionView cards = graveyardOrderViewForProperty(game, sourceController, card); if (cards.isEmpty() || !card.equals(cards.get(0))) { return false; } @@ -2102,6 +2105,17 @@ boolean check() { return true; } + /** + * Sequence used for Top/BottomGraveyard* properties. In DanDan, use the activating or paying + * player's view of the shared graveyard so it matches {@link forge.game.cost.CostExile} ("your graveyard"). + */ + private static CardCollectionView graveyardOrderViewForProperty(final Game game, final Player sourceController, final Card card) { + if (game != null && game.getRules().isDanDan() && sourceController != null) { + return sourceController.getCardsIn(ZoneType.Graveyard); + } + return card.getOwner().getCardsIn(ZoneType.Graveyard); + } + private static boolean hasTimestampMatch(final Card card, final CardCollectionView coll) { if (coll == null) { return false; diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index c3921bd7eb2..797e5bb9922 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -1358,13 +1358,28 @@ public String getImageKey(Iterable viewers) { return getCard().getFacedownImageKey(); } if (canBeShownToAny(viewers)) { - if (isCloned() && StaticData.instance().useSourceImageForClone()) { - return getBackup().getCurrentState().getImageKey(viewers); + if (getCard().isCloned() && StaticData.instance().useSourceImageForClone()) { + return getCard().getBackup().getCurrentState().getImageKey(viewers); } return get(TrackableProperty.ImageKey); } return ImageKeys.getTokenKey(ImageKeys.HIDDEN_CARD); } + + /** + * Art key ignoring hidden-zone rules (library, another player's hand, etc.). + * Used when the UI layer has already decided the card may be shown (e.g. dev "view all cards"). + * Still respects face-down and clone-source image preference. + */ + public String getPhysicalCardImageKey(final Iterable viewers) { + if (getState() == CardStateName.FaceDown) { + return getCard().getFacedownImageKey(); + } + if (getCard().isCloned() && StaticData.instance().useSourceImageForClone()) { + return getCard().getBackup().getCurrentState().getPhysicalCardImageKey(viewers); + } + return get(TrackableProperty.ImageKey); + } /* * Use this for revealing purposes only * */ diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 4a903fe2d21..cadc183893a 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -1181,7 +1181,15 @@ public final CardCollectionView drawCards(final int n, SpellAbility cause, Map revealed, SpellAbility sa, Map params, PlayerZone hand) { final CardCollection drawn = new CardCollection(); - final PlayerZone library = getZone(ZoneType.Library); + final GameRules rules = game.getRules(); + final boolean isDanDan = rules != null && rules.isDanDan(); + final PlayerZone library; + if (isDanDan && !game.getPlayers().isEmpty()) { + // DanDan uses one shared library; always draw from the canonical shared zone. + library = game.getPlayers().get(0).getZone(ZoneType.Library); + } else { + library = getZone(ZoneType.Library); + } SpellAbility cause = sa; if (cause != null && cause.isReplacementAbility()) { @@ -1220,6 +1228,17 @@ private CardCollectionView doDraw(Map revealed, SpellAbi c = game.getAction().moveTo(hand, c, cause, params); drawn.add(c); + // In DanDan, the physical library is shared; ensure the drawn card becomes controlled by + // the drawing player so they can cast/play it. + if (isDanDan && c != null) { + if (c.getOwner() != this) { + c.setOwner(this); + } + if (c.getController() != this) { + c.setController(this, game.getNextTimestamp()); + } + } + // CR 121.6c additional actions can't be performed when draw gets replaced // but "drawn this way" effects should still count them if (cause != null && cause.hasParam("RememberDrawn") && cause.getParam("RememberDrawn").equals("AllReplaced")) { @@ -1289,10 +1308,40 @@ public final int numDrawnThisDrawStep() { * Returns PlayerZone corresponding to the given zone of game. */ public final PlayerZone getZone(final ZoneType zone) { + final GameRules rules = game == null ? null : game.getRules(); + final boolean isDanDan = rules != null && rules.isDanDan(); + if (zone != null && game != null && isDanDan + && (zone == ZoneType.Library || zone == ZoneType.Graveyard) + && !game.getPlayers().isEmpty()) { + // DanDan library/graveyard are shared; always resolve through player 0's canonical zone. + final Player canonical = game.getPlayers().get(0); + final PlayerZone shared = canonical.zones.get(zone); + if (shared != null) { + return shared; + } + } return zones.get(zone); } + + public void useSharedZoneFrom(final Player sharedPlayer, final ZoneType zone) { + if (sharedPlayer == null || zone == null) { + return; + } + zones.put(zone, sharedPlayer.getZone(zone)); + updateZoneForView(getZone(zone)); + } public void updateZoneForView(PlayerZone zone) { view.updateZone(zone); + final GameRules rules = game == null ? null : game.getRules(); + final boolean isDanDan = rules != null && rules.isDanDan(); + if (isDanDan + && (zone.is(ZoneType.Library) || zone.is(ZoneType.Graveyard))) { + for (final Player other : game.getPlayers()) { + if (other != this && other.getZone(zone.getZoneType()) == zone) { + other.getView().updateZone(zone.getZoneType(), zone.getCards(false), other); + } + } + } } public void updateAllZonesForView() { diff --git a/forge-game/src/main/java/forge/game/player/PlayerView.java b/forge-game/src/main/java/forge/game/player/PlayerView.java index c135ca514b7..e9df9348a64 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerView.java +++ b/forge-game/src/main/java/forge/game/player/PlayerView.java @@ -485,17 +485,21 @@ public boolean hasDelirium() { } void updateZone(PlayerZone zone) { - TrackableProperty prop = zone.getZoneType().getTrackableProperty(); + updateZone(zone.getZoneType(), zone.getCards(false), zone.getPlayer()); + } + + void updateZone(final ZoneType zoneType, final Iterable cards, final Player flashbackOwner) { + TrackableProperty prop = zoneType.getTrackableProperty(); if (prop == null) { return; } - set(prop, CardView.getCollection(zone.getCards(false))); + set(prop, CardView.getCollection(cards)); //update flashback zone when relevant zones change - switch (zone.getZoneType()) { + switch (zoneType) { case Command: case Graveyard: case Library: case Exile: - updateFlashback(zone.getPlayer()); + updateFlashback(flashbackOwner); break; default: break; diff --git a/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java b/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java index a28951a7e11..389d954c275 100644 --- a/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java +++ b/forge-game/src/main/java/forge/game/player/RegisteredPlayer.java @@ -56,6 +56,13 @@ public final Deck getDeck() { return currentDeck; } + public void useSharedDeckFrom(final RegisteredPlayer shared) { + if (shared == null) { + return; + } + this.currentDeck = shared.getDeck(); + } + public final int getStartingLife() { return startingLife; } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java index 4d4af075abc..5c5cefd7672 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java @@ -197,6 +197,8 @@ public final void setRestrictions(final Map params) { public final boolean checkZoneRestrictions(final Card c, final SpellAbility sa) { final Player activator = sa.getActivatingPlayer(); final Zone cardZone = c.getLastKnownZone(); + final boolean useRelaxedOwnershipForCardProperties = activator != null && activator.getGame() != null + && activator.getGame().getRules().relaxesControllerOwnershipForCardProperties(cardZone); Card cp = c; // for Bestow need to check the animated State @@ -242,7 +244,9 @@ public final boolean checkZoneRestrictions(final Card c, final SpellAbility sa) // NOTE: this assumes that it's always possible to cast cards from hand and you don't // need special permissions for that. If WotC ever prints a card that forbids casting // cards from hand, this may become relevant. - if (!o.grantsZonePermissions() && cardZone != null && (!cardZone.is(ZoneType.Hand) || activator != c.getOwner())) { + if (!o.grantsZonePermissions() && cardZone != null + && (!cardZone.is(ZoneType.Hand) || activator != c.getOwner()) + && !useRelaxedOwnershipForCardProperties) { final List opts = c.mayPlay(activator); boolean hasOtherGrantor = false; for (CardPlayOption opt : opts) { diff --git a/forge-gui-desktop/src/main/java/forge/CachedCardImage.java b/forge-gui-desktop/src/main/java/forge/CachedCardImage.java index 33b5a8c2c3e..b66561363ea 100644 --- a/forge-gui-desktop/src/main/java/forge/CachedCardImage.java +++ b/forge-gui-desktop/src/main/java/forge/CachedCardImage.java @@ -4,6 +4,7 @@ import forge.game.card.CardView; import forge.game.player.PlayerView; +import forge.screens.match.CMatchUI; import forge.util.ImageFetcher; import forge.util.SwingImageFetcher; import org.tinylog.Logger; @@ -13,18 +14,26 @@ public abstract class CachedCardImage implements ImageFetcher.Callback { final Iterable viewers; final int width; final int height; + /** When non-null, hidden-zone cards the match allows viewing use real card art. */ + final CMatchUI matchUI; static final SwingImageFetcher fetcher = new SwingImageFetcher(); public CachedCardImage(final CardView card, final Iterable viewers, final int width, final int height) { + this(card, viewers, width, height, null); + } + + public CachedCardImage(final CardView card, final Iterable viewers, final int width, final int height, + final CMatchUI matchUI) { this.card = card; this.viewers = viewers; this.width = width; this.height = height; + this.matchUI = matchUI; if (ImageCache.isSupportedImageSize(width, height)) { - BufferedImage image = ImageCache.getImageNoDefault(card, viewers, width, height); + BufferedImage image = ImageCache.getImageNoDefault(card, viewers, width, height, matchUI); if (image == null) { - String key = card.getCurrentState().getImageKey(viewers); + String key = ImageCache.imageKeyForCardDisplay(card, viewers, matchUI); Logger.debug("Fetch due to missing key: " + key + " for " + card); fetcher.fetchImage(key, this); } @@ -32,7 +41,7 @@ public CachedCardImage(final CardView card, final Iterable viewers, } public BufferedImage getImage() { - return ImageCache.getImage(card, viewers, width, height); + return ImageCache.getImage(card, viewers, width, height, matchUI); } public abstract void onImageFetched(); diff --git a/forge-gui-desktop/src/main/java/forge/ImageCache.java b/forge-gui-desktop/src/main/java/forge/ImageCache.java index 90c3e070594..13569e66d9f 100644 --- a/forge-gui-desktop/src/main/java/forge/ImageCache.java +++ b/forge-gui-desktop/src/main/java/forge/ImageCache.java @@ -53,6 +53,7 @@ import forge.localinstance.properties.ForgePreferences.FPref; import forge.localinstance.skin.FSkinProp; import forge.model.FModel; +import forge.screens.match.CMatchUI; import forge.toolbox.FSkin; import forge.toolbox.FSkin.SkinIcon; import forge.toolbox.imaging.FCardImageRenderer; @@ -141,8 +142,20 @@ public static void clear() { * retrieve an image from the cache. returns null if the image is not found in the cache * and cannot be loaded from disk. pass -1 for width and/or height to avoid resizing in that dimension. */ + static String imageKeyForCardDisplay(final CardView card, final Iterable viewers, final CMatchUI matchUI) { + if (matchUI != null && matchUI.mayView(card)) { + return card.getCurrentState().getPhysicalCardImageKey(viewers); + } + return card.getCurrentState().getImageKey(viewers); + } + public static BufferedImage getImage(final CardView card, final Iterable viewers, final int width, final int height) { - final String key = card.getCurrentState().getImageKey(viewers); + return getImage(card, viewers, width, height, null); + } + + public static BufferedImage getImage(final CardView card, final Iterable viewers, final int width, final int height, + final CMatchUI matchUI) { + final String key = imageKeyForCardDisplay(card, viewers, matchUI); return scaleImage(key, width, height, true, card); } @@ -152,7 +165,12 @@ public static BufferedImage getImage(final CardView card, final Iterable viewers, final int width, final int height) { - final String key = card.getCurrentState().getImageKey(viewers); + return getImageNoDefault(card, viewers, width, height, null); + } + + public static BufferedImage getImageNoDefault(final CardView card, final Iterable viewers, final int width, final int height, + final CMatchUI matchUI) { + final String key = imageKeyForCardDisplay(card, viewers, matchUI); return scaleImage(key, width, height, false, card); } diff --git a/forge-gui-desktop/src/main/java/forge/deckchooser/DecksComboBox.java b/forge-gui-desktop/src/main/java/forge/deckchooser/DecksComboBox.java index 6c020aee99f..ad6703059aa 100644 --- a/forge-gui-desktop/src/main/java/forge/deckchooser/DecksComboBox.java +++ b/forge-gui-desktop/src/main/java/forge/deckchooser/DecksComboBox.java @@ -9,6 +9,7 @@ import com.google.common.collect.Lists; import forge.deck.DeckType; +import forge.game.GameType; import forge.gui.MouseUtil; import forge.toolbox.FComboBox.TextAlignment; import forge.toolbox.FComboBoxWrapper; @@ -24,10 +25,12 @@ public DecksComboBox() { addActionListener(getDeckTypeComboListener()); } - public void refresh(final DeckType deckType, final boolean isForCommander) { - if(isForCommander){ + public void refresh(final DeckType deckType, final GameType gameType) { + if (gameType.getDeckFormat().hasCommander()) { setModel(new DefaultComboBoxModel<>(DeckType.CommanderOptions)); - }else { + } else if (gameType == GameType.DanDan) { + setModel(new DefaultComboBoxModel<>(DeckType.DanDanOptions)); + } else { setModel(new DefaultComboBoxModel<>(DeckType.ConstructedOptions)); } setSelectedItem(deckType); diff --git a/forge-gui-desktop/src/main/java/forge/deckchooser/FDeckChooser.java b/forge-gui-desktop/src/main/java/forge/deckchooser/FDeckChooser.java index 11897b53557..81bccfb7508 100644 --- a/forge-gui-desktop/src/main/java/forge/deckchooser/FDeckChooser.java +++ b/forge-gui-desktop/src/main/java/forge/deckchooser/FDeckChooser.java @@ -144,6 +144,9 @@ private void updateCustom() { case TinyLeaders: updateDecks(DeckProxy.getAllTinyLeadersDecks(), ItemManagerConfig.COMMANDER_DECKS); break; + case DanDan: + updateDecks(DeckProxy.getAllDanDanDecks(), ItemManagerConfig.CONSTRUCTED_DECKS); + break; default: updateDecks(DeckProxy.getAllConstructedDecks(), ItemManagerConfig.CONSTRUCTED_DECKS); break; @@ -342,9 +345,10 @@ public void setIsAi(final boolean isAiDeck) { @Override public void deckTypeSelected(final DecksComboBoxEvent ev) { - if (ev.getDeckType() == DeckType.NET_ARCHIVE_STANDARD_DECK && !refreshingDeckType) { - if (lstDecks.getGameType() != GameType.Constructed) - return; + if(lstDecks.getGameType() != GameType.Constructed && lstDecks.getGameType() != GameType.DanDan) { + return; + } + else if (ev.getDeckType() == DeckType.NET_ARCHIVE_STANDARD_DECK && !refreshingDeckType) { //needed for loading net decks FThreads.invokeInBackgroundThread(() -> { final NetDeckArchiveStandard category = NetDeckArchiveStandard.selectAndLoad(lstDecks.getGameType()); @@ -365,8 +369,6 @@ public void deckTypeSelected(final DecksComboBoxEvent ev) { return; } else if (ev.getDeckType() == DeckType.NET_ARCHIVE_PIONEER_DECK && !refreshingDeckType) { - if (lstDecks.getGameType() != GameType.Constructed) - return; //needed for loading net decks FThreads.invokeInBackgroundThread(() -> { final NetDeckArchivePioneer category = NetDeckArchivePioneer.selectAndLoad(lstDecks.getGameType()); @@ -386,8 +388,6 @@ public void deckTypeSelected(final DecksComboBoxEvent ev) { return; } else if (ev.getDeckType() == DeckType.NET_ARCHIVE_MODERN_DECK && !refreshingDeckType) { - if (lstDecks.getGameType() != GameType.Constructed) - return; //needed for loading net decks FThreads.invokeInBackgroundThread(() -> { final NetDeckArchiveModern category = NetDeckArchiveModern.selectAndLoad(lstDecks.getGameType()); @@ -407,8 +407,6 @@ public void deckTypeSelected(final DecksComboBoxEvent ev) { return; } else if (ev.getDeckType() == DeckType.NET_ARCHIVE_PAUPER_DECK && !refreshingDeckType) { - if (lstDecks.getGameType() != GameType.Constructed) - return; //needed for loading net decks FThreads.invokeInBackgroundThread(() -> { final NetDeckArchivePauper category = NetDeckArchivePauper.selectAndLoad(lstDecks.getGameType()); @@ -428,8 +426,6 @@ public void deckTypeSelected(final DecksComboBoxEvent ev) { return; } else if (ev.getDeckType() == DeckType.NET_ARCHIVE_LEGACY_DECK && !refreshingDeckType) { - if (lstDecks.getGameType() != GameType.Constructed) - return; //needed for loading net decks FThreads.invokeInBackgroundThread(() -> { final NetDeckArchiveLegacy category = NetDeckArchiveLegacy.selectAndLoad(lstDecks.getGameType()); @@ -449,8 +445,6 @@ public void deckTypeSelected(final DecksComboBoxEvent ev) { return; } else if (ev.getDeckType() == DeckType.NET_ARCHIVE_VINTAGE_DECK && !refreshingDeckType) { - if (lstDecks.getGameType() != GameType.Constructed) - return; //needed for loading net decks FThreads.invokeInBackgroundThread(() -> { final NetDeckArchiveVintage category = NetDeckArchiveVintage.selectAndLoad(lstDecks.getGameType()); @@ -470,8 +464,6 @@ public void deckTypeSelected(final DecksComboBoxEvent ev) { return; } else if (ev.getDeckType() == DeckType.NET_ARCHIVE_BLOCK_DECK && !refreshingDeckType) { - if (lstDecks.getGameType() != GameType.Constructed) - return; //needed for loading net decks FThreads.invokeInBackgroundThread(() -> { final NetDeckArchiveBlock category = NetDeckArchiveBlock.selectAndLoad(lstDecks.getGameType()); @@ -538,7 +530,7 @@ private void refreshDecksList(final DeckType deckType, final boolean forceRefres if (ev == null) { refreshingDeckType = true; - decksComboBox.refresh(deckType, isForCommander); + decksComboBox.refresh(deckType, lstDecks.getGameType()); refreshingDeckType = false; } lstDecks.setCaption(deckType.toString()); @@ -547,6 +539,9 @@ private void refreshDecksList(final DeckType deckType, final boolean forceRefres case CUSTOM_DECK: updateCustom(); break; + case DAN_DAN_DECK: + updateDecks(DeckProxy.getAllDanDanDecks(), ItemManagerConfig.CONSTRUCTED_DECKS); + break; case COMMANDER_DECK: updateCustom(); break; diff --git a/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java b/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java index dabd0d5968e..42559f9e4b7 100644 --- a/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java +++ b/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java @@ -54,6 +54,7 @@ public enum EDocID { EDITOR_DECKGEN (VDeckgen.SINGLETON_INSTANCE), EDITOR_COMMANDER (VCommanderDecks.SINGLETON_INSTANCE), EDITOR_BRAWL (VBrawlDecks.SINGLETON_INSTANCE), + EDITOR_DANDAN (VDandanDecks.SINGLETON_INSTANCE), EDITOR_TINY_LEADERS (VTinyLeadersDecks.SINGLETON_INSTANCE), EDITOR_OATHBREAKER (VOathbreakerDecks.SINGLETON_INSTANCE), EDITOR_LOG(VEditorLog.SINGLETON_INSTANCE), diff --git a/forge-gui-desktop/src/main/java/forge/gui/framework/FScreen.java b/forge-gui-desktop/src/main/java/forge/gui/framework/FScreen.java index 97000772173..a88350cf153 100644 --- a/forge-gui-desktop/src/main/java/forge/gui/framework/FScreen.java +++ b/forge-gui-desktop/src/main/java/forge/gui/framework/FScreen.java @@ -262,17 +262,37 @@ public boolean onClosing() { } public FileLocation getLayoutFile() { + if (isMatch && controller instanceof CMatchUI) { + return ((CMatchUI) controller).getActiveMatchLayoutFile(); + } return layoutFile; } public boolean deleteLayoutFile() { - if (layoutFile == null) { return false; } - return deleteLayoutFile(layoutFile); + final FileLocation file = getLayoutFile(); + if (file == null) { return false; } + return deleteUserPrefLayoutFile(file); } public static boolean deleteMatchLayoutFile() { - return deleteLayoutFile(ForgeConstants.MATCH_LAYOUT_FILE); + boolean anyRemoved = false; + anyRemoved |= deleteUserPrefLayoutFileIfPresent(ForgeConstants.MATCH_LAYOUT_FILE); + anyRemoved |= deleteUserPrefLayoutFileIfPresent(ForgeConstants.MATCH_DANDAN_LAYOUT_FILE); + return anyRemoved; + } + private static boolean deleteUserPrefLayoutFileIfPresent(final FileLocation file) { + try { + final File f = new File(file.userPrefLoc); + if (!f.exists()) { + return false; + } + return f.delete(); + } catch (final Exception e) { + e.printStackTrace(); + FOptionPane.showErrorDialog(Localizer.getInstance().getMessage("txerrFailedtodeletelayoutfile")); + } + return false; } - private static boolean deleteLayoutFile(final FileLocation file) { + private static boolean deleteUserPrefLayoutFile(final FileLocation file) { try { final File f = new File(file.userPrefLoc); f.delete(); diff --git a/forge-gui-desktop/src/main/java/forge/itemmanager/DeckManager.java b/forge-gui-desktop/src/main/java/forge/itemmanager/DeckManager.java index 1c70eb0e822..f879e43e36b 100644 --- a/forge-gui-desktop/src/main/java/forge/itemmanager/DeckManager.java +++ b/forge-gui-desktop/src/main/java/forge/itemmanager/DeckManager.java @@ -326,6 +326,11 @@ public void editDeck(final DeckProxy deck) { DeckPreferences.setCurrentDeck((deck != null) ? deck.toString() : ""); editorCtrl = new CEditorConstructed(getCDetailPicture(), this.gameType); break; + case DanDan: + screen = FScreen.DECK_EDITOR_CONSTRUCTED; + DeckPreferences.setDanDanDeck((deck != null) ? deck.toString() : ""); + editorCtrl = new CEditorConstructed(getCDetailPicture(), this.gameType); + break; case Commander: screen = FScreen.DECK_EDITOR_CONSTRUCTED; // re-use "Deck Editor", rather than creating a new top level tab DeckPreferences.setCommanderDeck((deck != null) ? deck.toString() : ""); @@ -397,6 +402,7 @@ public boolean deleteDeck(final DeckProxy deck) { case Commander: case Oathbreaker: case TinyLeaders: + case DanDan: case Constructed: case Draft: case Sealed: diff --git a/forge-gui-desktop/src/main/java/forge/itemmanager/ItemManager.java b/forge-gui-desktop/src/main/java/forge/itemmanager/ItemManager.java index c454877b03c..3658e3431c0 100644 --- a/forge-gui-desktop/src/main/java/forge/itemmanager/ItemManager.java +++ b/forge-gui-desktop/src/main/java/forge/itemmanager/ItemManager.java @@ -101,7 +101,13 @@ public abstract class ItemManager extends JPanel implem .text("") .fontSize(12) .build(); + private final FLabel lblDeckType = new FLabel.Builder() + .text(Localizer.getInstance().getMessage("lblDeck") + " " + Localizer.getInstance().getMessage("lblType") + ":") + .fontAlign(SwingConstants.LEFT) + .fontSize(12) + .build(); private FComboBox cbxSection = new FComboBox(); + private FComboBox cbxDeckType = new FComboBox(); private static final SkinIcon VIEW_OPTIONS_ICON = FSkin.getIcon(FSkinProp.ICO_SETTINGS).resize(20, 20); private final FLabel btnViewOptions = new FLabel.Builder() @@ -173,6 +179,10 @@ public void initialize() { this.add(this.lblEmpty); this.cbxSection.setVisible(false); this.add(this.cbxSection); + this.lblDeckType.setVisible(false); + this.add(this.lblDeckType); + this.cbxDeckType.setVisible(false); + this.add(this.cbxDeckType); for (final ItemView view : this.views) { this.add(view.getButton()); view.getButton().setSelected(view == this.currentView); @@ -344,6 +354,8 @@ public void doLayout() { final int ratioWidth = this.lblRatio.getAutoSizeWidth(); int captionWidth = this.lblCaption.getAutoSizeWidth(); final int cbxSectionWidth = this.cbxSection.isVisible() ? this.cbxSection.getAutoSizeWidth() : 0; + final int lblDeckTypeWidth = this.lblDeckType.isVisible() ? this.lblDeckType.getAutoSizeWidth() : 0; + final int cbxDeckTypeWidth = this.cbxDeckType.isVisible() ? this.cbxDeckType.getAutoSizeWidth() : 0; final int viewButtonCount = this.views.size() + 1; // +1 is for the options button final int widthViewButtons = viewButtonCount * viewButtonWidth + helper.getGapX() * (viewButtonCount); @@ -351,6 +363,8 @@ public void doLayout() { int availableCaptionWidth = helper.getParentWidth() - viewButtonWidth // btnFilters - cbxSectionWidth + - lblDeckTypeWidth + - cbxDeckTypeWidth - ratioWidth - widthViewButtons; @@ -368,6 +382,10 @@ public void doLayout() { helper.include(this.cbxSection, cbxSectionWidth, FTextField.HEIGHT); helper.offset(helper.getGapX(), 0); helper.include(this.lblRatio, ratioWidth, FTextField.HEIGHT); + helper.offset(helper.getGapX(), 0); + helper.include(this.lblDeckType, lblDeckTypeWidth, FTextField.HEIGHT); + helper.offset(helper.getGapX(), 0); + helper.include(this.cbxDeckType, cbxDeckTypeWidth, FTextField.HEIGHT); helper.fillLine(this.lblEmpty, FTextField.HEIGHT, widthViewButtons); for (final ItemView view : this.views) { helper.include(view.getButton(), viewButtonWidth, FTextField.HEIGHT); @@ -1056,6 +1074,14 @@ public FComboBox getCbxSection() { return this.cbxSection; } + public FLabel getLblDeckType() { + return this.lblDeckType; + } + + public FComboBox getCbxDeckType() { + return this.cbxDeckType; + } + /** * * isIncrementalSearchActive. diff --git a/forge-gui-desktop/src/main/java/forge/itemmanager/views/DeckNameCommentRenderer.java b/forge-gui-desktop/src/main/java/forge/itemmanager/views/DeckNameCommentRenderer.java index 36b1f3bfe31..1cfb08c800c 100644 --- a/forge-gui-desktop/src/main/java/forge/itemmanager/views/DeckNameCommentRenderer.java +++ b/forge-gui-desktop/src/main/java/forge/itemmanager/views/DeckNameCommentRenderer.java @@ -23,7 +23,6 @@ import javax.swing.JLabel; import javax.swing.JTable; - import org.apache.commons.lang3.StringUtils; import forge.deck.Deck; @@ -62,6 +61,7 @@ private static DeckProxy deckProxyFromRow(final JTable table, final int row) { if (!(table.getModel() instanceof ItemListView.ItemTableModel)) { return null; } + final ItemListView.ItemTableModel tm = (ItemListView.ItemTableModel) table.getModel(); final Entry entry = tm.rowToItem(row); if (entry != null && entry.getKey() instanceof DeckProxy) { diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/CDeckEditorUI.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/CDeckEditorUI.java index 666f3a5faef..fa33180fb43 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/CDeckEditorUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/CDeckEditorUI.java @@ -63,6 +63,7 @@ public enum CDeckEditorUI implements ICDoc { private final VCommanderDecks vCommanderDecks; private final VOathbreakerDecks vOathbreakerDecks; private final VBrawlDecks vBrawlDecks; + private final VDandanDecks vDandanDecks; private final VTinyLeadersDecks vTinyLeadersDecks; private final VEditorLog vEditorLog; @@ -77,6 +78,8 @@ public enum CDeckEditorUI implements ICDoc { this.vOathbreakerDecks.setCDetailPicture(cDetailPicture); this.vBrawlDecks = VBrawlDecks.SINGLETON_INSTANCE; this.vBrawlDecks.setCDetailPicture(cDetailPicture); + this.vDandanDecks = VDandanDecks.SINGLETON_INSTANCE; + this.vDandanDecks.setCDetailPicture(cDetailPicture); this.vTinyLeadersDecks = VTinyLeadersDecks.SINGLETON_INSTANCE; this.vTinyLeadersDecks.setCDetailPicture(cDetailPicture); this.vEditorLog = VEditorLog.SINGLETON_INSTANCE; diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/SEditorIO.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/SEditorIO.java index d68c38d8ff8..2da7f216a1b 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/SEditorIO.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/SEditorIO.java @@ -7,6 +7,7 @@ import com.google.common.collect.ImmutableList; import forge.Singletons; +import forge.game.GameType; import forge.deck.DeckProxy; import forge.deck.io.DeckPreferences; import forge.gui.framework.FScreen; @@ -29,6 +30,8 @@ public class SEditorIO { public static boolean saveDeck() { final DeckController controller = CDeckEditorUI.SINGLETON_INSTANCE.getCurrentEditorController().getDeckController(); final String name = VCurrentDeck.SINGLETON_INSTANCE.getTxfTitle().getText(); + final String comment = VCurrentDeck.SINGLETON_INSTANCE.getTxfDescription().getText(); + controller.getModel().setComment(StringUtils.isBlank(comment) ? null : comment); final String deckStr = DeckProxy.getDeckString(controller.getModelPath(), name); boolean performSave = false; @@ -61,6 +64,10 @@ else if (FOptionPane.showConfirmDialog(Localizer.getInstance().getMessage("lblTh CBrawlDecks.SINGLETON_INSTANCE.refresh(); VBrawlDecks.SINGLETON_INSTANCE.getLstDecks().setSelectedString(deckStr); break; + case DanDan: + CDandanDecks.SINGLETON_INSTANCE.refresh(); + VDandanDecks.SINGLETON_INSTANCE.getLstDecks().setSelectedString(deckStr); + break; case Commander: CCommanderDecks.SINGLETON_INSTANCE.refresh(); VCommanderDecks.SINGLETON_INSTANCE.getLstDecks().setSelectedString(deckStr); @@ -85,7 +92,11 @@ else if (FOptionPane.showConfirmDialog(Localizer.getInstance().getMessage("lblTh } if (Singletons.getControl().getCurrentScreen() == FScreen.DECK_EDITOR_CONSTRUCTED) { - DeckPreferences.setCurrentDeck(deckStr); + if (CDeckEditorUI.SINGLETON_INSTANCE.getCurrentEditorController().getGameType() == GameType.DanDan) { + DeckPreferences.setDanDanDeck(deckStr); + } else { + DeckPreferences.setCurrentDeck(deckStr); + } } return performSave; diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/ACEditorBase.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/ACEditorBase.java index 4375ccb9706..77b8ed8632d 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/ACEditorBase.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/ACEditorBase.java @@ -221,15 +221,10 @@ protected ItemPool getAllowedAdditions(final Iterable cardAmountInfo = IterableUtil.find(cardsByName, t -> t.getKey().equals(card.getRules().getNormalizedName()), null); @@ -250,6 +245,17 @@ protected ItemPool getAllowedAdditions(final Iterable { + private static final GameType[] EDITABLE_DECK_TYPES = { + GameType.Constructed, + GameType.DanDan, + GameType.Commander, + GameType.Oathbreaker, + GameType.Brawl, + GameType.TinyLeaders + }; + private DeckController controller; private final List allSections = new ArrayList<>(); private ItemPool normalPool, avatarPool, planePool, schemePool, conspiracyPool, @@ -81,6 +92,7 @@ public CEditorConstructed(final CDetailPicture cDetailPicture0, final GameType g switch (this.gameType) { case Constructed: + case DanDan: allSections.add(DeckSection.Avatar); allSections.add(DeckSection.Conspiracy); @@ -156,6 +168,9 @@ public CEditorConstructed(final CDetailPicture cDetailPicture0, final GameType g case Constructed: this.controller = new DeckController<>(FModel.getDecks().getConstructed(), this, newCreator); break; + case DanDan: + this.controller = new DeckController<>(FModel.getDecks().getDanDan(), this, newCreator); + break; case Commander: this.controller = new DeckController<>(FModel.getDecks().getCommander(), this, newCreator); break; @@ -184,6 +199,7 @@ protected CardLimit getCardLimit() { if (FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) { switch (this.gameType) { case Constructed: + case DanDan: return CardLimit.Default; case Commander: case Oathbreaker: @@ -196,6 +212,11 @@ protected CardLimit getCardLimit() { return CardLimit.None; //if not enforcing deck legality, don't enforce default limit } + @Override + protected int getMaxCardCopiesAllowed(final PaperCard card) { + return this.gameType.getDeckFormat().getMaxCardCopies(card); + } + public static void onAddItems(ACEditorBase editor, Iterable> items, boolean toAlternate) { DeckSection sectionMode = editor.sectionMode; DeckController controller = editor.getDeckController(); @@ -481,7 +502,7 @@ public void setEditorMode(DeckSection sectionMode) { deckManager.setPool(this.controller.getModel().getOrCreate(DeckSection.Schemes)); break; case Commander: - if(gameType == GameType.Constructed) + if(gameType == GameType.Constructed || gameType == GameType.DanDan) break; this.getCatalogManager().setup(ItemManagerConfig.COMMANDER_POOL); this.getCatalogManager().setPool(commanderPool, true); @@ -549,10 +570,43 @@ public void update() { setEditorMode(ds); }); this.getCbxSection().setVisible(true); + configureDeckTypeSelector(); this.controller.refreshModel(); } + private void configureDeckTypeSelector() { + final FComboBox deckTypeSelector = this.getCbxDeckType(); + deckTypeSelector.removeAllItems(); + for (final GameType editableDeckType : EDITABLE_DECK_TYPES) { + deckTypeSelector.addItem(editableDeckType); + } + deckTypeSelector.setSelectedItem(this.gameType); + + for (final ActionListener listener : deckTypeSelector.getActionListeners()) { + deckTypeSelector.removeActionListener(listener); + } + + deckTypeSelector.addActionListener(actionEvent -> { + final Object selectedItem = deckTypeSelector.getSelectedItem(); + if (!(selectedItem instanceof GameType selectedGameType) || selectedGameType == this.gameType) { + return; + } + + if (!SEditorIO.confirmSaveChanges(FScreen.DECK_EDITOR_CONSTRUCTED, false)) { + deckTypeSelector.setSelectedItem(this.gameType); + return; + } + + CDeckEditorUI.SINGLETON_INSTANCE + .setEditorController(new CEditorConstructed(getCDetailPicture(), selectedGameType)); + CDeckEditorUI.SINGLETON_INSTANCE.getCurrentEditorController().getDeckController().loadDeck(new Deck()); + }); + + getLblDeckType().setVisible(true); + deckTypeSelector.setVisible(true); + } + /* (non-Javadoc) * @see forge.gui.deckeditor.controllers.ACEditorBase#canSwitchAway() */ diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorDraftingProcess.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorDraftingProcess.java index c6856ea7f53..df38daea7ca 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorDraftingProcess.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorDraftingProcess.java @@ -59,6 +59,7 @@ public class CEditorDraftingProcess extends ACEditorBase i private DragCell commanderDecksParent = null; private DragCell oathbreakerDecksParent = null; private DragCell brawlDecksParent = null; + private DragCell dandanDecksParent = null; private DragCell tinyLeadersDecksParent = null; private DragCell deckGenParent = null; private DragCell draftLogParent = null; @@ -366,6 +367,7 @@ public void update() { commanderDecksParent = removeTab(VCommanderDecks.SINGLETON_INSTANCE); oathbreakerDecksParent = removeTab(VOathbreakerDecks.SINGLETON_INSTANCE); brawlDecksParent = removeTab(VBrawlDecks.SINGLETON_INSTANCE); + dandanDecksParent = removeTab(VDandanDecks.SINGLETON_INSTANCE); tinyLeadersDecksParent = removeTab(VTinyLeadersDecks.SINGLETON_INSTANCE); // set catalog table to single-selection only mode @@ -419,6 +421,9 @@ public void resetUIChanges() { if (brawlDecksParent!= null) { brawlDecksParent.addDoc(VBrawlDecks.SINGLETON_INSTANCE); } + if (dandanDecksParent != null) { + dandanDecksParent.addDoc(VDandanDecks.SINGLETON_INSTANCE); + } if (tinyLeadersDecksParent != null) { tinyLeadersDecksParent.addDoc(VTinyLeadersDecks.SINGLETON_INSTANCE); } diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorLimited.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorLimited.java index 45855fe6387..014770bcff1 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorLimited.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorLimited.java @@ -41,6 +41,7 @@ import forge.screens.deckeditor.SEditorIO; import forge.screens.deckeditor.views.VAllDecks; import forge.screens.deckeditor.views.VBrawlDecks; +import forge.screens.deckeditor.views.VDandanDecks; import forge.screens.deckeditor.views.VCommanderDecks; import forge.screens.deckeditor.views.VCurrentDeck; import forge.screens.deckeditor.views.VDeckgen; @@ -67,6 +68,7 @@ public final class CEditorLimited extends CDeckEditor { private DragCell commanderDecksParent = null; private DragCell oathbreakerDecksParent = null; private DragCell brawlDecksParent = null; + private DragCell dandanDecksParent = null; private DragCell tinyLeadersDecksParent = null; private DragCell deckGenParent = null; private final List allSections = new ArrayList<>(); @@ -241,6 +243,7 @@ public void update() { VCurrentDeck.SINGLETON_INSTANCE.getBtnPrintProxies().setVisible(false); VCurrentDeck.SINGLETON_INSTANCE.getTxfTitle().setEnabled(false); + VCurrentDeck.SINGLETON_INSTANCE.getTxfDescription().setEnabled(false); this.getCbxSection().setVisible(true); deckGenParent = removeTab(VDeckgen.SINGLETON_INSTANCE); @@ -248,6 +251,7 @@ public void update() { commanderDecksParent = removeTab(VCommanderDecks.SINGLETON_INSTANCE); oathbreakerDecksParent = removeTab(VOathbreakerDecks.SINGLETON_INSTANCE); brawlDecksParent = removeTab(VBrawlDecks.SINGLETON_INSTANCE); + dandanDecksParent = removeTab(VDandanDecks.SINGLETON_INSTANCE); tinyLeadersDecksParent = removeTab(VTinyLeadersDecks.SINGLETON_INSTANCE); } @@ -283,6 +287,9 @@ public void resetUIChanges() { if (brawlDecksParent!= null) { brawlDecksParent.addDoc(VBrawlDecks.SINGLETON_INSTANCE); } + if (dandanDecksParent != null) { + dandanDecksParent.addDoc(VDandanDecks.SINGLETON_INSTANCE); + } if (tinyLeadersDecksParent != null) { tinyLeadersDecksParent.addDoc(VTinyLeadersDecks.SINGLETON_INSTANCE); } diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuestDraftingProcess.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuestDraftingProcess.java index 04be6aeb104..8684158135e 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuestDraftingProcess.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuestDraftingProcess.java @@ -61,6 +61,7 @@ public void setDraftQuest(CSubmenuQuestDraft draftQuest0) { private DragCell commanderDecksParent = null; private DragCell oathbreakerDecksParent = null; private DragCell brawlDecksParent = null; + private DragCell dandanDecksParent = null; private DragCell tinyLeadersDecksParent = null; private DragCell deckGenParent = null; private boolean saved = false; @@ -284,6 +285,7 @@ public void update() { commanderDecksParent = removeTab(VCommanderDecks.SINGLETON_INSTANCE); oathbreakerDecksParent = removeTab(VOathbreakerDecks.SINGLETON_INSTANCE); brawlDecksParent = removeTab(VBrawlDecks.SINGLETON_INSTANCE); + dandanDecksParent = removeTab(VDandanDecks.SINGLETON_INSTANCE); tinyLeadersDecksParent = removeTab(VTinyLeadersDecks.SINGLETON_INSTANCE); // set catalog table to single-selection only mode @@ -340,6 +342,9 @@ public void resetUIChanges() { if (brawlDecksParent!= null) { brawlDecksParent.addDoc(VBrawlDecks.SINGLETON_INSTANCE); } + if (dandanDecksParent != null) { + dandanDecksParent.addDoc(VDandanDecks.SINGLETON_INSTANCE); + } if (tinyLeadersDecksParent != null) { tinyLeadersDecksParent.addDoc(VTinyLeadersDecks.SINGLETON_INSTANCE); } diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuestLimited.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuestLimited.java index 90ed2abe851..8d12a2ab55c 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuestLimited.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/CEditorQuestLimited.java @@ -231,6 +231,7 @@ public void update() { VCurrentDeck.SINGLETON_INSTANCE.getBtnSave().setVisible(true); VCurrentDeck.SINGLETON_INSTANCE.getTxfTitle().setEnabled(false); + VCurrentDeck.SINGLETON_INSTANCE.getTxfDescription().setEnabled(false); deckGenParent = removeTab(VDeckgen.SINGLETON_INSTANCE); allDecksParent = removeTab(VAllDecks.SINGLETON_INSTANCE); diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/DeckController.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/DeckController.java index ba849807ebe..9de5500934a 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/DeckController.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/controllers/DeckController.java @@ -326,6 +326,10 @@ public void save() { return; } + if (model instanceof Deck deckModel) { + deckModel.setDeckFormat(view.getGameType().getDeckFormat()); + } + // copy to new instance before adding to current folder so further changes are auto-saved currentFolder.add((T) model.copyTo(model.getName())); model.setDirectory(DeckProxy.getDeckDirectory(currentFolder)); @@ -410,6 +414,7 @@ public void updateCaptions() { VCurrentDeck.SINGLETON_INSTANCE.getTabLabel().setText(tabCaption); VCurrentDeck.SINGLETON_INSTANCE.getTxfTitle().setText(title); + VCurrentDeck.SINGLETON_INSTANCE.getTxfDescription().setText(model != null && model.getComment() != null ? model.getComment() : ""); VCurrentDeck.SINGLETON_INSTANCE.getItemManager().setCaption(itemManagerCaption); DeckFileMenu.updateSaveEnabled(); } diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCurrentDeck.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCurrentDeck.java index 12cd0d0ccda..e50ec41a50f 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCurrentDeck.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCurrentDeck.java @@ -14,6 +14,7 @@ import forge.screens.deckeditor.controllers.CCurrentDeck; import forge.toolbox.FLabel; import forge.toolbox.FSkin; +import forge.toolbox.FTextArea; import forge.toolbox.FTextField; import forge.util.Localizer; import net.miginfocom.swing.MigLayout; @@ -80,10 +81,12 @@ public enum VCurrentDeck implements IVDoc { .opaque(true).hoverable(true).build(); private final FTextField txfTitle = new FTextField.Builder().ghostText("[" + localizer.getMessage("lblNewDeck") +"]").build(); + private final FTextArea txfDescription = new FTextArea(); private final JPanel pnlHeader = new JPanel(); - private final FLabel lblTitle = new FLabel.Builder().text(localizer.getMessage("lblTitle")).fontSize(14).build(); + private final FLabel lblTitle = new FLabel.Builder().text(localizer.getMessage("lblTitle") + ":").fontSize(14).build(); + private final FLabel lblDescription = new FLabel.Builder().text(localizer.getMessage("lblDescription") + ":").fontSize(14).build(); private final ItemManagerContainer itemManagerContainer = new ItemManagerContainer(); private ItemManager itemManager; @@ -103,7 +106,11 @@ public enum VCurrentDeck implements IVDoc { pnlHeader.add(btnLoad, "w 26px!, h 26px!"); pnlHeader.add(btnSaveAs, "w 26px!, h 26px!"); pnlHeader.add(btnPrintProxies, "w 26px!, h 26px!"); - pnlHeader.add(btnImport, "w 61px!, h 26px!"); + pnlHeader.add(btnImport, "w 61px!, h 26px!, wrap"); + pnlHeader.add(lblDescription, "h 26px!"); + txfDescription.setFocusable(true); + txfDescription.setEditable(true); + pnlHeader.add(txfDescription, "pushx, growx, h 72px!, spanx 7"); } //========== Overridden from IVDoc @@ -169,6 +176,7 @@ public void setItemManager(final ItemManager itemManage } public FLabel getLblTitle() { return lblTitle; } + public FLabel getLblDescription() { return lblDescription; } //========== Retrieval @@ -202,6 +210,10 @@ public FTextField getTxfTitle() { return txfTitle; } + public FTextArea getTxfDescription() { + return txfDescription; + } + /** @return {@link javax.swing.JPanel} */ public JPanel getPnlHeader() { return pnlHeader; diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VDandanDecks.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VDandanDecks.java new file mode 100644 index 00000000000..07867a88796 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VDandanDecks.java @@ -0,0 +1,75 @@ +package forge.screens.deckeditor.views; + +import javax.swing.JPanel; + +import forge.deck.io.DeckPreferences; +import forge.game.GameType; +import forge.gui.framework.DragCell; +import forge.gui.framework.DragTab; +import forge.gui.framework.EDocID; +import forge.gui.framework.IVDoc; +import forge.itemmanager.DeckManager; +import forge.itemmanager.ItemManagerContainer; +import forge.screens.deckeditor.controllers.CDandanDecks; +import forge.screens.match.controllers.CDetailPicture; +import forge.util.Localizer; +import net.miginfocom.swing.MigLayout; + +/** + * Deck list tab for DanDan decks in the deck editor. + */ +public enum VDandanDecks implements IVDoc { + SINGLETON_INSTANCE; + + private DragCell parentCell; + final Localizer localizer = Localizer.getInstance(); + private final DragTab tab = new DragTab(localizer.getMessage("lblDanDan")); + + private DeckManager lstDecks; + + @Override + public EDocID getDocumentID() { + return EDocID.EDITOR_DANDAN; + } + + @Override + public DragTab getTabLabel() { + return tab; + } + + @Override + public CDandanDecks getLayoutControl() { + return CDandanDecks.SINGLETON_INSTANCE; + } + + @Override + public void setParentCell(DragCell cell0) { + this.parentCell = cell0; + } + + @Override + public DragCell getParentCell() { + return this.parentCell; + } + + @Override + public void populate() { + CDandanDecks.SINGLETON_INSTANCE.refresh(); + String preferredDeck = DeckPreferences.getDanDanDeck(); + + JPanel parentBody = parentCell.getBody(); + parentBody.setLayout(new MigLayout("insets 5, gap 0, wrap, hidemode 3")); + parentBody.add(new ItemManagerContainer(lstDecks), "push, grow"); + + VAllDecks.editPreferredDeck(lstDecks, preferredDeck); + } + + public DeckManager getLstDecks() { + return lstDecks; + } + + public void setCDetailPicture(final CDetailPicture cDetailPicture) { + this.lstDecks = new DeckManager(GameType.DanDan, cDetailPicture); + this.lstDecks.setCaption(localizer.getMessage("lblDanDanDecks")); + } +} diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/VLobby.java b/forge-gui-desktop/src/main/java/forge/screens/home/VLobby.java index 3ec3b3ef7fd..c0731613785 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/VLobby.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/VLobby.java @@ -89,13 +89,14 @@ public class VLobby implements ILobbyView { private final VariantCheckBox vntOathbreaker = new VariantCheckBox(GameType.Oathbreaker); private final VariantCheckBox vntTinyLeaders = new VariantCheckBox(GameType.TinyLeaders); private final VariantCheckBox vntBrawl = new VariantCheckBox(GameType.Brawl); + private final VariantCheckBox vntDanDan = new VariantCheckBox(GameType.DanDan); private final VariantCheckBox vntPlanechase = new VariantCheckBox(GameType.Planechase); private final VariantCheckBox vntArchenemy = new VariantCheckBox(GameType.Archenemy); private final VariantCheckBox vntArchenemyRumble = new VariantCheckBox(GameType.ArchenemyRumble); private final ImmutableList vntBoxesLocal = - ImmutableList.of(vntVanguard, vntMomirBasic, vntMoJhoSto, vntCommander, vntOathbreaker, vntBrawl, vntTinyLeaders, vntPlanechase, vntArchenemy, vntArchenemyRumble); + ImmutableList.of(vntVanguard, vntMomirBasic, vntMoJhoSto, vntCommander, vntOathbreaker, vntBrawl, vntDanDan, vntTinyLeaders, vntPlanechase, vntArchenemy, vntArchenemyRumble); private final ImmutableList vntBoxesNetwork = - ImmutableList.of(vntVanguard, vntMomirBasic, vntMoJhoSto, vntCommander, vntOathbreaker, vntBrawl, vntTinyLeaders /*, vntPlanechase, vntArchenemy, vntArchenemyRumble */); + ImmutableList.of(vntVanguard, vntMomirBasic, vntMoJhoSto, vntCommander, vntOathbreaker, vntBrawl, vntDanDan, vntTinyLeaders /*, vntPlanechase, vntArchenemy, vntArchenemyRumble */); // Player frame elements private final JPanel playersFrame = new JPanel(new MigLayout("insets 0, gap 0 5, wrap, hidemode 3")); @@ -306,7 +307,19 @@ public void update(final boolean fullUpdate) { changePlayerFocus(i); } } else { - panel.getDeckChooser().setIsAi(isSlotAI); + final FDeckChooser existingDeckChooser = panel.getDeckChooser(); + final GameType expectedGameType = lobby.getGameType(); + if (existingDeckChooser.getLstDecks().getGameType() != expectedGameType) { + final FDeckChooser deckChooser = createDeckChooser(expectedGameType, i, isSlotAI); + deckChooser.populate(); + panel.setDeckChooser(deckChooser); + if (type == LobbySlotType.LOCAL || isSlotAI) { + // Ensure lobby deck selection updates immediately when the variant changes. + deckChooser.getLstDecks().getSelectCommand().run(); + } + } else { + existingDeckChooser.setIsAi(isSlotAI); + } } if (fullUpdate && (type == LobbySlotType.LOCAL || isSlotAI)) { // Deck section selection @@ -447,12 +460,19 @@ private void selectMainDeck(final FDeckChooser mainChooser, final int playerInde final Collection selectedDecks = mainChooser.getLstDecks().getSelectedItems(); if (playerIndex < activePlayersNum && lobby.mayEdit(playerIndex)) { final String text = type.toString() + ": " + Lang.joinHomogenous(selectedDecks, DeckProxy::getName); - if (isCommanderDeck) { + if (!isCommanderDeck && hasVariant(GameType.DanDan)) { + // DanDan uses one shared library, so apply the same deck to all active players. + for (int i = 0; i < activePlayersNum; i++) { + getPlayerPanel(i).setDeckSelectorButtonText(text); + fireDeckChangeListener(i, deck); + } + } else if (isCommanderDeck) { getPlayerPanel(playerIndex).setCommanderDeckSelectorButtonText(text); + fireDeckChangeListener(playerIndex, deck); } else { getPlayerPanel(playerIndex).setDeckSelectorButtonText(text); + fireDeckChangeListener(playerIndex, deck); } - fireDeckChangeListener(playerIndex, deck); } mainChooser.saveState(); } @@ -593,6 +613,10 @@ private void populateDeckPanel(final GameType forGameType) { case Brawl: decksFrame.add(getDeckChooser(playerWithFocus), "grow, push"); break; + case DanDan: + // DanDan uses a shared deck/library for all players, so expose one chooser. + decksFrame.add(getDeckChooser(0), "grow, push"); + break; case Planechase: decksFrame.add(planarDeckPanels.get(playerWithFocus), "grow, push"); break; @@ -803,6 +827,11 @@ private FDeckChooser createDeckChooser(final GameType type, final int iSlot, fin deckType = iSlot == 0 ? DeckType.BRAWL_DECK : DeckType.CUSTOM_DECK; prefKey = FPref.BRAWL_DECK_STATES[iSlot]; break; + case DanDan: + forCommander = false; + deckType = iSlot == 0 ? DeckType.DAN_DAN_DECK : DeckType.COLOR_DECK; + prefKey = FPref.DAN_DAN_DECK_STATES[iSlot]; + break; default: forCommander = false; deckType = iSlot == 0 ? DeckType.PRECONSTRUCTED_DECK : DeckType.COLOR_DECK; @@ -810,7 +839,7 @@ private FDeckChooser createDeckChooser(final GameType type, final int iSlot, fin break; } return cachedDeckChoosers.computeIfAbsent(prefKey, (key) -> { - final GameType gameType = forCommander ? type : GameType.Constructed; + final GameType gameType = type == GameType.DanDan ? GameType.DanDan : (forCommander ? type : GameType.Constructed); final FDeckChooser fdc = new FDeckChooser(null, ai, gameType, forCommander); fdc.initialize(prefKey, deckType); fdc.getLstDecks().setSelectCommand(() -> selectMainDeck(fdc, iSlot, forCommander)); 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 66e172b5eee..694d8c477f0 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 @@ -50,7 +50,9 @@ import forge.deck.Deck; import forge.deckchooser.FDeckViewer; import forge.game.GameEntityView; +import forge.game.GameRules; import forge.game.GameView; +import forge.game.Match; import forge.game.card.Card; import forge.game.card.CardView; import forge.game.card.CardView.CardStateView; @@ -87,6 +89,7 @@ import forge.gui.util.SOptionPane; import forge.item.InventoryItem; import forge.item.PaperCard; +import forge.localinstance.properties.FileLocation; import forge.localinstance.properties.ForgeConstants; import forge.localinstance.properties.ForgePreferences; import forge.localinstance.properties.ForgePreferences.FPref; @@ -128,6 +131,7 @@ import forge.util.collect.FCollectionView; import forge.view.FView; import forge.view.arcane.CardPanel; +import forge.game.DanDanViewZones; import forge.view.arcane.FloatingZone; import net.miginfocom.layout.LinkHandler; import net.miginfocom.swing.MigLayout; @@ -207,6 +211,26 @@ private static boolean isPreferenceEnabled(final ForgePreferences.FPref preferen FScreen getScreen() { return this.screen; } + + /** User/default layout file for the current match (DanDan vs other game types). */ + public FileLocation getActiveMatchLayoutFile() { + final GameView gv = getGameView(); + if (gv == null) { + return ForgeConstants.MATCH_LAYOUT_FILE; + } + // Lobby uses {@link GameType#Constructed} with {@link GameType#DanDan} as an applied variant; + // match-level rules always carry that variant. Prefer match rules so we do not depend on + // {@link GameView#getGame()} (e.g. trackable/copy edge cases). + final Match match = gv.getMatch(); + if (match != null) { + final GameRules rules = match.getRules(); + if (rules != null && rules.isDanDan()) { + return ForgeConstants.MATCH_DANDAN_LAYOUT_FILE; + } + } + return ForgeConstants.MATCH_LAYOUT_FILE; + } + public boolean isCurrentScreen() { return Singletons.getControl().getCurrentScreen() == this.screen; } @@ -215,6 +239,16 @@ private boolean isInGame() { return getGameView() != null; } + /** + * Canonical zone cards for UI display. + *

+ * For DanDan shared {@link ZoneType#Library}/{@link ZoneType#Graveyard}, this returns a single + * canonical sequence so all UI panels (floating zones, docked tabs, counts/tooltips) remain in sync. + */ + public FCollectionView cardsForZoneDisplay(final PlayerView player, final ZoneType zone) { + return DanDanViewZones.cardsForZoneDisplay(getGameView(), player, zone); + } + public String getAvatarImage(final String playerName) { return avatarImages.get(playerName); } @@ -339,10 +373,11 @@ private void initMatch(final FCollectionView sortedPlayers, final Co private void initHandViews() { final List hands = new ArrayList<>(); final Iterable localPlayers = getLocalPlayers(); + final boolean danDanHandsForAllSeats = getGameView() != null && DanDanViewZones.isDanDan(getGameView()); int i = 0; for (final PlayerView p : sortedPlayers) { - if (allHands || isLocalPlayer(p) || CardView.mayViewAny(p.getHand(), localPlayers)) { + if (allHands || danDanHandsForAllSeats || isLocalPlayer(p) || CardView.mayViewAny(p.getHand(), localPlayers)) { final EDocID doc = EDocID.Hands[i]; final VHand newHand = new VHand(this, doc, p); newHand.getLayoutControl().initialize(); @@ -455,7 +490,11 @@ public void updateZones(final Iterable zonesToUpdate) { } boolean setupPlayZone = false, updateHand = false, updateAnte = false, updateZones = false; + boolean touchedSharedDanDanZone = false; for (final ZoneType zone : update.getZones()) { + if (zone == ZoneType.Library || zone == ZoneType.Graveyard) { + touchedSharedDanDanZone = true; + } switch (zone) { case Battlefield: setupPlayZone = true; @@ -466,11 +505,11 @@ public void updateZones(final Iterable zonesToUpdate) { case Hand: updateHand = true; updateZones = true; - FloatingZone.refresh(owner, zone); + FloatingZone.refreshZoneAfterModelUpdate(this, owner, zone); break; default: updateZones = true; - FloatingZone.refresh(owner, zone); + FloatingZone.refreshZoneAfterModelUpdate(this, owner, zone); break; } } @@ -501,7 +540,16 @@ public void updateZones(final Iterable zonesToUpdate) { vField.updateDetails(); } if (updateZones) { - vField.updateZones(); + if (touchedSharedDanDanZone && getGameView() != null && DanDanViewZones.isDanDan(getGameView())) { + for (final PlayerView p : getGameView().getPlayers()) { + final VField vf = getFieldViewFor(p); + if (vf != null) { + vf.updateZones(); + } + } + } else { + vField.updateZones(); + } } } } @@ -594,7 +642,7 @@ public void updateCards(final Iterable cards) { } break; default: - FloatingZone.refresh(c.getController(),zone); // in case the card is visible in the zone + FloatingZone.refreshZoneAfterModelUpdate(this, c.getController(), zone); break; } } diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CDev.java b/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CDev.java index b115aed946e..9ce95fa1c07 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CDev.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/controllers/CDev.java @@ -7,8 +7,10 @@ import com.google.common.collect.Lists; +import forge.game.player.PlayerView; import forge.gui.framework.ICDoc; import forge.interfaces.IGameController; +import forge.player.PlayerControllerHuman; import forge.screens.match.CMatchUI; import forge.screens.match.views.IDevListener; import forge.screens.match.views.VDev; @@ -85,7 +87,12 @@ public void togglePlayManyLandsPerTurn() { public void toggleViewAllCards() { final boolean newValue = !view.getLblViewAll().getToggled(); - getController().cheat().setViewAllCards(newValue); + for (final PlayerView pv : matchUI.getLocalPlayers()) { + final IGameController gc = matchUI.getGameController(pv); + if (gc instanceof PlayerControllerHuman) { + ((PlayerControllerHuman) gc).setMayLookAtAllCards(newValue); + } + } update(); } @@ -187,7 +194,7 @@ public void update() { final IGameController controller = getController(); if (controller != null) { final boolean canPlayUnlimitedLands = controller.canPlayUnlimitedLands(); - final boolean mayLookAtAllCards = controller.mayLookAtAllCards(); + final boolean mayLookAtAllCards = matchUI.anyLocalMayLookAtAllCards(); for (final IDevListener listener : listeners) { listener.update(canPlayUnlimitedLands, mayLookAtAllCards); } diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/views/VField.java b/forge-gui-desktop/src/main/java/forge/screens/match/views/VField.java index b463b915aed..f34222c3ca4 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/views/VField.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/views/VField.java @@ -102,7 +102,7 @@ public VField(final CMatchUI matchUI, final EDocID id0, final PlayerView p, fina if (p != null) { tab.setText(Localizer.getInstance().getMessage("lblPlayField", p.getName())); } else { tab.setText(Localizer.getInstance().getMessage("lblNoPlayerForEDocID", docID.toString())); } - detailsPanel = new PlayerDetailsPanel(player, CMatchUI.FLOATING_ZONE_TYPES); + detailsPanel = new PlayerDetailsPanel(player, CMatchUI.FLOATING_ZONE_TYPES, matchUI); // TODO player is hard-coded into tabletop...should be dynamic // (haven't looked into it too deeply). Doublestrike 12-04-12 diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/views/VZone.java b/forge-gui-desktop/src/main/java/forge/screens/match/views/VZone.java index 0837f3b8bfc..dc1b2a1b796 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/views/VZone.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/views/VZone.java @@ -11,6 +11,7 @@ import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; +import forge.game.DanDanViewZones; import forge.game.card.CardView; import forge.game.player.PlayerView; import forge.game.zone.ZoneType; @@ -93,13 +94,13 @@ public void mouseClicked(final MouseEvent e) { /** Refresh card panels from zone data. */ public void refresh() { final List cardPanels = new ArrayList<>(); - final Iterable cards = player.getCards(zone); + final Iterable cards = matchUI.cardsForZoneDisplay(player, zone); if (cards != null) { final List cardList = new ArrayList<>(); for (final CardView card : cards) { cardList.add(card); } - if (sortedByName) { + if (sortedByName && !suppressNameSortForZone()) { cardList.sort(Comparator.comparing(CardView::getName)); } else if (zone == ZoneType.Flashback) { cardList.sort(FloatingZone.ZONE_ORDER_COMPARATOR); @@ -159,6 +160,15 @@ private void toggleSorted() { refresh(); } + /** Match {@link FloatingZone} name-sort suppression for shared DanDan zones and dev view-all. */ + private boolean suppressNameSortForZone() { + if (DanDanViewZones.isDanDan(matchUI.getGameView()) + && (zone == ZoneType.Library || zone == ZoneType.Graveyard)) { + return true; + } + return zone == ZoneType.Library && matchUI.anyLocalMayLookAtAllCards(); + } + public CMatchUI getMatchUI() { return matchUI; } public PlayerView getPlayer() { return player; } public ZoneType getZone() { return zone; } diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/special/PlayerDetailsPanel.java b/forge-gui-desktop/src/main/java/forge/toolbox/special/PlayerDetailsPanel.java index 4bbd16e7947..035b77d3610 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/special/PlayerDetailsPanel.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/special/PlayerDetailsPanel.java @@ -15,14 +15,16 @@ import org.apache.commons.lang3.StringUtils; import forge.card.mana.ManaAtom; +import forge.game.DanDanViewZones; +import forge.game.GameView; import forge.game.player.PlayerView; import forge.localinstance.skin.FSkinProp; +import forge.screens.match.CMatchUI; import forge.toolbox.FLabel; import forge.toolbox.FMouseAdapter; import forge.toolbox.FSkin; import forge.toolbox.FSkin.SkinFont; import forge.toolbox.FSkin.SkinnedPanel; -import forge.trackable.TrackableProperty; import forge.util.Localizer; import net.miginfocom.swing.MigLayout; import org.apache.commons.text.WordUtils; @@ -31,17 +33,19 @@ public class PlayerDetailsPanel extends JPanel { private static final long serialVersionUID = -6531759554646891983L; private final PlayerView player; + private final CMatchUI matchUI; // Info labels private final Map zoneLabels = new EnumMap<>(ZoneType.class); private final List manaLabels = new ArrayList<>(); private final DetailLabelExtra extraLabel; - public PlayerDetailsPanel(final PlayerView player, final EnumSet supportedZones) { + public PlayerDetailsPanel(final PlayerView player, final EnumSet supportedZones, final CMatchUI matchUI) { this.player = player; + this.matchUI = matchUI; zoneLabels.put(ZoneType.Hand, new DetailLabelZone(ZoneType.Hand, "lblHandNOfMax", PlayerView::getMaxHandString)); - zoneLabels.put(ZoneType.Graveyard, new DetailLabelZone(ZoneType.Graveyard, "lblGraveyardNCardsNTypes", p -> p.getZoneTypes(TrackableProperty.Graveyard))); + zoneLabels.put(ZoneType.Graveyard, new DetailLabelZone(ZoneType.Graveyard, "lblGraveyardNCardsNTypes", this::graveyardTypesForDisplay)); zoneLabels.put(ZoneType.Library, new DetailLabelZone(ZoneType.Library, "lblLibraryNCards")); zoneLabels.put(ZoneType.Exile, new DetailLabelZone(ZoneType.Exile, "lblExileNCards")); zoneLabels.put(ZoneType.Flashback, new DetailLabelZone(ZoneType.Flashback, "lblFlashbackNCards")); @@ -73,6 +77,23 @@ public static FSkinProp iconFromZone(ZoneType zoneType) { return FSkinProp.iconFromZone(zoneType, false); } + /** + * Zone count shown on player detail labels. For DanDan {@link ZoneType#Library} and + * {@link ZoneType#Graveyard}, uses {@link DanDanViewZones#zoneCountForDisplay}. + */ + public static int zoneCountForDisplay(final GameView gameView, final PlayerView p, final ZoneType zone) { + if (DanDanViewZones.isDanDan(gameView) + && (zone == ZoneType.Library || zone == ZoneType.Graveyard)) { + return DanDanViewZones.zoneCountForDisplay(gameView, p, zone); + } + return p.getZoneSize(zone); + } + + /** @see #zoneCountForDisplay(GameView, PlayerView, ZoneType) */ + public static int zoneCountForDisplay(final CMatchUI matchUI, final PlayerView p, final ZoneType zone) { + return zoneCountForDisplay(matchUI == null ? null : matchUI.getGameView(), p, zone); + } + /** Adds various labels to pool area JPanel container. */ private void populateDetails() { final SkinnedPanel row1 = new SkinnedPanel(new MigLayout("insets 0, gap 0")); @@ -265,11 +286,19 @@ public DetailLabelZone(ZoneType zone, String toolTipLabel) { this(zone, toolTipLabel, null); } private DetailLabelZone(ZoneType zone, String toolTipLabel, Function toolTipExtraArg) { - super(iconFromZone(zone), toolTipLabel, (PlayerView p) -> p.getZoneSize(zone), toolTipExtraArg); + super(iconFromZone(zone), toolTipLabel, (PlayerView p) -> getZoneCountForDisplay(p, zone), toolTipExtraArg); this.zone = zone; } } + private int getZoneCountForDisplay(final PlayerView p, final ZoneType zone) { + return zoneCountForDisplay(matchUI, p, zone); + } + + private Integer graveyardTypesForDisplay(final PlayerView p) { + return DanDanViewZones.graveyardTypeCountForDisplay(matchUI == null ? null : matchUI.getGameView(), p); + } + private class DetailLabelMana extends DetailLabelNumeric { public final String color; diff --git a/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java b/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java index 3243e66dc5b..18f8b4792b5 100644 --- a/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java +++ b/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java @@ -348,8 +348,14 @@ public static Match simulateOffthreadGame(List decks, GameType format, int private static Deck deckFromCommandLineParameter(String deckname, GameType type) { int dotpos = deckname.lastIndexOf('.'); if (dotpos > 0 && dotpos == deckname.length() - 4) { - String baseDir = type.equals(GameType.Commander) ? - ForgeConstants.DECK_COMMANDER_DIR : ForgeConstants.DECK_CONSTRUCTED_DIR; + final String baseDir; + if (type.equals(GameType.Commander)) { + baseDir = ForgeConstants.DECK_COMMANDER_DIR; + } else if (type.equals(GameType.DanDan)) { + baseDir = ForgeConstants.DECK_DANDAN_DIR; + } else { + baseDir = ForgeConstants.DECK_CONSTRUCTED_DIR; + } File f = new File(baseDir + deckname); if (!f.exists()) { @@ -364,6 +370,8 @@ private static Deck deckFromCommandLineParameter(String deckname, GameType type) // Add other game types here... if (type.equals(GameType.Commander)) { deckStore = FModel.getDecks().getCommander(); + } else if (type.equals(GameType.DanDan)) { + deckStore = FModel.getDecks().getDanDan(); } else { deckStore = FModel.getDecks().getConstructed(); } diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index c460da415eb..d309d8efccc 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java @@ -235,7 +235,7 @@ private void updateImage() { final float screenScale = GuiBase.getInterface().getScreenScale(); int imageWidth = Math.round(imagePanel.getWidth() * screenScale); int imageHeight = Math.round(imagePanel.getHeight() * screenScale); - cachedImage = new CachedCardImage(card, matchUI.getLocalPlayers(), imageWidth, imageHeight) { + cachedImage = new CachedCardImage(card, matchUI.getLocalPlayers(), imageWidth, imageHeight, matchUI) { @Override public void onImageFetched() { if (cachedImage != null) { diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/FloatingZone.java b/forge-gui-desktop/src/main/java/forge/view/arcane/FloatingZone.java index af5ceac6ccf..396919216c0 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/FloatingZone.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/FloatingZone.java @@ -34,6 +34,8 @@ import javax.swing.WindowConstants; import javax.swing.border.Border; +import forge.game.DanDanViewZones; +import forge.game.GameView; import forge.game.card.CardView; import forge.game.player.PlayerView; import forge.game.zone.ZoneType; @@ -59,7 +61,6 @@ import forge.util.Localizer; import forge.util.collect.FCollection; import forge.view.FView; - public class FloatingZone extends FloatingCardArea { private static final long serialVersionUID = 1927906492186378596L; @@ -70,6 +71,21 @@ private static int getKey(final PlayerView player, final ZoneType zone) { return 40 * player.getId() + zone.hashCode(); } + /** @see DanDanViewZones#cardsForZoneDisplay(GameView, PlayerView, ZoneType) */ + public static Iterable cardsForZoneDisplay(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) { + return matchUI == null ? null : matchUI.cardsForZoneDisplay(player, zone); + } + + /** @see DanDanViewZones#cardsForZoneDisplay(GameView, PlayerView, ZoneType) */ + public static Iterable cardsForZoneDisplay(final GameView gameView, final PlayerView player, final ZoneType zone) { + return DanDanViewZones.cardsForZoneDisplay(gameView, player, zone); + } + + /** @see DanDanViewZones#isDanDan(GameView) */ + public static boolean isDanDanGameView(final GameView gameView) { + return DanDanViewZones.isDanDan(gameView); + } + // ========== Tab mode preference ========== /** Returns true if the given zone type should open as a docked tab. */ @@ -273,6 +289,32 @@ public static void refresh(final PlayerView player, final ZoneType zone) { } } + /** + * After a zone model change, refresh UI for that zone. In DanDan, library and graveyard are shared + * in the model but each player has their own FloatingZone/VZone; zone updates are often attributed + * only to the canonical zone owner, so without fan-out the other player's window stays stale. + */ + public static void refreshZoneAfterModelUpdate(final CMatchUI matchUI, final PlayerView zoneUpdateOwner, final ZoneType zone) { + if (matchUI != null && isDanDanSharedLibraryOrGraveyard(matchUI, zone)) { + final GameView gv = matchUI.getGameView(); + if (gv != null && gv.getPlayers() != null) { + for (final PlayerView p : gv.getPlayers()) { + refresh(p, zone); + } + return; + } + } + refresh(zoneUpdateOwner, zone); + } + + private static boolean isDanDanSharedLibraryOrGraveyard(final CMatchUI matchUI, final ZoneType zone) { + if (zone != ZoneType.Library && zone != ZoneType.Graveyard) { + return false; + } + final GameView gv = matchUI.getGameView(); + return gv != null && isDanDanGameView(gv); + } + public static void closeAll() { for (final FloatingZone cardArea : floatingAreas.values()) { cardArea.window.setVisible(false); @@ -528,10 +570,10 @@ private static int zoneOrder(final ZoneType zone) { }; protected Iterable getCards() { - Iterable zoneCards = player.getCards(zone); + Iterable zoneCards = cardsForZoneDisplay(getMatchUI(), player, zone); if (zoneCards != null) { cardList = new FCollection<>(zoneCards); - if (sortedByName) { + if (sortedByName && !suppressNameSortForZone()) { cardList.sort(comp); } else if (zone == ZoneType.Flashback) { cardList.sort(ZONE_ORDER_COMPARATOR); @@ -542,6 +584,15 @@ protected Iterable getCards() { } } + /** Keep deck order for DanDan shared zones; use any local seat's dev view-all for library ordering. */ + private boolean suppressNameSortForZone() { + if (getMatchUI().getGameView() != null && isDanDanGameView(getMatchUI().getGameView()) + && (zone == ZoneType.Library || zone == ZoneType.Graveyard)) { + return true; + } + return zone == ZoneType.Library && getMatchUI().anyLocalMayLookAtAllCards(); + } + private FloatingZone(final CMatchUI matchUI, final PlayerView player0, final ZoneType zone0) { super(matchUI, new FScrollPane(false, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER)); window.add(getScrollPane(), "grow, push"); diff --git a/forge-gui-desktop/src/test/java/forge/game/ChangeZoneEffectTest.java b/forge-gui-desktop/src/test/java/forge/game/ChangeZoneEffectTest.java new file mode 100644 index 00000000000..b5abed068a3 --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/game/ChangeZoneEffectTest.java @@ -0,0 +1,136 @@ +package forge.game; + +import com.google.common.collect.Lists; +import forge.ai.AITest; +import forge.ai.LobbyPlayerAi; +import forge.deck.Deck; +import forge.game.ability.AbilityFactory; +import forge.game.ability.ApiType; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.player.RegisteredPlayer; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; +import java.util.List; + +public class ChangeZoneEffectTest extends AITest { + + @Test + public void mistyRainforestPutsFetchedLandOnActivatingPlayersBattlefield() { + final Game game = initAndCreateGame(); + final Player p1 = game.getPlayers().get(0); + final Player p2 = game.getPlayers().get(1); + + final Card misty = addCardToZone("Misty Rainforest", p2, ZoneType.Battlefield); + addCardToZone("Island", p2, ZoneType.Library); + + SpellAbility fetchAbility = null; + for (final SpellAbility sa : misty.getSpellAbilities()) { + if (sa.getApi() == ApiType.ChangeZone) { + fetchAbility = sa; + break; + } + } + AssertJUnit.assertNotNull("Misty Rainforest should have fetch ability", fetchAbility); + fetchAbility.setActivatingPlayer(p2); + fetchAbility.resolve(); + + final long islandsOnP2Battlefield = p2.getCardsIn(ZoneType.Battlefield).stream() + .filter(c -> "Island".equals(c.getName())) + .count(); + final long islandsOnP1Battlefield = p1.getCardsIn(ZoneType.Battlefield).stream() + .filter(c -> "Island".equals(c.getName())) + .count(); + + AssertJUnit.assertEquals("Fetched Island should enter activating player's battlefield", 1L, islandsOnP2Battlefield); + AssertJUnit.assertEquals("Fetched Island should not enter opponent battlefield", 0L, islandsOnP1Battlefield); + } + + @Test + public void hiddenLibraryToBattlefieldIgnoresTemporaryControllerLeak() { + final Game game = initAndCreateGame(); + final Player p1 = game.getPlayers().get(0); + final Player p2 = game.getPlayers().get(1); + + final Card misty = addCardToZone("Misty Rainforest", p2, ZoneType.Battlefield); + final Card island = addCardToZone("Island", p2, ZoneType.Library); + + // Simulate a leaked temporary control effect on the hidden-zone card. + island.addTempController(p1, game.getNextTimestamp()); + AssertJUnit.assertEquals("Precondition: library card should report temporary controller", p1, island.getController()); + + SpellAbility fetchAbility = null; + for (final SpellAbility sa : misty.getSpellAbilities()) { + if (sa.getApi() == ApiType.ChangeZone) { + fetchAbility = sa; + break; + } + } + AssertJUnit.assertNotNull("Misty Rainforest should have fetch ability", fetchAbility); + fetchAbility.setActivatingPlayer(p2); + fetchAbility.resolve(); + + final long islandsOnP2Battlefield = p2.getCardsIn(ZoneType.Battlefield).stream() + .filter(c -> "Island".equals(c.getName())) + .count(); + final long islandsOnP1Battlefield = p1.getCardsIn(ZoneType.Battlefield).stream() + .filter(c -> "Island".equals(c.getName())) + .count(); + final long islandsInLibrary = p2.getCardsIn(ZoneType.Library).stream() + .filter(c -> "Island".equals(c.getName())) + .count(); + final long islandsInBattlefield = game.getCardsIn(ZoneType.Battlefield).stream() + .filter(c -> "Island".equals(c.getName())) + .count(); + + AssertJUnit.assertEquals("Fetched Island should leave library", 0L, islandsInLibrary); + AssertJUnit.assertEquals("Fetched Island should enter battlefield exactly once", 1L, islandsInBattlefield); + AssertJUnit.assertEquals("Fetched Island should enter one player's battlefield", 1L, islandsOnP2Battlefield + islandsOnP1Battlefield); + } + + // @Test() + public void dandanBadRiverPutsFetchedLandOnActivatingPlayersBattlefield() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final Match match = new Match(new GameRules(GameType.DanDan), players, "DanDan Bad River fetch routing"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + + final Card badRiver = addCardToZone("Bad River", p2, ZoneType.Battlefield); + final Card island = addCardToZone("Island", p2, ZoneType.Library); + badRiver.addRemembered(island); + final long islandsOnBattlefieldBefore = game.getCardsIn(ZoneType.Battlefield).stream() + .filter(c -> "Island".equals(c.getName())) + .count(); + + final SpellAbility fetchAbility = AbilityFactory.getAbility( + "AB$ ChangeZone | Cost$ 0 | Mandatory$ True | Origin$ Library | Destination$ Battlefield | Defined$ Remembered", + badRiver); + AssertJUnit.assertNotNull("Should build deterministic Bad River-style ChangeZone ability", fetchAbility); + fetchAbility.setActivatingPlayer(p2); + fetchAbility.resolve(); + + AssertJUnit.assertEquals("Precondition: activating player should be p2", p2, fetchAbility.getActivatingPlayer()); + final long islandsOnBattlefieldAfter = game.getCardsIn(ZoneType.Battlefield).stream() + .filter(c -> "Island".equals(c.getName())) + .count(); + AssertJUnit.assertEquals("DanDan fetchland should move exactly one Island to battlefield", + islandsOnBattlefieldBefore + 1L, islandsOnBattlefieldAfter); + final boolean movedIslandControlledByActivator = game.getCardsIn(ZoneType.Battlefield).stream() + .filter(c -> "Island".equals(c.getName())) + .anyMatch(c -> c.getController() == p2); + AssertJUnit.assertTrue("DanDan fetchland should put searched land under activating player's control", + movedIslandControlledByActivator); + } +} diff --git a/forge-gui-desktop/src/test/java/forge/game/DanDanSharedZonesTest.java b/forge-gui-desktop/src/test/java/forge/game/DanDanSharedZonesTest.java new file mode 100644 index 00000000000..bc98825ca7d --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/game/DanDanSharedZonesTest.java @@ -0,0 +1,552 @@ +package forge.game; + +import com.google.common.collect.Lists; +import forge.ai.AITest; +import forge.ai.LobbyPlayerAi; +import forge.deck.DeckSection; +import forge.deck.Deck; +import forge.game.ability.AbilityUtils; +import forge.game.card.Card; +import forge.game.card.CardProperty; +import forge.game.card.CardView; +import forge.game.cost.Cost; +import forge.game.cost.CostExile; +import forge.game.player.Player; +import forge.game.player.PlayerView; +import forge.game.player.RegisteredPlayer; +import forge.game.spellability.SpellAbility; +import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerType; +import forge.item.PaperCard; +import forge.StaticData; +import forge.game.zone.ZoneType; +import forge.toolbox.special.PlayerDetailsPanel; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.util.List; + +public class DanDanSharedZonesTest extends AITest { + + private void seedSharedLibrary(final Player p, final int n) { + for (int i = 0; i < n; i++) { + addCardToZone("Island", p, ZoneType.Library); + } + // Ensure PlayerViews are updated for assertions and UI display helpers. + p.updateZoneForView(p.getZone(ZoneType.Library)); + } + + @Test + public void dandanPlayersShareLibraryAndGraveyardZones() { + // Ensure model/card DB initialization is done for match setup. + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final GameRules rules = new GameRules(GameType.DanDan); + final Match match = new Match(rules, players, "DanDan shared zones"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + seedSharedLibrary(p1, 20); + + AssertJUnit.assertSame("DanDan should use one shared library zone", + p1.getZone(ZoneType.Library), p2.getZone(ZoneType.Library)); + AssertJUnit.assertSame("DanDan should use one shared graveyard zone", + p1.getZone(ZoneType.Graveyard), p2.getZone(ZoneType.Graveyard)); + AssertJUnit.assertSame("DanDan should use one shared registered deck object", + game.getMatch().getPlayers().get(0).getDeck(), game.getMatch().getPlayers().get(1).getDeck()); + } + + @Test + public void dandanTopOfLibraryAffectsBothPlayersDraws() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final GameRules rules = new GameRules(GameType.DanDan); + final Match match = new Match(rules, players, "DanDan shared draw"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + seedSharedLibrary(p1, 20); + + final Card cardToTop = p1.getZone(ZoneType.Library).get(3); + p1.getGame().getAction().moveToLibrary(cardToTop, 0, null, null); + + final Card drawnByP2 = p2.drawCard().getFirst(); + AssertJUnit.assertEquals("Player 2 should draw the card player 1 put on top of shared library", + cardToTop, drawnByP2); + AssertJUnit.assertEquals("Drawn card should be controlled by the drawing player in DanDan shared library", + p2, drawnByP2.getController()); + AssertJUnit.assertEquals("Drawn card should be owned by the drawing player in DanDan shared library", + p2, drawnByP2.getOwner()); + } + + @Test + public void dandanShuffleAndTopManipulationStaySharedAcrossPlayers() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final GameRules rules = new GameRules(GameType.DanDan); + final Match match = new Match(rules, players, "DanDan shared shuffle and draw"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + seedSharedLibrary(p1, 30); + + // Pick a specific card object from the shared library, shuffle, then force it to top via the other player. + final Card marker = p1.getZone(ZoneType.Library).get(5); + p1.shuffle(null); + p2.getGame().getAction().moveToLibrary(marker, 0, null, null); + + final Card drawnByP1 = p1.drawCard().getFirst(); + AssertJUnit.assertEquals("Player 1 should draw the marker card player 2 moved to top after shuffle", + marker, drawnByP1); + } + + @Test + public void dandanPlayerViewsStayInSyncForSharedZonesAfterMixedEvents() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final GameRules rules = new GameRules(GameType.DanDan); + final Match match = new Match(rules, players, "DanDan shared view parity"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + seedSharedLibrary(p1, 40); + + final Card topCandidate = p1.getZone(ZoneType.Library).get(4); + game.getAction().moveToLibrary(topCandidate, 0, null, null); + p2.drawCard(); + // Avoid Player.mill here (requires a non-null SpellAbility in newer engine versions). + game.getAction().moveTo(ZoneType.Graveyard, p1.getZone(ZoneType.Library).get(0), null, null); + game.getAction().moveTo(ZoneType.Graveyard, p1.getZone(ZoneType.Library).get(0), null, null); + p1.shuffle(null); + + // Ensure both PlayerViews receive shared-zone updates for assertions below. + p1.updateZoneForView(p1.getZone(ZoneType.Library)); + p1.updateZoneForView(p1.getZone(ZoneType.Graveyard)); + + final PlayerView pv1 = p1.getView(); + final PlayerView pv2 = p2.getView(); + + // UI display source parity: both players should render from one canonical sequence. + final Iterable displayedLibraryP1 = DanDanViewZones.cardsForZoneDisplay(game.getView(), pv1, ZoneType.Library); + final Iterable displayedLibraryP2 = DanDanViewZones.cardsForZoneDisplay(game.getView(), pv2, ZoneType.Library); + final Iterable displayedGraveyardP1 = DanDanViewZones.cardsForZoneDisplay(game.getView(), pv1, ZoneType.Graveyard); + final Iterable displayedGraveyardP2 = DanDanViewZones.cardsForZoneDisplay(game.getView(), pv2, ZoneType.Graveyard); + + assertSameOrderAndIds("displayed library", displayedLibraryP1, displayedLibraryP2); + assertSameOrderAndIds("displayed graveyard", displayedGraveyardP1, displayedGraveyardP2); + AssertJUnit.assertEquals("displayed library count should match for both players", + count(displayedLibraryP1), count(displayedLibraryP2)); + AssertJUnit.assertEquals("displayed graveyard count should match for both players", + count(displayedGraveyardP1), count(displayedGraveyardP2)); + + // Label-layer count must match displayed zone list size (PlayerDetailsPanel zone badges). + for (final ZoneType z : new ZoneType[] { ZoneType.Library, ZoneType.Graveyard }) { + final int displayed = count(DanDanViewZones.cardsForZoneDisplay(game.getView(), pv1, z)); + AssertJUnit.assertEquals("label count vs displayed list (P1) " + z, displayed, + PlayerDetailsPanel.zoneCountForDisplay(game.getView(), pv1, z)); + AssertJUnit.assertEquals("label count vs displayed list (P2) " + z, displayed, + PlayerDetailsPanel.zoneCountForDisplay(game.getView(), pv2, z)); + } + } + + @Test + public void dandanSharedGraveyardTreatsYouOwnAsSharedAccess() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final GameRules rules = new GameRules(GameType.DanDan); + final Match match = new Match(rules, players, "DanDan shared graveyard ownership checks"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + + final Card c = addCardToZone("Opt", p1, ZoneType.Graveyard); + c.setOwner(p2); + + AssertJUnit.assertTrue("DanDan shared graveyard should allow YouOwn checks from either player (p1)", + CardProperty.cardHasProperty(c, "YouOwn", p1, c, null)); + AssertJUnit.assertTrue("DanDan shared graveyard should still allow YouOwn checks for owner (p2)", + CardProperty.cardHasProperty(c, "YouOwn", p2, c, null)); + } + + @Test + public void dandanTopGraveyardCreatureUsesActingPlayerGraveyardOrder() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final GameRules rules = new GameRules(GameType.DanDan); + final Match match = new Match(rules, players, "DanDan TopGraveyardCreature payer view"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + + addCardToZone("Runeclaw Bear", p1, ZoneType.Graveyard); + final Card topOwnedByP2 = addCardToZone("Grizzly Bears", p2, ZoneType.Graveyard); + + final Card barrowGhoul = addCardToZone("Barrow Ghoul", p1, ZoneType.Battlefield); + + AssertJUnit.assertTrue("P1 paying upkeep should treat P2-owned top creature as TopGraveyardCreature (Barrow Ghoul / shared GY)", + CardProperty.cardHasProperty(topOwnedByP2, "TopGraveyardCreature", p1, barrowGhoul, null)); + } + + @Test + public void dandanTopGraveyardCreatureAfterExileToGraveyard() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final GameRules rules = new GameRules(GameType.DanDan); + final Match match = new Match(rules, players, "DanDan TopGraveyardCreature exile to GY"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + + addCardToZone("Opt", p1, ZoneType.Graveyard); + + // P1-owned creature so P2 paying Barrow upkeep matches "other player's" shared-graveyard case. + final Card bear = addCardToZone("Grizzly Bears", p1, ZoneType.Battlefield); + final Card inExile = game.getAction().exile(bear, null, null); + final Card inGy = game.getAction().moveToGraveyard(inExile, null, null); + + AssertJUnit.assertTrue("Creature returned from exile to shared GY should still be a creature", + inGy.isCreature()); + AssertJUnit.assertFalse("Returned card should not be immutable (Card.TopGraveyardCreature prefix)", + inGy.isImmutable()); + + final Card barrowP2 = addCardToZone("Barrow Ghoul", p2, ZoneType.Battlefield); + + AssertJUnit.assertTrue("P2 paying upkeep should treat exile→GY creature as TopGraveyardCreature (DanDan shared GY)", + CardProperty.cardHasProperty(inGy, "TopGraveyardCreature", p2, barrowP2, null)); + AssertJUnit.assertNotSame("Exile→GY regression: top creature may be owned by a different player than the payer", + p2, inGy.getOwner()); + + SpellAbility upkeepSa = null; + for (Trigger tr : barrowP2.getTriggers()) { + if (tr.getMode() == TriggerType.Phase) { + upkeepSa = tr.ensureAbility(); + break; + } + } + AssertJUnit.assertNotNull("Barrow Ghoul should have a Phase trigger SA", upkeepSa); + upkeepSa.setActivatingPlayer(p2); + final String unlessCostStr = upkeepSa.getParam("UnlessCost"); + AssertJUnit.assertNotNull("Barrow Ghoul upkeep should define UnlessCost", unlessCostStr); + final Cost unlessCost = AbilityUtils.calculateUnlessCost(upkeepSa, unlessCostStr, false); + AssertJUnit.assertNotNull(unlessCost); + final CostExile exilePart = unlessCost.getCostPartByType(CostExile.class); + AssertJUnit.assertNotNull("Unless cost should include CostExile", exilePart); + AssertJUnit.assertTrue("Barrow Ghoul UnlessCost (exile top GY creature) should be payable for P2", + exilePart.canPay(upkeepSa, p2, false)); + } + + @Test + public void dandanSharedGraveyardTreatsYouCtrlAsSharedAccess() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final GameRules rules = new GameRules(GameType.DanDan); + final Match match = new Match(rules, players, "DanDan shared graveyard controller checks"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + + final Card c = addCardToZone("Memory Lapse", p1, ZoneType.Graveyard); + c.setController(p2, 0); + + AssertJUnit.assertTrue("DanDan shared graveyard should allow YouCtrl checks from either player (p1)", + CardProperty.cardHasProperty(c, "YouCtrl", p1, c, null)); + AssertJUnit.assertTrue("DanDan shared graveyard should still allow YouCtrl checks for controller (p2)", + CardProperty.cardHasProperty(c, "YouCtrl", p2, c, null)); + } + + @Test + public void dandanSkipsSideboardingBetweenGames() { + initAndCreateGame(); + + final PaperCard island = StaticData.instance().getCommonCards().getCard("Island"); + final PaperCard swamp = StaticData.instance().getCommonCards().getCard("Swamp"); + AssertJUnit.assertNotNull(island); + AssertJUnit.assertNotNull(swamp); + + final Deck firstDeck = new Deck("DanDan P1"); + firstDeck.getOrCreate(DeckSection.Main).add(island, 60); + firstDeck.getOrCreate(DeckSection.Sideboard).add(swamp, 1); + firstDeck.setAiHints("SideboardingPlan$Island->Swamp"); + + final Deck secondDeck = new Deck("DanDan P2"); + secondDeck.getOrCreate(DeckSection.Main).add(island, 60); + secondDeck.getOrCreate(DeckSection.Sideboard).add(swamp, 1); + secondDeck.setAiHints("SideboardingPlan$Island->Swamp"); + + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final GameRules rules = new GameRules(GameType.DanDan); + // Ensure AI sideboarding would be deterministic if it were allowed. + rules.setAISideboardingEnabled(true); + + final Match match = new Match(rules, players, "DanDan skip sideboarding between games"); + + // Game 1: not a sideboarding moment (first game) + final Game game1 = match.createGame(); + match.startGame(game1); + game1.setGameOver(GameEndReason.Draw); + + AssertJUnit.assertEquals(60, firstDeck.get(DeckSection.Main).countByName("Island")); + AssertJUnit.assertEquals(0, firstDeck.get(DeckSection.Main).countByName("Swamp")); + AssertJUnit.assertEquals(1, firstDeck.get(DeckSection.Sideboard).countByName("Swamp")); + + AssertJUnit.assertEquals(60, secondDeck.get(DeckSection.Main).countByName("Island")); + AssertJUnit.assertEquals(0, secondDeck.get(DeckSection.Main).countByName("Swamp")); + AssertJUnit.assertEquals(1, secondDeck.get(DeckSection.Sideboard).countByName("Swamp")); + + // Game 2: sideboarding moment, but DanDan should skip it. + final Game game2 = match.createGame(); + match.startGame(game2); + + AssertJUnit.assertEquals("DanDan main should not be swapped (p1)", + 60, firstDeck.get(DeckSection.Main).countByName("Island")); + AssertJUnit.assertEquals("DanDan main should not gain Swamps (p1)", + 0, firstDeck.get(DeckSection.Main).countByName("Swamp")); + AssertJUnit.assertEquals("DanDan sideboard should retain original Swamp (p1)", + 1, firstDeck.get(DeckSection.Sideboard).countByName("Swamp")); + + AssertJUnit.assertEquals("DanDan main should not be swapped (p2)", + 60, secondDeck.get(DeckSection.Main).countByName("Island")); + AssertJUnit.assertEquals("DanDan main should not gain Swamps (p2)", + 0, secondDeck.get(DeckSection.Main).countByName("Swamp")); + AssertJUnit.assertEquals("DanDan sideboard should retain original Swamp (p2)", + 1, secondDeck.get(DeckSection.Sideboard).countByName("Swamp")); + } + + @Test + public void dandanSharedGraveyardToHandCanRouteToActingPlayerHand() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final Match match = new Match(new GameRules(GameType.DanDan), players, "DanDan graveyard to acting hand"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + final Card c = addCardToZone("Opt", p1, ZoneType.Graveyard); + c.setOwner(p1); + + final Card moved = game.getAction().moveToHand(c, p2, null, null); + + AssertJUnit.assertTrue("Card should move to acting player's hand under DanDan recipient routing", + p2.getZone(ZoneType.Hand).contains(moved)); + AssertJUnit.assertFalse("Card should not stay in owner's hand in recipient-routed DanDan move", + p1.getZone(ZoneType.Hand).contains(moved)); + AssertJUnit.assertEquals("Returned card should be owned by hand player for DanDan visibility", + p2, moved.getOwner()); + AssertJUnit.assertEquals("Returned card should be controlled by hand player for DanDan visibility", + p2, moved.getController()); + } + + @Test + public void dandanSharedLibraryToHandCanRouteToActingPlayerHand() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final Match match = new Match(new GameRules(GameType.DanDan), players, "DanDan library to acting hand"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + seedSharedLibrary(p1, 8); + final Card top = p1.getZone(ZoneType.Library).get(0); + top.setOwner(p1); + + final Card moved = game.getAction().moveToHand(top, p2, null, null); + + AssertJUnit.assertTrue("Top shared-library card should move to acting player's hand in DanDan recipient routing", + p2.getZone(ZoneType.Hand).contains(moved)); + AssertJUnit.assertEquals("Top card should be owned by hand player for DanDan visibility", + p2, moved.getOwner()); + AssertJUnit.assertEquals("Top card should be controlled by hand player for DanDan visibility", + p2, moved.getController()); + } + + @Test + public void dandanSharedLibraryToBattlefieldCanUseActingPlayerAsController() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final Match match = new Match(new GameRules(GameType.DanDan), players, "DanDan library to battlefield control"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + final Card c = addCardToZone("Delver of Secrets", p1, ZoneType.Library); + c.setOwner(p1); + c.setController(p2, 0); + + game.getAction().moveToPlay(c, p2, null, null); + + AssertJUnit.assertTrue("Card should be on battlefield", + c.isInZone(ZoneType.Battlefield)); + AssertJUnit.assertEquals("Card should enter under acting player's control in recipient-routed DanDan move", + p2, c.getController()); + } + + @Test + public void dandanSharedGraveyardToBattlefieldCanUseActingPlayerAsController() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final Match match = new Match(new GameRules(GameType.DanDan), players, "DanDan graveyard to battlefield control"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + final Card c = addCardToZone("Delver of Secrets", p1, ZoneType.Graveyard); + c.setOwner(p1); + c.setController(p2, 0); + + game.getAction().moveToPlay(c, p2, null, null); + + AssertJUnit.assertTrue("Card should be on battlefield", + c.isInZone(ZoneType.Battlefield)); + AssertJUnit.assertEquals("Card should enter under acting player's control in DanDan move from shared graveyard", + p2, c.getController()); + } + + @Test + public void dandanMoveToHandOwnerIntentPathRemainsOwnerHand() { + initAndCreateGame(); + + final Deck firstDeck = new Deck("DanDan P1"); + final Deck secondDeck = new Deck("DanDan P2"); + final List players = Lists.newArrayList(); + players.add(new RegisteredPlayer(firstDeck).setPlayer(new LobbyPlayerAi("p1", null))); + players.add(new RegisteredPlayer(secondDeck).setPlayer(new LobbyPlayerAi("p2", null))); + + final Match match = new Match(new GameRules(GameType.DanDan), players, "DanDan owner hand control"); + final Game game = match.createGame(); + match.startGame(game); + + final Player p1 = game.getRegisteredPlayers().get(0); + final Player p2 = game.getRegisteredPlayers().get(1); + final Card c = addCardToZone("Opt", p1, ZoneType.Graveyard); + c.setOwner(p1); + + game.getAction().moveToHand(c, null, null); + + AssertJUnit.assertTrue("Owner-intent moveToHand overload should still route to owner hand", + p1.getZone(ZoneType.Hand).contains(c)); + AssertJUnit.assertFalse("Owner-intent moveToHand overload should not route to other player hand", + p2.getZone(ZoneType.Hand).contains(c)); + } + + private static void assertSameOrderAndIds(final String zoneName, final Iterable left, final Iterable right) { + final java.util.Iterator li = left.iterator(); + final java.util.Iterator ri = right.iterator(); + int index = 0; + while (li.hasNext() && ri.hasNext()) { + final CardView l = li.next(); + final CardView r = ri.next(); + AssertJUnit.assertEquals(zoneName + " mismatch at index " + index, l.getId(), r.getId()); + index++; + } + AssertJUnit.assertEquals(zoneName + " size mismatch", li.hasNext(), ri.hasNext()); + } + + private static int count(final Iterable cards) { + int c = 0; + for (final CardView ignored : cards) { + c++; + } + return c; + } +} diff --git a/forge-gui-mobile/src/forge/deck/FDeckChooser.java b/forge-gui-mobile/src/forge/deck/FDeckChooser.java index 8dbdaff4b5b..8609cf718d2 100644 --- a/forge-gui-mobile/src/forge/deck/FDeckChooser.java +++ b/forge-gui-mobile/src/forge/deck/FDeckChooser.java @@ -210,6 +210,9 @@ else if (selectedDeckType == DeckType.PAUPER_CARDGEN_DECK){ case Oathbreaker: case TinyLeaders: case Brawl: + case DanDan: + initialize(null, DeckType.DAN_DAN_DECK); + break; case Gauntlet: initialize(null, DeckType.CUSTOM_DECK); break; @@ -273,6 +276,9 @@ public void onActivate() { case Brawl: lstDecks.setSelectedString(DeckPreferences.getBrawlDeck()); break; + case DanDan: + lstDecks.setSelectedString(DeckPreferences.getDanDanDeck()); + break; case Archenemy: lstDecks.setSelectedString(DeckPreferences.getSchemeDeck()); break; @@ -293,6 +299,9 @@ public void onActivate() { case BRAWL_DECK: lstDecks.setSelectedString(DeckPreferences.getBrawlDeck()); break; + case DAN_DAN_DECK: + lstDecks.setSelectedString(DeckPreferences.getDanDanDeck()); + break; case SCHEME_DECK: lstDecks.setSelectedString(DeckPreferences.getSchemeDeck()); break; @@ -387,7 +396,7 @@ private void createNewDeck() { } } else { - setSelectedDeckType(DeckType.CUSTOM_DECK); + setSelectedDeckType(lstDecks.getGameType() == GameType.DanDan ? DeckType.DAN_DAN_DECK : DeckType.CUSTOM_DECK); } } }); @@ -405,6 +414,7 @@ private void editSelectedDeck() { case OATHBREAKER_DECK: case TINY_LEADERS_DECK: case BRAWL_DECK: + case DAN_DAN_DECK: case SCHEME_DECK: case PLANAR_DECK: case DRAFT_DECK: @@ -438,6 +448,9 @@ private void editSelectedDeck() { case Brawl: storage = FModel.getDecks().getBrawl(); break; + case DanDan: + storage = FModel.getDecks().getDanDan(); + break; case TinyLeaders: storage = FModel.getDecks().getTinyLeaders(); break; @@ -469,6 +482,8 @@ private FDeckEditor.DeckEditorConfig getEditorConfig() { return FDeckEditor.EditorConfigTinyLeaders; case BRAWL_DECK: return FDeckEditor.EditorConfigBrawl; + case DAN_DAN_DECK: + return FDeckEditor.EditorConfigDanDan; case SCHEME_DECK: return FDeckEditor.EditorConfigArchenemy; case PLANAR_DECK: @@ -488,6 +503,8 @@ private FDeckEditor.DeckEditorConfig getEditorConfig() { return FDeckEditor.EditorConfigTinyLeaders; case Brawl: return FDeckEditor.EditorConfigBrawl; + case DanDan: + return FDeckEditor.EditorConfigDanDan; case Archenemy: return FDeckEditor.EditorConfigArchenemy; case Planechase: @@ -524,6 +541,9 @@ private void editDeck(DeckProxy deck) { case Constructed: DeckPreferences.setCurrentDeck(deck.getName()); break; + case DanDan: + DeckPreferences.setDanDanDeck(deck.getName()); + break; default: break; } @@ -541,6 +561,35 @@ public void initialize(FPref savedStateSetting, DeckType defaultDeckType) { cmbDeckTypes = new FComboBox<>(); cmbDeckTypes.setAutoClose(false); switch (lstDecks.getGameType()) { + case DanDan: + cmbDeckTypes.addItem(DeckType.DAN_DAN_DECK); + cmbDeckTypes.addItem(DeckType.PRECONSTRUCTED_DECK); + cmbDeckTypes.addItem(DeckType.QUEST_OPPONENT_DECK); + cmbDeckTypes.addItem(DeckType.COLOR_DECK); + cmbDeckTypes.addItem(DeckType.STANDARD_COLOR_DECK); + cmbDeckTypes.addItem(DeckType.MODERN_COLOR_DECK); + cmbDeckTypes.addItem(DeckType.PAUPER_COLOR_DECK); + cmbDeckTypes.addItem(DeckType.RANDOM_DECK); + cmbDeckTypes.addItem(DeckType.THEME_DECK); + if(FModel.isdeckGenMatrixLoaded()) { + cmbDeckTypes.addItem(DeckType.STANDARD_CARDGEN_DECK); + cmbDeckTypes.addItem(DeckType.MODERN_CARDGEN_DECK); + cmbDeckTypes.addItem(DeckType.PAUPER_CARDGEN_DECK); + cmbDeckTypes.addItem(DeckType.LEGACY_CARDGEN_DECK); + cmbDeckTypes.addItem(DeckType.VINTAGE_CARDGEN_DECK); + cmbDeckTypes.addItem(DeckType.PIONEER_CARDGEN_DECK); + cmbDeckTypes.addItem(DeckType.HISTORIC_CARDGEN_DECK); + } + cmbDeckTypes.addItem(DeckType.NET_DECK); + cmbDeckTypes.addItem(DeckType.NET_ARCHIVE_STANDARD_DECK); + cmbDeckTypes.addItem(DeckType.NET_ARCHIVE_PIONEER_DECK); + cmbDeckTypes.addItem(DeckType.NET_ARCHIVE_MODERN_DECK); + cmbDeckTypes.addItem(DeckType.NET_ARCHIVE_PAUPER_DECK); + cmbDeckTypes.addItem(DeckType.NET_ARCHIVE_LEGACY_DECK); + cmbDeckTypes.addItem(DeckType.NET_ARCHIVE_VINTAGE_DECK); + cmbDeckTypes.addItem(DeckType.NET_ARCHIVE_BLOCK_DECK); + + break; case Constructed: case Gauntlet: cmbDeckTypes.addItem(DeckType.CUSTOM_DECK); @@ -595,6 +644,7 @@ public void initialize(FPref savedStateSetting, DeckType defaultDeckType) { cmbDeckTypes.addItem(DeckType.OATHBREAKER_DECK); cmbDeckTypes.addItem(DeckType.TINY_LEADERS_DECK); cmbDeckTypes.addItem(DeckType.BRAWL_DECK); + cmbDeckTypes.addItem(DeckType.DAN_DAN_DECK); cmbDeckTypes.addItem(DeckType.SCHEME_DECK); cmbDeckTypes.addItem(DeckType.PLANAR_DECK); cmbDeckTypes.addItem(DeckType.DRAFT_DECK); @@ -874,6 +924,10 @@ private void refreshDecksList(DeckType deckType, boolean forceRefresh, FEvent ev pool = DeckProxy.getAllBrawlDecks(); config = ItemManagerConfig.COMMANDER_DECKS; break; + case DanDan: + pool = DeckProxy.getAllDanDanDecks(); + config = ItemManagerConfig.CONSTRUCTED_DECKS; + break; case Archenemy: pool = DeckProxy.getAllSchemeDecks(); config = ItemManagerConfig.SCHEME_DECKS; @@ -912,6 +966,10 @@ private void refreshDecksList(DeckType deckType, boolean forceRefresh, FEvent ev pool = DeckProxy.getAllBrawlDecks(); config = ItemManagerConfig.COMMANDER_DECKS; break; + case DAN_DAN_DECK: + pool = DeckProxy.getAllDanDanDecks(); + config = ItemManagerConfig.CONSTRUCTED_DECKS; + break; case RANDOM_COMMANDER_DECK: pool = CommanderDeckGenerator.getCommanderDecks(lstDecks.getGameType().getDeckFormat(),isAi, false); config = ItemManagerConfig.STRING_ONLY; @@ -1415,6 +1473,11 @@ private void testSelectedDeck() { return; } + if (selectedDeckType == DeckType.DAN_DAN_DECK) { + testVariantDeck(userDeck, GameType.DanDan); + return; + } + GuiChoose.getInteger(Forge.getLocalizer().getMessage("lblHowManyOpponents"), 1, 50, numOpponents -> { if (numOpponents == null) { return; } List deckTypes = Lists.newArrayList( diff --git a/forge-gui-mobile/src/forge/deck/FDeckEditor.java b/forge-gui-mobile/src/forge/deck/FDeckEditor.java index a71b6e02140..967ca4d953f 100644 --- a/forge-gui-mobile/src/forge/deck/FDeckEditor.java +++ b/forge-gui-mobile/src/forge/deck/FDeckEditor.java @@ -346,6 +346,9 @@ public List getBasicLandSets(Deck currentDeck) { new FileDeckController<>(FModel.getDecks().getBrawl(), Deck::new, DeckPreferences::setBrawlDeck)) .setCardFilter(DeckFormat.Brawl.isLegalCardPredicate()); + public static DeckEditorConfig EditorConfigDanDan = new GameTypeDeckEditorConfig(GameType.DanDan, + new FileDeckController<>(FModel.getDecks().getDanDan(), Deck::new, DeckPreferences::setDanDanDeck)); + public static DeckEditorConfig EditorConfigArchenemy = new GameTypeDeckEditorConfig(GameType.Archenemy, new FileDeckController<>(FModel.getDecks().getScheme(), Deck::new, DeckPreferences::setSchemeDeck)) .setCatalogConfig(ItemManagerConfig.SCHEME_POOL, "lblSchemes") diff --git a/forge-gui-mobile/src/forge/screens/constructed/LobbyScreen.java b/forge-gui-mobile/src/forge/screens/constructed/LobbyScreen.java index 326129719d2..e17b578b116 100644 --- a/forge-gui-mobile/src/forge/screens/constructed/LobbyScreen.java +++ b/forge-gui-mobile/src/forge/screens/constructed/LobbyScreen.java @@ -148,6 +148,7 @@ public LobbyScreen(String headerCaption, FPopupMenu menu, GameLobby lobby0) { cbVariants.addItem(GameType.Oathbreaker); cbVariants.addItem(GameType.TinyLeaders); cbVariants.addItem(GameType.Brawl); + cbVariants.addItem(GameType.DanDan); cbVariants.addItem(GameType.Planechase); cbVariants.addItem(GameType.Archenemy); cbVariants.addItem(GameType.ArchenemyRumble); @@ -184,14 +185,14 @@ else if (cbVariants.getSelectedIndex() == cbVariants.getItemCount() - 1) { updatePlayersFromPrefs(); FThreads.invokeInBackgroundThread(() -> { - playerPanels.get(0).initialize(FPref.CONSTRUCTED_P1_DECK_STATE, FPref.COMMANDER_P1_DECK_STATE, FPref.OATHBREAKER_P1_DECK_STATE, FPref.TINY_LEADER_P1_DECK_STATE, FPref.BRAWL_P1_DECK_STATE, DeckType.PRECONSTRUCTED_DECK); - playerPanels.get(1).initialize(FPref.CONSTRUCTED_P2_DECK_STATE, FPref.COMMANDER_P2_DECK_STATE, FPref.OATHBREAKER_P2_DECK_STATE, FPref.TINY_LEADER_P2_DECK_STATE, FPref.BRAWL_P2_DECK_STATE, DeckType.COLOR_DECK); + playerPanels.get(0).initialize(FPref.CONSTRUCTED_P1_DECK_STATE, FPref.COMMANDER_P1_DECK_STATE, FPref.OATHBREAKER_P1_DECK_STATE, FPref.TINY_LEADER_P1_DECK_STATE, FPref.BRAWL_P1_DECK_STATE, FPref.DAN_DAN_P1_DECK_STATE, DeckType.PRECONSTRUCTED_DECK); + playerPanels.get(1).initialize(FPref.CONSTRUCTED_P2_DECK_STATE, FPref.COMMANDER_P2_DECK_STATE, FPref.OATHBREAKER_P2_DECK_STATE, FPref.TINY_LEADER_P2_DECK_STATE, FPref.BRAWL_P2_DECK_STATE, FPref.DAN_DAN_P2_DECK_STATE, DeckType.COLOR_DECK); try { if (getNumPlayers() > 2) { - playerPanels.get(2).initialize(FPref.CONSTRUCTED_P3_DECK_STATE, FPref.COMMANDER_P3_DECK_STATE, FPref.OATHBREAKER_P3_DECK_STATE, FPref.TINY_LEADER_P3_DECK_STATE, FPref.BRAWL_P3_DECK_STATE, DeckType.COLOR_DECK); + playerPanels.get(2).initialize(FPref.CONSTRUCTED_P3_DECK_STATE, FPref.COMMANDER_P3_DECK_STATE, FPref.OATHBREAKER_P3_DECK_STATE, FPref.TINY_LEADER_P3_DECK_STATE, FPref.BRAWL_P3_DECK_STATE, FPref.DAN_DAN_P3_DECK_STATE, DeckType.COLOR_DECK); } if (getNumPlayers() > 3) { - playerPanels.get(3).initialize(FPref.CONSTRUCTED_P4_DECK_STATE, FPref.COMMANDER_P4_DECK_STATE, FPref.OATHBREAKER_P3_DECK_STATE, FPref.TINY_LEADER_P4_DECK_STATE, FPref.BRAWL_P4_DECK_STATE, DeckType.COLOR_DECK); + playerPanels.get(3).initialize(FPref.CONSTRUCTED_P4_DECK_STATE, FPref.COMMANDER_P4_DECK_STATE, FPref.OATHBREAKER_P3_DECK_STATE, FPref.TINY_LEADER_P4_DECK_STATE, FPref.BRAWL_P4_DECK_STATE, FPref.DAN_DAN_P4_DECK_STATE, DeckType.COLOR_DECK); } } catch (Exception e) {} /*playerPanels.get(4).initialize(FPref.CONSTRUCTED_P5_DECK_STATE, DeckType.COLOR_DECK); @@ -456,6 +457,7 @@ private MultiVariantSelect() { lstVariants.addItem(new Variant(GameType.Oathbreaker)); lstVariants.addItem(new Variant(GameType.TinyLeaders)); lstVariants.addItem(new Variant(GameType.Brawl)); + lstVariants.addItem(new Variant(GameType.DanDan)); lstVariants.addItem(new Variant(GameType.Planechase)); lstVariants.addItem(new Variant(GameType.Archenemy)); lstVariants.addItem(new Variant(GameType.ArchenemyRumble)); @@ -587,6 +589,24 @@ public final void update(final int slot, final LobbySlotType type) { default: break; } + final FDeckChooser danDanDeckChooser = playerPanels.get(slot).getDanDanDeckChooser(); + selectedDeckType = danDanDeckChooser.getSelectedDeckType(); + switch (selectedDeckType){ + case STANDARD_CARDGEN_DECK: + case PIONEER_CARDGEN_DECK: + case HISTORIC_CARDGEN_DECK: + case MODERN_CARDGEN_DECK: + case LEGACY_CARDGEN_DECK: + case VINTAGE_CARDGEN_DECK: + case PAUPER_CARDGEN_DECK: + case COLOR_DECK: + case STANDARD_COLOR_DECK: + case MODERN_COLOR_DECK: + danDanDeckChooser.refreshDeckListForAI(); + break; + default: + break; + } } @Override @@ -618,9 +638,9 @@ public void update(final boolean fullUpdate) { else { panel = new PlayerPanel(this, allowNetworking, i, slot, lobby.mayEdit(i), lobby.hasControl()); if (i == 2) { - panel.initialize(FPref.CONSTRUCTED_P3_DECK_STATE, FPref.COMMANDER_P3_DECK_STATE, FPref.OATHBREAKER_P3_DECK_STATE, FPref.TINY_LEADER_P3_DECK_STATE, FPref.BRAWL_P3_DECK_STATE, DeckType.COLOR_DECK); + panel.initialize(FPref.CONSTRUCTED_P3_DECK_STATE, FPref.COMMANDER_P3_DECK_STATE, FPref.OATHBREAKER_P3_DECK_STATE, FPref.TINY_LEADER_P3_DECK_STATE, FPref.BRAWL_P3_DECK_STATE, FPref.DAN_DAN_P3_DECK_STATE, DeckType.COLOR_DECK); } else if (i == 3) { - panel.initialize(FPref.CONSTRUCTED_P4_DECK_STATE, FPref.COMMANDER_P4_DECK_STATE, FPref.OATHBREAKER_P4_DECK_STATE, FPref.TINY_LEADER_P4_DECK_STATE, FPref.BRAWL_P4_DECK_STATE, DeckType.COLOR_DECK); + panel.initialize(FPref.CONSTRUCTED_P4_DECK_STATE, FPref.COMMANDER_P4_DECK_STATE, FPref.OATHBREAKER_P4_DECK_STATE, FPref.TINY_LEADER_P4_DECK_STATE, FPref.BRAWL_P4_DECK_STATE, FPref.DAN_DAN_P4_DECK_STATE, DeckType.COLOR_DECK); } playerPanels.add(panel); playersScroll.add(panel); @@ -729,6 +749,14 @@ else if (hasVariant(GameType.Brawl)) { deckName = Forge.getLocalizer().getMessage("lblBrawlDeck") + ": " + playerPanel.getBrawlDeckChooser().getDeck().getName(); } + } + else if (hasVariant(GameType.DanDan)) { + deck = playerPanel.getDanDanDeck(); + if (deck != null) { + playerPanel.getDanDanDeckChooser().saveState(); + deckName = Forge.getLocalizer().getMessage("lblDanDanDeck") + ": " + + playerPanel.getDanDanDeckChooser().getDeck().getName(); + } }else { deck = playerPanel.getDeck(); if (deck != null) { diff --git a/forge-gui-mobile/src/forge/screens/constructed/PlayerPanel.java b/forge-gui-mobile/src/forge/screens/constructed/PlayerPanel.java index 7c9cf7981cd..791350d06db 100644 --- a/forge-gui-mobile/src/forge/screens/constructed/PlayerPanel.java +++ b/forge-gui-mobile/src/forge/screens/constructed/PlayerPanel.java @@ -76,10 +76,11 @@ public class PlayerPanel extends FContainer { private final FLabel btnOathbreakDeck = new FLabel.ButtonBuilder().text(Forge.getLocalizer().getMessage("lblOathbreakerDeckRandomGenerated")).build(); private final FLabel btnTinyLeadersDeck = new FLabel.ButtonBuilder().text(Forge.getLocalizer().getMessage("lblTinyLeadersDeckRandomGenerated")).build(); private final FLabel btnBrawlDeck = new FLabel.ButtonBuilder().text(Forge.getLocalizer().getMessage("lblBrawlDeckRandomGenerated")).build(); + private final FLabel btnDanDanDeck = new FLabel.ButtonBuilder().text(Forge.getLocalizer().getMessage("lblDanDanDeckRandomGenerated")).build(); private final FLabel btnPlanarDeck = new FLabel.ButtonBuilder().text(Forge.getLocalizer().getMessage("lblPlanarDeckRandomGenerated")).build(); private final FLabel btnVanguardAvatar = new FLabel.ButtonBuilder().text(Forge.getLocalizer().getMessage("lblVanguardAvatarRandom")).build(); - private final FDeckChooser deckChooser, lstSchemeDecks, lstCommanderDecks, lstOathbreakerDecks, lstTinyLeadersDecks, lstBrawlDecks, lstPlanarDecks; + private final FDeckChooser deckChooser, lstSchemeDecks, lstCommanderDecks, lstOathbreakerDecks, lstTinyLeadersDecks, lstBrawlDecks, lstDanDanDecks, lstPlanarDecks; private final FVanguardChooser lstVanguardAvatars; public PlayerPanel(final LobbyScreen screen0, final boolean allowNetworking0, final int index0, final LobbySlot slot, final boolean mayEdit0, final boolean mayControl0) { @@ -180,6 +181,21 @@ public void handleEvent(FEvent e) { } } }); + lstDanDanDecks = new FDeckChooser(GameType.DanDan, isAi, new FEventHandler() { + @Override + public void handleEvent(FEvent e) { + if (((DeckManager) e.getSource()).getSelectedItem() != null) { + btnDanDanDeck.setText(Forge.getLocalizer().getMessage("lblDanDanDeck") + + ":" + (Forge.isLandscapeMode() ? " " : "\n") + ((DeckManager) e.getSource()).getSelectedItem().getName()); + lstDanDanDecks.saveState(); + if (allowNetworking && btnDanDanDeck.isEnabled() && humanAiSwitch.isToggled()) { + screen.updateMyDeck(index); + } + } else { + btnDanDanDeck.setText(Forge.getLocalizer().getMessage("lblDanDanDeck")); + } + } + }); lstSchemeDecks = new FDeckChooser(GameType.Archenemy, isAi, e -> { if( ((DeckManager)e.getSource()).getSelectedItem() != null){ btnSchemeDeck.setText(Forge.getLocalizer().getMessage("lblSchemeDeck") @@ -261,6 +277,11 @@ public void handleEvent(FEvent e) { lstBrawlDecks.setHeaderCaption(Forge.getLocalizer().getMessage("lblSelectBrawlDeckFor").replace("%s", txtPlayerName.getText())); Forge.openScreen(lstBrawlDecks); }); + add(btnDanDanDeck); + btnDanDanDeck.setCommand(e -> { + lstDanDanDecks.setHeaderCaption(Forge.getLocalizer().getMessage("lblSelectDanDanDeckFor").replace("%s", txtPlayerName.getText())); + Forge.openScreen(lstDanDanDecks); + }); add(btnSchemeDeck); btnSchemeDeck.setCommand(e -> { lstSchemeDecks.setHeaderCaption(Forge.getLocalizer().getMessage("lblSelectSchemeDeckFor").replace("%s", txtPlayerName.getText())); @@ -286,7 +307,7 @@ public void handleEvent(FEvent e) { setMayControl(mayControl0); } - public void initialize(FPref savedStateSetting, FPref savedStateSettingCommander, FPref savedStateSettingOathbreaker, FPref savedStateSettingTinyLeader, FPref savedStateSettingBrawl, DeckType defaultDeckType) { + public void initialize(FPref savedStateSetting, FPref savedStateSettingCommander, FPref savedStateSettingOathbreaker, FPref savedStateSettingTinyLeader, FPref savedStateSettingBrawl, FPref savedStateSettingDanDan, DeckType defaultDeckType) { //order by last variant.. Set gameTypes = FModel.getPreferences().getGameType(FPref.UI_APPLIED_VARIANTS); if (gameTypes.contains(GameType.Commander)) { @@ -294,20 +315,31 @@ public void initialize(FPref savedStateSetting, FPref savedStateSettingCommander lstOathbreakerDecks.initialize(savedStateSettingOathbreaker, DeckType.OATHBREAKER_DECK); lstTinyLeadersDecks.initialize(savedStateSettingTinyLeader, DeckType.TINY_LEADERS_DECK); lstBrawlDecks.initialize(savedStateSettingBrawl, DeckType.BRAWL_DECK); + lstDanDanDecks.initialize(savedStateSettingDanDan, DeckType.DAN_DAN_DECK); deckChooser.initialize(savedStateSetting, defaultDeckType); } else if (gameTypes.contains(GameType.Oathbreaker)) { lstOathbreakerDecks.initialize(savedStateSettingOathbreaker, DeckType.OATHBREAKER_DECK); lstCommanderDecks.initialize(savedStateSettingCommander, DeckType.COMMANDER_DECK); lstTinyLeadersDecks.initialize(savedStateSettingTinyLeader, DeckType.TINY_LEADERS_DECK); lstBrawlDecks.initialize(savedStateSettingBrawl, DeckType.BRAWL_DECK); + lstDanDanDecks.initialize(savedStateSettingDanDan, DeckType.DAN_DAN_DECK); deckChooser.initialize(savedStateSetting, defaultDeckType); } else if (gameTypes.contains(GameType.TinyLeaders)) { lstTinyLeadersDecks.initialize(savedStateSettingTinyLeader, DeckType.TINY_LEADERS_DECK); lstOathbreakerDecks.initialize(savedStateSettingOathbreaker, DeckType.OATHBREAKER_DECK); lstCommanderDecks.initialize(savedStateSettingCommander, DeckType.COMMANDER_DECK); lstBrawlDecks.initialize(savedStateSettingBrawl, DeckType.BRAWL_DECK); + lstDanDanDecks.initialize(savedStateSettingDanDan, DeckType.DAN_DAN_DECK); deckChooser.initialize(savedStateSetting, defaultDeckType); } else if (gameTypes.contains(GameType.Brawl)) { + lstBrawlDecks.initialize(savedStateSettingBrawl, DeckType.BRAWL_DECK); + lstTinyLeadersDecks.initialize(savedStateSettingTinyLeader, DeckType.TINY_LEADERS_DECK); + lstOathbreakerDecks.initialize(savedStateSettingOathbreaker, DeckType.OATHBREAKER_DECK); + lstCommanderDecks.initialize(savedStateSettingCommander, DeckType.COMMANDER_DECK); + lstDanDanDecks.initialize(savedStateSettingDanDan, DeckType.DAN_DAN_DECK); + deckChooser.initialize(savedStateSetting, defaultDeckType); + } else if (gameTypes.contains(GameType.DanDan)) { + lstDanDanDecks.initialize(savedStateSettingDanDan, DeckType.DAN_DAN_DECK); lstBrawlDecks.initialize(savedStateSettingBrawl, DeckType.BRAWL_DECK); lstTinyLeadersDecks.initialize(savedStateSettingTinyLeader, DeckType.TINY_LEADERS_DECK); lstOathbreakerDecks.initialize(savedStateSettingOathbreaker, DeckType.OATHBREAKER_DECK); @@ -319,6 +351,7 @@ public void initialize(FPref savedStateSetting, FPref savedStateSettingCommander lstOathbreakerDecks.initialize(savedStateSettingOathbreaker, DeckType.OATHBREAKER_DECK); lstTinyLeadersDecks.initialize(savedStateSettingTinyLeader, DeckType.TINY_LEADERS_DECK); lstBrawlDecks.initialize(savedStateSettingBrawl, DeckType.BRAWL_DECK); + lstDanDanDecks.initialize(savedStateSettingDanDan, DeckType.DAN_DAN_DECK); } lstPlanarDecks.initialize(null, DeckType.RANDOM_DECK); lstSchemeDecks.initialize(null, DeckType.RANDOM_DECK); @@ -412,6 +445,10 @@ else if (btnBrawlDeck.isVisible()) { btnBrawlDeck.setBounds(x, y, w, fieldHeight); y += dy; } + else if (btnDanDanDeck.isVisible()) { + btnDanDanDeck.setBounds(x, y, w, fieldHeight); + y += dy; + } else if (btnDeck.isVisible()) { btnDeck.setBounds(x, y, w, fieldHeight); y += dy; @@ -435,7 +472,7 @@ public float getPreferredHeight() { if(Forge.isLandscapeMode()) rows--; } - if (btnCommanderDeck.isVisible() || btnOathbreakDeck.isVisible() || btnTinyLeadersDeck.isVisible() || btnBrawlDeck.isVisible()) { + if (btnCommanderDeck.isVisible() || btnOathbreakDeck.isVisible() || btnTinyLeadersDeck.isVisible() || btnBrawlDeck.isVisible() || btnDanDanDeck.isVisible()) { if(Forge.isLandscapeMode()) rows++; } @@ -504,6 +541,7 @@ private void onIsAiChanged(boolean isAi) { lstCommanderDecks.setIsAi(isAi); lstTinyLeadersDecks.setIsAi(isAi); lstBrawlDecks.setIsAi(isAi); + lstDanDanDecks.setIsAi(isAi); lstPlanarDecks.setIsAi(isAi); lstSchemeDecks.setIsAi(isAi); lstVanguardAvatars.setIsAi(isAi); @@ -582,6 +620,9 @@ public void setDeckSelectorButtonText(String text) { if (btnBrawlDeck.isVisible()) btnBrawlDeck.setText(text); + + if (btnDanDanDeck.isVisible()) + btnDanDanDeck.setText(text); } public void setVanguarAvatarName(String text) { @@ -607,6 +648,7 @@ public void updateVariantControlsVisibility() { boolean isOathbreakerApplied = false; boolean isTinyLeadersApplied = false; boolean isBrawlApplied = false; + boolean isDanDanApplied = false; boolean isPlanechaseApplied = false; boolean isVanguardApplied = false; boolean isArchenemyApplied = false; @@ -645,6 +687,11 @@ public void updateVariantControlsVisibility() { isDeckBuildingAllowed = false; //Tiny Leaders deck replaces basic deck, so hide that replacedbasicdeck = true; break; + case DanDan: + isDanDanApplied = true; + isDeckBuildingAllowed = false; + replacedbasicdeck = true; + break; case Planechase: isPlanechaseApplied = true; break; @@ -691,6 +738,12 @@ public void updateVariantControlsVisibility() { } else { btnBrawlDeck.setVisible(false); } + if (isDanDanApplied) { + btnDanDanDeck.setVisible(true); + btnDanDanDeck.setEnabled(mayEdit); + } else { + btnDanDanDeck.setVisible(false); + } if (archenemyVisiblity) { btnSchemeDeck.setVisible(true); btnSchemeDeck.setEnabled(mayEdit); @@ -724,6 +777,7 @@ public void updateVariantControlsVisibility() { btnOathbreakDeck.setVisible(isOathbreakerApplied && mayEdit); btnTinyLeadersDeck.setVisible(isTinyLeadersApplied && mayEdit); btnBrawlDeck.setVisible(isBrawlApplied && mayEdit); + btnDanDanDeck.setVisible(isDanDanApplied && mayEdit); btnSchemeDeck.setVisible(archenemyVisiblity && mayEdit); @@ -977,6 +1031,7 @@ public void setMayEdit(boolean mayEdit0) { btnOathbreakDeck.setEnabled(mayEdit); btnTinyLeadersDeck.setEnabled(mayEdit); btnBrawlDeck.setEnabled(mayEdit); + btnDanDanDeck.setEnabled(mayEdit); btnSchemeDeck.setEnabled(mayEdit); btnPlanarDeck.setEnabled(mayEdit); cbArchenemyTeam.setEnabled(mayEdit); @@ -1019,6 +1074,10 @@ public FDeckChooser getBrawlDeckChooser() { return lstBrawlDecks; } + public FDeckChooser getDanDanDeckChooser() { + return lstDanDanDecks; + } + public Deck getDeck() { return deckChooser.getDeck(); } @@ -1037,6 +1096,10 @@ public Deck getBrawlDeck() { return lstBrawlDecks.getDeck(); } + public Deck getDanDanDeck() { + return lstDanDanDecks.getDeck(); + } + public Deck getSchemeDeck() { return lstSchemeDecks.getDeck(); } diff --git a/forge-gui-mobile/src/forge/screens/match/views/VDevMenu.java b/forge-gui-mobile/src/forge/screens/match/views/VDevMenu.java index 2130e8d032f..e26c98ee8fe 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VDevMenu.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VDevMenu.java @@ -1,9 +1,12 @@ package forge.screens.match.views; import forge.Forge; +import forge.game.player.PlayerView; +import forge.interfaces.IGameController; import forge.menu.FCheckBoxMenuItem; import forge.menu.FDropDownMenu; import forge.menu.FMenuItem; +import forge.player.PlayerControllerHuman; import forge.screens.match.MatchController; import forge.util.ThreadUtil; @@ -70,9 +73,17 @@ protected void buildMenu() { addItem(new FCheckBoxMenuItem(Forge.getLocalizer().getMessage("lblUnlimitedLands"), unlimitedLands, e -> MatchController.instance.getGameController().cheat().setCanPlayUnlimitedLands(!unlimitedLands) )); - final boolean viewAll = MatchController.instance.getGameController().mayLookAtAllCards(); + final boolean viewAll = MatchController.instance.anyLocalMayLookAtAllCards(); addItem(new FCheckBoxMenuItem(Forge.getLocalizer().getMessage("lblViewAll"), viewAll, e -> - MatchController.instance.getGameController().cheat().setViewAllCards(!viewAll) + ThreadUtil.invokeInGameThread(() -> { + final boolean newVal = !MatchController.instance.anyLocalMayLookAtAllCards(); + for (final PlayerView pv : MatchController.instance.getLocalPlayers()) { + final IGameController gc = MatchController.instance.getGameController(pv); + if (gc instanceof PlayerControllerHuman) { + ((PlayerControllerHuman) gc).setMayLookAtAllCards(newVal); + } + } + }) )); addItem(new FMenuItem(Forge.getLocalizer().getMessage("lblAddCounterPermanent"), e -> ThreadUtil.invokeInGameThread(() -> MatchController.instance.getGameController().cheat().addCountersToPermanent()) diff --git a/forge-gui-mobile/src/forge/screens/match/views/VPlayerPanel.java b/forge-gui-mobile/src/forge/screens/match/views/VPlayerPanel.java index 25735826693..2c3ab18f725 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VPlayerPanel.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VPlayerPanel.java @@ -15,6 +15,8 @@ import forge.assets.FSkinFont; import forge.assets.FSkinImage; import forge.assets.FSkinImageInterface; +import forge.game.DanDanViewZones; +import forge.game.GameView; import forge.game.card.CardView; import forge.game.card.CounterEnumType; import forge.game.player.PlayerView; @@ -899,7 +901,7 @@ public boolean isAlignedRightForAltDisplay() { @Override protected FSkinColor getSelectedBackgroundColor() { - if ((this.zoneType == ZoneType.Graveyard) && player.hasDelirium()) + if ((this.zoneType == ZoneType.Graveyard) && DanDanViewZones.hasDeliriumForDisplay(MatchController.instance.getGameView(), player)) return getDeliriumHighlight(); return super.getSelectedBackgroundColor(); } @@ -942,7 +944,8 @@ private InfoTabExtra() { super(DEFAULT_ICON); this.displayAreas = new EnumMap<>(ZoneType.class); for (ZoneType zoneType : EXTRA_ZONES) { - FCollectionView cards = player.getCards(zoneType); + final GameView gv = MatchController.instance.getGameView(); + FCollectionView cards = DanDanViewZones.cardsForZoneDisplay(gv, player, zoneType); if (cards == null || cards.isEmpty()) continue; createZoneIfMissing(zoneType); @@ -1028,7 +1031,8 @@ public void update(ZoneType zoneType) { if (!displayAreas.containsKey(zoneType)) { if (!EXTRA_ZONES.contains(zoneType)) return; - FCollectionView cards = player.getCards(zoneType); + final GameView gv = MatchController.instance.getGameView(); + FCollectionView cards = DanDanViewZones.cardsForZoneDisplay(gv, player, zoneType); if (cards == null || cards.isEmpty()) return; createZoneIfMissing(zoneType); diff --git a/forge-gui-mobile/src/forge/screens/match/views/VZoneDisplay.java b/forge-gui-mobile/src/forge/screens/match/views/VZoneDisplay.java index f919270df28..85d1302ec55 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VZoneDisplay.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VZoneDisplay.java @@ -4,10 +4,15 @@ import forge.Forge; import forge.Graphics; +import forge.game.DanDanViewZones; +import forge.game.GameView; +import forge.game.card.CardView; import forge.game.player.PlayerView; import forge.game.zone.ZoneType; +import forge.screens.match.MatchController; import forge.toolbox.FCardPanel; import forge.toolbox.FDisplayObject; +import forge.util.collect.FCollectionView; public class VZoneDisplay extends VCardDisplayArea { private final PlayerView player; @@ -25,7 +30,9 @@ public ZoneType getZoneType() { @Override public void update() { - refreshCardPanels(player.getCards(zoneType)); + final GameView gv = MatchController.instance.getGameView(); + final FCollectionView cards = DanDanViewZones.cardsForZoneDisplay(gv, player, zoneType); + refreshCardPanels(cards); } @Override diff --git a/forge-gui/res/cardsfolder/d/dandan_test.txt b/forge-gui/res/cardsfolder/d/dandan_test.txt new file mode 100644 index 00000000000..ee3c25d6362 --- /dev/null +++ b/forge-gui/res/cardsfolder/d/dandan_test.txt @@ -0,0 +1,6 @@ +Name:Dandan Test +ManaCost:U +Types:Instant +A:SP$ Scry | ScryNum$ 3 | SpellDescription$ Scry 3, then discard two cards. | SubAbility$ DBDiscard +SVar:DBDiscard:DB$ Discard | Defined$ You | NumCards$ 2 | Mode$ TgtChoose +Oracle:Scry 3, then discard two cards. diff --git a/forge-gui/res/dandan/.gitkeep b/forge-gui/res/dandan/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/forge-gui/res/dandan/BlackDanDan_GamblingGhoul.dck b/forge-gui/res/dandan/BlackDanDan_GamblingGhoul.dck new file mode 100644 index 00000000000..f2c12be1810 --- /dev/null +++ b/forge-gui/res/dandan/BlackDanDan_GamblingGhoul.dck @@ -0,0 +1,28 @@ +[metadata] +Name=BlackDanDan_GamblingGhoul +Comment=This black version of DanDan was created by messum/Powerful Nothing. Instead of the blue creature DanDan, the deck revolves around the black creature Barrow Ghoul and manipulating the graveyard. Special thanks to Nick Floyd, creator of the DanDan variant. +Deck Type=DanDan +[Main] +2 Activated Sleeper|DMC|[24] +2 Barren Moor|ONS|[312] +12 Barrow Ghoul|WTH|[61] +2 Chainer's Edict|TOR|[57] +2 Corpse Churn|OGW|[83] +2 Dakmor Salvage|FUT|[169] +2 Dream Salvage|SHM|[160] +2 Ebon Stronghold|FEM|[95] +2 Force of Despair|MH1|[92] +2 Foul Renewal|DTK|[101] +2 Gravepurge|DKA|[65] +4 Guerrilla Tactics|ALL|[74a] +4 Hymn to Tourach|FEM|[38b] +2 Loxodon Smiter|RTR|[178] +6 Misinformation|ALL|[56] +2 Mortuary Mire|BFZ|[240] +2 Murderous Cut|KTK|[81] +2 Orzhov Basilica|BRC|[191] +2 Polluted Mire|USG|[323] +2 Pull from Eternity|TSP|[35] +12 Swamp|LEB|[295] +4 Temple of Silence|BRC|[209] +6 Whispers of Emrakul|EMN|[114] diff --git a/forge-gui/res/dandan/DanDan_FloydOG.dck b/forge-gui/res/dandan/DanDan_FloydOG.dck new file mode 100644 index 00000000000..e90b2e547ee --- /dev/null +++ b/forge-gui/res/dandan/DanDan_FloydOG.dck @@ -0,0 +1,30 @@ +[metadata] +Name=DanDan_FloydOG +Deck Type=DanDan +Comment=This is Nick Floyd's original DanDan deck. Special thanks to Nick Floyd for creating the variant and Rhystic Studies and Braden for spreading the word on the format. +[Main] +4 Accumulated Knowledge|NMS|[26] +2 Brainstorm|ICE|[61] +2 Crystal Spray|INV|[50] +2 Dance of the Skywise|DTK|[50] +10 Dandân|ARN|[12] +2 Diminishing Returns|ALL|[26] +2 Halimar Depths|SLD|[2159] +2 Insidious Will|KLD|[52] +18 Island|LEB|[293] +2 Izzet Boilerworks|BRC|[188] +2 Lonely Sandbar|SLD|[2161] +6 Memory Lapse|HML|[32a] +2 Metamorphose|SCG|[40] +2 Mind Bend|MIR|[77] +2 Mystic Retrieval|INR|[363] +2 Mystic Sanctuary|TSR|[408] +2 Mystical Tutor|MIR|[80] +2 Predict|ODY|[94] +2 Ray of Command|MIR|[86] +2 Remote Isle|SLD|[2162] +2 Supplant Form|FRF|[54] +2 Svyelunite Temple|FEM|[102] +2 Temple of Epiphany|FDN|[699] +2 Unsubstantiate|SLD|[2158] +2 Vision Charm|VIS|[49] diff --git a/forge-gui/res/dandan/DanDan_SecretLair.dck b/forge-gui/res/dandan/DanDan_SecretLair.dck new file mode 100644 index 00000000000..eb7fffc02e9 --- /dev/null +++ b/forge-gui/res/dandan/DanDan_SecretLair.dck @@ -0,0 +1,34 @@ +[metadata] +Name=DanDan_SecretLair +Comment=This is the DanDan deck from the March 2026 Secret Lair that sold out almost instantly and was widely underprinted. What a wasted opportunity to spread the joys of DanDan. But, special thanks to Nick Floyd for creating this format. +Deck Type=DanDan +[Main] +4 Accumulated Knowledge|SLD|[2140] +2 Brainstorm|SLD|[2148] +2 Capture of Jingzhou|SLD|[2149] +2 Chart a Course|SLD|[2150] +2 Control Magic|SLD|[2151] +2 Crystal Spray|SLD|[2152] +5 Dandân|SLD|[2138] +5 Dandân|SLD|[2139] +2 Day's Undoing|SLD|[2153] +2 Halimar Depths|SLD|[2159] +2 Haunted Fengraf|SLD|[2160] +5 Island|SLD|[2144] +5 Island|SLD|[2145] +5 Island|SLD|[2146] +5 Island|SLD|[2147] +2 Lonely Sandbar|SLD|[2161] +2 Magical Hack|SLD|[2141] +8 Memory Lapse|SLD|[2142] +2 Mental Note|SLD|[2154] +2 Metamorphose|SLD|[2155] +2 Mystic Sanctuary|SLD|[2143] +2 Predict|SLD|[2156] +2 Remote Isle|SLD|[2162] +2 Svyelunite Temple|SLD|[2164] +2 Telling Time|SLD|[2157] +2 The Surgical Bay|SLD|[2163] +2 Unsubstantiate|SLD|[2158] +[Sideboard] +2 Vision Charm|VIS|[49] diff --git a/forge-gui/res/dandan/DanDan_TolarianCC.dck b/forge-gui/res/dandan/DanDan_TolarianCC.dck new file mode 100644 index 00000000000..4310262c274 --- /dev/null +++ b/forge-gui/res/dandan/DanDan_TolarianCC.dck @@ -0,0 +1,29 @@ +[metadata] +Name=DanDan_TolarianCC +Comment=This is the blue tempo DanDan deck featured in the Tolarian Communicty College video featuring Rhystic Studies, Georg, and the Professor. There are no sweepers and there are more sorceries than many versions. Special thanks to Nick Floyd for creating this variant. +Deck Type=DanDan +[Main] +2 Brainstorm|ICE|[61] +2 Chart a Course|SLD|[2150] +10 Dandân|ARN|[12] +2 Diminishing Returns|ALL|[26] +2 Flood Plain|MIR|[326] +2 Gone Missing|SOI|[67] +2 Halimar Depths|SLD|[2159] +18 Island|LEB|[293] +2 Izzet Boilerworks|BRC|[188] +2 Lonely Sandbar|SLD|[2161] +2 Magical Hack|LEB|[64] +8 Memory Lapse|HML|[32a] +2 Metamorphose|SCG|[40] +2 Mind Bend|MIR|[77] +2 Mystic Retrieval|INR|[363] +2 Mystic Sanctuary|TSR|[408] +2 Narset's Reversal|WAR|[62] +2 Predict|ODY|[94] +2 Remote Isle|SLD|[2162] +2 Repulse|INV|[70] +2 Supplant Form|FRF|[54] +4 Take Inventory|EMN|[76] +2 Temple of Epiphany|FDN|[699] +2 Unsubstantiate|SLD|[2158] diff --git a/forge-gui/res/dandan/RedDanDan_CragCrag.dck b/forge-gui/res/dandan/RedDanDan_CragCrag.dck new file mode 100644 index 00000000000..8b8c3dde286 --- /dev/null +++ b/forge-gui/res/dandan/RedDanDan_CragCrag.dck @@ -0,0 +1,27 @@ +[metadata] +Name=RedDanDan_CragCrag +Deck Type=Constructed +Comment=Special thanks to xXSpaghetti_StealerXx for this red DanDan variant where the goal is to try to keep control of Crag Saurian. Special thanks to Nick Floyd for creating the DanDan format. +[Main] +2 Abrade|BRC|[111] +2 Arcbond|FRF|[91] +2 Balduvian Trading Post|ALL|[137] +2 Blazing Volley|AKH|[119] +2 Browbeat|JUD|[82] +2 Cast into the Fire|LTR|[118] +2 Cleansing Wildfire|ZNR|[137] +10 Crag Saurian|MMQ|[185] +4 Faithless Looting|BRC|[116] +2 Flame Jab|EVE|[53] +2 Forgotten Cave|ONS|[317] +2 Granite Shard|MRD|[182] +4 Grove of the Burnwillows|V12|[8] +22 Mountain|LEB|[298] +2 Oasis|ARN|[78] +4 Punishing Fire|ZEN|[142] +2 Shield of the Realm|DOM|[228] +2 Shock|STH|[98] +4 Squee's Toy|TMP|[309] +2 Struggle // Survive|HOU|[151] +2 Twin Bolt|TDM|[128] +2 Wheel of Fortune|LEB|[184] diff --git a/forge-gui/res/dandan/RedDanDan_RiskFactor.dck b/forge-gui/res/dandan/RedDanDan_RiskFactor.dck new file mode 100644 index 00000000000..7d33c541962 --- /dev/null +++ b/forge-gui/res/dandan/RedDanDan_RiskFactor.dck @@ -0,0 +1,26 @@ +[metadata] +Name=RedDanDan_RiskFactor +Deck Type=Constructed +Comment=Special thanks to Erik Dragon Highlander for this red DanDan variant focused on Risk Factor. Special thanks to Nick Floyd for creating the DanDan variant. +[Main] +2 Burning Inquiry|M10|[128] +2 Chef's Kiss|MH2|[120] +2 Desperate Ravings|PLST|[C15-149] +2 Dwarven Ruins|FEM|[94] +4 Faithless Looting|BRC|[116] +2 Fireblast|VIS|[79] +2 Forgotten Cave|ONS|[317] +2 Izzet Boilerworks|BRC|[188] +2 Light Up the Stage|RVR|[338] +8 Molten Influence|ODY|[207] +20 Mountain|LEB|[298] +2 Reforge the Soul|INR|[167] +4 Rekindled Flame|EVE|[61] +2 Reverberate|M11|[155] +6 Risk Factor|GRN|[113] +4 Rite of Flame|CSP|[96] +6 Shreds of Sanity|EMN|[141] +2 Smoldering Crater|USG|[328] +2 Temple of Epiphany|BRC|[207] +2 Tibalt's Trickery|KHM|[153] +2 Zhalfirin Void|AFC|[273] diff --git a/forge-gui/res/dandan/RedDanDan_ThunderousWrath.dck b/forge-gui/res/dandan/RedDanDan_ThunderousWrath.dck new file mode 100644 index 00000000000..ed6f3d237d7 --- /dev/null +++ b/forge-gui/res/dandan/RedDanDan_ThunderousWrath.dck @@ -0,0 +1,20 @@ +[metadata] +Name=RedDanDan_ThunderousWrath +Deck Type=Constructed +Comment=Thanks to Harrison Fang for this red DanDan variant featuring Thunderous Wrath where the goal is to manipulate the top of the deck to miraculously cast Thunderous Wrath. Or failing a miracle, pay full freight. This was featured in Rhystic Studies' video about Forgetful Fish. Special thanks to Nick Floyd for the DanDan variant. +[Main] +8 Brainstorm|ICE|[61] +2 Day's Undoing|SLD|[2153] +2 Disrupt|WTH|[37] +4 Halimar Depths|SLD|[2159] +8 Island|LEB|[293] +4 Izzet Boilerworks|2X2|[326] +2 Lonely Sandbar|ONS|[320] +8 Memory Lapse|HML|[32a] +8 Portent|ICE|[90] +4 Predict|ODY|[94] +2 Remote Isle|USG|[324] +4 Smoldering Crater|USG|[328] +8 Temple of Epiphany|M21|[252] +4 Thought Scour|AA2|[5] +12 Thunderous Wrath|AVR|[160] diff --git a/forge-gui/res/dandan/WhiteDanDan_LostLeonin.dck b/forge-gui/res/dandan/WhiteDanDan_LostLeonin.dck new file mode 100644 index 00000000000..2821c4ad541 --- /dev/null +++ b/forge-gui/res/dandan/WhiteDanDan_LostLeonin.dck @@ -0,0 +1,24 @@ +[metadata] +Name=WhiteDanDan_LostLeonin +Deck Type=DanDan +Comment=This white version of DanDan was created by Corrus555. The goal is to give your opponent 10 poison counters with Lost Leonin. Special thanks to Nick Floyd for creating the format. +[Main] +2 Act of Aggression|NPH|[78] +6 Azorius Chancery|BRC|[175] +2 Brave the Elements|EA2|[4] +2 Brought Back|M20|[9] +2 Curtain of Light|SOK|[6] +2 Day's Undoing|SLD|[2153] +1 Gods Willing|THS|[16] +3 Gut Shot|NPH|[86] +3 Judge Unworthy|TSR|[21] +4 Lapse of Certainty|CON|[9] +3 Loran's Escape|BRO|[14] +12 Lost Leonin|NPH|[13] +4 Marrow Shards|NPH|[15] +4 Noxious Revival|NPH|[118] +10 Plains|LEB|[289] +4 Postmortem Lunge|NPH|[70] +2 Reinforcements|ALL|[12a] +10 Secluded Steppe|ONS|[324] +4 The Fair Basilica|ONE|[252] diff --git a/forge-gui/res/defaults/editor.xml b/forge-gui/res/defaults/editor.xml index 558ea9e46a9..a9455d3a5e0 100644 --- a/forge-gui/res/defaults/editor.xml +++ b/forge-gui/res/defaults/editor.xml @@ -17,6 +17,7 @@ EDITOR_COMMANDER EDITOR_OATHBREAKER EDITOR_BRAWL + EDITOR_DANDAN EDITOR_TINY_LEADERS EDITOR_DECKGEN diff --git a/forge-gui/res/defaults/match_dandan.xml b/forge-gui/res/defaults/match_dandan.xml new file mode 100644 index 00000000000..c03e5492be6 --- /dev/null +++ b/forge-gui/res/defaults/match_dandan.xml @@ -0,0 +1,32 @@ + + + + REPORT_STACK + REPORT_COMBAT + REPORT_LOG + REPORT_DEPENDENCIES + + + REPORT_MESSAGE + BUTTON_DOCK + + + FIELD_1 + + + HAND_1 + + + CARD_DETAIL + CARD_PICTURE + + + FIELD_0 + + + HAND_0 + + + ZONE_GRAVEYARD + + diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 61ffe0c1340..4aaac0664e5 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -570,6 +570,8 @@ lblTinyLeaders=Tiny Leaders lblTinyLeadersDesc=Each player has a legendary \"General\" card which can be cast at any time and determines deck colors. Each card must have mana value less than 4. lblBrawl=Brawl lblBrawlDesc=Each player has a legendary \"General\" card which can be cast at any time and determines deck colors. Only cards legal in Standard may be used. +lblDanDan=DanDan +lblDanDanDesc=Each player uses a 60+ card deck saved in the DanDan decks folder. Players share a library and graveyard. lblPlaneswalkerDesc=Each player has a Planeswalker card which can be cast at any time. lblPlanechase=Planechase lblPlanechaseDesc=Plane cards apply global effects. The Plane card changes when a player rolls \"Planeswalk\" on the planar die. @@ -708,6 +710,7 @@ lblRandomCommanderCard-basedDecks=Random Commander Card-based Decks lblOathbreakerDecks=Oathbreaker Decks lblTinyLeadersDecks=Tiny Leaders Decks lblBrawlDecks=Brawl Decks +lblDanDanDecks=DanDan Decks lblSchemeDecks=Scheme Decks lblPlanarDecks=Planar Decks lblPrecon=Precon @@ -984,6 +987,7 @@ ttbtnPrintProxies=Print to HTML file (Ctrl+P) lblImport=Import ttImportDeck=Attempt to import a deck from a non-Forge format (Ctrl+I) lblTitle=Title +lblDescription=Description #ImageView.java lblExpandallgroups=Expand all groups lblCollapseallgroups=Collapse all groups @@ -1283,6 +1287,7 @@ lblCommanderDeckRandomGenerated=Commander Deck: Random Generated Deck lblOathbreakerDeckRandomGenerated=Oathbreaker Deck: Random Generated Deck lblTinyLeadersDeckRandomGenerated=Tiny Leaders Deck: Random Generated Deck lblBrawlDeckRandomGenerated=Brawl Deck: Random Generated Deck +lblDanDanDeckRandomGenerated=DanDan Deck: Random Generated Deck lblPlanarDeckRandomGenerated=Planar Deck: Random Generated Deck lblVanguardAvatarRandom=Vanguard Avatar: Random lblNotReady=Not Ready @@ -1296,6 +1301,8 @@ lblSelectCommanderDeckFor=Select Commander Deck for %s lblSelectOathbreakerDeckFor=Select Oathbreaker Deck for %s lblSelectTinyLeadersDeckFor=Select Tiny Leaders Deck for %s lblSelectBrawlDeckFor=Select Brawl Deck for %s +lblDanDanDeck=DanDan Deck +lblSelectDanDanDeckFor=Select DanDan Deck for %s lblSelectSchemeDeckFor=Select Scheme Deck for %s lblSelectPlanarDeckFor=Select Planar Deck for %s lblSelectVanguardFor=Select Vanguard for %s diff --git a/forge-gui/src/main/java/forge/deck/DeckProxy.java b/forge-gui/src/main/java/forge/deck/DeckProxy.java index bb8c91b135e..a9021793762 100644 --- a/forge-gui/src/main/java/forge/deck/DeckProxy.java +++ b/forge-gui/src/main/java/forge/deck/DeckProxy.java @@ -492,6 +492,20 @@ public static Iterable getAllBrawlDecks(Predicate filter) { return result; } + public static Iterable getAllDanDanDecks() { + return getAllDanDanDecks(null); + } + public static Iterable getAllDanDanDecks(Predicate filter) { + final List result = new ArrayList<>(); + if (filter == null) { + filter = DeckFormat.DanDan.hasLegalCardsPredicate(FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)); + } else { + filter = filter.and(DeckFormat.DanDan.hasLegalCardsPredicate(FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY))); + } + addDecksRecursivelly("DanDan", GameType.DanDan, result, "", FModel.getDecks().getDanDan(), filter); + return result; + } + public static Iterable getAllSchemeDecks() { return getAllSchemeDecks(null); } diff --git a/forge-gui/src/main/java/forge/deck/DeckType.java b/forge-gui/src/main/java/forge/deck/DeckType.java index 2f20910ea2d..c0333b6bdc2 100644 --- a/forge-gui/src/main/java/forge/deck/DeckType.java +++ b/forge-gui/src/main/java/forge/deck/DeckType.java @@ -5,6 +5,8 @@ public enum DeckType { CUSTOM_DECK("lblCustomUserDecks"), + /** Saved under {@code res/dandan}. */ + DAN_DAN_DECK("lblDanDanDecks"), CONSTRUCTED_DECK("lblConstructedDecks"), COMMANDER_DECK("lblCommanderDecks"), RANDOM_COMMANDER_DECK("lblRandomCommanderDecks"), @@ -43,6 +45,7 @@ public enum DeckType { NET_ARCHIVE_BLOCK_DECK("lblNetArchiveBlockDecks"); public static DeckType[] ConstructedOptions; + public static DeckType[] DanDanOptions; public static DeckType[] CommanderOptions; static { @@ -73,6 +76,32 @@ public enum DeckType { DeckType.NET_ARCHIVE_VINTAGE_DECK, DeckType.NET_ARCHIVE_BLOCK_DECK }; + DanDanOptions = new DeckType[]{ + DeckType.DAN_DAN_DECK, + DeckType.PRECONSTRUCTED_DECK, + DeckType.QUEST_OPPONENT_DECK, + DeckType.COLOR_DECK, + DeckType.STANDARD_COLOR_DECK, + DeckType.MODERN_COLOR_DECK, + DeckType.PAUPER_COLOR_DECK, + DeckType.STANDARD_CARDGEN_DECK, + DeckType.MODERN_CARDGEN_DECK, + DeckType.PAUPER_CARDGEN_DECK, + DeckType.LEGACY_CARDGEN_DECK, + DeckType.VINTAGE_CARDGEN_DECK, + DeckType.PIONEER_CARDGEN_DECK, + DeckType.HISTORIC_CARDGEN_DECK, + DeckType.THEME_DECK, + DeckType.RANDOM_DECK, + DeckType.NET_DECK, + DeckType.NET_ARCHIVE_STANDARD_DECK, + DeckType.NET_ARCHIVE_PIONEER_DECK, + DeckType.NET_ARCHIVE_MODERN_DECK, + DeckType.NET_ARCHIVE_PAUPER_DECK, + DeckType.NET_ARCHIVE_LEGACY_DECK, + DeckType.NET_ARCHIVE_VINTAGE_DECK, + DeckType.NET_ARCHIVE_BLOCK_DECK + }; } else { ConstructedOptions = new DeckType[]{ DeckType.CUSTOM_DECK, @@ -93,6 +122,25 @@ public enum DeckType { DeckType.NET_ARCHIVE_VINTAGE_DECK, DeckType.NET_ARCHIVE_BLOCK_DECK }; + DanDanOptions = new DeckType[]{ + DeckType.DAN_DAN_DECK, + DeckType.PRECONSTRUCTED_DECK, + DeckType.QUEST_OPPONENT_DECK, + DeckType.COLOR_DECK, + DeckType.STANDARD_COLOR_DECK, + DeckType.MODERN_COLOR_DECK, + DeckType.PAUPER_COLOR_DECK, + DeckType.THEME_DECK, + DeckType.RANDOM_DECK, + DeckType.NET_DECK, + DeckType.NET_ARCHIVE_STANDARD_DECK, + DeckType.NET_ARCHIVE_PIONEER_DECK, + DeckType.NET_ARCHIVE_MODERN_DECK, + DeckType.NET_ARCHIVE_PAUPER_DECK, + DeckType.NET_ARCHIVE_LEGACY_DECK, + DeckType.NET_ARCHIVE_VINTAGE_DECK, + DeckType.NET_ARCHIVE_BLOCK_DECK + }; } } static { diff --git a/forge-gui/src/main/java/forge/deck/RandomDeckGenerator.java b/forge-gui/src/main/java/forge/deck/RandomDeckGenerator.java index b85bbef1194..53656ec55a8 100644 --- a/forge-gui/src/main/java/forge/deck/RandomDeckGenerator.java +++ b/forge-gui/src/main/java/forge/deck/RandomDeckGenerator.java @@ -86,6 +86,10 @@ private Deck getGeneratedDeck() { return DeckgenUtil.generateCommanderDeck(isAi, GameType.TinyLeaders); case Brawl: return DeckgenUtil.generateCommanderDeck(isAi, GameType.Brawl); + case DanDan: + if (!Iterables.isEmpty(DeckProxy.getAllDanDanDecks())) { + return Aggregates.random(DeckProxy.getAllDanDanDecks()).getDeck(); + } case Archenemy: return DeckgenUtil.generateSchemeDeck(); case Planechase: @@ -161,6 +165,9 @@ private Deck getUserDeck() { case Brawl: decks = DeckProxy.getAllBrawlDecks(DeckFormat.Brawl.isLegalDeckPredicate()); break; + case DanDan: + decks = DeckProxy.getAllDanDanDecks(DeckFormat.DanDan.isLegalDeckPredicate()); + break; case Archenemy: decks = DeckProxy.getAllSchemeDecks(DeckFormat.Archenemy.isLegalDeckPredicate()); break; diff --git a/forge-gui/src/main/java/forge/deck/io/DeckPreferences.java b/forge-gui/src/main/java/forge/deck/io/DeckPreferences.java index ce6aaafff22..d4a47fe5873 100644 --- a/forge-gui/src/main/java/forge/deck/io/DeckPreferences.java +++ b/forge-gui/src/main/java/forge/deck/io/DeckPreferences.java @@ -21,7 +21,7 @@ */ public class DeckPreferences { private static String selectedDeckType = "", currentDeck = "", draftDeck = "", sealedDeck = "", commanderDeck = "", - oathbreakerDeck = "", tinyLeadersDeck = "", brawlDeck = "", planarDeck = "", schemeDeck = ""; + oathbreakerDeck = "", tinyLeadersDeck = "", brawlDeck = "", danDanDeck = "", planarDeck = "", schemeDeck = ""; private static Map allPrefs = new HashMap<>(); public static DeckType getSelectedDeckType() { @@ -97,6 +97,15 @@ public static void setBrawlDeck(String brawlDeck0) { save(); } + public static String getDanDanDeck() { + return danDanDeck; + } + public static void setDanDanDeck(String danDanDeck0) { + if (danDanDeck.equals(danDanDeck0)) { return; } + danDanDeck = danDanDeck0; + save(); + } + public static String getPlanarDeck() { return planarDeck; } @@ -136,6 +145,7 @@ public static void load() { commanderDeck = root.getAttribute("commanderDeck"); oathbreakerDeck = root.getAttribute("oathbreakerDeck"); brawlDeck = root.getAttribute("brawlDeck"); + danDanDeck = root.getAttribute("danDanDeck"); tinyLeadersDeck = root.getAttribute("tinyLeadersDeck"); planarDeck = root.getAttribute("planarDeck"); schemeDeck = root.getAttribute("schemeDeck"); @@ -169,6 +179,7 @@ private static void save() { root.setAttribute("commanderDeck", commanderDeck); root.setAttribute("oathbreakerDeck", oathbreakerDeck); root.setAttribute("brawlDeck", brawlDeck); + root.setAttribute("danDanDeck", danDanDeck); root.setAttribute("tinyLeadersDeck", tinyLeadersDeck); root.setAttribute("planarDeck", planarDeck); root.setAttribute("schemeDeck", schemeDeck); 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 f520efcde2b..5caf7cb091f 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java @@ -140,6 +140,22 @@ public final IGameController getGameController(final PlayerView player) { return gameControllers.get(player); } + /** + * True if any local seat has dev "view all cards" enabled. {@link #getGameController()} is + * turn-based (current player only), so multi-seat local games must not use it alone for this flag. + */ + public final boolean anyLocalMayLookAtAllCards() { + if (spectator != null && spectator.mayLookAtAllCards()) { + return true; + } + for (final IGameController c : gameControllers.values()) { + if (c != null && c.mayLookAtAllCards()) { + return true; + } + } + return false; + } + public final Collection getOriginalGameControllers() { return originalGameControllers.values(); } @@ -234,14 +250,16 @@ public boolean mayView(final CardView c) { return true; } try { - if (getGameController().mayLookAtAllCards()) { // when it bugged here, the game thinks the spectator (null) + if (anyLocalMayLookAtAllCards()) { // when it bugged here, the game thinks the spectator (null) return true; // is the humancontroller here (maybe because there is an existing game thread???) } } catch (NullPointerException e) { return true; // return true so it will work as normal } - } else if (getGameController().mayLookAtAllCards()) { - return true; + } else { + if (anyLocalMayLookAtAllCards()) { + return true; + } } return c.canBeShownToAny(getLocalPlayers()); } diff --git a/forge-gui/src/main/java/forge/gamemodes/match/GameLobby.java b/forge-gui/src/main/java/forge/gamemodes/match/GameLobby.java index ada27f0713d..6c0fb22b580 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/GameLobby.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/GameLobby.java @@ -228,14 +228,17 @@ public void applyVariant(final GameType variant) { switch (variant) { case Archenemy: data.appliedVariants.remove(GameType.ArchenemyRumble); + data.appliedVariants.remove(GameType.DanDan); break; case ArchenemyRumble: data.appliedVariants.remove(GameType.Archenemy); + data.appliedVariants.remove(GameType.DanDan); break; case Commander: data.appliedVariants.remove(GameType.Oathbreaker); data.appliedVariants.remove(GameType.TinyLeaders); data.appliedVariants.remove(GameType.Brawl); + data.appliedVariants.remove(GameType.DanDan); data.appliedVariants.remove(GameType.MomirBasic); data.appliedVariants.remove(GameType.MoJhoSto); break; @@ -243,6 +246,7 @@ public void applyVariant(final GameType variant) { data.appliedVariants.remove(GameType.Commander); data.appliedVariants.remove(GameType.TinyLeaders); data.appliedVariants.remove(GameType.Brawl); + data.appliedVariants.remove(GameType.DanDan); data.appliedVariants.remove(GameType.MomirBasic); data.appliedVariants.remove(GameType.MoJhoSto); break; @@ -250,6 +254,7 @@ public void applyVariant(final GameType variant) { data.appliedVariants.remove(GameType.Commander); data.appliedVariants.remove(GameType.Oathbreaker); data.appliedVariants.remove(GameType.Brawl); + data.appliedVariants.remove(GameType.DanDan); data.appliedVariants.remove(GameType.MomirBasic); data.appliedVariants.remove(GameType.MoJhoSto); break; @@ -257,18 +262,32 @@ public void applyVariant(final GameType variant) { data.appliedVariants.remove(GameType.Commander); data.appliedVariants.remove(GameType.Oathbreaker); data.appliedVariants.remove(GameType.TinyLeaders); + data.appliedVariants.remove(GameType.DanDan); data.appliedVariants.remove(GameType.MomirBasic); data.appliedVariants.remove(GameType.MoJhoSto); break; + case DanDan: + data.appliedVariants.remove(GameType.Commander); + data.appliedVariants.remove(GameType.Oathbreaker); + data.appliedVariants.remove(GameType.TinyLeaders); + data.appliedVariants.remove(GameType.Brawl); + data.appliedVariants.remove(GameType.MomirBasic); + data.appliedVariants.remove(GameType.Vanguard); + data.appliedVariants.remove(GameType.MoJhoSto); + data.appliedVariants.remove(GameType.Archenemy); + data.appliedVariants.remove(GameType.ArchenemyRumble); + break; case Vanguard: data.appliedVariants.remove(GameType.MomirBasic); data.appliedVariants.remove(GameType.MoJhoSto); + data.appliedVariants.remove(GameType.DanDan); break; case MomirBasic: data.appliedVariants.remove(GameType.Commander); data.appliedVariants.remove(GameType.Oathbreaker); data.appliedVariants.remove(GameType.TinyLeaders); data.appliedVariants.remove(GameType.Brawl); + data.appliedVariants.remove(GameType.DanDan); data.appliedVariants.remove(GameType.Vanguard); data.appliedVariants.remove(GameType.MoJhoSto); break; @@ -277,6 +296,7 @@ public void applyVariant(final GameType variant) { data.appliedVariants.remove(GameType.Oathbreaker); data.appliedVariants.remove(GameType.TinyLeaders); data.appliedVariants.remove(GameType.Brawl); + data.appliedVariants.remove(GameType.DanDan); data.appliedVariants.remove(GameType.Vanguard); data.appliedVariants.remove(GameType.MomirBasic); break; @@ -301,6 +321,8 @@ public void removeVariant(final GameType variant) { currentGameType = GameType.TinyLeaders; } else if (hasVariant(GameType.Brawl)) { currentGameType = GameType.Brawl; + } else if (hasVariant(GameType.DanDan)) { + currentGameType = GameType.DanDan; } else { currentGameType = GameType.Constructed; } @@ -399,9 +421,12 @@ public Runnable startGame() { //Auto-generated decks don't need to be checked here //Commander deck replaces regular deck and is checked later if (checkLegality && autoGenerateVariant == null && !isCommanderMatch) { + final DeckFormat nonCommanderFormat = variantTypes.contains(GameType.DanDan) + ? GameType.DanDan.getDeckFormat() + : GameType.Constructed.getDeckFormat(); for (final LobbySlot slot : activeSlots) { final String name = slot.getName(); - final String errMsg = GameType.Constructed.getDeckFormat().getDeckConformanceProblem(slot.getDeck()); + final String errMsg = nonCommanderFormat.getDeckConformanceProblem(slot.getDeck()); if (null != errMsg) { SOptionPane.showErrorDialog(Localizer.getInstance().getMessage("lblPlayerDeckError", name, errMsg), Localizer.getInstance().getMessage("lblInvalidDeck")); return null; diff --git a/forge-gui/src/main/java/forge/gui/control/FControlGameEventHandler.java b/forge-gui/src/main/java/forge/gui/control/FControlGameEventHandler.java index dcc7db61487..1af09db3f5b 100644 --- a/forge-gui/src/main/java/forge/gui/control/FControlGameEventHandler.java +++ b/forge-gui/src/main/java/forge/gui/control/FControlGameEventHandler.java @@ -4,6 +4,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.eventbus.Subscribe; +import forge.game.GameType; import forge.game.card.CardView; import forge.game.event.*; import forge.game.player.PlayerView; @@ -184,6 +185,14 @@ private Void updateZone(final PlayerView p, final ZoneType z) { synchronized (zonesUpdate) { zonesUpdate.add(new PlayerZoneUpdate(p, z)); + if (matchController.getGameView().getGameType() == GameType.DanDan + && (z == ZoneType.Library || z == ZoneType.Graveyard)) { + for (final PlayerView playerView : matchController.getGameView().getPlayers()) { + if (playerView != p) { + zonesUpdate.add(new PlayerZoneUpdate(playerView, z)); + } + } + } } return processEvent(); } diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java index 7fb977880d9..d8d754d466a 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java @@ -264,6 +264,7 @@ public final class ForgeConstants { public static final String CONQUEST_SAVE_DIR = USER_CONQUEST_DIR + "saves" + PATH_SEPARATOR; public static final String DECK_TINY_LEADERS_DIR = DECK_BASE_DIR + "tiny_leaders" + PATH_SEPARATOR; public static final String DECK_BRAWL_DIR = DECK_BASE_DIR + "brawl" + PATH_SEPARATOR; + public static final String DECK_DANDAN_DIR = RES_DIR + "dandan" + PATH_SEPARATOR; public static final String MAIN_PREFS_FILE = USER_PREFS_DIR + "forge.preferences"; public static final String SERVER_PREFS_FILE = USER_PREFS_DIR + "server.preferences"; public static final String CARD_PREFS_FILE = USER_PREFS_DIR + "card.preferences"; @@ -281,6 +282,7 @@ public final class ForgeConstants { public static final String STARS_FILE = _DEFAULTS_DIR + "stars.png"; public static final FileLocation WINDOW_LAYOUT_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "window.xml"); public static final FileLocation MATCH_LAYOUT_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "match.xml"); + public static final FileLocation MATCH_DANDAN_LAYOUT_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "match_dandan.xml"); public static final FileLocation WORKSHOP_LAYOUT_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "workshop.xml"); public static final FileLocation EDITOR_LAYOUT_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "editor.xml"); public static final FileLocation GAUNTLET_DIR = new FileLocation(_DEFAULTS_DIR, USER_DIR, "gauntlet" + PATH_SEPARATOR); diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java index 27184469192..d20aac54b83 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java @@ -72,6 +72,14 @@ public enum FPref implements PreferencesStore.IPref { BRAWL_P6_DECK_STATE(""), BRAWL_P7_DECK_STATE(""), BRAWL_P8_DECK_STATE(""), + DAN_DAN_P1_DECK_STATE(""), + DAN_DAN_P2_DECK_STATE(""), + DAN_DAN_P3_DECK_STATE(""), + DAN_DAN_P4_DECK_STATE(""), + DAN_DAN_P5_DECK_STATE(""), + DAN_DAN_P6_DECK_STATE(""), + DAN_DAN_P7_DECK_STATE(""), + DAN_DAN_P8_DECK_STATE(""), UI_LANDSCAPE_MODE ("false"), UI_MATCHES_PER_GAME("3"), UI_APPLIED_VARIANTS(""), @@ -363,6 +371,12 @@ private static String defaultCustomLogTypes() { BRAWL_P5_DECK_STATE, BRAWL_P6_DECK_STATE, BRAWL_P7_DECK_STATE, BRAWL_P8_DECK_STATE }; + public static FPref[] DAN_DAN_DECK_STATES = { + DAN_DAN_P1_DECK_STATE, DAN_DAN_P2_DECK_STATE, + DAN_DAN_P3_DECK_STATE, DAN_DAN_P4_DECK_STATE, + DAN_DAN_P5_DECK_STATE, DAN_DAN_P6_DECK_STATE, + DAN_DAN_P7_DECK_STATE, DAN_DAN_P8_DECK_STATE }; + /** Phase stop prefs in PhaseType order (UPKEEP through CLEANUP, skipping UNTAP). */ public static FPref[] PHASES_AI = { PHASE_AI_UPKEEP, PHASE_AI_DRAW, PHASE_AI_MAIN1, diff --git a/forge-gui/src/main/java/forge/localinstance/properties/PreferencesStore.java b/forge-gui/src/main/java/forge/localinstance/properties/PreferencesStore.java index ada7f44a8df..e3efd584485 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/PreferencesStore.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/PreferencesStore.java @@ -168,6 +168,8 @@ else if (gameType.equals("Tiny Leaders")) result.add(GameType.TinyLeaders); else if (gameType.equals("Brawl")) result.add(GameType.Brawl); + else if (gameType.equals("DanDan")) + result.add(GameType.DanDan); else if (gameType.equals("Planechase")) result.add(GameType.Planechase); else if (gameType.equals("Archenemy")) diff --git a/forge-gui/src/main/java/forge/model/CardCollections.java b/forge-gui/src/main/java/forge/model/CardCollections.java index 24ea118d045..2ea95175452 100644 --- a/forge-gui/src/main/java/forge/model/CardCollections.java +++ b/forge-gui/src/main/java/forge/model/CardCollections.java @@ -45,6 +45,7 @@ public class CardCollections { private IStorage oathbreaker; private IStorage tinyLeaders; private IStorage brawl; + private IStorage danDan; private IStorage genetic; private IStorage customStarter; @@ -149,6 +150,15 @@ public IStorage getBrawl() { return brawl; } + public IStorage getDanDan() { + if (danDan == null) { + danDan = new StorageImmediatelySerialized<>("DanDan decks", + new DeckStorage(new File(ForgeConstants.DECK_DANDAN_DIR), ForgeConstants.RES_DIR), + true); + } + return danDan; + } + public final IStorage getGeneticAIDecks() { if (genetic == null) { genetic = new StorageImmediatelySerialized<>("Genetic AI decks",