Skip to content

fisket litt på små ting #169

Merged
merged 1 commit into from
May 27, 2026
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -51,8 +47,8 @@ public class CreateGameView extends ViewElement<StackPane, CreateGameActions> {
/** Filename input field. */
private TextField fileNameField;

/** Starting-capital chooser. */
private ChoiceBox<Double> startingCapitalChoice;
/** Starting-capital input. Free-form numeric entry. */
private TextField startingCapitalField;

/** "Bruk default aksje data" button. */
private Button useDefaultStocksButton;
Expand Down Expand Up @@ -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}.
* <p>Accepts both "," and "." as decimal separators and ignores
* whitespace, so "10 000", "10000", "10000.5" and "10000,5" all
* parse to the same value.</p>
*
* @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;
}
}

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -275,7 +288,7 @@ protected void initLayout() {
ChangeListener<Object> enableListener = (obs, oldV, newV) ->
refreshCreateButtonState();
fileNameField.textProperty().addListener(enableListener);
startingCapitalChoice.valueProperty().addListener(enableListener);
startingCapitalField.textProperty().addListener(enableListener);

registerButton(CreateGameActions.USE_DEFAULT_STOCKS,
useDefaultStocksButton);
Expand Down Expand Up @@ -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<Double> 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;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

/**
* Enum representing the available sorting modes in {@link MarketView}.
*
* <p>{@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.</p>
* */
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*
* <p>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.</p>
*
* @param sort the new sort mode.
* */
public void setSort(final MarketSort sort) {
Expand All @@ -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.
*
* <p>{@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.</p>
* */
public void renderStocks() {
if (stocksGrid == null) {
Expand All @@ -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;
Expand All @@ -262,15 +292,45 @@ 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.
* */
private Comparator<Stock> 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();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
11 changes: 11 additions & 0 deletions src/main/resources/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down