diff --git a/forge-game/src/main/java/forge/game/event/GameEventSpellAbilityCast.java b/forge-game/src/main/java/forge/game/event/GameEventSpellAbilityCast.java index 1cc94a2c872..c65b6823a78 100644 --- a/forge-game/src/main/java/forge/game/event/GameEventSpellAbilityCast.java +++ b/forge-game/src/main/java/forge/game/event/GameEventSpellAbilityCast.java @@ -12,10 +12,12 @@ public class GameEventSpellAbilityCast extends GameEvent { public final SpellAbility sa; public final SpellAbilityStackInstance si; public final boolean replicate; + public final int stackIndex; - public GameEventSpellAbilityCast(SpellAbility sp, SpellAbilityStackInstance si, boolean replicate) { + public GameEventSpellAbilityCast(SpellAbility sp, SpellAbilityStackInstance si, int stackIndex, boolean replicate) { sa = sp; this.si = si; + this.stackIndex = stackIndex; this.replicate = replicate; } diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index d0040eb9745..56ceee1a273 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -414,6 +414,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable manipulateCardList(final CMatchUI gui, final String title, final Iterable cards, final Iterable manipulable, final boolean toTop, final boolean toBottom, final boolean toAnywhere) { gui.setSelectables(manipulable); - final Callable> callable = new Callable>() { - @Override - public List call() { - ListCardArea tempArea = ListCardArea.show(gui,title,cards,manipulable,toTop,toBottom,toAnywhere); - // tempArea.pack(); - tempArea.show(); - final List cardList = tempArea.getCards(); - return cardList; - } - }; + @SuppressWarnings("Convert2Lambda") // Avoid lambdas to maintain compatibility with Android 5 API + final Callable> callable = new Callable>() { + @Override + public List call() { + ListCardArea tempArea = ListCardArea.show(gui,title,cards,manipulable,toTop,toBottom,toAnywhere); + + // tempArea.pack(); + tempArea.setVisible(true); + return tempArea.getCards(); + } + }; final FutureTask> ft = new FutureTask<>(callable); FThreads.invokeInEdtAndWait(ft); gui.clearSelectables(); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/quest/VSubmenuQuestData.java b/forge-gui-desktop/src/main/java/forge/screens/home/quest/VSubmenuQuestData.java index 65454ac06ce..c3a505d3d76 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/quest/VSubmenuQuestData.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/quest/VSubmenuQuestData.java @@ -17,10 +17,9 @@ import forge.screens.home.EMenuGroup; import forge.screens.home.IVSubmenu; import forge.screens.home.VHomeUI; import forge.toolbox.*; -import forge.util.storage.IStorage; import forge.util.Localizer; +import forge.util.WordUtil; import net.miginfocom.swing.MigLayout; -import org.apache.commons.lang3.text.WordUtils; import javax.swing.*; import javax.swing.plaf.basic.BasicComboBoxRenderer; @@ -243,17 +242,15 @@ public enum VSubmenuQuestData implements IVSubmenu { cboAllowUnlocks.setSelected(true); final Map preconDescriptions = new HashMap<>(); - final IStorage preconDecks = QuestController.getPrecons(); - for (final PreconDeck preconDeck : preconDecks) { + for (final PreconDeck preconDeck : QuestController.getPrecons()) { if (QuestController.getPreconDeals(preconDeck).getMinWins() > 0) { continue; } final String name = preconDeck.getName(); cbxPreconDeck.addItem(name); String description = preconDeck.getDescription(); - description = "" + WordUtils.wrap(description, 40, "
", false) + ""; - preconDescriptions.put(name, description); + preconDescriptions.put(name, WordUtil.wordWrapAsHTML(description)); } // The cbx needs strictly typed renderer 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 00876eec897..96080f2a500 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 @@ -227,6 +227,7 @@ public enum CSubmenuPreferences implements ICDoc { initializeDefaultFontSizeComboBox(); initializeMulliganRuleComboBox(); initializeAiProfilesComboBox(); + initializeStackAdditionsComboBox(); initializeColorIdentityCombobox(); initializeAutoYieldModeComboBox(); initializeCounterDisplayTypeComboBox(); @@ -408,6 +409,16 @@ public enum CSubmenuPreferences implements ICDoc { panel.setComboBox(comboBox, selectedItem); } + private void initializeStackAdditionsComboBox() { + final String[] elems = {ForgeConstants.STACK_EFFECT_NOTIFICATION_NEVER, ForgeConstants.STACK_EFFECT_NOTIFICATION_ALWAYS, + ForgeConstants.STACK_EFFECT_NOTIFICATION_AI_AND_TRIGGERED}; + final FPref userSetting = FPref.UI_STACK_EFFECT_NOTIFICATION_POLICY; + final FComboBoxPanel panel = this.view.getCbpStackAdditionsComboBoxPanel(); + final FComboBox comboBox = createComboBox(elems, userSetting); + final String selectedItem = this.prefs.getPref(userSetting); + panel.setComboBox(comboBox, selectedItem); + } + private void initializeColorIdentityCombobox() { final String[] elems = {ForgeConstants.DISP_CURRENT_COLORS_NEVER, ForgeConstants.DISP_CURRENT_COLORS_CHANGED, ForgeConstants.DISP_CURRENT_COLORS_MULTICOLOR, ForgeConstants.DISP_CURRENT_COLORS_MULTI_OR_CHANGED, diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java index aba5acb71d0..e5d6c8658cf 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java @@ -116,6 +116,7 @@ public enum VSubmenuPreferences implements IVSubmenu { private final FComboBoxPanel cbpDefaultFontSize = new FComboBoxPanel<>(localizer.getMessage("cbpDefaultFontSize")+":"); private final FComboBoxPanel cbpMulliganRule = new FComboBoxPanel<>(localizer.getMessage("cbpMulliganRule")+":"); private final FComboBoxPanel cbpAiProfiles = new FComboBoxPanel<>(localizer.getMessage("cbpAiProfiles")+":"); + private final FComboBoxPanel cbpStackAdditions = new FComboBoxPanel<>(localizer.getMessage("cbpStackAdditions")+":"); private final FComboBoxPanel cbpDisplayCurrentCardColors = new FComboBoxPanel<>(localizer.getMessage("cbpDisplayCurrentCardColors")+":"); private final FComboBoxPanel cbpAutoYieldMode = new FComboBoxPanel<>(localizer.getMessage("cbpAutoYieldMode")+":"); private final FComboBoxPanel cbpCounterDisplayType = new FComboBoxPanel<>(localizer.getMessage("cbpCounterDisplayType")+":"); @@ -194,6 +195,9 @@ public enum VSubmenuPreferences implements IVSubmenu { pnlPrefs.add(cbManaLostPrompt, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlManaLostPrompt")), descriptionConstraints); + pnlPrefs.add(cbpStackAdditions, comboBoxConstraints); + pnlPrefs.add(new NoteLabel(localizer.getMessage("nlpStackAdditions")), descriptionConstraints); + pnlPrefs.add(cbEnforceDeckLegality, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlEnforceDeckLegality")), descriptionConstraints); @@ -649,6 +653,10 @@ public enum VSubmenuPreferences implements IVSubmenu { return cbpAiProfiles; } + public FComboBoxPanel getCbpStackAdditionsComboBoxPanel() { + return cbpStackAdditions; + } + public FComboBoxPanel getGameLogVerbosityComboBoxPanel() { return cbpGameLogEntryType; } @@ -766,7 +774,7 @@ public enum VSubmenuPreferences implements IVSubmenu { public final JCheckBox getCbManaLostPrompt() { return cbManaLostPrompt; } - + public final JCheckBox getCbDetailedPaymentDesc() { return cbDetailedPaymentDesc; } 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 f6af4afd542..5619e695d94 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 @@ -18,7 +18,9 @@ package forge.screens.match; import java.awt.Component; +import java.awt.Dimension; import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -30,6 +32,8 @@ import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicReference; import javax.swing.JMenu; +import javax.swing.JOptionPane; +import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; @@ -41,25 +45,43 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import forge.FThreads; +import forge.GuiBase; import forge.ImageCache; import forge.LobbyPlayer; import forge.Singletons; +import forge.StaticData; import forge.assets.FSkinProp; +import forge.card.CardStateName; import forge.control.KeyboardShortcuts; import forge.deck.CardPool; import forge.deck.Deck; import forge.deckchooser.FDeckViewer; +import forge.game.GameEntity; import forge.game.GameEntityView; import forge.game.GameView; +import forge.game.ability.AbilityKey; +import forge.game.card.Card; import forge.game.card.CardView; +import forge.game.card.CardView.CardStateView; import forge.game.combat.CombatView; +import forge.game.event.GameEventSpellAbilityCast; +import forge.game.event.GameEventSpellRemovedFromStack; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.DelayedReveal; import forge.game.player.IHasIcon; import forge.game.player.PlayerView; +import forge.game.spellability.SpellAbility; +import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityView; +import forge.game.spellability.StackItemView; +import forge.game.spellability.TargetChoices; import forge.game.zone.ZoneType; -import forge.gui.*; +import forge.gui.FNetOverlay; +import forge.gui.GuiChoose; +import forge.gui.GuiDialog; +import forge.gui.GuiUtils; +import forge.gui.SOverlayUtils; import forge.gui.framework.DragCell; import forge.gui.framework.EDocID; import forge.gui.framework.FScreen; @@ -74,6 +96,7 @@ import forge.match.AbstractGuiGame; import forge.menus.IMenuProvider; import forge.model.FModel; import forge.player.PlayerZoneUpdate; +import forge.properties.ForgeConstants; import forge.properties.ForgePreferences; import forge.properties.ForgePreferences.FPref; import forge.screens.match.controllers.CAntes; @@ -88,19 +111,25 @@ import forge.screens.match.menus.CMatchUIMenus; import forge.screens.match.views.VField; import forge.screens.match.views.VHand; import forge.toolbox.FButton; +import forge.toolbox.FLabel; import forge.toolbox.FOptionPane; import forge.toolbox.FSkin; import forge.toolbox.FSkin.SkinImage; +import forge.toolbox.FTextArea; +import forge.toolbox.imaging.FImagePanel; +import forge.toolbox.imaging.FImagePanel.AutoSizeImageMode; +import forge.toolbox.imaging.FImageUtil; import forge.toolbox.special.PhaseIndicator; import forge.toolbox.special.PhaseLabel; import forge.trackable.TrackableCollection; +import forge.util.ITriggerEvent; import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; -import forge.util.ITriggerEvent; import forge.util.gui.SOptionPane; import forge.view.FView; import forge.view.arcane.CardPanel; import forge.view.arcane.FloatingZone; +import net.miginfocom.swing.MigLayout; /** * Constructs instance of match UI controller, used as a single point of @@ -136,6 +165,7 @@ public final class CMatchUI private final CLog cLog = new CLog(this); private final CPrompt cPrompt = new CPrompt(this); private final CStack cStack = new CStack(this); + private int nextNotifiableStackIndex = 0; public CMatchUI() { this.view = new VMatchUI(this); @@ -1180,4 +1210,204 @@ public final class CMatchUI return reply == 0; } + @Override + public void notifyStackAddition(GameEventSpellAbilityCast event) { + SpellAbility sa = event.sa; + String stackNotificationPolicy = FModel.getPreferences().getPref(FPref.UI_STACK_EFFECT_NOTIFICATION_POLICY); + boolean isAi = sa.getActivatingPlayer().isAI(); + boolean isTrigger = sa.isTrigger(); + int stackIndex = event.stackIndex; + if(stackIndex == nextNotifiableStackIndex) { + if(ForgeConstants.STACK_EFFECT_NOTIFICATION_ALWAYS.equals(stackNotificationPolicy) || (ForgeConstants.STACK_EFFECT_NOTIFICATION_AI_AND_TRIGGERED.equals(stackNotificationPolicy) && (isAi || isTrigger))) { + // We can go and show the modal + SpellAbilityStackInstance si = event.si; + + MigLayout migLayout = new MigLayout("insets 15, left, gap 30, fill"); + JPanel mainPanel = new JPanel(migLayout); + final Dimension parentSize = JOptionPane.getRootFrame().getSize(); + Dimension maxSize = new Dimension(1400, parentSize.height - 100); + mainPanel.setMaximumSize(maxSize); + mainPanel.setOpaque(false); + + // Big Image + addBigImageToStackModalPanel(mainPanel, si); + + // Text + addTextToStackModalPanel(mainPanel,sa,si); + + // Small images + int numSmallImages = 0; + + // If current effect is a triggered/activated ability of an enchantment card, I want to show the enchanted card + GameEntityView enchantedEntityView = null; + Card hostCard = sa.getHostCard(); + if(hostCard.isEnchantment()) { + GameEntity enchantedEntity = hostCard.getEntityAttachedTo(); + if(enchantedEntity != null) { + enchantedEntityView = enchantedEntity.getView(); + numSmallImages++; + } else if ((sa.getRootAbility() != null) + && (sa.getRootAbility().getPaidList("Sacrificed") != null) + && !sa.getRootAbility().getPaidList("Sacrificed").isEmpty()) { + // If the player activated its ability by sacrificing the enchantment, the enchantment has not anything attached anymore and the ex-enchanted card has to be searched in other ways.. for example, the green enchantment "Carapace" + enchantedEntity = sa.getRootAbility().getPaidList("Sacrificed").get(0).getEnchantingCard(); + if(enchantedEntity != null) { + enchantedEntityView = enchantedEntity.getView(); + numSmallImages++; + } + } + } + + // If current effect is a triggered ability, I want to show the triggering card if present + SpellAbility sourceSA = (SpellAbility) si.getTriggeringObject(AbilityKey.SourceSA); + CardView sourceCardView = null; + if(sourceSA != null) { + sourceCardView = sourceSA.getHostCard().getView(); + numSmallImages++; + } + + // I also want to show each type of targets (both cards and players) + List targets = getTargets(si,new ArrayList()); + numSmallImages = numSmallImages + targets.size(); + + // Now I know how many small images - on to render them + if(enchantedEntityView != null) { + addSmallImageToStackModalPanel(enchantedEntityView,mainPanel,numSmallImages); + } + if(sourceCardView != null) { + addSmallImageToStackModalPanel(sourceCardView,mainPanel,numSmallImages); + } + for(GameEntityView gev : targets) { + addSmallImageToStackModalPanel(gev, mainPanel, numSmallImages); + } + + FOptionPane.showOptionDialog(null, "Forge", null, mainPanel, ImmutableList.of("OK")); + // here the user closed the modal - time to update the next notifiable stack index + + } + // In any case, I have to increase the counter + nextNotifiableStackIndex++; + + } else { + + // Not yet time to show the modal - schedule the method again, and try again later + Runnable tryAgainThread = new Runnable() { + @Override + public void run() { + notifyStackAddition(event); + } + }; + GuiBase.getInterface().invokeInEdtLater(tryAgainThread); + + } + } + + private List getTargets(SpellAbilityStackInstance si, List result){ + if(si == null) { + return result; + } else { + FCollectionView targetCards = CardView.getCollection(si.getTargetChoices().getTargetCards()); + for(CardView currCardView: targetCards) { + result.add(currCardView); + } + + for(SpellAbility currSA : si.getTargetChoices().getTargetSpells()) { + CardView currCardView = currSA.getCardView(); + result.add(currCardView); + } + + FCollectionView targetPlayers = PlayerView.getCollection(si.getTargetChoices().getTargetPlayers()); + for(PlayerView currPlayerView : targetPlayers) { + result.add(currPlayerView); + } + + return getTargets(si.getSubInstance(),result); + } + } + + private void addBigImageToStackModalPanel(JPanel mainPanel, SpellAbilityStackInstance si) { + StackItemView siv = si.getView(); + int rotation = getRotation(si.getCardView()); + + FImagePanel imagePanel = new FImagePanel(); + BufferedImage bufferedImage = FImageUtil.getImage(siv.getSourceCard().getCurrentState()); + imagePanel.setImage(bufferedImage, rotation, AutoSizeImageMode.SOURCE); + int imageWidth = 433; + int imageHeight = 600; + Dimension imagePanelDimension = new Dimension(imageWidth,imageHeight); + imagePanel.setMinimumSize(imagePanelDimension); + + mainPanel.add(imagePanel, "cell 0 0, spany 3"); + } + + private void addTextToStackModalPanel(JPanel mainPanel, SpellAbility sa, SpellAbilityStackInstance si) { + String who = sa.getActivatingPlayer().getName(); + String action = sa.isSpell() ? " cast " : sa.isTrigger() ? " triggered " : " activated "; + String what = sa.getStackDescription().startsWith("Morph ") ? "Morph" : sa.getHostCard().toString(); + + StringBuilder sb = new StringBuilder(); + sb.append(who).append(action).append(what); + + if (sa.getTargetRestrictions() != null) { + sb.append(" targeting "); + TargetChoices targets = si.getTargetChoices(); + sb.append(targets.getTargetedString()); + } + sb.append("."); + String message1 = sb.toString(); + String message2 = si.getStackDescription(); + String messageTotal = message1 + "\n\n" + message2; + + final FTextArea prompt1 = new FTextArea(messageTotal); + prompt1.setFont(FSkin.getFont(21)); + prompt1.setAutoSize(true); + prompt1.setMinimumSize(new Dimension(475,200)); + mainPanel.add(prompt1, "cell 1 0, aligny top"); + } + + private void addSmallImageToStackModalPanel(GameEntityView gameEntityView, JPanel mainPanel, int numTarget) { + if(gameEntityView instanceof CardView) { + CardView cardView = (CardView) gameEntityView; + int currRotation = getRotation(cardView); + FImagePanel targetPanel = new FImagePanel(); + BufferedImage bufferedImage = FImageUtil.getImage(cardView.getCurrentState()); + targetPanel.setImage(bufferedImage, currRotation, AutoSizeImageMode.SOURCE); + int imageWidth = 217; + int imageHeight = 300; + Dimension targetPanelDimension = new Dimension(imageWidth,imageHeight); + targetPanel.setMinimumSize(targetPanelDimension); + mainPanel.add(targetPanel, "cell 1 1, split " + numTarget+ ", aligny bottom"); + } else if(gameEntityView instanceof PlayerView) { + PlayerView playerView = (PlayerView) gameEntityView; + SkinImage playerAvatar = getPlayerAvatar(playerView, 0); + final FLabel lblIcon = new FLabel.Builder().icon(playerAvatar).build(); + Dimension dimension = playerAvatar.getSizeForPaint(JOptionPane.getRootFrame().getGraphics()); + mainPanel.add(lblIcon, "cell 1 1, split " + numTarget+ ", w " + dimension.getWidth() + ", h " + dimension.getHeight() + ", aligny bottom"); + } + } + + private int getRotation(CardView cardView) { + final int rotation; + if (cardView.isSplitCard()) { + String cardName = cardView.getName(); + if (cardName.isEmpty()) { cardName = cardView.getAlternateState().getName(); } + + PaperCard pc = StaticData.instance().getCommonCards().getCard(cardName); + boolean hasKeywordAftermath = pc != null && Card.getCardForUi(pc).hasKeyword(Keyword.AFTERMATH); + + rotation = cardView.isFaceDown() ? 0 : hasKeywordAftermath ? (CardStateName.LeftSplit.equals(cardView.getState(false).getState()) ? 0 : 270) : 90; // rotate Aftermath splits the other way to correctly show the right split (graveyard) half + } else { + CardStateView cardStateView = cardView.getState(false); + rotation = cardStateView.isPlane() || cardStateView.isPhenomenon() ? 90 : 0; + } + + return rotation; + } + + @Override + public void notifyStackRemoval(GameEventSpellRemovedFromStack event) { + // I always decrease the counter + nextNotifiableStackIndex--; + } + } diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/FOptionPane.java b/forge-gui-desktop/src/main/java/forge/toolbox/FOptionPane.java index 01872d6b5f7..bc6cf09a612 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/FOptionPane.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/FOptionPane.java @@ -78,6 +78,10 @@ public class FOptionPane extends FDialog { return showOptionDialog(message, title, icon, options, 0); } + public static int showOptionDialog(final String message, final String title, final SkinImage icon, Component comp, final List options) { + return showOptionDialog(message, title, icon, comp, options, 0); + } + public static int showOptionDialog(final String message, final String title, final SkinImage icon, final List options, final int defaultOption) { final FOptionPane optionPane = new FOptionPane(message, title, icon, null, options, defaultOption); optionPane.setVisible(true); @@ -86,6 +90,14 @@ public class FOptionPane extends FDialog { return dialogResult; } + public static int showOptionDialog(final String message, final String title, final SkinImage icon, final Component comp, final List options, final int defaultOption) { + final FOptionPane optionPane = new FOptionPane(message, title, icon, comp, options, defaultOption); + optionPane.setVisible(true); + final int dialogResult = optionPane.result; + optionPane.dispose(); + return dialogResult; + } + public static String showInputDialog(final String message, final String title) { return showInputDialog(message, title, null, "", null); } diff --git a/forge-gui-desktop/src/main/java/forge/toolbox/FSkin.java b/forge-gui-desktop/src/main/java/forge/toolbox/FSkin.java index b0ecc7338b4..a80b635e0fe 100644 --- a/forge-gui-desktop/src/main/java/forge/toolbox/FSkin.java +++ b/forge-gui-desktop/src/main/java/forge/toolbox/FSkin.java @@ -28,8 +28,8 @@ import forge.properties.ForgeConstants; import forge.properties.ForgePreferences; import forge.properties.ForgePreferences.FPref; import forge.util.OperatingSystem; +import forge.util.WordUtil; import forge.view.FView; -import org.apache.commons.lang3.text.WordUtils; import javax.imageio.ImageIO; import javax.swing.*; @@ -1085,7 +1085,7 @@ public class FSkin { allSkins = new ArrayList<>(); final List skinDirectoryNames = getSkinDirectoryNames(); for (String skinDirectoryName : skinDirectoryNames) { - allSkins.add(WordUtils.capitalize(skinDirectoryName.replace('_', ' '))); + allSkins.add(WordUtil.capitalize(skinDirectoryName.replace('_', ' '))); } Collections.sort(allSkins); } 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 bccc0c274d9..4a37d331223 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 @@ -1,31 +1,24 @@ package forge.toolbox.special; -import java.awt.Component; -import java.awt.Font; -import java.awt.Graphics; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.List; - -import javax.swing.JPanel; -import javax.swing.SwingConstants; - -import forge.card.mana.ManaAtom; -import forge.trackable.TrackableProperty; -import net.miginfocom.swing.MigLayout; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; - import com.google.common.base.Function; - import forge.assets.FSkinProp; +import forge.card.mana.ManaAtom; import forge.game.player.PlayerView; 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 net.miginfocom.swing.MigLayout; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; public class PlayerDetailsPanel extends JPanel { private static final long serialVersionUID = -6531759554646891983L; 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 c5b9f0fad36..a680839fcb4 100644 --- a/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java +++ b/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java @@ -12,8 +12,8 @@ import forge.game.*; import forge.properties.ForgeConstants; import forge.tournament.system.*; import forge.util.TextUtil; +import forge.util.WordUtil; import forge.util.storage.IStorage; -import org.apache.commons.lang3.text.WordUtils; import org.apache.commons.lang3.time.StopWatch; import forge.deck.Deck; @@ -75,7 +75,7 @@ public class SimulateMatch { GameType type = GameType.Constructed; if (params.containsKey("f")) { - type = GameType.valueOf(WordUtils.capitalize(params.get("f").get(0))); + type = GameType.valueOf(WordUtil.capitalize(params.get("f").get(0))); } GameRules rules = new GameRules(type); diff --git a/forge-gui-mobile/src/forge/animation/GifDecoder.java b/forge-gui-mobile/src/forge/animation/GifDecoder.java index 258b8d7036c..5ff29d18df5 100644 --- a/forge-gui-mobile/src/forge/animation/GifDecoder.java +++ b/forge-gui-mobile/src/forge/animation/GifDecoder.java @@ -687,7 +687,7 @@ public class GifDecoder { } while ((blockSize > 0) && !err()); } - public Animation getAnimation(PlayMode playType) { + private Animation getAnimation(PlayMode playType) { int nrFrames = getFrameCount(); Pixmap frame = getFrame(0); int width = frame.getWidth(); @@ -725,12 +725,11 @@ public class GifDecoder { } float frameDuration = (float)getDelay(0); frameDuration /= 1000; // convert milliseconds into seconds - Animation result = new Animation(frameDuration, texReg, playType); - return result; + return new Animation<>(frameDuration, texReg, playType); } - public static Animation loadGIFAnimation(PlayMode playType, InputStream is) { + public static Animation loadGIFAnimation(PlayMode playType, InputStream is) { GifDecoder gdec = new GifDecoder(); gdec.read(is); return gdec.getAnimation(playType); diff --git a/forge-gui-mobile/src/forge/assets/FSkin.java b/forge-gui-mobile/src/forge/assets/FSkin.java index 053fdb349ea..4d64d273ee2 100644 --- a/forge-gui-mobile/src/forge/assets/FSkin.java +++ b/forge-gui-mobile/src/forge/assets/FSkin.java @@ -3,9 +3,9 @@ package forge.assets; import java.util.HashMap; import java.util.Map; +import forge.util.WordUtil; import com.badlogic.gdx.utils.Array; import forge.Forge; -import org.apache.commons.lang3.text.WordUtils; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; @@ -102,7 +102,7 @@ public class FSkin { allSkins = new Array<>(); final Array skinDirectoryNames = getSkinDirectoryNames(); for (final String skinDirectoryName : skinDirectoryNames) { - allSkins.add(WordUtils.capitalize(skinDirectoryName.replace('_', ' '))); + allSkins.add(WordUtil.capitalize(skinDirectoryName.replace('_', ' '))); } allSkins.sort(); } diff --git a/forge-gui-mobile/src/forge/screens/quest/NewQuestScreen.java b/forge-gui-mobile/src/forge/screens/quest/NewQuestScreen.java index fa5d33c5ba1..eecf8fc8ed4 100644 --- a/forge-gui-mobile/src/forge/screens/quest/NewQuestScreen.java +++ b/forge-gui-mobile/src/forge/screens/quest/NewQuestScreen.java @@ -47,15 +47,10 @@ import forge.util.FileUtil; import forge.util.ThreadUtil; import forge.util.Utils; import forge.util.gui.SOptionPane; -import forge.util.storage.IStorage; - -import org.apache.commons.lang3.text.WordUtils; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -282,18 +277,12 @@ public class NewQuestScreen extends FScreen { cbAllowUnlocks.setSelected(true); - final Map preconDescriptions = new HashMap<>(); - IStorage preconDecks = QuestController.getPrecons(); - - for (PreconDeck preconDeck : preconDecks) { + for (PreconDeck preconDeck : QuestController.getPrecons()) { if (QuestController.getPreconDeals(preconDeck).getMinWins() > 0) { continue; } String name = preconDeck.getName(); cbxPreconDeck.addItem(name); - String description = preconDeck.getDescription(); - description = "" + WordUtils.wrap(description, 40, "
", false) + ""; - preconDescriptions.put(name, description); } // disable the very powerful sets -- they can be unlocked later for a high price diff --git a/forge-gui/res/cardsfolder/s/syr_konrad_the_grim.txt b/forge-gui/res/cardsfolder/s/syr_konrad_the_grim.txt index 3d7b61f5af9..eb62eaeb821 100644 --- a/forge-gui/res/cardsfolder/s/syr_konrad_the_grim.txt +++ b/forge-gui/res/cardsfolder/s/syr_konrad_the_grim.txt @@ -3,7 +3,7 @@ ManaCost:3 B B Types:Legendary Creature Human Knight PT:5/4 T:Mode$ ChangesZone | Origin$ Any | Destination$ Graveyard | ValidCard$ Creature.Other | TriggerZones$ Battlefield | Execute$ TrigDmg | TriggerDescription$ Whenever another creature dies, or a creature card is put into a graveyard from anywhere than the battlefield, or a creature card leaves your graveyard, CARDNAME deals 1 damage to each opponent. -T:Mode$ ChangesZone | Origin$ Graveyard | Destination$ Any | ValidCard$ Card.Creature+Other | TriggerZones$ Battlefield | Execute$ TrigDmg | Secondary$ True | TriggerDescription$ Whenever another creature dies, or a creature card is put into a graveyard from anywhere than the battlefield, or a creature card leaves your graveyard, CARDNAME deals 1 damage to each opponent. +T:Mode$ ChangesZone | Origin$ Graveyard | Destination$ Any | ValidCard$ Card.Creature+Other+YouOwn | TriggerZones$ Battlefield | Execute$ TrigDmg | Secondary$ True | TriggerDescription$ Whenever another creature dies, or a creature card is put into a graveyard from anywhere than the battlefield, or a creature card leaves your graveyard, CARDNAME deals 1 damage to each opponent. SVar:TrigDmg:DB$ DealDamage | Defined$ Player.Opponent | NumDmg$ 1 A:AB$ Mill | Cost$ 1 B | NumCards$ 1 | Defined$ Player | SpellDescription$ Each player puts the top card of their library into their graveyard. AI:RemoveDeck:All diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index d73de04d1ed..b80a6055556 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -102,6 +102,7 @@ cbpGameLogEntryType=Game Log Verbosity cbpCloseAction=Close Action cbpDefaultFontSize=Default Font Size cbpAiProfiles=AI Personality +cbpStackAdditions=Stack effect notifications cbpDisplayCurrentCardColors=Show Detailed Card Color cbpAutoYieldMode=Auto-Yield cbpCounterDisplayType=Counter Display Type @@ -116,6 +117,7 @@ nlUseSentry=When enabled, automatically submits bug reports to developers. GamePlay=Gameplay nlpMulliganRule=Choose the version of the Mulligan rule nlpAiProfiles=Choose your AI opponent +nlpStackAdditions=Choose when you want to get visual notifications for an effect added to the stack: Never, always, or only for the effects cast/activated by a AI player or triggered by any player nlAnte=Determines whether or not the game is played for ante. nlAnteMatchRarity=Attempts to make antes the same rarity for all players. nlEnableAICheats=Allow the AI to cheat to gain advantage (for personalities that have cheat shuffling options set). diff --git a/forge-gui/src/main/java/forge/control/FControlGameEventHandler.java b/forge-gui/src/main/java/forge/control/FControlGameEventHandler.java index 6fcf716e1eb..413bd938184 100644 --- a/forge-gui/src/main/java/forge/control/FControlGameEventHandler.java +++ b/forge-gui/src/main/java/forge/control/FControlGameEventHandler.java @@ -239,8 +239,18 @@ public class FControlGameEventHandler extends IGameEventVisitor.Base { @Override public Void visit(final GameEventSpellAbilityCast event) { - needStackUpdate = true; - return processEvent(); + needStackUpdate = true; + processEvent(); + + final Runnable notifyStackAddition = new Runnable() { + @Override + public void run() { + matchController.notifyStackAddition(event); + } + }; + GuiBase.getInterface().invokeInEdtLater(notifyStackAddition); + + return null; } @Override @@ -252,7 +262,17 @@ public class FControlGameEventHandler extends IGameEventVisitor.Base { @Override public Void visit(final GameEventSpellRemovedFromStack event) { needStackUpdate = true; - return processEvent(); + processEvent(); + + final Runnable notifyStackAddition = new Runnable() { + @Override + public void run() { + matchController.notifyStackRemoval(event); + } + }; + GuiBase.getInterface().invokeInEdtLater(notifyStackAddition); + + return null; } @Override @@ -340,10 +360,10 @@ public class FControlGameEventHandler extends IGameEventVisitor.Base { if(event.to.getZoneType() == ZoneType.Battlefield) refreshFieldUpdate = true; //pfps the change to the zones have already been performed with add and remove calls - // this is only for playing a sound - // updateZone(event.from); + // this is only for playing a sound + // updateZone(event.from); //return updateZone(event.to); - return processEvent(); + return processEvent(); } @@ -373,9 +393,9 @@ public class FControlGameEventHandler extends IGameEventVisitor.Base { @Override public Void visit(final GameEventShuffle event) { //pfps the change to the library has already been performed by a setCards call - // this is only for playing a sound - // return updateZone(event.player.getZone(ZoneType.Library)); - return processEvent(); + // this is only for playing a sound + // return updateZone(event.player.getZone(ZoneType.Library)); + return processEvent(); } @Override diff --git a/forge-gui/src/main/java/forge/download/GuiDownloadService.java b/forge-gui/src/main/java/forge/download/GuiDownloadService.java index d6183fe207c..4e72ae2842d 100644 --- a/forge-gui/src/main/java/forge/download/GuiDownloadService.java +++ b/forge-gui/src/main/java/forge/download/GuiDownloadService.java @@ -264,12 +264,8 @@ public abstract class GuiDownloadService implements Runnable { count++; cardSkipped = true; //assume skipped unless saved successfully String url = kv.getValue(); - /* - * decode URL Key, Reverted to old version, - * on Android 6.0 it throws an error - * when you download the card price - */ - String decodedKey = URLDecoder.decode(kv.getKey()); + + String decodedKey = decodeURL(kv.getKey()); final File fileDest = new File(decodedKey); final String filePath = fileDest.getPath(); final String subLastIndex = filePath.contains("pics") ? "\\pics\\" : "\\db\\"; @@ -365,6 +361,16 @@ public abstract class GuiDownloadService implements Runnable { GuiBase.getInterface().preventSystemSleep(false); } + @SuppressWarnings("deprecation") + private static String decodeURL(String key) { + /* + * decode URL Key, Reverted to old version, + * on Android 6.0 it throws an error + * when you download the card price + */ + return URLDecoder.decode(key); + } + protected Proxy getProxy() { if (type == 0) { return Proxy.NO_PROXY; @@ -385,7 +391,7 @@ public abstract class GuiDownloadService implements Runnable { protected static void addMissingItems(Map list, String nameUrlFile, String dir) { for (Pair nameUrlPair : FileUtil.readNameUrlFile(nameUrlFile)) { - File f = new File(dir, URLDecoder.decode(nameUrlPair.getLeft())); + File f = new File(dir, decodeURL(nameUrlPair.getLeft())); //System.out.println(f.getAbsolutePath()); if (!f.exists()) { list.put(f.getAbsolutePath(), nameUrlPair.getRight()); diff --git a/forge-gui/src/main/java/forge/interfaces/IGuiGame.java b/forge-gui/src/main/java/forge/interfaces/IGuiGame.java index 33c6999dab8..aac6d6aaf34 100644 --- a/forge-gui/src/main/java/forge/interfaces/IGuiGame.java +++ b/forge-gui/src/main/java/forge/interfaces/IGuiGame.java @@ -12,6 +12,8 @@ import forge.deck.CardPool; import forge.game.GameEntityView; import forge.game.GameView; import forge.game.card.CardView; +import forge.game.event.GameEventSpellAbilityCast; +import forge.game.event.GameEventSpellRemovedFromStack; import forge.game.phase.PhaseType; import forge.game.player.DelayedReveal; import forge.game.player.IHasIcon; @@ -47,6 +49,8 @@ public interface IGuiGame { void showManaPool(PlayerView player); void hideManaPool(PlayerView player); void updateStack(); + void notifyStackAddition(final GameEventSpellAbilityCast event); + void notifyStackRemoval(final GameEventSpellRemovedFromStack event); Iterable tempShowZones(PlayerView controller, Iterable zonesToUpdate); void hideZones(PlayerView controller, Iterable zonesToUpdate); void updateZones(Iterable zonesToUpdate); diff --git a/forge-gui/src/main/java/forge/match/AbstractGuiGame.java b/forge-gui/src/main/java/forge/match/AbstractGuiGame.java index 0e571e8c99a..d155759e9c7 100644 --- a/forge-gui/src/main/java/forge/match/AbstractGuiGame.java +++ b/forge-gui/src/main/java/forge/match/AbstractGuiGame.java @@ -24,6 +24,8 @@ import forge.assets.FSkinProp; import forge.game.GameView; import forge.game.card.CardView; import forge.game.card.CardView.CardStateView; +import forge.game.event.GameEventSpellAbilityCast; +import forge.game.event.GameEventSpellRemovedFromStack; import forge.game.player.PlayerView; import forge.interfaces.IGameController; import forge.interfaces.IGuiGame; @@ -251,16 +253,16 @@ public abstract class AbstractGuiGame implements IGuiGame, IMayViewCards { private final Set selectableCards = Sets.newHashSet(); public void setSelectables(final Iterable cards) { - for ( CardView cv : cards ) { selectableCards.add(cv); } + for ( CardView cv : cards ) { selectableCards.add(cv); } } public void clearSelectables() { - selectableCards.clear(); + selectableCards.clear(); } public boolean isSelectable(final CardView card) { - return selectableCards.contains(card); + return selectableCards.contains(card); } public boolean isSelecting() { - return !selectableCards.isEmpty(); + return !selectableCards.isEmpty(); } public boolean isGamePaused() { return gamePause; } public void setgamePause(boolean pause) { gamePause = pause; } @@ -694,6 +696,14 @@ public abstract class AbstractGuiGame implements IGuiGame, IMayViewCards { final String yesButtonText, final String noButtonText) { return showConfirmDialog(message, title, yesButtonText, noButtonText, true); } - + + @Override + public void notifyStackAddition(GameEventSpellAbilityCast event) { + } + + @Override + public void notifyStackRemoval(GameEventSpellRemovedFromStack event) { + } + // End of Choice code } diff --git a/forge-gui/src/main/java/forge/properties/ForgeConstants.java b/forge-gui/src/main/java/forge/properties/ForgeConstants.java index 42d07add4e3..ec4be9400c4 100644 --- a/forge-gui/src/main/java/forge/properties/ForgeConstants.java +++ b/forge-gui/src/main/java/forge/properties/ForgeConstants.java @@ -310,6 +310,11 @@ public final class ForgeConstants { public static final String GRAVEYARD_ORDERING_OWN_CARDS = "With Relevant Cards"; public static final String GRAVEYARD_ORDERING_ALWAYS = "Always"; + // Constants for Stack effect addition notification policy + public static final String STACK_EFFECT_NOTIFICATION_NEVER = "Never"; + public static final String STACK_EFFECT_NOTIFICATION_ALWAYS = "Always"; + public static final String STACK_EFFECT_NOTIFICATION_AI_AND_TRIGGERED = "AI cast/activated, or triggered by any player"; + // Set boolean constant for landscape mode for gdx port public static final boolean isGdxPortLandscape = FileUtil.doesFileExist(ASSETS_DIR + "switch_orientation.ini"); diff --git a/forge-gui/src/main/java/forge/properties/ForgePreferences.java b/forge-gui/src/main/java/forge/properties/ForgePreferences.java index 941ac2a1ed0..f9b7c3329a4 100644 --- a/forge-gui/src/main/java/forge/properties/ForgePreferences.java +++ b/forge-gui/src/main/java/forge/properties/ForgePreferences.java @@ -119,6 +119,7 @@ public class ForgePreferences extends PreferencesStore { UI_HIDE_GAME_TABS ("false"), // Visibility of tabs in match screen. UI_CLOSE_ACTION ("NONE"), UI_MANA_LOST_PROMPT ("false"), // Prompt on losing mana when passing priority + UI_STACK_EFFECT_NOTIFICATION_POLICY ("Never"), UI_PAUSE_WHILE_MINIMIZED("false"), UI_TOKENS_IN_SEPARATE_ROW("false"), // Display tokens in their own battlefield row. UI_DISPLAY_CURRENT_COLORS(ForgeConstants.DISP_CURRENT_COLORS_NEVER), diff --git a/forge-gui/src/main/java/forge/util/WordUtil.java b/forge-gui/src/main/java/forge/util/WordUtil.java new file mode 100644 index 00000000000..83251e58ec9 --- /dev/null +++ b/forge-gui/src/main/java/forge/util/WordUtil.java @@ -0,0 +1,87 @@ +package forge.util; + +import org.apache.commons.lang3.StringUtils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class WordUtil { + public static String capitalize(String str) { + if (StringUtils.isEmpty(str)) { + return str; + } + final char[] buffer = str.toCharArray(); + boolean capitalizeNext = true; + for (int i = 0; i < buffer.length; i++) { + final char ch = buffer[i]; + if (Character.isWhitespace(ch)) { + capitalizeNext = true; + } else if (capitalizeNext) { + buffer[i] = Character.toTitleCase(ch); + capitalizeNext = false; + } + } + return new String(buffer); + } + + private final static Pattern patternToWrapOn = Pattern.compile(" "); + public static String wordWrapAsHTML(String str) { + String result = null; + int wrapLength = 40; + String newLineStr = "
"; + if (str != null) { + final int inputLineLength = str.length(); + int offset = 0; + final StringBuilder wrappedLine = new StringBuilder(inputLineLength + 32); + while (offset < inputLineLength) { + int spaceToWrapAt = -1; + Matcher matcher = patternToWrapOn.matcher(str.substring(offset, Math.min(offset + wrapLength + 1, inputLineLength))); + if (matcher.find()) { + if (matcher.start() == 0) { + offset += matcher.end(); + continue; + } + spaceToWrapAt = matcher.start() + offset; + } + + // only last line without leading spaces is left + if (inputLineLength - offset <= wrapLength) { + break; + } + + while (matcher.find()) { + spaceToWrapAt = matcher.start() + offset; + } + + if (spaceToWrapAt >= offset) { + // normal case + wrappedLine.append(str, offset, spaceToWrapAt); + wrappedLine.append(newLineStr); + offset = spaceToWrapAt + 1; + + } else { + // do not wrap really long word, just extend beyond limit + matcher = patternToWrapOn.matcher(str.substring(offset + wrapLength)); + if (matcher.find()) { + spaceToWrapAt = matcher.start() + offset + wrapLength; + } + + if (spaceToWrapAt >= 0) { + wrappedLine.append(str, offset, spaceToWrapAt); + wrappedLine.append(newLineStr); + offset = spaceToWrapAt + 1; + } else { + wrappedLine.append(str, offset, str.length()); + offset = inputLineLength; + } + } + }// Whatever is left in line is short enough to just pass through + wrappedLine.append(str, offset, str.length()); + result = wrappedLine.toString(); + } + + return "" + result + ""; + } + + private WordUtil() {} +}