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 bc2fee612c6..984f38217a7 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 @@ -155,6 +155,7 @@ public void initialize() { lstControls.add(Pair.of(view.getCbOpenPacksIndiv(), FPref.UI_OPEN_PACKS_INDIV)); lstControls.add(Pair.of(view.getCbTokensInSeparateRow(), FPref.UI_TOKENS_IN_SEPARATE_ROW)); lstControls.add(Pair.of(view.getCbStackCreatures(), FPref.UI_STACK_CREATURES)); + lstControls.add(Pair.of(view.getCbAnimateFlying(), FPref.UI_ANIMATE_FLYING)); lstControls.add(Pair.of(view.getCbManaLostPrompt(), FPref.UI_MANA_LOST_PROMPT)); lstControls.add(Pair.of(view.getCbEscapeEndsTurn(), FPref.UI_ALLOW_ESC_TO_END_TURN)); lstControls.add(Pair.of(view.getCbDetailedPaymentDesc(), FPref.UI_DETAILED_SPELLDESC_IN_PROMPT)); 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 469eb15cfdd..578c4ef8b3f 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 @@ -109,6 +109,7 @@ public enum VSubmenuPreferences implements IVSubmenu { private final JCheckBox cbOpenPacksIndiv = new OptionsCheckBox(localizer.getMessage("cbOpenPacksIndiv")); private final JCheckBox cbTokensInSeparateRow = new OptionsCheckBox(localizer.getMessage("cbTokensInSeparateRow")); private final JCheckBox cbStackCreatures = new OptionsCheckBox(localizer.getMessage("cbStackCreatures")); + private final JCheckBox cbAnimateFlying = new OptionsCheckBox(localizer.getMessage("cbAnimateFlying")); private final JCheckBox cbFilterLandsByColorId = new OptionsCheckBox(localizer.getMessage("cbFilterLandsByColorId")); private final JCheckBox cbShowStormCount = new OptionsCheckBox(localizer.getMessage("cbShowStormCount")); private final JCheckBox cbRemindOnPriority = new OptionsCheckBox(localizer.getMessage("cbRemindOnPriority")); @@ -423,6 +424,9 @@ public enum VSubmenuPreferences implements IVSubmenu { pnlPrefs.add(cbStackCreatures, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlStackCreatures")), descriptionConstraints); + pnlPrefs.add(cbAnimateFlying, titleConstraints); + pnlPrefs.add(new NoteLabel(localizer.getMessage("nlAnimateFlying")), descriptionConstraints); + pnlPrefs.add(cbTimedTargOverlay, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlTimedTargOverlay")), descriptionConstraints); @@ -973,6 +977,10 @@ public final JCheckBox getCbStackCreatures() { return cbStackCreatures; } + public final JCheckBox getCbAnimateFlying() { + return cbAnimateFlying; + } + public final JCheckBox getCbManaLostPrompt() { return cbManaLostPrompt; } 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 d2377eb3739..a5a9850e676 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 @@ -112,6 +112,11 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl private boolean displayEnabled = true; private boolean isAnimationPanel; private int cardXOffset, cardYOffset, cardWidth, cardHeight; + int baseY = 0; + float flyingPhase = 0f; + static final float FLYING_AMPLITUDE = 3f; + static final float FLYING_SPEED = 0.12f; + static final float FLYING_PHASE_MAX = (float)(Math.PI * 2); private boolean isSelected; private boolean hasFlash; private CachedCardImage cachedImage; @@ -969,7 +974,17 @@ public final void setCardBounds(final int x, final int y, int width, int height) cardYOffset = -yOffset; width = -xOffset + rotCenterX + rotCenterToTopCorner; height = -yOffset + rotCenterY + rotCenterToBottomCorner; - setBounds(x + xOffset, y + yOffset, width, height); + + // Store baseY when not floating or when position changes significantly + boolean shouldFloat = shouldFloat(); + if (!shouldFloat || Math.abs(y - baseY) > 5) { + baseY = y; + } + + // Calculate floating offset + int floatOffset = shouldFloat ? (int)Math.round(Math.sin(flyingPhase) * FLYING_AMPLITUDE) : 0; + + setBounds(x + xOffset, y + yOffset + floatOffset, width, height); } @Override @@ -1157,6 +1172,22 @@ private boolean showAbilityIcons() { return isShowingOverlays() && isPreferenceEnabled(FPref.UI_OVERLAY_ABILITY_ICONS); } + boolean shouldFloat() { + if (!FModel.getPreferences().getPrefBoolean(FPref.UI_ANIMATE_FLYING)) { + return false; + } + if (card == null || !card.getCurrentState().hasFlying()) { + return false; + } + if (isAnimationPanel || !displayEnabled) { + return false; + } + if (!ZoneType.Battlefield.equals(card.getZone())) { + return false; + } + return true; + } + public void repaintOverlays() { repaint(); doLayout(); 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 4cebb525099..2911ee2f864 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 @@ -22,6 +22,8 @@ import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.util.*; +import java.util.WeakHashMap; +import javax.swing.Timer; import com.google.common.collect.Lists; @@ -49,6 +51,10 @@ public class PlayArea extends CardPanelContainer implements CardPanelMouseListener { private static final long serialVersionUID = 8333013579724492513L; + private static final WeakHashMap registeredPlayAreas = new WeakHashMap<>(); + private static Timer animationTimer; + private static final int ANIMATION_DELAY_MS = 50; // 20 FPS + private static final int GUTTER_Y = 5; private static final int GUTTER_X = 5; static final float EXTRA_CARD_SPACING_X = 0.04f; @@ -85,6 +91,14 @@ public PlayArea(final CMatchUI matchUI, final FScrollPane scrollPane, final bool this.zone = zone; this.makeTokenRow = FModel.getPreferences().getPrefBoolean(FPref.UI_TOKENS_IN_SEPARATE_ROW); this.stackCreatures = FModel.getPreferences().getPrefBoolean(FPref.UI_STACK_CREATURES); + + // Register Battlefield PlayArea for flying animation + if (FModel.getPreferences().getPrefBoolean(FPref.UI_ANIMATE_FLYING) && ZoneType.Battlefield.equals(zone)) { + synchronized (registeredPlayAreas) { + registeredPlayAreas.put(this, null); + startAnimationTimerIfNeeded(); + } + } } private CardStackRow collectAllLands(List remainingPanels) { @@ -950,4 +964,62 @@ private void setCardWidth(int cardWidth0) { this.stackSpacingX = Math.round(this.cardWidth * PlayArea.STACK_SPACING_X); this.stackSpacingY = Math.round(this.cardHeight * PlayArea.STACK_SPACING_Y); } -} + + private static synchronized void startAnimationTimerIfNeeded() { + if (animationTimer != null || !FModel.getPreferences().getPrefBoolean(FPref.UI_ANIMATE_FLYING)) { + return; + } + animationTimer = new Timer(ANIMATION_DELAY_MS, e -> updateFlyingAnimationForAllPlayAreas()); + animationTimer.start(); + } + + private static synchronized void stopAnimationTimerIfNeeded() { + if (animationTimer != null) { + animationTimer.stop(); + animationTimer = null; + } + } + + static void updateFlyingAnimationForAllPlayAreas() { + if (!FModel.getPreferences().getPrefBoolean(FPref.UI_ANIMATE_FLYING)) { + synchronized (registeredPlayAreas) { + registeredPlayAreas.clear(); + stopAnimationTimerIfNeeded(); + } + return; + } + + List playAreasToUpdate; + synchronized (registeredPlayAreas) { + playAreasToUpdate = new ArrayList<>(registeredPlayAreas.keySet()); + } + + for (PlayArea playArea : playAreasToUpdate) { + if (playArea == null || !ZoneType.Battlefield.equals(playArea.zone)) { + continue; + } + + for (CardPanel panel : playArea.getCardPanels()) { + if (!panel.shouldFloat()) { + continue; + } + + // Update flying phase + panel.flyingPhase += CardPanel.FLYING_SPEED; + if (panel.flyingPhase >= CardPanel.FLYING_PHASE_MAX) { + panel.flyingPhase -= CardPanel.FLYING_PHASE_MAX; + } + + // Update position using baseY + panel.setCardBounds(panel.getCardX(), panel.baseY, panel.getCardWidth(), panel.getCardHeight()); + } + } + + // Check if we should stop timer (no more registered areas) + synchronized (registeredPlayAreas) { + if (registeredPlayAreas.isEmpty()) { + stopAnimationTimerIfNeeded(); + } + } + } +} \ No newline at end of file diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index ebbbf217683..74f1233bf83 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -135,6 +135,7 @@ cbCardTextHideReminder=Hide Reminder Text for Card Text Renderer cbOpenPacksIndiv=Open Packs Individually cbTokensInSeparateRow=Display Tokens in a Separate Row cbStackCreatures=Stack Creatures +cbAnimateFlying=Animate Flying Creatures cbFilterLandsByColorId=Filter Lands by Color in Activated Abilities cbShowStormCount=Show Storm Count in Prompt Pane cbRemindOnPriority=Visually Alert on Receipt of Priority @@ -240,6 +241,7 @@ nlCardTextHideReminder=When render card images, skip rendering reminder text. nlOpenPacksIndiv=When opening Fat Packs and Booster Boxes, booster packs will be opened and displayed one at a time. nlTokensInSeparateRow=Displays tokens in a separate row on the battlefield below the non-token creatures. nlStackCreatures=Stacks identical creatures on the battlefield like lands, artifacts, and enchantments. +nlAnimateFlying=Gives flying creatures a floating animation. (Requires starting a new match) nlTimedTargOverlay=Enables throttling-based optimization of targeting overlay to reduce CPU use (only disable if you experience choppiness on older hardware, requires starting a new match). nlCounterDisplayType=Selects the style of the in-game counter display for cards. Text-based is a new tab-like display on the cards. Image-based is the old counter image. Hybrid displays both at once. nlCounterDisplayLocation=Determines where to position the text-based counters on the card: close to the top or close to the bottom. 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 55d702a3d6a..3e044833c4a 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java @@ -97,6 +97,7 @@ public enum FPref implements PreferencesStore.IPref { UI_SR_OPTIMIZE ("false"), UI_OPEN_PACKS_INDIV ("false"), UI_STACK_CREATURES ("false"), + UI_ANIMATE_FLYING ("false"), UI_TOKENS_IN_SEPARATE_ROW("false"), UI_UPLOAD_DRAFT ("false"), UI_SCALE_LARGER ("true"),