gui, deck: reworked edh power level (close #5361, close #11732, related to #13341):

* added power level info in deck validation panel;
* added detail calculation info (hint with cards and their power levels);
* fixed that deck's edh power level ignore individual card's levels and used only commanders;
* removed outdated deck restrictions by commander colors;
* now players can really limit allowed decks by edh power level;
This commit is contained in:
Oleg Agafonov 2025-05-24 19:49:21 +04:00
parent 11dcc18049
commit f3ba897536
9 changed files with 235 additions and 76 deletions

View file

@ -0,0 +1,76 @@
package mage.client.components;
import mage.cards.decks.Deck;
import mage.client.util.GUISizeHelper;
import mage.client.util.gui.GuiDisplayUtil;
import mage.deck.Commander;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Inject power level info inside validation panel
*
* @author JayDi85
*/
public class EdhPowerLevelLegalityLabel extends LegalityLabel {
private final Commander commanderDeckType = new Commander();
private final List<String> foundPowerCards = new ArrayList<>();
public EdhPowerLevelLegalityLabel() {
super("EDH Power Level: ?", null);
setPreferredSize(DIM_PREFERRED_X3);
}
@Override
public List<String> selectCards() {
// choose cards with power level
return this.foundPowerCards;
}
@Override
public void validateDeck(Deck deck) {
// find and save power level and card hints
List<String> foundInfo = new ArrayList<>();
int level = this.commanderDeckType.getEdhPowerLevel(deck, foundPowerCards, foundInfo);
this.setText(String.format("EDH Power Level: %d", level));
// sort by score "+5 from xxx"
Pattern pattern = Pattern.compile("\\+(\\d+)");
foundInfo.sort((o1, o2) -> {
Matcher matcher = pattern.matcher(o1);
int score1 = matcher.find() ? Integer.parseInt(matcher.group(1)) : 0;
matcher = pattern.matcher(o2);
int score2 = matcher.find() ? Integer.parseInt(matcher.group(1)) : 0;
if (score1 != score2) {
return Integer.compare(score2, score1);
}
return o1.compareTo(o2);
});
showStateInfo(formatCardsInfoTooltip(level, foundInfo));
}
private String formatCardsInfoTooltip(int level, List<String> foundInfo) {
// use 60% font for better and compatible list
int infoFontSize = Math.round(GUISizeHelper.cardTooltipFont.getSize() * 0.6f);
int maxLimit = 25;
String extraInfo = this.foundPowerCards.size() <= maxLimit ? "" : String.format("<li style=\"margin-bottom: 2px;\">and %d more cards</li>", maxLimit - this.foundPowerCards.size());
return foundInfo.stream()
.limit(maxLimit)
.reduce("<html><body>"
+ "<p>EDH Power Level: <span style='color:#b8860b;font-weight:bold;'>" + level + "</span></p>"
+ "<br>"
+ "<u>Found <span style='font-weight:bold;'>" + this.foundPowerCards.size() + "</span> cards with power levels (click to select it)</u>"
+ "<br>"
+ "<ul style=\"font-size: " + infoFontSize + "px; width: " + TOOLTIP_TABLE_WIDTH + "px; padding-left: 10px; margin: 0;\">",
(str, info) -> str + String.format("<li style=\"margin-bottom: 2px;\">%s</li>", info), String::concat)
+ extraInfo
+ "</ul>"
+ "</body></html>";
}
}

View file

@ -3,17 +3,18 @@ package mage.client.components;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidator;
import mage.cards.decks.DeckValidatorError;
import mage.cards.decks.importer.DeckImporter;
import org.unbescape.html.HtmlEscape;
import org.unbescape.html.HtmlEscapeLevel;
import org.unbescape.html.HtmlEscapeType;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Elandril
* @author Elandril, JayDi85
*/
public class LegalityLabel extends JLabel {
@ -25,8 +26,10 @@ public class LegalityLabel extends JLabel {
protected static final Dimension DIM_MINIMUM = new Dimension(75, 25);
protected static final Dimension DIM_MAXIMUM = new Dimension(150, 75);
protected static final Dimension DIM_PREFERRED = new Dimension(75, 25);
protected static final Dimension DIM_PREFERRED_X2 = new Dimension(DIM_PREFERRED.width * 2 + 5, 25);
protected static final Dimension DIM_PREFERRED_X3 = new Dimension(DIM_PREFERRED.width * 3 + 5 + 5, 25);
protected static final int TOOLTIP_TABLE_WIDTH = 300; // size of the label's tooltip
protected static final int TOOLTIP_TABLE_WIDTH = 400; // size of the label's tooltip
protected static final int TOOLTIP_MAX_ERRORS = 20; // max errors to show in tooltip
protected Deck currentDeck;
@ -92,19 +95,6 @@ public class LegalityLabel extends JLabel {
return button;
}
public String getErrorMessage() {
return errorMessage;
}
public DeckValidator getValidator() {
return validator;
}
public void setValidator(DeckValidator validator) {
this.validator = validator;
revalidateDeck();
}
protected String escapeHtml(String string) {
return HtmlEscape.escapeHtml(string, HtmlEscapeType.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA, HtmlEscapeLevel.LEVEL_0_ONLY_MARKUP_SIGNIFICANT_EXCEPT_APOS);
}
@ -146,25 +136,33 @@ public class LegalityLabel extends JLabel {
setBackground(color);
}
public void showState(Color color, String tooltip) {
public void showState(Color color, String tooltip, boolean useErrors) {
setBackground(color);
setToolTipText(appendErrorMessage(tooltip));
if (useErrors) {
setToolTipText(appendErrorMessage(tooltip));
} else {
setToolTipText(tooltip);
}
}
public void showStateInfo(String tooltip) {
showState(COLOR_LEGAL, tooltip, false);
}
public void showStateUnknown(String tooltip) {
showState(COLOR_UNKNOWN, tooltip);
showState(COLOR_UNKNOWN, tooltip, true);
}
public void showStateLegal(String tooltip) {
showState(COLOR_LEGAL, tooltip);
showState(COLOR_LEGAL, tooltip, true);
}
public void showStatePartlyLegal(String tooltip) {
showState(COLOR_PARTLY_LEGAL, tooltip);
showState(COLOR_PARTLY_LEGAL, tooltip, true);
}
public void showStateNotLegal(String tooltip) {
showState(COLOR_NOT_LEGAL, tooltip);
showState(COLOR_NOT_LEGAL, tooltip, true);
}
public void validateDeck(Deck deck) {
@ -191,28 +189,13 @@ public class LegalityLabel extends JLabel {
}
}
public void validateDeck(File deckFile) {
deckFile = deckFile.getAbsoluteFile();
if (!deckFile.exists()) {
errorMessage = String.format("Deck file '%s' does not exist.", deckFile.getAbsolutePath());
showStateUnknown("<html><body><b>No Deck loaded!</b></body></html>");
return;
public java.util.List<String> selectCards() {
if (this.validator == null) {
return Collections.emptyList();
}
try {
StringBuilder errorMessages = new StringBuilder();
Deck deck = Deck.load(DeckImporter.importDeckFromFile(deckFile.getAbsolutePath(), errorMessages, false), true, true);
errorMessage = errorMessages.toString();
validateDeck(deck);
} catch (Exception ex) {
errorMessage = String.format("Error importing deck from file '%s'!", deckFile.getAbsolutePath());
}
}
public void revalidateDeck() {
validateDeck(currentDeck);
}
public void validateDeck(String deckFile) {
validateDeck(new File(deckFile));
return this.validator.getErrorsList().stream()
.map(DeckValidatorError::getCardName)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}

View file

@ -533,10 +533,10 @@
<Component class="mage.client.deckeditor.DeckLegalityPanel" name="deckLegalityDisplay">
<Properties>
<Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[245, 155]"/>
<Dimension value="[245, 255]"/>
</Property>
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[85, 155]"/>
<Dimension value="[85, 255]"/>
</Property>
<Property name="opaque" type="boolean" value="false"/>
</Properties>

View file

@ -121,12 +121,8 @@ public class DeckEditorPanel extends javax.swing.JPanel {
if (!SwingUtilities.isLeftMouseButton(e)) {
return;
}
List<String> cardNames = new ArrayList<>();
LegalityLabel label = (LegalityLabel) e.getComponent();
label.getValidator().getErrorsList().stream()
.map(DeckValidatorError::getCardName)
.filter(Objects::nonNull)
.forEach(cardNames::add);
List<String> cardNames = new ArrayList<>(label.selectCards());
deckArea.getDeckList().deselectAll();
deckArea.getDeckList().selectByName(cardNames);
deckArea.getSideboardList().deselectAll();
@ -1290,8 +1286,8 @@ public class DeckEditorPanel extends javax.swing.JPanel {
panelInfo.setOpaque(false);
deckLegalityDisplay.setMaximumSize(new java.awt.Dimension(245, 155));
deckLegalityDisplay.setMinimumSize(new java.awt.Dimension(85, 155));
deckLegalityDisplay.setMaximumSize(new java.awt.Dimension(245, 255));
deckLegalityDisplay.setMinimumSize(new java.awt.Dimension(85, 255));
deckLegalityDisplay.setOpaque(false);
deckLegalityDisplay.setVisible(false);
@ -1313,7 +1309,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
panelInfoLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(panelInfoLayout.createSequentialGroup()
.addContainerGap()
.addComponent(deckLegalityDisplay, javax.swing.GroupLayout.PREFERRED_SIZE, 155, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(deckLegalityDisplay, javax.swing.GroupLayout.PREFERRED_SIZE, 255, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(bigCard, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap())

View file

@ -5,6 +5,7 @@ import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidator;
import mage.cards.mock.MockCard;
import mage.cards.mock.MockSplitCard;
import mage.client.components.EdhPowerLevelLegalityLabel;
import mage.client.components.LegalityLabel;
import mage.deck.*;
import org.apache.log4j.Logger;
@ -15,7 +16,7 @@ import java.util.stream.Stream;
/**
* @author Elandril
* @author Elandril, JayDi85
*/
public class DeckLegalityPanel extends javax.swing.JPanel {
@ -101,6 +102,10 @@ public class DeckLegalityPanel extends javax.swing.JPanel {
new Frontier(), new HistoricalType2(), new PennyDreadfulCommander(), new EuropeanHighlander(), new CanadianHighlander()
// not used: new Eternal(), new Momir(), new TinyLeaders()
).forEach(this::addLegalityLabel);
// extra buttons like score
this.add(new EdhPowerLevelLegalityLabel());
addHidePanelButton();
revalidate();
@ -147,5 +152,4 @@ public class DeckLegalityPanel extends javax.swing.JPanel {
.map(LegalityLabel.class::cast)
.forEach(label -> label.validateDeck(deckToValidate));
}
}

View file

@ -19,6 +19,7 @@ public class CardViewEDHPowerLevelComparator implements CardViewComparator {
return "EDH: " + getPowerLevel(sample);
}
// TODO: it's outdated code, must migrate to shared code from AbstractCommander
private int getPowerLevel(CardView card) {
int thisMaxPower = 0;