From 172413c03b94932ad561f2788cf7595579f16009 Mon Sep 17 00:00:00 2001 From: Per Eric Trapnes Date: Thu, 14 May 2026 01:11:17 +0200 Subject: [PATCH] Created a few popups to handle end of week, warnings and gameover --- .claude/settings.local.json | 7 + .../gruppe42/Audio/AudioManager.java | 47 +-- .../idatt2003/gruppe42/Audio/SoundEffect.java | 14 +- .../AppControllers/StockAppController.java | 45 ++- .../gruppe42/Controller/GameController.java | 2 +- .../gruppe42/Controller/PopupsController.java | 75 +++-- .../Controller/TimeAndWeatherController.java | 20 +- .../DesktopViewController.java | 280 +++++++++++----- .../ViewControllers/StartViewController.java | 72 ++-- .../ntnu/idi/idatt2003/gruppe42/Millions.java | 74 +++-- .../idi/idatt2003/gruppe42/Model/App.java | 32 +- .../Model/Calculator/SaleCalculator.java | 2 +- .../idatt2003/gruppe42/Model/Difficulty.java | 21 -- .../idatt2003/gruppe42/Model/Exchange.java | 14 +- .../idi/idatt2003/gruppe42/Model/Player.java | 36 ++ .../idatt2003/gruppe42/Model/Portfolio.java | 6 +- .../idi/idatt2003/gruppe42/Model/Share.java | 11 +- .../idi/idatt2003/gruppe42/Model/Status.java | 2 +- .../idatt2003/gruppe42/View/Apps/BankApp.java | 48 +-- .../gruppe42/View/Apps/GameOverApp.java | 54 +++ .../gruppe42/View/Apps/StockApp.java | 3 +- .../gruppe42/View/Apps/WarningApp.java | 132 ++++++++ .../gruppe42/View/Apps/WeekendRapportApp.java | 251 ++++++++++++++ .../idi/idatt2003/gruppe42/View/Popup.java | 49 ++- .../View/Views/Components/Receipt.java | 235 +++++++++---- .../gruppe42/View/Views/DesktopView.java | 263 ++++++++++++--- .../gruppe42/View/Views/StartView.java | 8 +- src/main/resources/css/apps.css | 313 +++++++++++++----- src/main/resources/css/desktop.css | 7 +- src/main/resources/css/global.css | 36 +- src/main/resources/css/login.css | 14 +- src/main/resources/css/popup.css | 36 +- 32 files changed, 1701 insertions(+), 508 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/GameOverApp.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WarningApp.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WeekendRapportApp.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..2fd8d31 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(del \"C:\\\\Users\\\\perer\\\\Documents\\\\GitHub\\\\Millions\\\\src\\\\main\\\\java\\\\edu\\\\ntnu\\\\idi\\\\idatt2003\\\\gruppe42\\\\View\\\\Apps\\\\DebtWarningApp.java\")" + ] + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/AudioManager.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/AudioManager.java index 6587275..aa43a53 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/AudioManager.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/AudioManager.java @@ -1,39 +1,48 @@ package edu.ntnu.idi.idatt2003.gruppe42.Audio; -import javafx.scene.media.AudioClip; - import java.util.EnumMap; import java.util.Map; +import javafx.scene.Node; +import javafx.scene.input.MouseEvent; +import javafx.scene.media.AudioClip; -public class AudioManager { +/** + * Zero-latency sound effect player. + * + *

All assets in {@link SoundEffect} are decoded into in-memory + * {@link AudioClip}s on first instantiation, so {@link #play(SoundEffect)} + * fires immediately without any I/O.

+ */ +public final class AudioManager { - private static AudioManager instance = new AudioManager(); - private Map sounds = new EnumMap<>(SoundEffect.class); - private double volume = 1.0; + private static final AudioManager INSTANCE = new AudioManager(); - public AudioManager() { + private final Map clips = new EnumMap<>(SoundEffect.class); + private double volume = 1.0; + private AudioManager() { for (SoundEffect effect : SoundEffect.values()) { - AudioClip clip = new AudioClip( - getClass() - .getResource(effect.getPath()) - .toExternalForm() + getClass().getResource(effect.getPath()).toExternalForm() ); - - sounds.put(effect, clip); + clips.put(effect, clip); } } public static AudioManager getInstance() { - return instance; + return INSTANCE; } - public void play(SoundEffect sound) { - AudioClip audioClip = sounds.get(sound); - - if (audioClip != null) { - audioClip.play(volume); + /** Plays the given sound at the current master volume. */ + public void play(final SoundEffect sound) { + AudioClip clip = clips.get(sound); + if (clip != null) { + clip.play(volume); } } + + /** Sets the master volume in the range [0.0, 1.0]. */ + public void setVolume(final double volume) { + this.volume = Math.max(0.0, Math.min(1.0, volume)); + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/SoundEffect.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/SoundEffect.java index 3341a6e..bc72d1a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/SoundEffect.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Audio/SoundEffect.java @@ -1,16 +1,22 @@ package edu.ntnu.idi.idatt2003.gruppe42.Audio; +/** + * Catalog of every short sound effect played by the game. + * The {@link AudioManager} pre-loads each entry on startup so that + * {@link AudioManager#play(SoundEffect)} fires with no fetch/decode lag. + */ public enum SoundEffect { - OPEN_APP("/Audio/open_app.wav"), - CLOSE_APP("/Audio/close_app.wav"); + CLOSE_APP("/Audio/close_app.wav"), + CLICK("/Audio/mouse_click.wav"); - private String path; + private final String path; - SoundEffect(String path) { + SoundEffect(final String path) { this.path = path; } + /** Classpath resource path of the audio asset. */ public String getPath() { return path; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/StockAppController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/StockAppController.java index 9ebdf3e..7143144 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/StockAppController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/StockAppController.java @@ -9,6 +9,7 @@ import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.control.Button; import javafx.scene.control.ListCell; import javafx.scene.control.Label; import javafx.scene.layout.HBox; @@ -28,6 +29,12 @@ public final class StockAppController implements AppController { private final Player player; private Stock currentStock; + /** True while the game is on Saturday or Sunday. */ + private boolean isWeekend = false; + + /** Called when the player clicks Trade during a weekend. */ + private Runnable onMarketClosed; + /** * Constructs a new StockAppController. * @@ -158,10 +165,46 @@ protected void updateItem(final Stock stock, final boolean empty) { }; } + /** + * Enables or disables trading based on whether it is a weekend. + * When weekend is {@code true} the Trade button is visually dimmed and + * the spinner is disabled; clicking Trade triggers the market-closed callback. + * + * @param weekend {@code true} on Saturday/Sunday, {@code false} on weekdays + */ + public void setWeekend(final boolean weekend) { + this.isWeekend = weekend; + Button tradeButton = stockApp.getConfirmButton(); + if (weekend) { + tradeButton.getStyleClass().add("trade-button-weekend"); + tradeButton.setOpacity(0.45); + stockApp.getQuantitySpinner().setDisable(true); + } else { + tradeButton.getStyleClass().remove("trade-button-weekend"); + tradeButton.setOpacity(1.0); + stockApp.getQuantitySpinner().setDisable(false); + } + } + + /** + * Registers the callback invoked when the player clicks Trade on a weekend. + * + * @param callback the action to run (typically shows a market-closed warning) + */ + public void setOnMarketClosed(final Runnable callback) { + this.onMarketClosed = callback; + } + /** * Handles the trade button action based on the spinner value. */ private void handleTransaction() { + if (isWeekend) { + if (onMarketClosed != null) { + onMarketClosed.run(); + } + return; + } if (currentStock == null) { return; } @@ -204,7 +247,7 @@ private void handleBuy(final Stock stock, final BigDecimal quantity) { */ private void handleSell(final Stock stock, final BigDecimal quantity) { player.getPortfolio().getShares().stream() - .filter(s -> s.getStock().getSymbol().equals(stock.getSymbol())) + .filter(share -> share.getStock().getSymbol().equals(stock.getSymbol())) .findFirst() .ifPresent(existingShare -> { if (existingShare.getQuantity().compareTo(quantity) < 0) { diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java index 1d9ff8a..0fb5fb2 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java @@ -36,7 +36,7 @@ public void setGameState(GameState gameState) { public void startGame() { timer = new Timer(true); final int delay = 0; - final int period = 1000; + final int period = 50; timer.scheduleAtFixedRate( new TimerTask() { diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupsController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupsController.java index 794a176..08d35fb 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupsController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/PopupsController.java @@ -5,49 +5,72 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.App; import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; import java.util.List; +import javafx.scene.control.Button; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; /** * Manages popup visibility, dragging, and bounds-clamping. + * */ public final class PopupsController { private final List popups; - private AudioManager audioManager = AudioManager.getInstance(); + private final AudioManager audioManager = AudioManager.getInstance(); - public PopupsController(List popups) { + public PopupsController(final List popups) { this.popups = popups; popups.forEach(this::initPopup); } - private void initPopup(Popup popup) { + private void initPopup(final Popup popup) { makeDraggable(popup); - popup.getCloseButton().setOnAction(event -> hide(popup)); + Button closeButton = popup.getCloseButton(); + closeButton.setOnAction(event -> hide(popup)); + + closeButton.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> { + if (popup.getRoot().isVisible()) { + audioManager.play(SoundEffect.CLOSE_APP); + } + }); } - public boolean show(App type) { + /** + * Wires an button to open the popup of the given type. + */ + public void bindOpenButton(final Button button, final App type) { + Popup target = findPopup(type); + button.setOnAction(event -> show(type)); + button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { + if (target != null && !target.getRoot().isVisible()) { + audioManager.play(SoundEffect.OPEN_APP); + } + }); + } + + /** + * Brings the popup of the given type to the front and shows it. + */ + public boolean show(final App type) { if (type == null) { return false; } - popups.stream() - .filter(popup -> popup.getType().equals(type)) - .findFirst() - .ifPresent(popup -> { - popup.getRoot().setVisible(true); - popup.getRoot().autosize(); - popup.getRoot().toFront(); - }); - audioManager.play(SoundEffect.OPEN_APP); + Popup popup = findPopup(type); + if (popup == null) { + return false; + } + popup.getRoot().setVisible(true); + popup.getRoot().autosize(); + popup.getRoot().toFront(); return true; } - public boolean hide(Popup popup) { + public boolean hide(final Popup popup) { if (popup == null) { return false; } popup.getRoot().setVisible(false); - audioManager.play(SoundEffect.CLOSE_APP); return true; } @@ -55,17 +78,27 @@ public List getPopups() { return popups; } - private void makeDraggable(Popup popup) { + private Popup findPopup(final App type) { + if (type == null) { + return null; + } + return popups.stream() + .filter(p -> p.getType().equals(type)) + .findFirst() + .orElse(null); + } + + private void makeDraggable(final Popup popup) { BorderPane root = popup.getRoot(); double[] offset = new double[2]; - double[] parentBounds = new double[2]; // cache here + double[] parentBounds = new double[2]; popup.getHeader().setOnMousePressed(event -> { offset[0] = event.getSceneX() - root.getLayoutX(); offset[1] = event.getSceneY() - root.getLayoutY(); - // snapshot bounds once on press, not on every drag tick + if (root.getParent() instanceof Pane parent) { - parentBounds[0] = parent.getWidth() - popup.getWidth(); + parentBounds[0] = parent.getWidth() - popup.getWidth(); parentBounds[1] = parent.getHeight() - popup.getHeight(); } }); @@ -77,4 +110,4 @@ private void makeDraggable(Popup popup) { event.getSceneY() - offset[1], parentBounds[1]))); }); } -} \ No newline at end of file +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/TimeAndWeatherController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/TimeAndWeatherController.java index 4fda836..67f9e25 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/TimeAndWeatherController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/TimeAndWeatherController.java @@ -34,8 +34,6 @@ public TimeAndWeatherController(GameController gameController) { public void nextTick() { int nextHour = hour.get() + 1; if (nextHour >= 24) { - System.out.println("This is the day" + dayIndex); - System.out.println("This is the hour" + hour); dayIndex.set((dayIndex.get() + 1) % 7); updateGameState(); updateWeather(); @@ -50,19 +48,16 @@ private void updateWeather() { // Range from -10 to 30 int change = random.nextInt(11) - 5; // -5 to +5 int newTemp = temperature.get() + change; - if (newTemp < -10) newTemp = -10; - if (newTemp > 30) newTemp = 30; + newTemp = Math.min(Math.max(newTemp, -10), 30); temperature.set(newTemp); - if (newTemp <= 0) { + + if (random.nextBoolean()) { + weather.set("Sunny"); + } else if (newTemp <= 0) { weather.set("Snow"); } else { - // If warm, could be sunny or rain - if (random.nextBoolean()) { - weather.set("Sunny"); - } else { - weather.set("Rain"); - } + weather.set("Rain"); } } @@ -106,13 +101,10 @@ public String getWeekString() { } public void updateGameState() { - if (dayIndex.get() == 6) { - System.out.println("det er lørdag"); gameController.setGameState(GameState.RENTDAY); return; } else if (dayIndex.get() == 0) { - System.out.println("det er søndag"); gameController.setGameState(GameState.FREEDAY); return; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/DesktopViewController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/DesktopViewController.java index 92ba9bc..20dc4b9 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/DesktopViewController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/DesktopViewController.java @@ -9,69 +9,80 @@ import edu.ntnu.idi.idatt2003.gruppe42.Controller.TimeAndWeatherController; import edu.ntnu.idi.idatt2003.gruppe42.Model.App; import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Purchase; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Sale; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Transaction; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.AppStoreApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.BankApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.GameOverApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.HustlersApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.MailApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.NewsApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.StockApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.WarningApp; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.WeekendRapportApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; import edu.ntnu.idi.idatt2003.gruppe42.View.Views.DesktopView; +import edu.ntnu.idi.idatt2003.gruppe42.View.Views.DesktopView.ModalLayer; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.scene.control.Button; import javafx.scene.input.ClipboardContent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; -import javafx.scene.layout.Pane; +import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; /** - * Controller for the desktop. - * Handles app button creation, resizing, click events, and drag-and-drop. + * Controller for the desktop view. Manages app popup lifecycle, drag-and-drop, + * the weekend rapport modal, warning popups, and game-over state. */ public final class DesktopViewController { + + private static final BigDecimal WEEKLY_RENT = new BigDecimal("50.00"); + private static final int SATURDAY_INDEX = 6; + private static final int SUNDAY_INDEX = 0; + private final PopupsController popupsController; private final DesktopView desktopView; private final Player player; private final TimeAndWeatherController timeAndWeatherController; + private final MarketController marketController; + private final StockAppController stockAppController; + + private final WeekendRapportApp weekendRapportApp = new WeekendRapportApp(); + private final WarningApp warningApp = new WarningApp(); + private final GameOverApp gameOverApp = new GameOverApp(); + + private Runnable onGameOver; - /** - * Constructs a new desktop view controller. - * - * @param player the player model. - * @param gameController the game controller to register app controllers. - */ - public DesktopViewController( - final Player player, - final GameController gameController - ) { + public DesktopViewController(final Player player, final GameController gameController) { this.player = player; this.timeAndWeatherController = new TimeAndWeatherController(gameController); gameController.addAppController(timeAndWeatherController); - List popups = new ArrayList<>(); + this.marketController = new MarketController(); AppStoreApp appStoreApp = new AppStoreApp(); gameController.addAppController(new AppStoreController(appStoreApp)); HustlersApp hustlersApp = new HustlersApp(); - + StockApp stockApp = new StockApp(player); - MarketController marketController = new MarketController(); - gameController.addAppController( - new StockAppController(stockApp, marketController, player) - ); + this.stockAppController = new StockAppController(stockApp, marketController, player); + gameController.addAppController(stockAppController); MailApp mailApp = new MailApp(); - NewsApp newsApp = new NewsApp(); BankApp bankApp = new BankApp(); gameController.addAppController(new BankAppController(bankApp, player)); + List popups = new ArrayList<>(); popups.add(appStoreApp); popups.add(hustlersApp); popups.add(stockApp); @@ -86,68 +97,182 @@ public DesktopViewController( stockApp.showStockPage(share.getStock()); }); + stockAppController.setOnMarketClosed(this::showMarketClosedWarning); + this.desktopView = new DesktopView(this); + BorderPane root = desktopView.getRoot(); + + for (Popup p : popupsController.getPopups()) { + desktopView.registerPopup(p.getRoot()); + root.getChildren().add(p.getRoot()); + } + + weekendRapportApp.centerInParent(); + warningApp.centerInParent(); + gameOverApp.centerInParent(); + + desktopView.registerModal(ModalLayer.RAPPORT, weekendRapportApp.getRoot()); + desktopView.registerModal(ModalLayer.WARNING, warningApp.getRoot()); + desktopView.registerModal(ModalLayer.GAME_OVER, gameOverApp.getRoot()); + + root.getChildren().addAll( + desktopView.getOverlay(ModalLayer.RAPPORT), weekendRapportApp.getRoot(), + desktopView.getOverlay(ModalLayer.WARNING), warningApp.getRoot(), + desktopView.getOverlay(ModalLayer.GAME_OVER), gameOverApp.getRoot() + ); + + desktopView.getOverlay(ModalLayer.RAPPORT).setOnMouseClicked(event -> + weekendRapportApp.getContinueButton().fire() + ); + + wireWeekendRapport(); + wireGameOver(); + + desktopView.getNextWeekButton().setOnAction(event -> handleAdvanceWeek()); + + timeAndWeatherController.dayIndexProperty().addListener((obs, oldDay, newDay) -> { + int day = newDay.intValue(); + boolean isWeekend = day == SATURDAY_INDEX || day == SUNDAY_INDEX; + Platform.runLater(() -> stockAppController.setWeekend(isWeekend)); + if (day == SATURDAY_INDEX) { + Platform.runLater(this::showWeekendRapport); + } + }); + + int startDay = timeAndWeatherController.dayIndexProperty().get(); + stockAppController.setWeekend(startDay == SATURDAY_INDEX || startDay == SUNDAY_INDEX); + } + + private void showWeekendRapport() { + int week = timeAndWeatherController.weekIndexProperty().get(); + List transactions = player.getTransactionArchive().getTransactions(week); + BigDecimal netIncome = calculateNetIncome(transactions); + BigDecimal cashBefore = player.getMoney(); + + player.deductRent(WEEKLY_RENT); + BigDecimal cashAfter = player.getMoney(); + + weekendRapportApp.populate(transactions, netIncome, cashBefore, WEEKLY_RENT, cashAfter); + weekendRapportApp.getRoot().setVisible(true); + desktopView.enterLayer(ModalLayer.RAPPORT); + } + + private void wireWeekendRapport() { + weekendRapportApp.getContinueButton().setOnAction(event -> { + weekendRapportApp.getRoot().setVisible(false); + desktopView.exitLayer(ModalLayer.RAPPORT); + }); + } + + private void handleAdvanceWeek() { + if (player.isInDebt()) { + showDebtWarning(); + } else { + doAdvanceWeek(); + } + } - this.desktopView.getRoot().getChildren().addAll( - popupsController.getPopups().stream() - .map(Popup::getRoot).toArray(Pane[]::new) + private void showDebtWarning() { + warningApp.configureDouble( + "💔", + "Starting Week in Debt", + "Your balance is negative. Proceeding to next week will cost you a life.", + "Stay on Sunday", + "Proceed Anyway" ); + warningApp.getSecondaryButton().setOnAction(event -> dismissWarning()); + warningApp.getPrimaryButton().setOnAction(event -> { + dismissWarning(); + player.loseLife(); + if (player.getLives() == 0) { + showGameOver(); + } else { + doAdvanceWeek(); + } + }); + showWarning(); + } + + private void showMarketClosedWarning() { + warningApp.configureSingle( + "🔒", + "Market Closed", + "Trading is suspended on weekends.\nThe market reopens on Monday.", + "Got it" + ); + warningApp.getPrimaryButton().setOnAction(event -> dismissWarning()); + showWarning(); + } + + private void showWarning() { + warningApp.getRoot().setVisible(true); + desktopView.enterLayer(ModalLayer.WARNING); + } + + private void dismissWarning() { + warningApp.getRoot().setVisible(false); + desktopView.exitLayer(ModalLayer.WARNING); + } - this.desktopView.getNextWeekButton().setOnAction( event -> { - marketController.advanceWeek(); - timeAndWeatherController.advanceWeek(); + private void showGameOver() { + gameOverApp.getRoot().setVisible(true); + desktopView.enterLayer(ModalLayer.GAME_OVER); + } + + private void wireGameOver() { + gameOverApp.getStartOverButton().setOnAction(event -> { + if (onGameOver != null) { + onGameOver.run(); + } }); } - /** - * Returns the player model. - * - * @return the player model - */ + public void setOnGameOver(final Runnable callback) { + this.onGameOver = callback; + } + + private void doAdvanceWeek() { + marketController.advanceWeek(); + timeAndWeatherController.advanceWeek(); + } + + private static BigDecimal calculateNetIncome(final List transactions) { + BigDecimal net = BigDecimal.ZERO; + for (Transaction t : transactions) { + if (t instanceof Sale) { + net = net.add(t.getCalculator().calculateTotal()); + } else if (t instanceof Purchase) { + net = net.subtract(t.getCalculator().calculateTotal()); + } + } + return net; + } + public Player getPlayer() { return player; } - /** - * Returns the time and weather controller. - * - * @return the time and weather controller - */ public TimeAndWeatherController getTimeAndWeatherController() { return timeAndWeatherController; } - /** - * Returns the desktop view. - * - * @return the desktop view - */ public DesktopView getDesktopView() { return desktopView; } - /** - * Creates an app button with click and drag functionality. - * - * @param type the type of the app. - * @return the app button. - */ public Button createAppButton(final App type) { - String buttonText = type.toString().substring(0, 1).toUpperCase() - + type.toString().substring(1).toLowerCase(); - Button button = new Button(buttonText); + Button button = new Button(type.getDisplayName()); button.getStyleClass().add("app-button"); button.setMinSize(0, 0); - final double sizeMultiplier = 0.8; - - // Bind button size to parent dimensions to keep it square and responsive button.parentProperty().addListener((observable, oldParent, newParent) -> { if (newParent instanceof Region region) { button.prefWidthProperty().bind( Bindings.min( - region.widthProperty().multiply(sizeMultiplier), - region.heightProperty()).multiply(sizeMultiplier)); + region.widthProperty().multiply(0.8), + region.heightProperty() + ).multiply(0.8) + ); button.prefHeightProperty().bind(button.prefWidthProperty()); } else { button.prefWidthProperty().unbind(); @@ -155,58 +280,33 @@ public Button createAppButton(final App type) { } }); - // Print when clicked and open popup - button.setOnAction(event -> { - openPopup(type); - }); + popupsController.bindOpenButton(button, type); - // Handle start of drag: initiate drag-and-drop button.setOnDragDetected(event -> { - Dragboard dragboard = button.startDragAndDrop(TransferMode.MOVE); - dragboard.setDragView(button.snapshot(null, null)); - dragboard.setDragViewOffsetX(event.getX()); - dragboard.setDragViewOffsetY(event.getY()); - ClipboardContent clipboardContent = new ClipboardContent(); - clipboardContent.putString("app_button"); - dragboard.setContent(clipboardContent); + Dragboard db = button.startDragAndDrop(TransferMode.MOVE); + db.setDragView(button.snapshot(null, null)); + db.setDragViewOffsetX(event.getX()); + db.setDragViewOffsetY(event.getY()); + ClipboardContent cc = new ClipboardContent(); + cc.putString("app_button"); + db.setContent(cc); event.consume(); }); return button; } - /** - * Handles the settings button action. - */ public void handleSettings() { - // Placeholder for settings logic - } - - /** - * Opens the correct popup based on the app type. - * - * @param type the app type. - */ - private void openPopup(final App type) { - popupsController.show(type); } - /** - * Configures a cell to be a drop target for app buttons. - * - * @param cell the stack pane cell to configure. - */ public void configureCellAsDropTarget(final StackPane cell) { - // Handle drag over cell: allow drop if cell is empty cell.setOnDragOver(event -> { - if (event.getGestureSource() instanceof Button - && cell.getChildren().isEmpty()) { + if (event.getGestureSource() instanceof Button && cell.getChildren().isEmpty()) { event.acceptTransferModes(TransferMode.MOVE); } event.consume(); }); - // Handle drop on cell: move the button to this cell cell.setOnDragDropped(event -> { boolean success = false; if (event.getGestureSource() instanceof Button button) { @@ -218,4 +318,4 @@ public void configureCellAsDropTarget(final StackPane cell) { event.consume(); }); } -} +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/StartViewController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/StartViewController.java index f116725..849c16c 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/StartViewController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/ViewControllers/StartViewController.java @@ -2,17 +2,23 @@ import edu.ntnu.idi.idatt2003.gruppe42.Millions; import edu.ntnu.idi.idatt2003.gruppe42.Model.Difficulty; -import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; +import edu.ntnu.idi.idatt2003.gruppe42.View.Apps.WarningApp; import edu.ntnu.idi.idatt2003.gruppe42.View.Views.StartView; import java.util.List; import java.util.Random; -import javafx.scene.control.Alert; +/** + * Controller for the start / login screen. + * Validates user input and delegates to {@link Millions#initGame} on success. + * Uses the shared {@link WarningApp} popup for validation feedback. + */ public class StartViewController { private String username = ""; + private final StartView startView; private final Millions application; + private static final List TATE_NAMES = List.of( "Top G", "Cobra Tate", @@ -26,54 +32,62 @@ public class StartViewController { "The Talisman" ); - public StartViewController(Millions application, StartView startView) { - + public StartViewController(final Millions application, final StartView startView) { this.application = application; this.startView = startView; - startView.getUsernameField().textProperty().addListener((obs, old, newValue) -> { - username = newValue; - }); + WarningApp warningApp = new WarningApp(); + warningApp.centerInParent(); + startView.getRoot().getChildren().add(warningApp.getRoot()); + + warningApp.configureSingle( + "⚠", + "Difficulty Required", + "Choose how much starting money you want before jumping in.", + "Got it" + ); + warningApp.getPrimaryButton().setOnAction(e -> + warningApp.getRoot().setVisible(false) + ); + + startView.getUsernameField().textProperty().addListener( + (obs, old, newValue) -> username = newValue + ); - startView.getLoginButton().setOnAction(event -> handleStart()); + startView.getLoginButton().setOnAction(event -> { + if (startView.getSelectedMode() == null) { + warningApp.getRoot().setVisible(true); + warningApp.getRoot().toFront(); + return; + } + handleStart(); + }); } private void handleStart() { - if (startView.getSelectedMode() == null) { - showAlert("How much money you got?"); - return; - } - String resolvedName = resolveUsername(username); String selectedMode = startView.getSelectedMode(); + Difficulty difficulty = Difficulty.EASY; if (selectedMode != null) { - if (selectedMode.startsWith("Medium")) { - difficulty = Difficulty.MEDIUM; - } else if (selectedMode.startsWith("Hard")) { - difficulty = Difficulty.HARD; - } + if (selectedMode.startsWith("Medium")) { + difficulty = Difficulty.MEDIUM; + } else if (selectedMode.startsWith("Hard")) { + difficulty = Difficulty.HARD; + } } application.initGame(resolvedName, difficulty); } - private String resolveUsername(String input) { + private String resolveUsername(final String input) { if (input == null || input.trim().isEmpty() || !isValidName(input)) { return TATE_NAMES.get(new Random().nextInt(TATE_NAMES.size())); } return input; } - private boolean isValidName(String value) { + private boolean isValidName(final String value) { return value.matches("[a-zA-Z0-9]+"); } - - private void showAlert(String message) { - Alert alert = new Alert(Alert.AlertType.WARNING); - alert.setTitle("Difficulty missing"); - alert.setHeaderText(null); - alert.setContentText(message); - alert.showAndWait(); - } -} \ No newline at end of file +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java index ad6e8d6..50f3db7 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java @@ -12,22 +12,28 @@ import javafx.scene.Scene; import javafx.stage.Stage; +/** + * JavaFX application entry point. + * + *

Owns the lifecycle of every game instance: a brand-new {@link Player}, + * {@link GameController}, {@link DesktopView}, and {@link DesktopViewController} + * is created per game session. {@link #resetGame()} is the one place that + * tears down a session and returns to a fresh start screen so no stale + * state ever survives.

+ */ public class Millions extends Application { private Scene scene; - private Stage stage; - private DesktopView desktopView; - private DesktopViewController desktopViewController; + + // Active game state. All four are reset together by resetGame(). private Player player; private GameController gameController; + private DesktopView desktopView; + private DesktopViewController desktopViewController; @Override - public void start(Stage stage) throws Exception { - this.stage = stage; - - StartView startView = new StartView(); - new StartViewController(this, startView); - + public void start(final Stage stage) { + StartView startView = installStartView(); scene = new Scene(startView.getRoot(), 900, 700); stage.setTitle("Millions"); @@ -37,25 +43,51 @@ public void start(Stage stage) throws Exception { stage.show(); } - public void initGame(String username, Difficulty difficulty) { + /** + * Begins a brand-new game session. Called by the start screen once the + * user has chosen a difficulty. + */ + public void initGame(final String username, final Difficulty difficulty) { player = GameFactory.createPlayer(username, difficulty); gameController = new GameController(); gameController.startGame(); - switchToDesktopView(); + + desktopViewController = new DesktopViewController(player, gameController); + desktopViewController.setOnGameOver(this::resetGame); + desktopView = desktopViewController.getDesktopView(); + scene.setRoot(desktopView.getRoot()); } - public void switchToStartView() { - stop(); - StartView startView = new StartView(); + /** + * Single, centralised teardown / reset path. + * + *

Stops the running game, drops every reference to the previous + * session's state (player, lives, cash, transactions, popups, …) and + * builds a fresh start screen with a fresh controller. After this + * call the app behaves exactly as if it had just launched.

+ */ + public void resetGame() { + if (gameController != null) { + gameController.stopGame(); + } + gameController = null; + desktopViewController = null; + desktopView = null; + player = null; + + StartView startView = installStartView(); scene.setRoot(startView.getRoot()); - stage.setScene(scene); } - public void switchToDesktopView() { - desktopViewController = new DesktopViewController(player, gameController); - desktopView = desktopViewController.getDesktopView(); - scene.setRoot(desktopView.getRoot()); - stage.setScene(scene); + /** + * Builds a fresh {@link StartView} and binds a fresh + * {@link StartViewController} to it. Used by both the initial launch + * and {@link #resetGame()}. + */ + private StartView installStartView() { + StartView startView = new StartView(); + new StartViewController(this, startView); + return startView; } @Override @@ -65,7 +97,7 @@ public void stop() { } } - public static void main(String[] args) { + public static void main(final String[] args) { launch(args); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java index 276b3fc..e1ad0ea 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/App.java @@ -4,16 +4,24 @@ * Enumeration of available applications in the simulation. */ public enum App { - /** The App Store for buying new apps. */ - APPSTORE, - /** Hustlers app for side jobs. */ - HUSTLERS, - /** Stock market trading app. */ - STOCK, - /** Mail application. */ - MAIL, - /** News application for market insights. */ - NEWS, - /** Bank application for financial overview. */ - BANK + APPSTORE("App Store"), + HUSTLERS("Hustlers"), + STOCK("Market"), + MAIL("Mail"), + NEWS("News"), + BANK("Bank"), + WEEKENDRAPPORT("Weekend Rapport"), + WARNING("Warning"), + GAMEOVER("Game Over"); + + private final String displayName; + + App(final String displayName) { + this.displayName = displayName; + } + + /** Title used in popup headers and button labels. */ + public String getDisplayName() { + return displayName; + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/SaleCalculator.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/SaleCalculator.java index 1925e88..325bdc9 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/SaleCalculator.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Calculator/SaleCalculator.java @@ -16,7 +16,7 @@ public final class SaleCalculator implements TransactionCalculator { * * @param share the share position being sold */ - public SaleCalculator(final Share share) { + public SaleCalculator(Share share) { this.purchasePrice = share.getPurchasePrice(); this.salesPrice = share.getStock().getSalesPrice(); this.quantity = share.getQuantity(); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Difficulty.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Difficulty.java index 9dfcffe..1d3cb52 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Difficulty.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Difficulty.java @@ -4,28 +4,7 @@ * Represents the game difficulty levels. */ public enum Difficulty { - /** Easy difficulty: starting with more money. */ EASY, - /** Medium difficulty: starting with moderate money. */ MEDIUM, - /** Hard difficulty: starting with no money. */ HARD; - - /** - * Converts a string value to a Difficulty level. - * - * @param value the string representation - * @return the corresponding Difficulty, or EASY if invalid/null - */ - public static Difficulty fromString(final String value) { - if (value == null || value.isEmpty()) { - return EASY; - } - return switch (value.charAt(0)) { - case 'E' -> EASY; - case 'M' -> MEDIUM; - case 'H' -> HARD; - default -> EASY; - }; - } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Exchange.java index 8fd64f3..8ff47df 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Exchange.java @@ -1,15 +1,14 @@ package edu.ntnu.idi.idatt2003.gruppe42.Model; -import edu.ntnu.idi.idatt2003.gruppe42.Model.Calculator.SaleCalculator; import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Purchase; import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Sale; import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Transaction; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; /** * Represents a stock exchange where players can buy and sell shares. @@ -144,28 +143,23 @@ public void advance() { /** * Returns the top stocks by price change given a limit. * - * @param limit the maximum number of stocks to return * @return a list of gainer stocks */ - public List getGainers(final int limit) { + public List getGainers() { return stockMap.values().stream() .sorted((a, b) -> b.getLatestPriceChange() .compareTo(a.getLatestPriceChange())) - .limit(limit) .toList(); } /** * Returns the bottom stocks by price change given a limit. * - * @param limit the maximum number of stocks to return * @return a list of loser stocks */ - public List getLosers(final int limit) { + public List getLosers() { return stockMap.values().stream() - .sorted((a, b) -> a.getLatestPriceChange() - .compareTo(b.getLatestPriceChange())) - .limit(limit) + .sorted(Comparator.comparing(Stock::getLatestPriceChange)) .toList(); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Player.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Player.java index 6fd4410..71e71b7 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Player.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Player.java @@ -3,6 +3,8 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.Exceptions.InsufficientFunds; import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.TransactionArchive; import java.math.BigDecimal; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; /** * Represents a player in the stock market simulation. @@ -13,11 +15,14 @@ *

*/ public class Player { + private static final int MAX_LIVES = 3; + private final String name; private final BigDecimal startingMoney; private BigDecimal money; private final Portfolio portfolio; private final TransactionArchive transactionArchive; + private final IntegerProperty lives = new SimpleIntegerProperty(MAX_LIVES); /** * Constructs a new {@code Player} with a name and starting money. @@ -53,6 +58,37 @@ public BigDecimal getMoney() { return money; } + public IntegerProperty getLivesProperty() { + return lives; + } + + public int getLives() { + return lives.get(); + } + + /** + * Deducts one life. Safe to call on the JavaFX thread only. + * Has no effect if already at 0. + */ + public void loseLife() { + lives.set(Math.max(0, lives.get() - 1)); + } + + /** Returns true if the player's cash balance is negative. */ + public boolean isInDebt() { + return money.compareTo(BigDecimal.ZERO) < 0; + } + + /** + * Deducts rent from the player's cash balance. + * Unlike {@link #withdrawMoney}, this allows the balance to go negative. + * + * @param amount the rent amount to deduct + */ + public void deductRent(final BigDecimal amount) { + money = money.subtract(amount); + } + /** * Adds the specified amount of money to the player's balance. * diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Portfolio.java index 6cfd480..da54723 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Portfolio.java @@ -1,6 +1,7 @@ package edu.ntnu.idi.idatt2003.gruppe42.Model; import edu.ntnu.idi.idatt2003.gruppe42.Model.Calculator.SaleCalculator; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Share; import java.math.BigDecimal; import java.math.RoundingMode; @@ -60,7 +61,6 @@ public boolean addShare(final Share share) { } } - // No matching symbol found — add as new position return shares.add(share); } @@ -93,10 +93,10 @@ public boolean removeShare(final Share newShare) { // Trying to sell more than owned return false; } else if (cmp == 0) { - // Selling entire position — remove from list + // Selling entire position return shares.remove(existing); } else { - // Partial sale — replace with new instance to ensure UI update + // Partial sale, replace with new instance to ensure UI update shares.remove(existing); return shares.add(new Share( existing.getStock(), diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Share.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Share.java index b64dc91..cd19f12 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Share.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Share.java @@ -11,11 +11,8 @@ *

*/ public class Share { - /** The stock this share represents. */ private final Stock stock; - /** The number of shares owned. */ private BigDecimal quantity; - /** The price per share at the time of purchase. */ private final BigDecimal purchasePrice; /** @@ -72,14 +69,14 @@ public BigDecimal getPurchasePrice() { } @Override - public boolean equals(Object o) { - if (this == o) { + public boolean equals(Object object) { + if (this == object) { return true; } - if (o == null || getClass() != o.getClass()) { + if (object == null || getClass() != object.getClass()) { return false; } - Share share = (Share) o; + Share share = (Share) object; return Objects.equals(stock.getSymbol(), share.stock.getSymbol()) && (quantity == null ? share.quantity == null : quantity.compareTo(share.quantity) == 0) && (purchasePrice == null ? share.purchasePrice == null : purchasePrice.compareTo(share.purchasePrice) == 0); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Status.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Status.java index 677409f..46e89ff 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Status.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Status.java @@ -4,7 +4,7 @@ * Represents the progression level of a player. *

* A player can have one of the following statuses based on their - * trading activity and net worth growth: + * trading activity and net worth growth *

*
    *
  • {@link #NOVICE}: Starting level with no requirements.
  • diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/BankApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/BankApp.java index d991cbd..4890724 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/BankApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/BankApp.java @@ -102,9 +102,9 @@ private VBox summaryItem(String title, Label valueLabel) { /** * Refreshes the summary labels with the latest financial figures. * - * @param netWorth total net worth (cash + portfolio value) - * @param cash uninvested cash available - * @param invested current market value of shares held + * @param netWorth total net worth (cash + portfolio value) + * @param cash uninvested cash available + * @param invested current market value of shares held * @param startingMoney initial balance used as the growth baseline */ public void updateStatus(double netWorth, double cash, double invested, double startingMoney) { @@ -129,8 +129,12 @@ public void setOnShareSelected(Consumer onShareSelected) { } private static double calculateGrowthPercent(double netWorth, double startingMoney) { - if (startingMoney > 0) return ((netWorth - startingMoney) / startingMoney) * 100; - if (netWorth > 0) return 100; + if (startingMoney > 0) { + return ((netWorth - startingMoney) / startingMoney) * 100; + } + if (netWorth > 0) { + return 100; + } return 0; } @@ -145,19 +149,19 @@ private class ShareCell extends ListCell { private final GridPane grid = new GridPane(); - // Column 0 — identity + // Column 0 private final Label symbolLabel = new Label(); private final Label companyLabel = new Label(); - // Column 1 — quantity & cost basis + // Column 1 private final Label quantityLabel = new Label(); private final Label avgPriceLabel = new Label(); - // Column 2 — current valuation + // Column 2 private final Label currentPriceLabel = new Label(); private final Label marketValueLabel = new Label(); - // Column 3 — gain / loss + // Column 3 private final Label gainLabel = new Label(); private final Label gainPctLabel = new Label(); @@ -168,7 +172,11 @@ private class ShareCell extends ListCell { grid.setPadding(new Insets(10, 5, 10, 5)); configureColumns(); addCellContent(); - grid.setOnMouseClicked(e -> { if (onShareSelected != null) onShareSelected.accept(getItem()); }); + grid.setOnMouseClicked(event -> { + if (onShareSelected != null) { + onShareSelected.accept(getItem()); + } + }); } private void configureColumns() { @@ -215,18 +223,18 @@ protected void updateItem(Share share, boolean empty) { } private void populateLabels(Share share) { - BigDecimal qty = share.getQuantity(); + BigDecimal quantity = share.getQuantity(); BigDecimal purchasePrice = share.getPurchasePrice(); - BigDecimal currentPrice = share.getStock().getSalesPrice(); - BigDecimal marketValue = currentPrice.multiply(qty); - BigDecimal totalCost = purchasePrice.multiply(qty); - BigDecimal gain = marketValue.subtract(totalCost); - BigDecimal gainPercent = computeGainPercent(gain, totalCost); + BigDecimal currentPrice = share.getStock().getSalesPrice(); + BigDecimal marketValue = currentPrice.multiply(quantity); + BigDecimal totalCost = purchasePrice.multiply(quantity); + BigDecimal gain = marketValue.subtract(totalCost); + BigDecimal gainPercent = computeGainPercent(gain, totalCost); symbolLabel.setText(share.getStock().getSymbol()); companyLabel.setText(share.getStock().getCompany()); - quantityLabel.setText(qty.stripTrailingZeros().toPlainString() + " Shares"); + quantityLabel.setText(quantity.stripTrailingZeros().toPlainString() + " Shares"); avgPriceLabel.setText("Avg: $" + String.format("%,.2f", purchasePrice)); currentPriceLabel.setText("$" + String.format("%,.2f", currentPrice)); @@ -240,13 +248,15 @@ private void populateLabels(Share share) { } private BigDecimal computeGainPercent(BigDecimal gain, BigDecimal totalCost) { - if (totalCost.compareTo(BigDecimal.ZERO) <= 0) return BigDecimal.ZERO; + if (totalCost.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } return gain.multiply(BigDecimal.valueOf(100)) .divide(totalCost, 2, RoundingMode.HALF_UP); } private void applyGainStyle(boolean positive) { - String add = positive ? POSITIVE_STYLE : NEGATIVE_STYLE; + String add = positive ? POSITIVE_STYLE : NEGATIVE_STYLE; String remove = positive ? NEGATIVE_STYLE : POSITIVE_STYLE; gainLabel.getStyleClass().removeAll(remove); gainPctLabel.getStyleClass().removeAll(remove); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/GameOverApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/GameOverApp.java new file mode 100644 index 0000000..5b98342 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/GameOverApp.java @@ -0,0 +1,54 @@ +package edu.ntnu.idi.idatt2003.gruppe42.View.Apps; + +import edu.ntnu.idi.idatt2003.gruppe42.Model.App; +import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + +/** + * Full-screen modal shown when the player runs out of lives. + */ +public class GameOverApp extends Popup { + + private final Button startOverButton = new Button("Start Over"); + + public GameOverApp() { + super(500, 360, 0, 0, App.GAMEOVER); + + getCloseButton().setDisable(true); + getCloseButton().setOpacity(0.3); + + startOverButton.getStyleClass().add("primary-button"); + startOverButton.setPrefWidth(200); + + content.setPadding(new Insets(40)); + content.setSpacing(20); + buildLayout(); + } + + private void buildLayout() { + Label title = new Label("Game Over"); + title.getStyleClass().add("game-over-title"); + + Label subtitle = new Label( + "You've run out of hearts.\nYour trading career has come to an end." + ); + subtitle.getStyleClass().add("game-over-subtitle"); + subtitle.setWrapText(true); + subtitle.setAlignment(Pos.CENTER); + + VBox inner = new VBox(20, title, subtitle, startOverButton); + inner.setAlignment(Pos.CENTER); + + content.setAlignment(Pos.CENTER); + content.getChildren().setAll(inner); + } + + /** Wired by the controller to return to the start screen. */ + public Button getStartOverButton() { + return startOverButton; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/StockApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/StockApp.java index 18f8ea3..69df799 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/StockApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/StockApp.java @@ -6,7 +6,6 @@ import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; import edu.ntnu.idi.idatt2003.gruppe42.View.Views.Components.Receipt; import edu.ntnu.idi.idatt2003.gruppe42.View.Views.Components.StockGraph; -import java.math.BigDecimal; import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -63,7 +62,7 @@ public StockApp(final Player player) { this.confirmButton.getStyleClass().add("primary-button"); this.confirmButton.setPrefHeight(35); - this.receipt = new Receipt(null, BigDecimal.ZERO, player); + this.receipt = new Receipt(player); showSearchPage(); } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WarningApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WarningApp.java new file mode 100644 index 0000000..fe3a249 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WarningApp.java @@ -0,0 +1,132 @@ +package edu.ntnu.idi.idatt2003.gruppe42.View.Apps; + +import edu.ntnu.idi.idatt2003.gruppe42.Model.App; +import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +/** + * Compact, reusable warning popup used across the entire game. + * + *

    Call {@link #configureSingle} for a single dismiss button, or + * {@link #configureDouble} for a two-button choice. Wire button actions after + * configuring, then show the root and overlay via the desktop controller.

    + * + *

    The close button is always re-wired by the configure methods so it + * mirrors the appropriate dismissal action.

    + */ +public final class WarningApp extends Popup { + + private final Label iconLabel = new Label(); + private final Label titleLabel = new Label(); + private final Label messageLabel = new Label(); + private final Button primaryButton = new Button(); + private final Button secondaryButton = new Button(); + + private final HBox buttons; + + public WarningApp() { + super(370, 230, 0, 0, App.WARNING); + + iconLabel.setStyle("-fx-font-size: 30px;"); + iconLabel.setAlignment(Pos.CENTER); + + titleLabel.getStyleClass().add("warning-title"); + titleLabel.setWrapText(true); + titleLabel.setAlignment(Pos.CENTER); + + messageLabel.getStyleClass().add("warning-message"); + messageLabel.setWrapText(true); + messageLabel.setAlignment(Pos.CENTER); + + primaryButton.getStyleClass().add("primary-button"); + primaryButton.setPrefWidth(150); + + secondaryButton.getStyleClass().add("back-button"); + secondaryButton.setPrefWidth(130); + + buttons = new HBox(12, secondaryButton, primaryButton); + buttons.setAlignment(Pos.CENTER); + + VBox inner = new VBox(14, iconLabel, titleLabel, messageLabel, buttons); + inner.setAlignment(Pos.CENTER); + + content.setPadding(new Insets(24, 28, 24, 28)); + content.setAlignment(Pos.CENTER); + content.getChildren().setAll(inner); + } + + /** + * Configures for a single-button (dismiss-only) scenario. + * The secondary button is hidden. The close button mirrors the primary. + * + * @param icon emoji icon shown at the top + * @param title bold heading inside the card + * @param message body text below the heading + * @param primaryLabel label on the single action button + */ + public void configureSingle( + final String icon, + final String title, + final String message, + final String primaryLabel + ) { + iconLabel.setText(icon); + titleLabel.setText(title); + messageLabel.setText(message); + primaryButton.setText(primaryLabel); + + secondaryButton.setVisible(false); + secondaryButton.setManaged(false); + + // Clear old handlers so callers can set fresh ones after configure + primaryButton.setOnAction(null); + secondaryButton.setOnAction(null); + getCloseButton().setOnAction(event -> primaryButton.fire()); + } + + /** + * Configures for a two-button (choice) scenario. + * The secondary button is the "safe" cancel option; the close button mirrors it. + * + * @param icon emoji icon shown at the top + * @param title bold heading inside the card + * @param message body text below the heading + * @param secondaryLabel label on the left / cancel button + * @param primaryLabel label on the right / action button + */ + public void configureDouble( + final String icon, + final String title, + final String message, + final String secondaryLabel, + final String primaryLabel + ) { + iconLabel.setText(icon); + titleLabel.setText(title); + messageLabel.setText(message); + primaryButton.setText(primaryLabel); + secondaryButton.setText(secondaryLabel); + + secondaryButton.setVisible(true); + secondaryButton.setManaged(true); + + primaryButton.setOnAction(null); + secondaryButton.setOnAction(null); + getCloseButton().setOnAction(event -> secondaryButton.fire()); + } + + /** The right-side action button (always visible). Wire with {@code setOnAction}. */ + public Button getPrimaryButton() { + return primaryButton; + } + + /** The left-side cancel button (visible only in double mode). */ + public Button getSecondaryButton() { + return secondaryButton; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WeekendRapportApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WeekendRapportApp.java new file mode 100644 index 0000000..d304766 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WeekendRapportApp.java @@ -0,0 +1,251 @@ +package edu.ntnu.idi.idatt2003.gruppe42.View.Apps; + +import edu.ntnu.idi.idatt2003.gruppe42.Model.App; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Transaction; +import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; +import edu.ntnu.idi.idatt2003.gruppe42.View.Views.Components.Receipt; +import java.math.BigDecimal; +import java.util.List; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +/** + * End-of-week financial report shown every Saturday. + * + *

    Designed to read like a real weekly statement:

    + *
      + *
    1. Hero: the week's net income, large and color-coded
    2. + *
    3. Verdict: a one-line mood label (e.g. "Strong week")
    4. + *
    5. Receipts: each transaction rendered with the shared {@link Receipt}
    6. + *
    7. Footer: cash → rent → balance, with the final balance large and colored
    8. + *
    + * + *

    Dismissed only via the "Continue to Sunday" button or by clicking the + * dim overlay (wired by {@code DesktopViewController}).

    + */ +public class WeekendRapportApp extends Popup { + + /** Threshold for "strong week" (net income covers rent.) */ + private static final BigDecimal STRONG_WEEK_THRESHOLD = new BigDecimal("50.00"); + + private final Label heroAmountLabel = new Label(); + private final Label heroCaptionLabel = new Label("Net Income This Week"); + private final Label verdictLabel = new Label(); + + private final Label cashLabel = new Label(); + private final Label rentLabel = new Label(); + private final Label balanceLabel = new Label(); + + private final VBox receiptList = new VBox(10); + + private final Button continueButton = new Button("Continue to Sunday →"); + + public WeekendRapportApp() { + super(740, 620, 0, 0, App.WEEKENDRAPPORT); + + // Close button mirrors the continue action + getCloseButton().setOnAction(e -> continueButton.fire()); + + continueButton.getStyleClass().add("primary-button"); + continueButton.setMaxWidth(Double.MAX_VALUE); + + receiptList.setAlignment(Pos.TOP_CENTER); + + content.setPadding(new Insets(28, 28, 24, 28)); + content.setSpacing(18); + buildLayout(); + } + + private void buildLayout() { + heroAmountLabel.getStyleClass().add("rapport-hero-amount"); + heroCaptionLabel.getStyleClass().add("rapport-hero-caption"); + verdictLabel.getStyleClass().add("rapport-verdict"); + + VBox hero = new VBox(2, heroAmountLabel, heroCaptionLabel); + hero.setAlignment(Pos.CENTER); + + VBox heroBlock = new VBox(10, hero, verdictLabel); + heroBlock.setAlignment(Pos.CENTER); + heroBlock.getStyleClass().add("rapport-hero-block"); + heroBlock.setPadding(new Insets(20, 16, 20, 16)); + + Label txHeader = sectionHeader("Transactions"); + + ScrollPane scroll = new ScrollPane(receiptList); + scroll.setFitToWidth(true); + scroll.getStyleClass().add("rapport-scroll"); + VBox.setVgrow(scroll, Priority.ALWAYS); + + cashLabel.getStyleClass().add("rapport-line-value"); + rentLabel.getStyleClass().addAll("rapport-line-value", "rapport-rent-value"); + + HBox cashLine = footerRow("Cash Available", cashLabel); + HBox rentLine = footerRow("Rent Due", rentLabel); + rentLine.getStyleClass().add("rapport-rent-row"); + + Region rule = new Region(); + rule.getStyleClass().add("rapport-rule"); + rule.setMinHeight(1); + rule.setMaxWidth(Double.MAX_VALUE); + + Label balanceHeader = new Label("Balance After Rent"); + balanceHeader.getStyleClass().add("rapport-balance-header"); + balanceLabel.getStyleClass().add("rapport-balance-value"); + + HBox balanceLine = new HBox(); + Region balSpacer = new Region(); + HBox.setHgrow(balSpacer, Priority.ALWAYS); + balanceLine.getChildren().addAll(balanceHeader, balSpacer, balanceLabel); + balanceLine.setAlignment(Pos.CENTER_LEFT); + balanceLine.setPadding(new Insets(2, 0, 0, 0)); + + VBox footer = new VBox(8, cashLine, rentLine, rule, balanceLine); + + content.getChildren().setAll( + heroBlock, + txHeader, scroll, + footer, + continueButton + ); + } + + private static Label sectionHeader(final String text) { + Label l = new Label(text); + l.getStyleClass().add("rapport-section-header"); + return l; + } + + private static HBox footerRow(final String label, final Label valueLabel) { + Label key = new Label(label); + key.getStyleClass().add("rapport-line-key"); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + HBox row = new HBox(key, spacer, valueLabel); + row.setAlignment(Pos.CENTER_LEFT); + return row; + } + + + /** + * Populates the rapport with the completed week's data. + * Rent has already been deducted from the player's balance before this is called. + * + * @param transactions all transactions that occurred this week + * @param netIncome net cash change from trading (sales − purchases) + * @param cashBeforeRent cash available before rent was deducted + * @param rent the rent amount that was deducted + * @param cashAfterRent cash balance after rent deduction (may be negative) + */ + public void populate( + final List transactions, + final BigDecimal netIncome, + final BigDecimal cashBeforeRent, + final BigDecimal rent, + final BigDecimal cashAfterRent + ) { + heroAmountLabel.setText(formatSigned(netIncome)); + applyMoodClass(heroAmountLabel, moodFor(netIncome)); + + String verdict = computeVerdict(netIncome, cashAfterRent); + verdictLabel.setText(verdict); + applyMoodClass(verdictLabel, moodFor(netIncome, cashAfterRent)); + + receiptList.getChildren().clear(); + if (transactions.isEmpty()) { + Label empty = new Label("No transactions this week."); + empty.getStyleClass().add("rapport-line-key"); + empty.setPadding(new Insets(20)); + receiptList.getChildren().add(empty); + } else { + for (Transaction tx : transactions) { + receiptList.getChildren().add(Receipt.forTransaction(tx)); + } + } + + cashLabel.setText(formatUsd(cashBeforeRent)); + rentLabel.setText("−" + formatUsd(rent)); + + balanceLabel.setText(formatBalance(cashAfterRent)); + balanceLabel.getStyleClass().removeAll("mood-positive", "mood-negative"); + balanceLabel.getStyleClass().add( + cashAfterRent.signum() < 0 ? "mood-negative" : "mood-positive" + ); + } + + public Button getContinueButton() { + return continueButton; + } + + /** + * One-line mood label derived from net income and final balance. + * Reads as a quick gut-check for how the week went. + */ + private static String computeVerdict(final BigDecimal net, final BigDecimal balance) { + if (balance.signum() < 0) { + return "Week ended in debt"; + } + if (net.compareTo(STRONG_WEEK_THRESHOLD) >= 0) { + return "Strong week"; + } + if (net.signum() > 0) { + return "Made some money"; + } + if (net.signum() == 0) { + return "Breaking even"; + } + return "Rough week"; + } + + /** Mood derived from net income alone (used for the hero amount). */ + private static String moodFor(final BigDecimal net) { + if (net.signum() > 0) { + return "mood-positive"; + } + if (net.signum() < 0) { + return "mood-negative"; + } + return "mood-neutral"; + } + + /** Mood derived from net income AND final balance (used for the verdict). */ + private static String moodFor(final BigDecimal net, final BigDecimal balance) { + if (balance.signum() < 0) { + return "mood-negative"; + } + if (net.compareTo(STRONG_WEEK_THRESHOLD) >= 0) { + return "mood-positive"; + } + if (net.signum() >= 0) { + return "mood-neutral"; + } + return "mood-negative"; + } + + private static void applyMoodClass(final Label label, final String moodClass) { + label.getStyleClass().removeAll("mood-positive", "mood-negative", "mood-neutral"); + label.getStyleClass().add(moodClass); + } + + private static String formatUsd(final BigDecimal amount) { + return "$" + String.format("%,.2f", amount.abs()); + } + + private static String formatSigned(final BigDecimal amount) { + String sign = amount.signum() >= 0 ? "+" : "−"; + return sign + "$" + String.format("%,.2f", amount.abs()); + } + + static String formatBalance(final BigDecimal amount) { + if (amount.signum() < 0) { + return "$" + String.format("%,.2f", amount.negate()) + " in debt"; + } + return "$" + String.format("%,.2f", amount); + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java index ddc977a..78e2f1a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Popup.java @@ -9,6 +9,7 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.shape.Circle; @@ -31,9 +32,9 @@ public abstract class Popup { protected final VBox content; protected Popup(int width, int height, int x, int y, App type) { - this.width = width; + this.width = width; this.height = height; - this.type = type; + this.type = type; content = new VBox(); content.getStyleClass().add("popup-content"); @@ -91,8 +92,7 @@ private HBox buildHeader(Button closeButton, String title) { } private String titleString() { - String s = type.toString(); - return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + return type.getDisplayName(); } private Rectangle roundedClip() { @@ -108,10 +108,39 @@ private String resource(String path) { return getClass().getResource(path).toExternalForm(); } - public App getType() { return type; } - public BorderPane getRoot() { return root; } - public HBox getHeader() { return header; } - public Button getCloseButton() { return closeButton; } - public int getWidth() { return width; } - public int getHeight() { return height; } + /** + * Binds this popup's layoutX/Y so it is always centered inside its parent. + */ + public void centerInParent() { + root.parentProperty().addListener((obs, oldParent, parent) -> { + if (parent instanceof Pane pane) { + root.layoutXProperty().bind(pane.widthProperty().subtract(width).divide(2)); + root.layoutYProperty().bind(pane.heightProperty().subtract(height).divide(2)); + } + }); + } + + public App getType() { + return type; + } + + public BorderPane getRoot() { + return root; + } + + public HBox getHeader() { + return header; + } + + public Button getCloseButton() { + return closeButton; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } } \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/Receipt.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/Receipt.java index df0a8bc..930b618 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/Receipt.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/Components/Receipt.java @@ -1,110 +1,203 @@ package edu.ntnu.idi.idatt2003.gruppe42.View.Views.Components; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; import edu.ntnu.idi.idatt2003.gruppe42.Model.Share; import edu.ntnu.idi.idatt2003.gruppe42.Model.Stock; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Purchase; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Transaction.Transaction; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Optional; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; +import javafx.scene.layout.Region; import javafx.scene.layout.VBox; /** - * Component for displaying a transaction receipt. + * The single shared receipt component used everywhere in the game. */ public final class Receipt extends VBox { - private final edu.ntnu.idi.idatt2003.gruppe42.Model.Player player; + + private static final BigDecimal COMMISSION_RATE = new BigDecimal("0.01"); + private static final BigDecimal TAX_RATE = new BigDecimal("0.22"); + private final Player player; + + /** Creates an empty live-preview receipt. Call {@link #update} to populate. */ + public Receipt(final Player player) { + this.player = player; + getStyleClass().add("receipt-container"); + setAlignment(Pos.TOP_CENTER); + setSpacing(8); + setMaxWidth(320); + renderEmpty(); + } /** - * Constructs a new Receipt. + * Creates a populated receipt for a completed transaction. + * Used by the Weekend Rapport (read-only). * - * @param stock the stock - * @param quantity the quantity - * @param player the player + * @param tx the completed transaction to render + * @return a new populated receipt card */ - public Receipt(final Stock stock, final BigDecimal quantity, final edu.ntnu.idi.idatt2003.gruppe42.Model.Player player) { - this.player = player; - this.getStyleClass().add("receipt-container"); - this.setAlignment(Pos.TOP_CENTER); - this.setSpacing(10); - this.setMaxWidth(300); - update(stock, quantity); + public static Receipt forTransaction(final Transaction tx) { + Receipt receipt = new Receipt(null); + boolean isBuy = tx instanceof Purchase; + Share share = tx.getShare(); + Stock stock = share.getStock(); + + BigDecimal gross = tx.getCalculator().calculateGross(); + BigDecimal commission = tx.getCalculator().calculateCommission(); + BigDecimal tax = tx.getCalculator().calculateTax(); + BigDecimal total = tx.getCalculator().calculateTotal(); + + receipt.renderPopulated( + isBuy, stock, share.getQuantity(), share.getPurchasePrice(), + gross, commission, tax, total, + "TAX:" + ); + return receipt; } /** - * Updates the receipt with new transaction data. - * - * @param stock the stock - * @param quantity the quantity + * Re-renders as a live preview for the given stock and signed quantity. + * A positive quantity is a BUY; negative is a SELL. Zero or null clears + * the receipt back to its empty placeholder. */ public void update(final Stock stock, final BigDecimal quantity) { - this.getChildren().clear(); - - Label header = new Label("TRANSACTION SLIP"); - header.getStyleClass().add("receipt-header"); - - Label line1 = new Label("=========================="); - line1.getStyleClass().add("receipt-text"); - Label line2 = new Label("--------------------------"); - line2.getStyleClass().add("receipt-text"); - Label line3 = new Label("=========================="); - line3.getStyleClass().add("receipt-text"); - - if (stock == null) { - this.getChildren().addAll(header, line1, new Label("Select a stock"), line3); + if (stock == null || quantity == null || quantity.signum() == 0) { + renderEmpty(); return; } + boolean isBuy = quantity.signum() > 0; + BigDecimal absoluteQuantity = quantity.abs(); + BigDecimal price = stock.getSalesPrice(); + BigDecimal gross = price.multiply(absoluteQuantity); + BigDecimal commission = gross.multiply(COMMISSION_RATE).setScale(2, RoundingMode.HALF_UP); + BigDecimal tax = isBuy ? BigDecimal.ZERO : estimateSaleTax(stock, absoluteQuantity); + BigDecimal total = isBuy + ? gross.add(commission) + : gross.subtract(commission).subtract(tax); - VBox details = new VBox(5); - details.setAlignment(Pos.TOP_LEFT); + renderPopulated( + isBuy, stock, absoluteQuantity, price, + gross, commission, tax, total, + "EST. TAX:" + ); + } - String type = quantity.compareTo(BigDecimal.ZERO) >= 0 ? "BUY" : "SELL"; - BigDecimal absQty = quantity.abs(); - - details.getChildren().add(createReceiptLabel("STOCK:", stock.getCompany())); - details.getChildren().add(createReceiptLabel("SYMBOL:", stock.getSymbol())); - details.getChildren().add(createReceiptLabel("TYPE:", type)); - details.getChildren().add(createReceiptLabel("QTY:", absQty.toString())); - details.getChildren().add(createReceiptLabel("PRICE:", "$" + String.format("%,.2f", stock.getSalesPrice()))); - - BigDecimal gross = stock.getSalesPrice().multiply(absQty); - BigDecimal commission = gross.multiply(new BigDecimal("0.01")).setScale(2, RoundingMode.HALF_UP); - - BigDecimal tax = BigDecimal.ZERO; - if (type.equals("SELL") && player != null) { - Optional match = player.getPortfolio().getShares().stream() - .filter(s -> s.getStock().getSymbol().equals(stock.getSymbol())) - .findFirst(); - - if (match.isPresent()) { - BigDecimal purchasePrice = match.get().getPurchasePrice(); - BigDecimal profit = stock.getSalesPrice().subtract(purchasePrice).multiply(absQty); - if (profit.compareTo(BigDecimal.ZERO) > 0) { - tax = profit.multiply(new BigDecimal("0.22")).setScale(2, RoundingMode.HALF_UP); - } - } + /** Computes the estimated capital-gains tax for a hypothetical sale. */ + private BigDecimal estimateSaleTax(final Stock stock, final BigDecimal qty) { + if (player == null) { + return BigDecimal.ZERO; + } + Optional match = player.getPortfolio().getShares().stream() + .filter(s -> s.getStock().getSymbol().equals(stock.getSymbol())) + .findFirst(); + if (match.isEmpty()) { + return BigDecimal.ZERO; } + BigDecimal profit = stock.getSalesPrice() + .subtract(match.get().getPurchasePrice()) + .multiply(qty); + if (profit.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return profit.multiply(TAX_RATE).setScale(2, RoundingMode.HALF_UP); + } - BigDecimal total = type.equals("BUY") ? gross.add(commission) : gross.subtract(commission).subtract(tax); + /** Empty placeholder receipt shown before the user picks a stock. */ + private void renderEmpty() { + getChildren().clear(); + getChildren().addAll( + headerLabel("RECEIPT"), + ruleStrong(), + receiptLine("Select a stock"), + ruleStrong() + ); + } - VBox totals = new VBox(5); - totals.getStyleClass().add("receipt-total"); - totals.setPadding(new Insets(10, 0, 0, 0)); + /** + * Renders the canonical receipt layout with the given values. + * Used by both live-preview and historical-transaction modes so the + * visual output is identical. + */ + private void renderPopulated( + final boolean isBuy, + final Stock stock, + final BigDecimal qty, + final BigDecimal price, + final BigDecimal gross, + final BigDecimal commission, + final BigDecimal tax, + final BigDecimal total, + final String taxLabel + ) { + getChildren().clear(); - totals.getChildren().add(createReceiptLabel("GROSS:", "$" + String.format("%,.2f", gross))); - totals.getChildren().add(createReceiptLabel("COMMISSION:", "$" + String.format("%,.2f", commission))); - if (type.equals("SELL")) { - totals.getChildren().add(createReceiptLabel("EST. TAX:", "$" + String.format("%,.2f", tax))); + VBox details = new VBox(4); + details.setAlignment(Pos.TOP_LEFT); + details.getChildren().addAll( + keyValueLine("STOCK:", stock.getCompany()), + keyValueLine("SYMBOL:", stock.getSymbol()), + keyValueLine("TYPE:", isBuy ? "BUY" : "SELL"), + keyValueLine("QTY:", qty.stripTrailingZeros().toPlainString()), + keyValueLine("PRICE:", formatUsd(price)) + ); + + VBox totals = new VBox(4); + totals.getStyleClass().add("receipt-total"); + totals.setPadding(new Insets(8, 0, 0, 0)); + totals.getChildren().addAll( + keyValueLine("GROSS:", formatUsd(gross)), + keyValueLine("COMMISSION:", formatUsd(commission)) + ); + if (!isBuy) { + totals.getChildren().add(keyValueLine(taxLabel, formatUsd(tax))); } - totals.getChildren().add(createReceiptLabel("TOTAL:", "$" + String.format("%,.2f", total))); + totals.getChildren().add(keyValueLine("TOTAL:", formatUsd(total))); + + getChildren().addAll( + headerLabel("RECEIPT"), + ruleStrong(), + details, + ruleSoft(), + totals, + ruleStrong() + ); + } + + private static Label headerLabel(final String text) { + Label l = new Label(text); + l.getStyleClass().add("receipt-header"); + return l; + } - this.getChildren().addAll(header, line1, details, line2, totals, line3); + private static Region ruleStrong() { + Region r = new Region(); + r.getStyleClass().add("receipt-divider-strong"); + return r; } - private Label createReceiptLabel(String label, String value) { - Label l = new Label(String.format("%-12s %s", label, value)); + private static Region ruleSoft() { + Region r = new Region(); + r.getStyleClass().add("receipt-divider"); + return r; + } + + private static Label receiptLine(final String text) { + Label l = new Label(text); + l.getStyleClass().add("receipt-text"); + return l; + } + + private static Label keyValueLine(final String key, final String value) { + Label l = new Label(String.format("%-12s %s", key, value)); l.getStyleClass().add("receipt-text"); return l; } -} \ No newline at end of file + + private static String formatUsd(final BigDecimal amount) { + return "$" + String.format("%,.2f", amount); + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java index 44bc856..9263982 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java @@ -3,59 +3,199 @@ import edu.ntnu.idi.idatt2003.gruppe42.Controller.TimeAndWeatherController; import edu.ntnu.idi.idatt2003.gruppe42.Controller.ViewControllers.DesktopViewController; import edu.ntnu.idi.idatt2003.gruppe42.Model.App; +import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.effect.GaussianBlur; import javafx.scene.layout.BorderPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.RowConstraints; import javafx.scene.layout.StackPane; /** - * Desktop view showing a 10x6 app grid and a bottom status bar. + * Desktop view: 10x6 app-icon grid, bottom status bar, and a layered + * modal overlay system. */ public final class DesktopView { + /** + * Z-stack of modal levels, ordered from lowest to highest. Each value + * is its own named token; no numeric z-index lives anywhere else. + */ + public enum ModalLayer { RAPPORT, WARNING, GAME_OVER } + private static final int COLS = 10; private static final int ROWS = 6; + private static final int HEART_COUNT = 3; + private static final double BACKDROP_BLUR = 8.0; private final DesktopViewController controller; private final BorderPane root; + private final Pane rapportOverlay = createOverlayPane(); + private final Pane warningOverlay = createOverlayPane(); + private final Pane gameOverOverlay = createOverlayPane(); + + private Node gridArea; + private HBox bottomBar; + + private final List popupRoots = new ArrayList<>(); + + private Node rapportRoot; + private Node warningRoot; + private Node gameOverRoot; + + private final EnumSet activeLayers = EnumSet.noneOf(ModalLayer.class); + private Button nextWeekButton; - public DesktopView(DesktopViewController controller) { + public DesktopView(final DesktopViewController controller) { this.controller = controller; this.root = new BorderPane(); } - /** - * Builds and returns the root node, constructing children on first call. - */ + /** Builds and returns the root node (lazy, built only on first call). */ public BorderPane getRoot() { if (root.getCenter() == null) { - root.setCenter(createGrid()); + gridArea = createGrid(); + bottomBar = createBottomBar(); + root.setCenter(gridArea); + root.setBottom(bottomBar); + root.getStylesheets().addAll( + resource("/css/global.css"), + resource("/css/desktop.css"), + resource("/css/apps.css") + ); + root.getStyleClass().add("desktop-root"); } - if (root.getBottom() == null) { - root.setBottom(createBottomBar()); + return root; + } + + // Layer registration: the controller adds nodes to the scene graph in + // the exact z-order required for the layering rules to work. + + /** Registers a non-modal popup (Stock, Bank, etc.) for blur tracking. */ + public void registerPopup(final Node popupRoot) { + popupRoots.add(popupRoot); + } + + /** + * Records the modal root for the given layer so the layer manager can + * blur it when a higher modal is active. Does NOT add it to the scene + * graph; the caller is responsible for that. + */ + public void registerModal(final ModalLayer layer, final Node modalRoot) { + switch (layer) { + case RAPPORT -> rapportRoot = modalRoot; + case WARNING -> warningRoot = modalRoot; + case GAME_OVER -> gameOverRoot = modalRoot; } + } - root.getStylesheets().addAll( - resource("/css/global.css"), - resource("/css/desktop.css"), - resource("/css/apps.css") - ); - root.getStyleClass().add("desktop-root"); + /** Returns the overlay {@code Pane} for the given layer (for click wiring). */ + public Pane getOverlay(final ModalLayer layer) { + return switch (layer) { + case RAPPORT -> rapportOverlay; + case WARNING -> warningOverlay; + case GAME_OVER -> gameOverOverlay; + }; + } - return root; + /** Marks the given layer as active and re-applies blur / disable state. */ + public void enterLayer(final ModalLayer layer) { + activeLayers.add(layer); + recompute(); + } + + /** Marks the given layer as inactive and re-applies blur / disable state. */ + public void exitLayer(final ModalLayer layer) { + activeLayers.remove(layer); + recompute(); + } + + // Single source of truth: derive overlay visibility, blur, and disable + // state from the active-layer set. + private void recompute() { + ModalLayer top = topmostActive(); + + rapportOverlay.setVisible(top == ModalLayer.RAPPORT); + warningOverlay.setVisible(top == ModalLayer.WARNING); + gameOverOverlay.setVisible(top == ModalLayer.GAME_OVER); + + boolean anyModal = top != null; + setLocked(gridArea, anyModal); + setLocked(bottomBar, anyModal); + for (Node p : popupRoots) { + setLocked(p, anyModal); + } + + if (rapportRoot != null) { + boolean rapportLocked = activeLayers.contains(ModalLayer.RAPPORT) + && top != ModalLayer.RAPPORT; + setLocked(rapportRoot, rapportLocked); + } + if (warningRoot != null) { + boolean warningLocked = activeLayers.contains(ModalLayer.WARNING) + && top != ModalLayer.WARNING; + setLocked(warningRoot, warningLocked); + } + if (gameOverRoot != null) { + // Game Over is always topmost when active, never locked. + setLocked(gameOverRoot, false); + } } + private ModalLayer topmostActive() { + if (activeLayers.contains(ModalLayer.GAME_OVER)) { + return ModalLayer.GAME_OVER; + } + if (activeLayers.contains(ModalLayer.WARNING)) { + return ModalLayer.WARNING; + } + if (activeLayers.contains(ModalLayer.RAPPORT)) { + return ModalLayer.RAPPORT; + } + return null; + } + + /** + * Applies (or removes) the blur effect AND the disable flag together, + * so blur and pointer-events are never out of sync. + */ + private static void setLocked(final Node node, final boolean locked) { + if (node == null) { + return; + } + node.setEffect(locked ? new GaussianBlur(BACKDROP_BLUR) : null); + node.setDisable(locked); + } + + private static Pane createOverlayPane() { + Pane pane = new Pane(); + pane.setManaged(false); + pane.setLayoutX(0); + pane.setLayoutY(0); + // Big enough to cover any viewport; the dim background blocks clicks. + pane.setPrefSize(8000, 8000); + pane.getStyleClass().add("modal-overlay"); + pane.setVisible(false); + return pane; + } + + // Grid + private GridPane createGrid() { GridPane grid = new GridPane(); grid.setAlignment(Pos.CENTER); @@ -78,43 +218,86 @@ private GridPane createGrid() { for (int col = 0; col < COLS; col++) { StackPane cell = new StackPane(); controller.configureCellAsDropTarget(cell); - if (row == 0 && col < apps.length) { + if (row == 0 && col < apps.length && isDesktopApp(apps[col])) { cell.getChildren().add(controller.createAppButton(apps[col])); } grid.add(cell, col, row); } } - return grid; } + /** Returns true for apps that should appear as icons on the desktop grid. */ + private static boolean isDesktopApp(final App app) { + return app != App.WEEKENDRAPPORT + && app != App.WARNING + && app != App.GAMEOVER; + } + + // Bottom bar + private HBox createBottomBar() { Button settingsButton = new Button("⚙"); settingsButton.getStyleClass().add("desktop-settings-button"); settingsButton.setOnAction(e -> controller.handleSettings()); - Label playerLabel = new Label("User: " + controller.getPlayer().getName()); + Player player = controller.getPlayer(); + Label playerLabel = new Label("User: " + player.getName()); playerLabel.getStyleClass().add("desktop-label-bold"); playerLabel.setMinWidth(Region.USE_PREF_SIZE); + HBox heartsBox = buildHeartsBox(player); + nextWeekButton = new Button("Advance to next week"); Region spacerLeft = new Region(); - HBox.setHgrow(spacerLeft, Priority.ALWAYS); - Region spacerRight = new Region(); + HBox.setHgrow(spacerLeft, Priority.ALWAYS); HBox.setHgrow(spacerRight, Priority.ALWAYS); - HBox bottomBar = new HBox(15, - settingsButton, playerLabel, spacerLeft, nextWeekButton, spacerRight, createStatusBox() + HBox bar = new HBox(15, + settingsButton, playerLabel, heartsBox, + spacerLeft, nextWeekButton, spacerRight, + createStatusBox() ); - bottomBar.setAlignment(Pos.CENTER_LEFT); - bottomBar.setPadding(new Insets(5, 15, 5, 15)); - bottomBar.getStyleClass().add("desktop-bottom-bar"); + bar.setAlignment(Pos.CENTER_LEFT); + bar.setPadding(new Insets(5, 15, 5, 15)); + bar.getStyleClass().add("desktop-bottom-bar"); + return bar; + } + + /** + * Builds the hearts display as an HBox of three filled-heart labels. + */ + private HBox buildHeartsBox(final Player player) { + HBox box = new HBox(4); + box.setAlignment(Pos.CENTER_LEFT); + box.getStyleClass().add("hearts-display"); + + Label[] hearts = new Label[HEART_COUNT]; + for (int i = 0; i < HEART_COUNT; i++) { + hearts[i] = new Label("♥"); + hearts[i].getStyleClass().add("heart-icon"); + box.getChildren().add(hearts[i]); + } + + Runnable refresh = () -> { + int lives = player.getLives(); + for (int i = 0; i < HEART_COUNT; i++) { + boolean active = i < lives; + hearts[i].getStyleClass().removeAll("heart-active", "heart-empty"); + hearts[i].getStyleClass().add(active ? "heart-active" : "heart-empty"); + } + }; + player.getLivesProperty().addListener(( + obs, ov, nv) -> Platform.runLater(refresh)); + refresh.run(); - return bottomBar; + return box; } + // Status box (clock / weather) + private HBox createStatusBox() { TimeAndWeatherController twc = controller.getTimeAndWeatherController(); @@ -130,16 +313,16 @@ private HBox createStatusBox() { week.setText(twc.getWeekString()); clock.setText(twc.getTimeString()); - twc.hourProperty().addListener((obs, o, n) + twc.hourProperty().addListener((o, ov, nv) -> Platform.runLater(() -> clock.setText(twc.getTimeString()))); - twc.dayIndexProperty().addListener((obs, o, n) + twc.dayIndexProperty().addListener((o, ov, nv) -> Platform.runLater(() -> day.setText(twc.getDayOfWeekString()))); - twc.weekIndexProperty().addListener((obs, o, n) + twc.weekIndexProperty().addListener((o, ov, nv) -> Platform.runLater(() -> week.setText(twc.getWeekString()))); - twc.weatherProperty().addListener((obs, o, n) - -> Platform.runLater(() -> weather.setText(n))); - twc.temperatureProperty().addListener((obs, o, n) - -> Platform.runLater(() -> temp.setText(n + "°C"))); + twc.weatherProperty().addListener((o, ov, nv) + -> Platform.runLater(() -> weather.setText(nv))); + twc.temperatureProperty().addListener((o, ov, nv) + -> Platform.runLater(() -> temp.setText(nv + "°C"))); HBox box = new HBox(10, weather, temp, day, week, clock); box.setAlignment(Pos.CENTER_RIGHT); @@ -147,17 +330,19 @@ private HBox createStatusBox() { return box; } - private Label styledLabel(String styleClass) { - Label label = new Label(); - label.getStyleClass().add(styleClass); - return label; + // Helpers + + private Label styledLabel(final String styleClass) { + Label l = new Label(); + l.getStyleClass().add(styleClass); + return l; } - private String resource(String path) { + private String resource(final String path) { return getClass().getResource(path).toExternalForm(); } public Button getNextWeekButton() { return nextWeekButton; } -} \ No newline at end of file +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/StartView.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/StartView.java index f2b25a8..e1c359a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/StartView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/StartView.java @@ -24,6 +24,9 @@ public class StartView { public StackPane getRoot() { + if (!root.getChildren().isEmpty()) { + return root; // already built + } // User input content VBox loginContainer = new VBox(20); @@ -81,7 +84,10 @@ public StackPane getRoot() { root.getChildren().add(loginContainer); root.getStyleClass().add("login-root"); - root.getStylesheets().add(getClass().getResource("/css/login.css").toExternalForm()); + root.getStylesheets().addAll( + getClass().getResource("/css/global.css").toExternalForm(), + getClass().getResource("/css/login.css").toExternalForm() + ); return root; } diff --git a/src/main/resources/css/apps.css b/src/main/resources/css/apps.css index 6626722..a165306 100644 --- a/src/main/resources/css/apps.css +++ b/src/main/resources/css/apps.css @@ -1,6 +1,27 @@ -/* App Specific Styles */ -/* Stock App */ +.primary-button { + -fx-background-color: -primary; + -fx-text-fill: white; + -fx-font-weight: bold; + -fx-background-radius: 10; + -fx-padding: 8 20 8 20; + -fx-cursor: hand; +} + +.primary-button:hover { + -fx-background-color: -primary-hover; +} + +.back-button { + -fx-background-color: transparent; + -fx-text-fill: -primary; + -fx-cursor: hand; +} + +.trade-button-weekend { + -fx-cursor: default; +} + .stock-list-item { -fx-border-color: #f0f0f0; -fx-border-width: 0 0 1 0; @@ -15,7 +36,7 @@ .stock-company { -fx-font-size: 12px; - -fx-text-fill: gray; + -fx-text-fill: -text-muted; } .stock-price { @@ -28,27 +49,27 @@ } .stock-price-positive { - -fx-text-fill: green; + -fx-text-fill: -success; } .stock-price-negative { - -fx-text-fill: red; + -fx-text-fill: -danger; } .stock-title { -fx-font-weight: bold; -fx-font-size: 20px; - -fx-text-fill: black; + -fx-text-fill: -text-strong; } .stock-symbol-large { - -fx-text-fill: gray; + -fx-text-fill: -text-muted; } .search-field { - -fx-background-color: white; + -fx-background-color: -surface; -fx-background-radius: 15; - -fx-border-color: #cccccc; + -fx-border-color: -popup-border; -fx-border-radius: 15; -fx-padding: 5 15 5 15; } @@ -60,8 +81,8 @@ .stock-quantity-spinner { -fx-background-radius: 5; - -fx-background-color: white; - -fx-border-color: #cccccc; + -fx-background-color: -surface; + -fx-border-color: -popup-border; -fx-border-radius: 5; } @@ -75,25 +96,39 @@ } .trade-panel { - -fx-background-color: #f8f8f8; + -fx-background-color: -surface-soft; -fx-background-radius: 10; } -.primary-button { - -fx-background-color: #007AFF; - -fx-text-fill: white; - -fx-font-weight: bold; - -fx-background-radius: 10; - -fx-padding: 8 20 8 20; +.stock-graph-container { + -fx-padding: 10; + -fx-background-color: -surface; } -.back-button { +.stock-area-chart { + -fx-create-symbols: false; + -fx-horizontal-grid-lines-visible: false; + -fx-vertical-grid-lines-visible: false; +} + +.stock-area-chart .chart-series-area-line { + -fx-stroke: -primary; + -fx-stroke-width: 2px; +} + +.stock-area-chart .chart-series-area-fill { + -fx-fill: rgba(0, 122, 255, 0.15); +} + +.stock-area-chart .chart-plot-background { -fx-background-color: transparent; - -fx-text-fill: #007AFF; - -fx-cursor: hand; } -/* Bank App */ +.stock-area-chart .axis { + -fx-tick-label-fill: -text-muted; + -fx-tick-length: 5; +} + .bank-summary-card { -fx-background-color: linear-gradient(to bottom right, #004e92, #000428); -fx-background-radius: 15; @@ -132,7 +167,7 @@ } .portfolio-list, .stock-list { - -fx-background-color: white; + -fx-background-color: -surface; -fx-background-radius: 10; -fx-border-color: #e0e0e0; -fx-border-radius: 10; @@ -152,99 +187,219 @@ -fx-cursor: hand; } -.bank-share-quantity { +.bank-share-quantity, .bank-share-value, .bank-share-gain { -fx-font-weight: bold; -fx-font-size: 14px; } -.bank-share-value { +.bank-share-avg-price, .bank-share-current-price { + -fx-font-size: 12px; + -fx-text-fill: -text-muted; +} + +.receipt-container { + -fx-background-color: -surface; + -fx-border-color: -popup-border; + -fx-border-style: dashed; + -fx-padding: 20; + -fx-background-radius: 4; + -fx-border-radius: 4; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.08), 4, 0, 0, 1); +} + +.receipt-text { + -fx-font-family: "Courier New", monospace; + -fx-font-size: 12px; + -fx-text-fill: -text-base; +} + +.receipt-divider { + -fx-pref-height: 1; + -fx-min-height: 1; + -fx-max-height: 1; + -fx-background-color: -text-muted; + -fx-opacity: 0.35; +} + +.receipt-divider-strong { + -fx-pref-height: 1; + -fx-min-height: 1; + -fx-max-height: 1; + -fx-background-color: -text-base; + -fx-opacity: 0.7; +} + +.receipt-header { + -fx-font-family: "Courier New", monospace; -fx-font-weight: bold; -fx-font-size: 14px; + -fx-alignment: center; + -fx-text-fill: -text-strong; } -.bank-share-avg-price, .bank-share-current-price { +.receipt-total { + -fx-font-weight: bold; + -fx-border-color: -text-base transparent transparent transparent; + -fx-border-width: 1 0 0 0; +} + +.receipt-total .label { + -fx-font-family: "Courier New", monospace; + -fx-font-weight: bold; -fx-font-size: 12px; - -fx-text-fill: #666666; } -.bank-share-gain { +.rapport-section-header { -fx-font-weight: bold; + -fx-font-size: 11px; + -fx-text-fill: -text-faint; + -fx-text-transform: uppercase; + -fx-letter-spacing: 1; +} + +.rapport-hero-block { + -fx-background-color: -surface-soft; + -fx-background-radius: 12; + -fx-border-color: -surface-line; + -fx-border-radius: 12; + -fx-border-width: 1; +} + +.rapport-hero-amount { + -fx-font-size: 44px; + -fx-font-weight: bold; + -fx-text-fill: -text-strong; +} + +.rapport-hero-caption { + -fx-font-size: 12px; + -fx-text-fill: -text-muted; + -fx-text-transform: uppercase; + -fx-letter-spacing: 1; +} + +.rapport-verdict { -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-padding: 4 14 4 14; + -fx-background-radius: 12; } +.mood-positive { + -fx-text-fill: -success; +} -/* Stock Graph */ -.stock-graph-container { - -fx-padding: 10; - -fx-background-color: white; +.mood-positive.rapport-verdict { + -fx-background-color: rgba(52, 199, 89, 0.15); + -fx-text-fill: derive(-success, -20%); } -.stock-area-chart { - -fx-create-symbols: false; - -fx-horizontal-grid-lines-visible: false; - -fx-vertical-grid-lines-visible: false; +.mood-negative { + -fx-text-fill: -danger; } -/* The actual line */ -.stock-area-chart .chart-series-area-line { - -fx-stroke: #007AFF; - -fx-stroke-width: 2px; +.mood-negative.rapport-verdict { + -fx-background-color: -danger-soft; + -fx-text-fill: derive(-danger, -20%); } -/* The filled area */ -.stock-area-chart .chart-series-area-fill { - -fx-fill: rgba(0, 122, 255, 0.15); +.mood-neutral { + -fx-text-fill: -text-base; } -/* Background */ -.stock-area-chart .chart-plot-background { - -fx-background-color: transparent; +.mood-neutral.rapport-verdict { + -fx-background-color: -surface-line; + -fx-text-fill: -text-base; } -/* Axes */ -.stock-area-chart .axis { - -fx-tick-label-fill: #888888; - -fx-tick-length: 5; +.rapport-line-key { + -fx-font-size: 13px; + -fx-text-fill: -text-muted; } -/* Receipt */ -.receipt-container { - -fx-background-color: white; - -fx-border-color: #cccccc; - -fx-border-style: dashed; - -fx-padding: 20; - -fx-effect: dropshadow( - three-pass-box, - rgba(0,0,0,0.1), - 5, - 0, - 0, - 2 - ); +.rapport-line-value { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: -text-base; } -/* Receipt */ -.receipt-container { - -fx-background-color: white; - -fx-border-color: #cccccc; - -fx-border-style: dashed; - -fx-padding: 20; - -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 5, 0, 0, 2); +.rapport-rent-row { + -fx-padding: 0 0 4 0; } -.receipt-text { - -fx-font-family: "Courier New", monospace; - -fx-font-size: 12px; +.rapport-rent-value { + -fx-text-fill: -danger; } -.receipt-header { +.rapport-balance-header { -fx-font-weight: bold; - -fx-font-size: 14px; - -fx-alignment: center; + -fx-font-size: 16px; + -fx-text-fill: -text-strong; } -.receipt-total { +.rapport-balance-value { + -fx-font-weight: bold; + -fx-font-size: 22px; +} + +.rapport-rule { + -fx-background-color: -surface-line; +} + +/* Scrollable receipt list */ +.rapport-scroll { + -fx-background-color: -surface-soft; + -fx-background-radius: 10; + -fx-border-color: -surface-line; + -fx-border-radius: 10; +} + +.rapport-scroll > .viewport { + -fx-background-color: transparent; +} + +.warning-title { -fx-font-weight: bold; + -fx-font-size: 17px; + -fx-text-fill: -text-strong; + -fx-text-alignment: center; +} + +.warning-message { + -fx-font-size: 13px; + -fx-text-fill: -text-base; + -fx-text-alignment: center; + -fx-line-spacing: 2; +} + +.game-over-title { + -fx-font-weight: bold; + -fx-font-size: 36px; + -fx-text-fill: -text-strong; +} + +.game-over-subtitle { -fx-font-size: 14px; - -fx-border-color: black transparent transparent transparent; + -fx-text-fill: -text-muted; + -fx-text-alignment: center; +} + +.modal-overlay { + -fx-background-color: -overlay; +} + +.hearts-display { + -fx-padding: 0 4 0 4; +} + +.heart-icon { + -fx-font-size: 22px; +} + +.heart-active { + -fx-text-fill: -accent; +} + +.heart-empty { + -fx-text-fill: rgba(255, 255, 255, 0.22); } diff --git a/src/main/resources/css/desktop.css b/src/main/resources/css/desktop.css index a079cc7..7e29ad3 100644 --- a/src/main/resources/css/desktop.css +++ b/src/main/resources/css/desktop.css @@ -1,3 +1,5 @@ + + .desktop-root { -fx-background-color: linear-gradient(#263B6A, #6984A9); } @@ -28,10 +30,11 @@ .app-button { -fx-background-radius: 12; -fx-background-color: #f0f0f0; - -fx-border-color: #d1d1d1; + -fx-border-color: -popup-border; -fx-border-radius: 12; - -fx-text-fill: #333333; + -fx-text-fill: -text-base; -fx-font-weight: bold; + -fx-cursor: hand; } .app-button:hover { diff --git a/src/main/resources/css/global.css b/src/main/resources/css/global.css index 46bd91a..cc3ce8c 100644 --- a/src/main/resources/css/global.css +++ b/src/main/resources/css/global.css @@ -1,9 +1,29 @@ -/* Global Styles */ + .root { -fx-font-family: "Segoe UI", system-ui, -apple-system, sans-serif; + + -primary: #007AFF; + -primary-hover: #0051d4; + + -success: #34C759; + -danger: #e54848; + -danger-soft: #ffe6e6; + -warning: #ff9500; + -accent: -danger; /* hearts, exit, destructive accents */ + + -text-strong: #1a1a1a; + -text-base: #333333; + -text-muted: #888888; + -text-faint: #aaaaaa; + + -surface: white; + -surface-soft: #f5f5f5; + -surface-line: #e8e8e8; + -popup-bg: white; + -popup-border: #d1d1d1; + -overlay: rgba(0, 0, 0, 0.45); } -/* Custom ScrollBar Styling */ .scroll-pane > .viewport { -fx-background-color: transparent; } @@ -31,26 +51,30 @@ -fx-background-color: transparent; } -.scroll-bar .increment-button, .scroll-bar .decrement-button { +.scroll-bar .increment-button, +.scroll-bar .decrement-button { -fx-padding: 0; -fx-background-color: transparent; -fx-shape: ""; -fx-opacity: 0; } -.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { +.scroll-bar .increment-arrow, +.scroll-bar .decrement-arrow { -fx-padding: 0; -fx-shape: ""; -fx-opacity: 0; } -.scroll-bar:vertical .increment-button, .scroll-bar:vertical .decrement-button { +.scroll-bar:vertical .increment-button, +.scroll-bar:vertical .decrement-button { -fx-pref-height: 0; -fx-min-height: 0; -fx-max-height: 0; } -.scroll-bar:horizontal .increment-button, .scroll-bar:horizontal .decrement-button { +.scroll-bar:horizontal .increment-button, +.scroll-bar:horizontal .decrement-button { -fx-pref-width: 0; -fx-min-width: 0; -fx-max-width: 0; diff --git a/src/main/resources/css/login.css b/src/main/resources/css/login.css index 5c7e283..4d602f6 100644 --- a/src/main/resources/css/login.css +++ b/src/main/resources/css/login.css @@ -1,5 +1,5 @@ + .login-root { - -fx-font-family: "Segoe UI", system-ui, -apple-system, sans-serif; -fx-background-color: linear-gradient(#FF9D23, #EA5252); } @@ -10,16 +10,16 @@ .avatar-container { -fx-padding: 5; - -fx-background-color: white; - -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.5), 10, 0, 0, 0); - -fx-border-color: #ffffff; + -fx-background-color: -surface; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.5), 10, 0, 0, 0); + -fx-border-color: -surface; -fx-border-width: 2; -fx-background-radius: 4; -fx-border-radius: 4; } .login-input-box { - -fx-background-color: white; + -fx-background-color: -surface; -fx-background-radius: 4; -fx-border-color: #abadb3; -fx-border-radius: 4; @@ -47,7 +47,7 @@ } .difficulty-label { - -fx-text-fill: white; + -fx-text-fill: -surface; -fx-font-size: 14px; - -fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 2, 0, 0, 1); + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 2, 0, 0, 1); } diff --git a/src/main/resources/css/popup.css b/src/main/resources/css/popup.css index 536c0bf..0a47f13 100644 --- a/src/main/resources/css/popup.css +++ b/src/main/resources/css/popup.css @@ -1,46 +1,48 @@ -.popup-wrapper { - -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 10, 0, 0, 5); - -fx-background-color: transparent; -} + .popup-root { - -fx-background-color: white; - -fx-background-radius: 10; - -fx-border-color: #d1d1d1; + -fx-background-color: -popup-bg; + -fx-background-radius: 12; + -fx-border-color: -popup-border; -fx-border-width: 1; - -fx-border-radius: 10; + -fx-border-radius: 12; -fx-background-insets: 0; -fx-border-insets: 0; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.18), 14, 0, 0, 4); } .popup-header { - -fx-background-color: linear-gradient(to bottom, #f0f0f0, #e0e0e0); - -fx-border-color: transparent transparent #d1d1d1 transparent; + -fx-background-color: linear-gradient(to bottom, #f4f4f4, #e6e6e6); + -fx-border-color: transparent transparent -popup-border transparent; -fx-border-width: 0 0 1 0; } .popup-close-button { - -fx-background-color: #ff5f57; + -fx-background-color: -accent; -fx-background-insets: 0; -fx-padding: 0; -fx-cursor: hand; } .popup-close-button:hover { - -fx-background-color: #ff4b42; + -fx-background-color: derive(-accent, -10%); } .popup-close-button:pressed { - -fx-background-color: #BF3630; + -fx-background-color: derive(-accent, -25%); } .popup-title { -fx-font-weight: bold; - -fx-text-fill: #333333; - -fx-font-size: 16px; + -fx-text-fill: -text-base; + -fx-font-size: 15px; } .popup-scroll-pane { - -fx-background-color: white; - -fx-viewport-background-color: white; + -fx-background-color: -popup-bg; + -fx-viewport-background-color: -popup-bg; +} + +.popup-content { + -fx-background-color: -popup-bg; }