From 3d65189e4d8bdcac49ade3ea4133bbdf271feae8 Mon Sep 17 00:00:00 2001 From: EspenTinius Date: Wed, 27 May 2026 04:19:07 +0200 Subject: [PATCH] =?UTF-8?q?fisket=20litt=20p=C3=A5=20sm=C3=A5=20ting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mappe/view/creategame/CreateGameView.java | 84 ++++++++----------- .../view/widgets/market/MarketActions.java | 4 +- .../view/widgets/market/MarketController.java | 11 ++- .../mappe/view/widgets/market/MarketSort.java | 13 ++- .../mappe/view/widgets/market/MarketView.java | 66 ++++++++++++++- .../minigames/games/FindStockGame.java | 1 + .../mappe/view/widgets/stats/StatsView.java | 6 ++ src/main/resources/styles.css | 11 +++ 8 files changed, 140 insertions(+), 56 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java index 2e256ff..f400532 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java @@ -6,7 +6,6 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; -import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.HBox; @@ -15,11 +14,8 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; -import javafx.util.StringConverter; import java.io.File; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; /** * View shown after the player clicks "Create new game" in the @@ -51,8 +47,8 @@ public class CreateGameView extends ViewElement { /** Filename input field. */ private TextField fileNameField; - /** Starting-capital chooser. */ - private ChoiceBox startingCapitalChoice; + /** Starting-capital input. Free-form numeric entry. */ + private TextField startingCapitalField; /** "Bruk default aksje data" button. */ private Button useDefaultStocksButton; @@ -112,13 +108,37 @@ public String getFileName() { } /** - * Returns the starting capital currently selected, or {@code null} - * if nothing is selected yet. + * Returns the starting capital currently entered, or {@code null} + * if nothing is entered yet or the input is not a valid positive + * number. * - * @return the selected starting capital, or {@code null}. + *

Accepts both "," and "." as decimal separators and ignores + * whitespace, so "10 000", "10000", "10000.5" and "10000,5" all + * parse to the same value.

+ * + * @return the entered starting capital, or {@code null} if invalid. */ public Double getStartingCapital() { - return startingCapitalChoice.getValue(); + if (startingCapitalField == null) { + return null; + } + String raw = startingCapitalField.getText(); + if (raw == null) { + return null; + } + String cleaned = raw.replace(" ", "").replace(",", ".").trim(); + if (cleaned.isEmpty()) { + return null; + } + try { + double parsed = Double.parseDouble(cleaned); + if (parsed <= 0 || Double.isNaN(parsed) || Double.isInfinite(parsed)) { + return null; + } + return parsed; + } catch (NumberFormatException _) { + return null; + } } /** @@ -179,8 +199,8 @@ public void resetFields() { if (fileNameField != null) { fileNameField.clear(); } - if (startingCapitalChoice != null) { - startingCapitalChoice.getSelectionModel().clearSelection(); + if (startingCapitalField != null) { + startingCapitalField.clear(); } this.stockSelection = StockSelection.NONE; this.customStockFile = null; @@ -208,17 +228,10 @@ protected void initLayout() { // Row 2 - starting capital Label capitalLabel = new Label("Start capital"); capitalLabel.getStyleClass().add("create-game-label"); - startingCapitalChoice = new ChoiceBox<>(); - startingCapitalChoice.getItems().addAll( - 1_000.0, - 10_000.0, - 100_000.0, - 1_000_000.0, - 10_000_000.0 - ); - startingCapitalChoice.setConverter(buildCapitalConverter()); - startingCapitalChoice.getStyleClass().add("create-game-input"); - VBox capitalRow = new VBox(6, capitalLabel, startingCapitalChoice); + startingCapitalField = new TextField(); + startingCapitalField.setPromptText("Set starting capital (NOK)..."); + startingCapitalField.getStyleClass().add("create-game-input"); + VBox capitalRow = new VBox(6, capitalLabel, startingCapitalField); // Row 3 - stock data choice (two buttons side by side) Label stockLabel = new Label("Stock data"); @@ -275,7 +288,7 @@ protected void initLayout() { ChangeListener enableListener = (obs, oldV, newV) -> refreshCreateButtonState(); fileNameField.textProperty().addListener(enableListener); - startingCapitalChoice.valueProperty().addListener(enableListener); + startingCapitalField.textProperty().addListener(enableListener); registerButton(CreateGameActions.USE_DEFAULT_STOCKS, useDefaultStocksButton); @@ -335,27 +348,4 @@ private void refreshCreateButtonState() { ? "create-game-button-enabled" : "create-game-button-disabled"); } - - /** - * Builds a {@link StringConverter} that renders the starting-capital - * values in the choice box using a "1 000 kr"-style format instead - * of the default raw double output. - */ - private StringConverter buildCapitalConverter() { - DecimalFormatSymbols symbols = new DecimalFormatSymbols(); - symbols.setGroupingSeparator(' '); - final DecimalFormat format = new DecimalFormat("#,##0", symbols); - return new StringConverter<>() { - @Override - public String toString(final Double value) { - return value == null ? "" : format.format(value) + " kr"; - } - - @Override - public Double fromString(final String text) { - // Not used - the choice box is not editable. - return null; - } - }; - } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketActions.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketActions.java index 65d9bd5..6282a24 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketActions.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketActions.java @@ -13,8 +13,8 @@ public enum MarketActions { SORT_TICKER, /** Sort stocks by current price (descending). */ SORT_PRICE, - /** Sort stocks by latest change (descending). */ + /** Cycle the change-filter between winners-only and losers-only. */ SORT_CHANGE, - /** Sort stocks by amount owned (descending). */ + /** Show only stocks the player currently owns. */ SORT_OWNED; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java index fce33d8..2c6683f 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java @@ -79,8 +79,17 @@ protected void initInteractions() { () -> getViewElement().setSort(MarketSort.TICKER)); getViewElement().setOnAction(MarketActions.SORT_PRICE, () -> getViewElement().setSort(MarketSort.PRICE)); + // The change button cycles between showing only winners and only + // losers - never both at once. The first press defaults to winners + // (the most common "what's going up?" question) and each subsequent + // press flips to the opposite side. getViewElement().setOnAction(MarketActions.SORT_CHANGE, - () -> getViewElement().setSort(MarketSort.CHANGE)); + () -> { + MarketSort next = + (getViewElement().getCurrentSort() == MarketSort.WINNERS) + ? MarketSort.LOSERS : MarketSort.WINNERS; + getViewElement().setSort(next); + }); getViewElement().setOnAction(MarketActions.SORT_OWNED, () -> getViewElement().setSort(MarketSort.OWNED)); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketSort.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketSort.java index 3bd9805..09503ba 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketSort.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketSort.java @@ -2,14 +2,21 @@ /** * Enum representing the available sorting modes in {@link MarketView}. + * + *

{@link #WINNERS} and {@link #LOSERS} are mutually exclusive views + * of the "change" filter button: pressing the button cycles between + * the two. {@link #OWNED} acts as a "show only owned" filter rather + * than a pure sort.

* */ public enum MarketSort { /** Alphabetical sort by ticker symbol. */ TICKER, /** Descending sort by current sales price. */ PRICE, - /** Descending sort by latest change percentage. */ - CHANGE, - /** Descending sort by amount of shares the player owns. */ + /** Show only stocks with positive change, sorted descending. */ + WINNERS, + /** Show only stocks with negative change, sorted ascending. */ + LOSERS, + /** Show only stocks the player currently owns, sorted by quantity. */ OWNED; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketView.java index 67420e2..09cdf3e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketView.java @@ -117,7 +117,10 @@ private HBox buildFilterBar() { sortButtonMap.put(MarketSort.TICKER, sortTickerButton); sortButtonMap.put(MarketSort.PRICE, sortPriceButton); - sortButtonMap.put(MarketSort.CHANGE, sortChangeButton); + // WINNERS and LOSERS share the same button, the controller cycles + // between them on each press. + sortButtonMap.put(MarketSort.WINNERS, sortChangeButton); + sortButtonMap.put(MarketSort.LOSERS, sortChangeButton); sortButtonMap.put(MarketSort.OWNED, sortOwnedButton); registerButton(MarketActions.SORT_TICKER, sortTickerButton); @@ -218,6 +221,10 @@ public MarketSort getCurrentSort() { * Updates the current sort mode, refreshes the active state of the filter * buttons, and re-renders the stock grid. * + *

The change button doubles as a winners/losers cycle - its label + * updates to reflect the active mode so the user can tell which one + * they're currently looking at.

+ * * @param sort the new sort mode. * */ public void setSort(final MarketSort sort) { @@ -229,12 +236,34 @@ public void setSort(final MarketSort sort) { active.getStyleClass().add("active"); } } + refreshChangeButtonLabel(); renderStocks(); } + /** + * Updates the label on the change button to reflect which filter mode + * is currently active. The button cycles winners -> losers -> winners + * on press, so showing the active mode makes the cycle discoverable. + * */ + private void refreshChangeButtonLabel() { + if (sortChangeButton == null) { + return; + } + sortChangeButton.setText(switch (currentSort) { + case WINNERS -> "winners"; + case LOSERS -> "losers"; + default -> "change"; + }); + } + /** * Rebuilds the stock card grid based on the current search term and * sort mode. + * + *

{@link MarketSort#WINNERS}, {@link MarketSort#LOSERS} and + * {@link MarketSort#OWNED} act as filters: in those modes the grid + * only shows the matching subset of stocks. The other modes show + * every stock that matches the search term.

* */ public void renderStocks() { if (stocksGrid == null) { @@ -247,11 +276,12 @@ public void renderStocks() { .filter(s -> s.getSymbol().toLowerCase().contains(term) || s.getCompany().toLowerCase().contains(term)) + .filter(this::matchesSortFilter) .sorted(comparatorFor(currentSort)) .toList(); if (filtered.isEmpty()) { - Label empty = new Label("no stocks match your search"); + Label empty = new Label(emptyMessageFor(currentSort)); empty.getStyleClass().add("market-empty"); stocksGrid.getChildren().add(empty); return; @@ -262,6 +292,34 @@ public void renderStocks() { } } + /** + * Filter predicate that strips out stocks that don't belong in the + * current mode. Sort-only modes (TICKER, PRICE) keep every stock; + * the others filter by change sign or by ownership. + * */ + private boolean matchesSortFilter(final Stock stock) { + return switch (currentSort) { + case WINNERS -> changePercent(stock) > 0; + case LOSERS -> changePercent(stock) < 0; + case OWNED -> ownedLookup.applyAsInt(stock.getSymbol()) > 0; + default -> true; + }; + } + + /** + * Returns the empty-state message that fits the current sort mode, + * so the user knows whether they actually have no matches or whether + * the filter excluded everything. + * */ + private String emptyMessageFor(final MarketSort sort) { + return switch (sort) { + case WINNERS -> "no winners right now"; + case LOSERS -> "no losers right now"; + case OWNED -> "you don't own any stocks yet"; + default -> "no stocks match your search"; + }; + } + /** * Builds a comparator for the given sort mode. * */ @@ -269,8 +327,10 @@ private Comparator comparatorFor(final MarketSort sort) { return switch (sort) { case TICKER -> Comparator.comparing(Stock::getSymbol); case PRICE -> Comparator.comparing(Stock::getSalesPrice).reversed(); - case CHANGE -> Comparator.comparingDouble( + case WINNERS -> Comparator.comparingDouble( (Stock s) -> changePercent(s)).reversed(); + case LOSERS -> Comparator.comparingDouble( + (Stock s) -> changePercent(s)); case OWNED -> Comparator.comparingInt( (Stock s) -> ownedLookup.applyAsInt(s.getSymbol())).reversed(); }; diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/minigames/games/FindStockGame.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/minigames/games/FindStockGame.java index 5e4c4f3..d841f72 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/minigames/games/FindStockGame.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/minigames/games/FindStockGame.java @@ -150,6 +150,7 @@ protected void initLayout() { @Override protected void initStyling() { getRootPane().getStyleClass().add("find-stock-minigame-root"); + targetLabel.getStyleClass().add("find-stock-minigame-target"); grid.getStyleClass().add("find-stock-minigame-grid"); } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsView.java index b0c5bb3..f3d6129 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsView.java @@ -202,6 +202,12 @@ private VBox buildKpiTile(final String labelText, Label label = new Label(labelText); label.getStyleClass().add("stats-kpi-label"); valueLabel.getStyleClass().add("stats-kpi-value"); + // Apply the shared sub-label styling here so every tile picks up + // the readable size/colour. The up/down tint added later in + // renderKpis() composes on top of this base. + if (!subLabel.getStyleClass().contains("stats-kpi-sub")) { + subLabel.getStyleClass().add("stats-kpi-sub"); + } VBox tile = new VBox(2, label, valueLabel, subLabel); tile.getStyleClass().add("stats-kpi"); diff --git a/src/main/resources/styles.css b/src/main/resources/styles.css index 8864002..f553d94 100644 --- a/src/main/resources/styles.css +++ b/src/main/resources/styles.css @@ -1472,6 +1472,13 @@ -fx-alignment: CENTER; } +.find-stock-minigame-target { + -fx-font-family: "Aptos"; + -fx-font-weight: bold; + -fx-font-size: 22px; + -fx-text-fill: #f0f4ff; +} + .find-stock-minigame-grid { -fx-hgap: 15px; -fx-vgap: 15px; @@ -2381,6 +2388,10 @@ -fx-effect: dropshadow(gaussian, rgba(10, 132, 217, 0.55), 14, 0.35, 0, 0); } +.light-mode .find-stock-minigame-target { + -fx-text-fill: #0f1b34; +} + /* ---------- IN-GAME : SETTINGS OVERLAY ---------- */ .light-mode .ingame-settings-overlay-dimmer { -fx-background-color: rgba(10, 27, 52, 0.35);