diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java index 9915861a8fa9..1c1a01f93691 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java @@ -619,8 +619,8 @@ private void initializeGraveyardOrderingComboBox() { private void initializeStackGroupPermanentsComboBox() { final Localizer localizer = Localizer.getInstance(); - final String[] keys = {"default", "stack", "group_creatures", "group_all"}; - final String[] labelKeys = {"lblGroupDefault", "lblGroupStack", "lblGroupCreatures", "lblGroupAll"}; + final String[] keys = {"default", "stack", "group_tokens", "group_creatures", "group_all"}; + final String[] labelKeys = {"lblGroupDefault", "lblGroupStack", "lblGroupTokens", "lblGroupCreatures", "lblGroupAll"}; final Map mapping = new LinkedHashMap<>(); final String[] labels = new String[keys.length]; for (int i = 0; i < keys.length; i++) { diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/menus/DisplayMenu.java b/forge-gui-desktop/src/main/java/forge/screens/match/menus/DisplayMenu.java index 7f86a7ff80f7..b1f97a4acca0 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/menus/DisplayMenu.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/menus/DisplayMenu.java @@ -71,9 +71,9 @@ private SkinnedMenu getSubmenu_StackGroupPermanents() { final ButtonGroup group = new ButtonGroup(); final String current = prefs.getPref(FPref.UI_GROUP_PERMANENTS); - final String[] keys = {"default", "stack", "group_creatures", "group_all"}; - final String[] labelKeys = {"lblGroupDefault", "lblGroupStack", "lblGroupCreatures", "lblGroupAll"}; - final String[] tooltipKeys = {"nlGroupDefault", "nlGroupStack", "nlGroupCreatures", "nlGroupAll"}; + final String[] keys = {"default", "stack", "group_tokens", "group_creatures", "group_all"}; + final String[] labelKeys = {"lblGroupDefault", "lblGroupStack", "lblGroupTokens", "lblGroupCreatures", "lblGroupAll"}; + final String[] tooltipKeys = {"nlGroupDefault", "nlGroupStack", "nlGroupTokens", "nlGroupCreatures", "nlGroupAll"}; for (int i = 0; i < keys.length; i++) { final SkinnedRadioButtonMenuItem item = MenuUtil.createStayOpenSkinnedRadioButton(localizer.getMessage(labelKeys[i])); item.setToolTipText(localizer.getMessage(tooltipKeys[i])); 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 4e010f8084b9..17c41b9c0004 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 @@ -562,6 +562,29 @@ private void displayCardNameOverlay(final boolean isVisible, final Dimension img titleText.setVisible(isVisible); } + private static String formatGroupCount(int count) { + if (count < 1000) { + return String.valueOf(count); + } + if (count < 1000000) { + return formatLargeValue(count / 1000.0) + "k"; + } + if (count < 1000000000) { + return formatLargeValue(count / 1000000.0) + "M"; + } + return formatLargeValue(count / 1000000000.0) + "B"; + } + + private static String formatLargeValue(double val) { + if (val >= 100) { + return String.format(java.util.Locale.ENGLISH, "%.0f", val); + } + if (val >= 10) { + return String.format(java.util.Locale.ENGLISH, "%.1f", val).replace(".0", ""); + } + return String.format(java.util.Locale.ENGLISH, "%.2f", val).replace(".00", "").replace(".0", ""); + } + private void drawGroupCountBadge(final Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @@ -571,7 +594,7 @@ private void drawGroupCountBadge(final Graphics g) { badgeFontCardWidth = cardWidth; } - String text = "\u00D7" + groupCount; + String text = "\u00D7" + formatGroupCount(groupCount); FontMetrics fm = g2d.getFontMetrics(badgeFont); int textWidth = fm.stringWidth(text); diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/PlayArea.java b/forge-gui-desktop/src/main/java/forge/view/arcane/PlayArea.java index d8b94469a24a..3968861cf368 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/PlayArea.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/PlayArea.java @@ -97,6 +97,7 @@ public class PlayArea extends CardPanelContainer implements CardPanelMouseListen private boolean makeTokenRow = true; private boolean stackCreatures = false; + private boolean groupTokens; private boolean groupTokensAndCreatures; private boolean groupAll; private boolean grouping; @@ -113,9 +114,10 @@ public PlayArea(final CMatchUI matchUI, final FScrollPane scrollPane, final bool private void updateGroupScope() { String groupScope = FModel.getPreferences().getPref(FPref.UI_GROUP_PERMANENTS); this.stackCreatures = "stack".equals(groupScope); + this.groupTokens = "group_tokens".equals(groupScope) || "group_creatures".equals(groupScope) || "group_all".equals(groupScope); this.groupTokensAndCreatures = "group_creatures".equals(groupScope) || "group_all".equals(groupScope); this.groupAll = "group_all".equals(groupScope); - this.grouping = groupTokensAndCreatures || groupAll; + this.grouping = groupTokens || groupTokensAndCreatures || groupAll; int prefDepth = FModel.getPreferences().getPrefInt(FPref.UI_MAX_STACK_DEPTH); this.maxStackDepth = Math.max(MIN_STACK_DEPTH, Math.min(MAX_STACK_DEPTH, prefDepth)); } @@ -134,11 +136,11 @@ private CardStackRow collectAllTokens(List remainingPanels) { && card.isSick() == first.isSick() && card.hasSamePT(first) && card.getText().equals(first.getText()) - && (!groupTokensAndCreatures || card.isTapped() == first.isTapped()) - && (!groupTokensAndCreatures || card.getDamage() == first.getDamage()); + && (!groupTokens || card.isTapped() == first.isTapped()) + && (!groupTokens || card.getDamage() == first.getDamage()); return collectStacked(remainingPanels, RowType.Token, base.and(this::compatibleUnderCombatSeparation), - maxStackDepth, groupTokensAndCreatures); + maxStackDepth, groupTokens); } private CardStackRow collectAllCreatures(List remainingPanels) { diff --git a/forge-gui-mobile/src/forge/card/CardRenderer.java b/forge-gui-mobile/src/forge/card/CardRenderer.java index a30b78b2f0ae..1d9e8d79f635 100644 --- a/forge-gui-mobile/src/forge/card/CardRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardRenderer.java @@ -725,12 +725,20 @@ public static void drawCard(Graphics g, CardView card, float x, float y, float w } public static void drawCardWithOverlays(Graphics g, CardView card, float x, float y, float w, float h, CardStackPosition pos) { - drawCardWithOverlays(g, card, x, y, w, h, pos, false, false, false); + drawCardWithOverlays(g, card, x, y, w, h, pos, false, false, false, 0); + } + + public static void drawCardWithOverlays(Graphics g, CardView card, float x, float y, float w, float h, CardStackPosition pos, int groupCount) { + drawCardWithOverlays(g, card, x, y, w, h, pos, false, false, false, groupCount); } static float markersHeight = 0f; public static void drawCardWithOverlays(Graphics g, CardView card, float x, float y, float w, float h, CardStackPosition pos, boolean stackview, boolean showAltState, boolean isChoiceList) { + drawCardWithOverlays(g, card, x, y, w, h, pos, stackview, showAltState, isChoiceList, 0); + } + + public static void drawCardWithOverlays(Graphics g, CardView card, float x, float y, float w, float h, CardStackPosition pos, boolean stackview, boolean showAltState, boolean isChoiceList, int groupCount) { boolean canShow = MatchController.instance.mayView(card); float oldAlpha = g.getfloatAlphaComposite(); boolean unselectable = !MatchController.instance.isSelectable(card) && MatchController.instance.isSelecting(); @@ -747,6 +755,10 @@ public static void drawCardWithOverlays(Graphics g, CardView card, float x, floa w -= 2 * padding; h -= 2 * padding; + if (groupCount >= 2) { + drawGroupCountBadge(g, groupCount, cx, cy, cw, ch); + } + // TODO: A hacky workaround is currently used to make the game not leak the color information for Morph cards. final CardStateView details = showAltState ? card.getAlternateState() : isChoiceList && card.isSplitCard() ? card.getLeftSplitState() : card.getCurrentState(); final boolean isFaceDown = card.isFaceDown(); @@ -1200,4 +1212,41 @@ public void dispose() { } }); } + + private static void drawGroupCountBadge(Graphics g, int count, float x, float y, float w, float h) { + String text = "×" + formatGroupCount(count); + FSkinFont font = FSkinFont.forHeight(h * 0.15f); + float textWidth = font.getBounds(text).width; + float textHeight = font.getCapHeight(); + float padX = w / 20f; + float padY = h / 30f; + float badgeWidth = textWidth + padX * 2; + float badgeHeight = textHeight + padY * 2; + + g.fillRect(new Color(0, 0, 0, 0.7f), x + 2, y + 2, badgeWidth, badgeHeight); + g.drawText(text, font, Color.WHITE, x + 2 + padX, y + 2 + padY, textWidth, textHeight, false, Align.left, false); + } + + private static String formatGroupCount(int count) { + if (count < 1000) { + return String.valueOf(count); + } + if (count < 1000000) { + return formatLargeValue(count / 1000.0) + "k"; + } + if (count < 1000000000) { + return formatLargeValue(count / 1000000.0) + "M"; + } + return formatLargeValue(count / 1000000000.0) + "B"; + } + + private static String formatLargeValue(double val) { + if (val >= 100) { + return String.format(java.util.Locale.ENGLISH, "%.0f", val); + } + if (val >= 10) { + return String.format(java.util.Locale.ENGLISH, "%.1f", val).replace(".0", ""); + } + return String.format(java.util.Locale.ENGLISH, "%.2f", val).replace(".00", "").replace(".0", ""); + } } diff --git a/forge-gui-mobile/src/forge/screens/match/views/VCardDisplayArea.java b/forge-gui-mobile/src/forge/screens/match/views/VCardDisplayArea.java index 35021f57fa04..231c9bb7d698 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VCardDisplayArea.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VCardDisplayArea.java @@ -153,6 +153,10 @@ public void clear() { } } + protected boolean isGroupingEnabled(CardAreaPanel cardPanel) { + return false; + } + private int addCards(CardAreaPanel cardPanel, float x, float y, float cardWidth, float cardHeight) { int totalCount = 0; List attachedPanels = cardPanel.getAttachedPanels(); @@ -174,9 +178,20 @@ private int addCards(CardAreaPanel cardPanel, float x, float y, float cardWidth, cardPanel.setBounds(x, y, cardWidth, cardHeight); if (cardPanel.getNextPanelInStack() != null) { //add next panel in stack if needed - x += cardWidth * getCardStackOffset(); + float offset = getCardStackOffset(); + if (isGroupingEnabled(cardPanel)) { + offset = 0; + } + x += cardWidth * offset; totalCount += addCards(cardPanel.getNextPanelInStack(), x, y, cardWidth, cardHeight); } + + if (cardPanel.getPrevPanelInStack() == null) { + cardPanel.setGroupCount(totalCount + 1); + } else { + cardPanel.setGroupCount(0); + } + return totalCount + 1; } @@ -201,7 +216,11 @@ protected ScrollBounds layoutAndGetScrollBounds(float visibleWidth, float visibl for (CardAreaPanel cardPanel : new ArrayList<>(cardPanels.get())) { if (cardPanel != null) { int count = addCards(cardPanel, x, y, cardWidth, cardHeight); - x += cardWidth + (count - 1) * cardWidth * getCardStackOffset(); + float offset = getCardStackOffset(); + if (isGroupingEnabled(cardPanel)) { + offset = 0; + } + x += cardWidth + (count - 1) * cardWidth * offset; } } diff --git a/forge-gui-mobile/src/forge/screens/match/views/VField.java b/forge-gui-mobile/src/forge/screens/match/views/VField.java index 95152eb553be..a218399b61ae 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VField.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VField.java @@ -266,6 +266,21 @@ private FieldRow() { setVisible(true); //make visible by default unlike other display areas } + @Override + protected boolean isGroupingEnabled(CardAreaPanel cardPanel) { + String groupScope = FModel.getPreferences().getPref(ForgePreferences.FPref.UI_GROUP_PERMANENTS); + if ("group_all".equals(groupScope)) { + return true; + } + if ("group_creatures".equals(groupScope)) { + return cardPanel.getCard().isCreature() || cardPanel.getCard().isToken(); + } + if ("group_tokens".equals(groupScope)) { + return cardPanel.getCard().isToken(); + } + return false; + } + @Override protected float getCardWidth(float cardHeight) { return cardHeight; //allow cards room to tap diff --git a/forge-gui-mobile/src/forge/toolbox/FCardPanel.java b/forge-gui-mobile/src/forge/toolbox/FCardPanel.java index 1ded0c144e1d..96cad40a5f83 100644 --- a/forge-gui-mobile/src/forge/toolbox/FCardPanel.java +++ b/forge-gui-mobile/src/forge/toolbox/FCardPanel.java @@ -161,6 +161,16 @@ public void draw(Graphics g) { } } + private int groupCount; + + public int getGroupCount() { + return groupCount; + } + + public void setGroupCount(int groupCount0) { + groupCount = groupCount0; + } + private void rotateTransform(Graphics g, float x, float y, float w, float h, float edgeOffset, boolean animate) { if (tapped) { g.startRotateTransform(x + edgeOffset, y + h - edgeOffset, getTappedAngle()); @@ -169,7 +179,7 @@ private void rotateTransform(Graphics g, float x, float y, float w, float h, flo transformAnimation.start(); transformAnimation.drawCard(g, card, x, y, w, h); } else { - CardRenderer.drawCardWithOverlays(g, card, x, y, w, h, getStackPosition()); + CardRenderer.drawCardWithOverlays(g, card, x, y, w, h, getStackPosition(), groupCount); if (Forge.hasGamepad() && isHovered()) g.drawRect(3f, Color.LIME, x, y, w, h); } diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 82674c4027f8..717e34c5de57 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -263,10 +263,12 @@ cbpStackGroupPermanents=Stack/Group Permanents cbpTokensSeparateRow=Tokens in Separate Row lblGroupDefault=Default lblGroupStack=Stack Creatures +lblGroupTokens=Group Tokens lblGroupCreatures=Group Creatures/Tokens lblGroupAll=Group All Permanents nlGroupDefault=Creatures are never grouped or stacked. Identical lands, tokens, artifacts, and enchantments are stacked. Stacking fans cards out so each copy is partially visible. nlGroupStack=Same as Default, but creatures are also stacked. +nlGroupTokens=Group identical tokens into a single compact pile with a count badge. Creatures are fanned out. nlGroupCreatures=Group identical creatures and tokens into a single compact pile with a count badge. nlGroupAll=Group all identical permanents into a single compact pile with a count badge. nlTokensSeparateRow=Show tokens in their own row instead of mixed with creatures. diff --git a/forge-gui/res/languages/es-ES.properties b/forge-gui/res/languages/es-ES.properties index f89889901fd7..f908636edf13 100644 --- a/forge-gui/res/languages/es-ES.properties +++ b/forge-gui/res/languages/es-ES.properties @@ -212,6 +212,22 @@ nlHideReminderText=Ocultar el texto del recordatorio en el panel de Detalle de l nlCardTextUseSansSerif=Renderiza las imágenes de las cartas utilizando la fuente Sans-serif para el texto de las cartas. (Requiere reiniciar) nlCardTextHideReminder=Cuando se renderizan las imágenes de las cartas, se omite la renderización del texto de recordatorio. nlOpenPacksIndiv=Al abrir packs de cartas (Fat Packs) y cajas de sobres, los sobres se abrirán y se mostrarán de uno en uno. +cbpStackGroupPermanents=Apilar/Agrupar permanentes +cbpTokensSeparateRow=Fichas en fila separada +lblGroupDefault=Por defecto +lblGroupStack=Apilar criaturas +lblGroupTokens=Agrupar fichas +lblGroupCreatures=Agrupar criaturas/fichas +lblGroupAll=Agrupar todos los permanentes +nlGroupDefault=Las criaturas nunca se agrupan ni se apilan. Las tierras, fichas, artefactos y encantamientos idénticos se apilan. El apilamiento despliega las cartas para que cada copia sea parcialmente visible. +nlGroupStack=Igual que por defecto, pero las criaturas también se apilan. +nlGroupTokens=Agrupa las fichas idénticas en una sola pila compacta con un contador. Las criaturas se despliegan individualmente. +nlGroupCreatures=Agrupa las criaturas y fichas idénticas en una sola pila compacta con un contador. +nlGroupAll=Agrupa todos los permanentes idénticos en una sola pila compacta con un contador. +nlTokensSeparateRow=Muestra las fichas en su propia fila en lugar de mezcladas con las criaturas. +nlGroupPermanents=Controla cómo se apilan o agrupan los permanentes idénticos en el campo de batalla. +cbpMaxStackDepth=Profundidad máxima de pila +nlMaxStackDepth=Máximo de cartas por pila o grupo en el campo de batalla. En los modos Por defecto y Apilar criaturas, las cartas idénticas adicionales forman una nueva pila junto a la original. En los modos de Agrupar, los extras se ocultan detrás de la carta superior con el contador. nlTokensInSeparateRow=Muestra las fichas en una fila separada en el campo de batalla debajo de las criaturas que no son fichas. nlStackCreatures=Apila criaturas idénticas en el campo de batalla, como tierras, artefactos y encantamientos. nlSeparateCombatStacks=Separa las criaturas apiladas mientras atacan o bloquean para que las flechas de combate sean más fáciles de seguir.