diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/GameStateLoader.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/GameStateLoader.java index 6f67aa5..0137ad2 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/GameStateLoader.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/GameStateLoader.java @@ -254,11 +254,8 @@ private void applySave(final SaveGame save) { if (save.getNetWorthHistory() != null && !save.getNetWorthHistory().isEmpty()) { player.setNetWorthHistory(save.getNetWorthHistory()); } else { - // No recorded history available - seed a minimal two-point - // history so the chart still has something to render. List seed = new ArrayList<>(); seed.add(BigDecimal.valueOf(save.getStartingCapital())); - seed.add(player.getNetWorth()); player.setNetWorthHistory(seed); } 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/dashboard/DashBoardView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardView.java index 1079fcf..6149a39 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardView.java @@ -282,7 +282,17 @@ protected void initLayout() { yAxis.setTickMarkVisible(true); yAxis.setMinorTickVisible(false); yAxis.setAutoRanging(false); - yAxis.setLabel("Price (NOK)"); + yAxis.setTickLabelFormatter(new javafx.util.StringConverter() { + @Override + public String toString(final Number value) { + return Math.round(value.floatValue() * 100f) / 100f + " NOK"; + } + + @Override + public Number fromString(final String string) { + return 0; + } + }); chart = new LineChart<>(xAxis, yAxis); chart.setCreateSymbols(false); 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/StatsController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java index b934d50..5fe9992 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java @@ -176,7 +176,6 @@ public void handleEvent(final EventData data) { balanceHistory.addAll(player.getNetWorthHistory()); } else { balanceHistory.add(player.getStartingMoney()); - balanceHistory.add(player.getNetWorth()); } pushSnapshot(); } @@ -277,7 +276,6 @@ public void handleContextUpdate(final Exchange updatedExchange, final Player upd this.balanceHistory.addAll(this.player.getNetWorthHistory()); } else { this.balanceHistory.add(this.player.getStartingMoney()); - this.balanceHistory.add(this.player.getNetWorth()); } pushSnapshot(); } 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..2f54361 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"); @@ -446,8 +452,15 @@ private void renderBalanceChart() { balanceChartPane.getChildren().add(seg); } - // X-axis labels (week numbers). + int xLabelStep = computeXLabelStep(n, stepX); + int lastIndex = n - 1; for (int i = 0; i < n; i++) { + boolean isEdge = (i == 0 || i == lastIndex); + boolean isStepAligned = (i % xLabelStep == 0) + && (lastIndex - i >= xLabelStep || i == 0); + if (!isEdge && !isStepAligned) { + continue; + } double x = padL + i * stepX; Text xLabel = new Text(0, h - 6, String.valueOf(i + 1)); xLabel.getStyleClass().add("stats-chart-axis"); @@ -643,6 +656,35 @@ private static String stripTrailingZeros(final BigDecimal v) { return v.stripTrailingZeros().toPlainString(); } + /** + * Picks a stride that lets us draw every {@code step}-th week label + * along the x-axis without overlap. + * + *

Targets at least ~28 px of horizontal space between adjacent + * rendered labels. The step is also rounded to a "nice" value + * (1, 2, 5, 10, 20, 50, ...) so the labels read as a clean + * progression rather than landing on awkward weeks.

+ * + * @param n the total number of data points on the chart. + * @param stepX the horizontal spacing between consecutive points. + * @return the index stride to use when rendering x-axis labels. + * */ + private static int computeXLabelStep(final int n, final double stepX) { + final double minLabelSpacing = 28.0; + if (stepX >= minLabelSpacing || n <= 1) { + return 1; + } + int rawStep = (int) Math.ceil(minLabelSpacing / stepX); + // Snap to a nice 1-2-5 progression so labels land on tidy weeks. + int[] niceSteps = {1, 2, 5, 10, 20, 50, 100}; + for (int s : niceSteps) { + if (s >= rawStep) { + return s; + } + } + return rawStep; + } + /** Internal data carrier for a single pie slice. */ private static final class Segment { final String label; 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);