diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 9a83f2c..ed5d7c7 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -1,24 +1,26 @@ package millions; - import java.math.BigDecimal; +import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; - import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.stage.Stage; import millions.controller.GameController; -import millions.controller.fileIO.CSV.CSVStockFileWriter; import millions.controller.fileIO.InvalidFormatException; import millions.controller.fileIO.UncheckedFileNotFoundException; +import millions.view.ExitView; import millions.view.GameView; import millions.view.StartView; /** Main JavaFX application entry point for the Millions stock trading game. */ public class App extends Application { private static final Logger logger = Logger.getLogger(App.class.getName()); + private static final String STYLESHEET = + Objects.requireNonNull(App.class.getResource("/styles/millions.css")).toExternalForm(); + @Override public void start(Stage stage) { GameController controller = new GameController(); @@ -35,11 +37,21 @@ public void start(Stage stage) { startView.getSelectedFile().toPath(), startView.getPreRunWeeks()); - GameView gameView = new GameView(controller); + GameView gameView = + new GameView( + controller, + () -> { + ExitView exitView = new ExitView(controller.getPlayer()); + exitView.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + Scene exitScene = new Scene(exitView, 500, 400); + exitScene.getStylesheets().add(STYLESHEET); + stage.setScene(exitScene); + }); controller.getPlayer().addListener(gameView); controller.getExchange().addListener(gameView); Scene gameScene = new Scene(gameView, 1920, 1080); + gameScene.getStylesheets().add(STYLESHEET); stage.setScene(gameScene); } catch (InvalidFormatException e) { logger.log(Level.WARNING, "InvalidFormatException: " + e.getMessage()); @@ -47,11 +59,12 @@ public void start(Stage stage) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Error with selected file"); - alert.setContentText(e.getMessage() + "\nPlease control the format of the selected file"); + alert.setContentText( + e.getMessage() + "\nPlease control the format of the selected file"); alert.showAndWait(); - } catch(UncheckedFileNotFoundException e) { + } catch (UncheckedFileNotFoundException e) { logger.log(Level.WARNING, "FileNotFoundException: " + e.getMessage()); Alert alert = new Alert(Alert.AlertType.ERROR); @@ -68,7 +81,10 @@ public void start(Stage stage) { }); Scene scene = new Scene(startView, 400, 350); + scene.getStylesheets().add(STYLESHEET); stage.setTitle("Millions"); + stage.setMinWidth(900); + stage.setMinHeight(600); stage.setScene(scene); stage.show(); } diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index e4abeae..515926e 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -63,6 +63,7 @@ public void withdrawMoney(BigDecimal amount) { /** * Calculates the skill level of the player + * * @return String player status level */ public String getStatus() { @@ -101,6 +102,13 @@ public Portfolio getPortfolio() { return this.portfolio; } + /** + * @return player startingMoney + */ + public BigDecimal getStartingMoneh() { + return this.startingMoney; + } + /** * @param share Share to be added */ diff --git a/src/main/java/millions/view/ExitView.java b/src/main/java/millions/view/ExitView.java new file mode 100644 index 0000000..f90a9e8 --- /dev/null +++ b/src/main/java/millions/view/ExitView.java @@ -0,0 +1,79 @@ +package millions.view; + +import java.math.BigDecimal; +import javafx.application.Platform; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import millions.model.Player; + +public class ExitView extends VBox { + + public ExitView(Player player) { + setSpacing(16); + setAlignment(Pos.CENTER); + setPadding(new Insets(60)); + + Label title = new Label("GAME OVER"); + title.getStyleClass().add("game-over-title"); + HBox titleRow = new HBox(title); + titleRow.setAlignment(Pos.CENTER); + titleRow.setMaxWidth(Double.MAX_VALUE); + + Separator sep = new Separator(); + sep.setMaxWidth(320); + + Label weeks = new Label(String.valueOf(player.getTransactionArchive().countDistinctWeeks())); + Label trades = + new Label(String.valueOf(player.getTransactionArchive().getTransactions().size())); + Label netWorth = new Label(ViewUtils.formatMoney(player.getNetWorth())); + + BigDecimal profit = player.getNetWorth().subtract(player.getStartingMoneh()); + String sign = profit.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; + Label netProfit = new Label(sign + ViewUtils.formatMoney(profit)); + netProfit + .getStyleClass() + .add(profit.compareTo(BigDecimal.ZERO) >= 0 ? "status-success" : "status-error"); + + GridPane stats = new GridPane(); + stats.setHgap(24); + stats.setVgap(12); + ColumnConstraints labelCol = new ColumnConstraints(); + labelCol.setHalignment(HPos.RIGHT); + labelCol.setHgrow(Priority.NEVER); + ColumnConstraints valueCol = new ColumnConstraints(); + valueCol.setHalignment(HPos.LEFT); + valueCol.setHgrow(Priority.ALWAYS); + stats.getColumnConstraints().addAll(labelCol, valueCol); + + stats.addRow(0, statlabel("Weeks traded"), weeks); + stats.addRow(1, statlabel("Trades made"), trades); + stats.addRow(2, statlabel("Final net worth"), netWorth); + stats.addRow(3, statlabel("Total profit"), netProfit); + + for (Label l : new Label[] {weeks, trades, netWorth, netProfit}) { + l.getStyleClass().add("stat-value"); + } + + Button quitButton = new Button("Quit"); + quitButton.getStyleClass().add("btn-primary"); + quitButton.setPrefWidth(160); + quitButton.setOnAction(event -> Platform.exit()); + + getChildren().addAll(titleRow, sep, stats, quitButton); + } + + private static Label statlabel(String text) { + Label l = new Label(text); + l.getStyleClass().add("section-title"); + return l; + } +} diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index d2339ff..9fefcc3 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import java.util.List; import javafx.beans.property.SimpleStringProperty; // For data binding for table string +import javafx.geometry.Orientation; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; @@ -11,6 +12,7 @@ import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.control.Separator; import javafx.scene.control.Slider; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; @@ -21,6 +23,7 @@ import javafx.scene.control.TextFormatter; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import millions.controller.GameController; import millions.model.Exchange; @@ -30,6 +33,8 @@ import millions.model.Share; import millions.model.Stock; import millions.model.Transaction; +import millions.model.calculators.PurchaseCalculator; +import millions.model.calculators.SaleCalculator; /** Main game screen with tabs */ public class GameView extends BorderPane implements PlayerListener, ExchangeListener { @@ -41,6 +46,13 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final Label netWorthLabel = new Label(); private final Label statusLabel = new Label(); + { + for (Label l : + new Label[] {playerNameLabel, weekLabel, moneyLabel, netWorthLabel, statusLabel}) { + l.getStyleClass().add("stat-value"); + } + } + private final TextField searchField = new TextField(); private final ListView stocksList = new ListView<>(); private final TableView portfolioTable = new TableView<>(); @@ -57,11 +69,30 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final Button buyButton = new Button("Buy"); private final Button sellButton = new Button("Sell"); private final Button advanceButton = new Button("Advance week"); + private final Button sellAllAndQuitButton = new Button("Sell all & quit"); + + private final Label stockHighLabel = new Label(); + private final Label stockLowLabel = new Label(); + private final Label stockChangeLabel = new Label(); + private final Label buyCostPreviewLabel = new Label(); + private final Label sellCostPreviewLabel = new Label(); + private final TableView gainersTable = new TableView<>(); + private final TableView losersTable = new TableView<>(); + private final TextField transactionSearchField = new TextField(); + + { + buyButton.getStyleClass().add("btn-primary"); + advanceButton.getStyleClass().add("btn-primary"); + sellAllAndQuitButton.getStyleClass().add("btn-danger"); + } + + private final Runnable onSellAllAndQuit; private final TabPane tabPane; private boolean updatingQuantityControls; - public GameView(GameController controller) { + public GameView(GameController controller, Runnable onSellAllAndQuit) { this.controller = controller; + this.onSellAllAndQuit = onSellAllAndQuit; this.tabPane = createTabs(); setTop(createHeader()); setCenter(tabPane); @@ -72,25 +103,51 @@ public GameView(GameController controller) { } private HBox createHeader() { - Label title = new Label("Millions"); - title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); + HBox title = ViewUtils.buildTitle(); + + javafx.scene.layout.Region spacer = new javafx.scene.layout.Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); HBox header = - new HBox(20, title, playerNameLabel, weekLabel, moneyLabel, netWorthLabel, statusLabel); + new HBox( + 20, + title, + statBox("PLAYER", playerNameLabel), + statBox("WEEK", weekLabel), + statBox("CASH", moneyLabel), + statBox("NET WORTH", netWorthLabel), + statBox("STATUS", statusLabel), + spacer, + sellAllAndQuitButton); + header.getStyleClass().add("header-panel"); + header.setAlignment(javafx.geometry.Pos.CENTER_LEFT); return header; } + private static VBox statBox(String key, Label valueLabel) { + Label keyLabel = new Label(key); + keyLabel.getStyleClass().add("section-title"); + VBox box = new VBox(1, keyLabel, valueLabel); + box.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + return box; + } + private TabPane createTabs() { TabPane tabPane = new TabPane(); tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); tabPane.getTabs().add(createStocksTab()); tabPane.getTabs().add(createPortfolioTab()); tabPane.getTabs().add(createTransactionsTab()); + tabPane.getTabs().add(createMarketTab()); return tabPane; } private Tab createStocksTab() { - VBox leftPane = new VBox(10, new Label("Search"), searchField, stocksList); + VBox leftPane = new VBox(10, searchField, stocksList); + leftPane.setPrefWidth(500); + stocksList.setPrefWidth(500); + stocksList.setMinWidth(500); + stocksList.setMaxWidth(Double.MAX_VALUE); searchField.setPromptText("Search"); searchField.textProperty().addListener((obs, oldVal, newVal) -> refreshStocks()); @@ -105,10 +162,20 @@ private Tab createStocksTab() { stockChart.setCreateSymbols(true); stockChart.setAnimated(false); stockChart.setPrefHeight(500); + stockChart.setMaxWidth(Double.MAX_VALUE); + + selectedStockLabel.getStyleClass().add("selected-stock-label"); - VBox rightPane = new VBox(10, selectedStockLabel, stockChart); + stockHighLabel.getStyleClass().add("section-title"); + stockLowLabel.getStyleClass().add("section-title"); + stockChangeLabel.getStyleClass().add("section-title"); + HBox stockStats = new HBox(20, stockHighLabel, stockLowLabel, stockChangeLabel); - quantityField.setPrefWidth(70); + VBox rightPane = new VBox(6, selectedStockLabel, stockStats, stockChart); + rightPane.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(rightPane, Priority.ALWAYS); + + quantityField.setPrefWidth(100); quantityField.setTextFormatter( new TextFormatter<>( change -> change.getControlNewText().matches("-?\\d*") ? change : null)); @@ -116,18 +183,36 @@ private Tab createStocksTab() { ViewUtils.configureOwnedSharesBox(ownedSharesBox); - HBox sellBox = new HBox(8, sellButton, new VBox(4, new Label("Owned Shares"), ownedSharesBox)); + Label buyHeader = new Label("BUY"); + buyHeader.getStyleClass().add("section-title"); + HBox buyControls = new HBox(8, quantityField, quantitySlider, buyButton); + buyControls.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + buyCostPreviewLabel.getStyleClass().add("cost-preview"); + VBox buySection = new VBox(4, buyHeader, buyControls, buyCostPreviewLabel); + + javafx.scene.control.Separator sep1 = + new javafx.scene.control.Separator(javafx.geometry.Orientation.VERTICAL); + + Label sellHeader = new Label("SELL"); + sellHeader.getStyleClass().add("section-title"); + HBox sellControls = new HBox(8, ownedQuantityLabel, ownedSharesBox, sellButton); + sellControls.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + sellCostPreviewLabel.getStyleClass().add("cost-preview"); + VBox sellSection = new VBox(4, sellHeader, sellControls, sellCostPreviewLabel); + + javafx.scene.control.Separator sep2 = + new javafx.scene.control.Separator(javafx.geometry.Orientation.VERTICAL); + + Label weekHeader = new Label("WEEK"); + weekHeader.getStyleClass().add("section-title"); + VBox weekSection = new VBox(4, weekHeader, advanceButton); + + HBox actionBar = new HBox(16, buySection, sep1, sellSection, sep2, weekSection); + actionBar.getStyleClass().add("toolbar-panel"); + actionBar.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - HBox actionBar = - new HBox( - 10, - ownedQuantityLabel, - quantityField, - quantitySlider, - buyButton, - sellBox, - advanceButton); HBox content = new HBox(12, leftPane, rightPane); + HBox.setHgrow(content, Priority.ALWAYS); VBox outer = new VBox(12, content, actionBar, actionStatusLabel); return new Tab("Stocks", outer); } @@ -136,39 +221,47 @@ private Tab createPortfolioTab() { portfolioTable.setPlaceholder(new Label("No shares yet")); TableColumn symbolColumn = new TableColumn<>("Symbol"); + symbolColumn.setPrefWidth(200); symbolColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getStock().getSymbol())); TableColumn quantityColumn = new TableColumn<>("Quantity"); + quantityColumn.setPrefWidth(100); quantityColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getQuantity().toPlainString())); TableColumn purchasePriceColumn = new TableColumn<>("Purchase price"); + purchasePriceColumn.setPrefWidth(150); purchasePriceColumn.setCellValueFactory( - data -> new SimpleStringProperty(data.getValue().getPurchasePrice().toPlainString())); + data -> + new SimpleStringProperty(ViewUtils.formatMoney(data.getValue().getPurchasePrice()))); TableColumn currentPriceColumn = new TableColumn<>("Current price"); + currentPriceColumn.setPrefWidth(150); currentPriceColumn.setCellValueFactory( data -> - new SimpleStringProperty(data.getValue().getStock().getSalesPrice().toPlainString())); + new SimpleStringProperty( + ViewUtils.formatMoney(data.getValue().getStock().getSalesPrice()))); TableColumn profitColumn = new TableColumn<>("Profit"); + profitColumn.setPrefWidth(100); profitColumn.setCellValueFactory( data -> - new SimpleStringProperty(ViewUtils.getShareProfit(data.getValue()).toPlainString())); + new SimpleStringProperty( + ViewUtils.formatMoney(ViewUtils.getShareProfit(data.getValue())))); profitColumn.setCellFactory( column -> new TableCell<>() { @Override protected void updateItem(String profit, boolean empty) { super.updateItem(profit, empty); + getStyleClass().removeAll("status-success", "status-error"); if (empty) { setText(null); - setStyle(""); return; } setText(profit); - setStyle(profit.startsWith("-") ? "-fx-text-fill: red;" : "-fx-text-fill: green;"); + getStyleClass().add(profit.startsWith("-") ? "status-error" : "status-success"); } }); TableColumn marketColumn = new TableColumn<>("Market"); @@ -205,37 +298,84 @@ private Tab createTransactionsTab() { transactionsTable.setPlaceholder(new Label("No transactions yet")); TableColumn typeColumn = new TableColumn<>("Type"); + typeColumn.setPrefWidth(100); typeColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getClass().getSimpleName())); TableColumn symbolColumn = new TableColumn<>("Symbol"); + symbolColumn.setPrefWidth(100); symbolColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getShare().getStock().getSymbol())); TableColumn quantityColumn = new TableColumn<>("Quantity"); + quantityColumn.setPrefWidth(100); quantityColumn.setCellValueFactory( data -> new SimpleStringProperty(data.getValue().getShare().getQuantity().toPlainString())); TableColumn weekColumn = new TableColumn<>("Week"); + weekColumn.setPrefWidth(100); weekColumn.setCellValueFactory( data -> new SimpleStringProperty(String.valueOf(data.getValue().getWeek()))); + TableColumn grossColumn = new TableColumn<>("Gross"); + grossColumn.setPrefWidth(150); + grossColumn.setCellValueFactory( + data -> + new SimpleStringProperty( + ViewUtils.formatMoney(data.getValue().getCalculator().calculateGross()))); + + TableColumn commissionColumn = new TableColumn<>("Commission"); + commissionColumn.setPrefWidth(150); + commissionColumn.setCellValueFactory( + data -> + new SimpleStringProperty( + ViewUtils.formatMoney(data.getValue().getCalculator().calculateCommission()))); + + TableColumn taxColumn = new TableColumn<>("Tax"); + taxColumn.setPrefWidth(150); + taxColumn.setCellValueFactory( + data -> + new SimpleStringProperty( + ViewUtils.formatMoney(data.getValue().getCalculator().calculateTax()))); + TableColumn totalColumn = new TableColumn<>("Total"); + totalColumn.setPrefWidth(150); totalColumn.setCellValueFactory( data -> new SimpleStringProperty( - data.getValue().getCalculator().calculateTotal().toPlainString())); + ViewUtils.formatMoney(data.getValue().getCalculator().calculateTotal()))); transactionsTable .getColumns() - .addAll(typeColumn, symbolColumn, quantityColumn, weekColumn, totalColumn); - return new Tab("Transactions", transactionsTable); + .addAll( + typeColumn, + symbolColumn, + quantityColumn, + weekColumn, + grossColumn, + commissionColumn, + taxColumn, + totalColumn); + + transactionSearchField.setPromptText("Search by symbol or type..."); + transactionSearchField + .textProperty() + .addListener((obs, oldVal, newVal) -> refreshTransactions()); + + VBox content = new VBox(8, transactionSearchField, transactionsTable); + VBox.setVgrow(transactionsTable, Priority.ALWAYS); + return new Tab("Transactions", content); } private void configureButtons() { buyButton.setOnAction(event -> buySelectedStock()); sellButton.setOnAction(event -> sellSelectedShare()); advanceButton.setOnAction(event -> advanceWeek()); + sellAllAndQuitButton.setOnAction(event -> sellAllAndQuit()); + ownedSharesBox + .getSelectionModel() + .selectedItemProperty() + .addListener((obs, oldShare, newShare) -> updateSellCostPreview(newShare)); } private void configureQuantityControls() { @@ -273,6 +413,7 @@ private void refreshAll() { refreshStocks(); refreshPortfolio(); refreshTransactions(); + refreshMarket(); } private void refreshPlayerInfo() { @@ -283,11 +424,11 @@ private void refreshPlayerInfo() { return; } - playerNameLabel.setText("Player: " + player.getName()); - weekLabel.setText("Week: " + exchange.getWeekNumber()); - moneyLabel.setText("Money: " + player.getMoney()); - netWorthLabel.setText("Net worth: " + player.getNetWorth()); - statusLabel.setText("Status: " + player.getStatus()); + playerNameLabel.setText(player.getName()); + weekLabel.setText(String.valueOf(exchange.getWeekNumber())); + moneyLabel.setText(ViewUtils.formatMoney(player.getMoney())); + netWorthLabel.setText(ViewUtils.formatMoney(player.getNetWorth())); + statusLabel.setText(player.getStatus()); } private void refreshStocks() { @@ -311,11 +452,22 @@ private void showStockChart(Stock stock) { if (stock == null) { selectedStockLabel.setText("Select a stock to see chart"); + stockHighLabel.setText(""); + stockLowLabel.setText(""); + stockChangeLabel.setText(""); refreshQuantityControls(null); return; } selectedStockLabel.setText(stock.getSymbol() + " - " + stock.getCompany()); + + BigDecimal change = stock.getLatestPriceChange(); + String changeSign = change.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; + stockHighLabel.setText("High: " + ViewUtils.formatMoney(stock.getHighestPrice())); + stockLowLabel.setText("Low: " + ViewUtils.formatMoney(stock.getLowestPrice())); + stockChangeLabel.setText("Change: " + changeSign + ViewUtils.formatMoney(change)); + stockChangeLabel.getStyleClass().removeAll("status-success", "status-error"); + stockChangeLabel.getStyleClass().add(change.compareTo(BigDecimal.ZERO) >= 0 ? "status-success" : "status-error"); refreshQuantityControls(stock); XYChart.Series series = new XYChart.Series<>(); @@ -357,6 +509,8 @@ private void refreshQuantityControls(Stock stock) { if (!ownedSharesBox.getItems().isEmpty()) { ownedSharesBox.getSelectionModel().selectFirst(); } + updateBuyCostPreview(stock, current); + updateSellCostPreview(ownedSharesBox.getSelectionModel().getSelectedItem()); } private void refreshPortfolio() { @@ -367,11 +521,23 @@ private void refreshPortfolio() { private void refreshTransactions() { Player player = controller.getPlayer(); - if (player == null) { - return; - } + if (player == null) return; - transactionsTable.getItems().setAll(player.getTransactionArchive().getTransactions()); + String filter = transactionSearchField.getText().trim().toLowerCase(); + List all = player.getTransactionArchive().getTransactions(); + if (filter.isBlank()) { + transactionsTable.getItems().setAll(all); + } else { + transactionsTable + .getItems() + .setAll( + all.stream() + .filter( + t -> + t.getShare().getStock().getSymbol().toLowerCase().contains(filter) + || t.getClass().getSimpleName().toLowerCase().contains(filter)) + .toList()); + } } private void buySelectedStock() { @@ -384,7 +550,17 @@ private void buySelectedStock() { try { Transaction transaction = controller.buyStock(selectedStock.getSymbol(), BigDecimal.valueOf(getQuantityValue())); - setActionStatus("Bought " + selectedStock.getSymbol(), true); + var calc = transaction.getCalculator(); + setActionStatus( + "Bought " + + selectedStock.getSymbol() + + " — Gross: " + + ViewUtils.formatMoney(calc.calculateGross()) + + " Commission: " + + ViewUtils.formatMoney(calc.calculateCommission()) + + " Total: " + + ViewUtils.formatMoney(calc.calculateTotal()), + true); refreshAll(); showStockChart(selectedStock); } catch (RuntimeException ex) { @@ -406,7 +582,19 @@ private void sellSelectedShare() { try { Transaction transaction = controller.sellShare(selectedShare); - setActionStatus("Sold " + selectedStock.getSymbol(), true); + var calc = transaction.getCalculator(); + setActionStatus( + "Sold " + + selectedStock.getSymbol() + + " — Gross: " + + ViewUtils.formatMoney(calc.calculateGross()) + + " Commission: " + + ViewUtils.formatMoney(calc.calculateCommission()) + + " Tax: " + + ViewUtils.formatMoney(calc.calculateTax()) + + " Net: " + + ViewUtils.formatMoney(calc.calculateTotal()), + true); refreshAll(); showStockChart(selectedStock); } catch (RuntimeException ex) { @@ -420,9 +608,24 @@ private void advanceWeek() { showStockChart(stocksList.getSelectionModel().getSelectedItem()); } + public void sellAllShares() { + List shares = + new java.util.ArrayList<>(controller.getPlayer().getPortfolio().getShares()); + for (Share share : shares) { + controller.sellShare(share); + } + refreshAll(); + } + + private void sellAllAndQuit() { + sellAllShares(); + onSellAllAndQuit.run(); + } + private void setActionStatus(String message, boolean success) { actionStatusLabel.setText(message); - actionStatusLabel.setStyle(success ? "-fx-text-fill: green;" : "-fx-text-fill: red;"); + actionStatusLabel.getStyleClass().removeAll("status-success", "status-error"); + actionStatusLabel.getStyleClass().add(success ? "status-success" : "status-error"); } private int getQuantityValue() { @@ -450,6 +653,7 @@ private void syncQuantityFromField(String newValue) { quantityField.setText(String.valueOf(clamped)); quantitySlider.setValue(clamped); updatingQuantityControls = false; + updateBuyCostPreview(stocksList.getSelectionModel().getSelectedItem(), clamped); } private void syncQuantityFromSlider(int newValue) { @@ -462,6 +666,7 @@ private void syncQuantityFromSlider(int newValue) { quantityField.setText(String.valueOf(clamped)); quantitySlider.setValue(clamped); updatingQuantityControls = false; + updateBuyCostPreview(stocksList.getSelectionModel().getSelectedItem(), clamped); } private int parseQuantity(String text) { @@ -488,6 +693,107 @@ private String formatStock(Stock stock) { return ViewUtils.formatStock(stock); } + private void updateBuyCostPreview(Stock stock, int quantity) { + if (stock == null || quantity <= 0) { + buyCostPreviewLabel.setText(""); + return; + } + Share tempShare = new Share(stock, BigDecimal.valueOf(quantity), stock.getSalesPrice()); + PurchaseCalculator calc = new PurchaseCalculator(tempShare); + buyCostPreviewLabel.setText( + "Gross: " + + ViewUtils.formatMoney(calc.calculateGross()) + + " Commission: " + + ViewUtils.formatMoney(calc.calculateCommission()) + + " Total: " + + ViewUtils.formatMoney(calc.calculateTotal())); + } + + private void updateSellCostPreview(Share share) { + if (share == null) { + sellCostPreviewLabel.setText(""); + return; + } + SaleCalculator calc = new SaleCalculator(share); + sellCostPreviewLabel.setText( + "Gross: " + + ViewUtils.formatMoney(calc.calculateGross()) + + " Commission: " + + ViewUtils.formatMoney(calc.calculateCommission()) + + " Tax: " + + ViewUtils.formatMoney(calc.calculateTax()) + + " Net: " + + ViewUtils.formatMoney(calc.calculateTotal())); + } + + private Tab createMarketTab() { + gainersTable.setPlaceholder(new Label("Advance a week to see data")); + losersTable.setPlaceholder(new Label("Advance a week to see data")); + + for (TableView table : new TableView[] {gainersTable, losersTable}) { + TableColumn symCol = new TableColumn<>("Symbol"); + symCol.setPrefWidth(100); + symCol.setCellValueFactory(d -> new SimpleStringProperty(d.getValue().getSymbol())); + + TableColumn nameCol = new TableColumn<>("Company"); + nameCol.setPrefWidth(200); + nameCol.setCellValueFactory(d -> new SimpleStringProperty(d.getValue().getCompany())); + + TableColumn priceCol = new TableColumn<>("Price"); + priceCol.setPrefWidth(100); + priceCol.setCellValueFactory( + d -> new SimpleStringProperty(ViewUtils.formatMoney(d.getValue().getSalesPrice()))); + + TableColumn changeCol = new TableColumn<>("Change"); + changeCol.setPrefWidth(100); + changeCol.setCellValueFactory( + d -> { + BigDecimal change = d.getValue().getLatestPriceChange(); + String sign = change.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; + return new SimpleStringProperty(sign + ViewUtils.formatMoney(change)); + }); + changeCol.setCellFactory( + col -> + new TableCell<>() { + @Override + protected void updateItem(String val, boolean empty) { + super.updateItem(val, empty); + getStyleClass().removeAll("status-success", "status-error"); + if (empty || val == null) { + setText(null); + return; + } + setText(val); + getStyleClass().add(val.startsWith("+") ? "status-success" : "status-error"); + } + }); + table.getColumns().addAll(symCol, nameCol, priceCol, changeCol); + } + + Label gainersTitle = new Label("TOP GAINERS"); + gainersTitle.getStyleClass().add("section-title"); + Label losersTitle = new Label("TOP LOSERS"); + losersTitle.getStyleClass().add("section-title"); + + VBox gainersBox = new VBox(6, gainersTitle, gainersTable); + VBox.setVgrow(gainersTable, Priority.ALWAYS); + VBox losersBox = new VBox(6, losersTitle, losersTable); + VBox.setVgrow(losersTable, Priority.ALWAYS); + + HBox content = new HBox(16, gainersBox, new Separator(Orientation.VERTICAL), losersBox); + HBox.setHgrow(gainersBox, Priority.ALWAYS); + HBox.setHgrow(losersBox, Priority.ALWAYS); + content.setPadding(new javafx.geometry.Insets(12)); + return new Tab("Market", content); + } + + private void refreshMarket() { + Exchange exchange = controller.getExchange(); + if (exchange == null) return; + gainersTable.getItems().setAll(exchange.getGainers(10)); + losersTable.getItems().setAll(exchange.getLosers(10)); + } + // Listener callbacks update the shared header and the stocks tab. @Override public void onMoneyChanged(BigDecimal newBalance) { diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index 9f9810d..481cff3 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -8,7 +8,6 @@ import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; - import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -18,7 +17,6 @@ import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; -import millions.App; /** The initial game setup screen where the player enters their info. */ public class StartView extends VBox { @@ -35,9 +33,6 @@ public StartView(Stage stage) { setSpacing(12); setPadding(new Insets(40)); - Label infoField = new Label("Select stock file, or press start to use default configuration"); - // infoField.setMaxWidth(250); - nameField = new TextField("user"); nameField.setPromptText("Player name:"); nameField.setMaxWidth(250); @@ -91,19 +86,20 @@ public StartView(Stage stage) { }); startButton = new Button("Start game"); + startButton.getStyleClass().add("btn-primary"); startButton.setDisable(true); - Label title = new Label("Millions"); - title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); + javafx.scene.layout.HBox title = ViewUtils.buildTitle(); + title.getStyleClass().add("title-hero"); + title.setAlignment(javafx.geometry.Pos.CENTER); getChildren() .addAll( title, - nameField, - startingAmountField, - preRunWeeksField, - infoField, - filepickerButton, + fieldBox("Player name", nameField), + fieldBox("Starting amount ($)", startingAmountField), + fieldBox("Pre-run weeks", preRunWeeksField), + fieldBox("Stock file", filepickerButton), startButton); checkStartButtonValid(); @@ -167,4 +163,13 @@ public File getSelectedFile() { public Button getStartButton() { return startButton; } + + private static javafx.scene.layout.VBox fieldBox(String labelText, javafx.scene.Node field) { + Label label = new Label(labelText); + label.getStyleClass().add("section-title"); + javafx.scene.layout.VBox box = new javafx.scene.layout.VBox(4, label, field); + box.setAlignment(Pos.CENTER_LEFT); + box.setMaxWidth(250); + return box; + } } diff --git a/src/main/java/millions/view/ViewUtils.java b/src/main/java/millions/view/ViewUtils.java index 5500438..a554e0d 100644 --- a/src/main/java/millions/view/ViewUtils.java +++ b/src/main/java/millions/view/ViewUtils.java @@ -1,8 +1,12 @@ package millions.view; import java.math.BigDecimal; +import java.math.RoundingMode; +import javafx.geometry.Pos; import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; import javafx.scene.control.ListCell; +import javafx.scene.layout.HBox; import millions.model.Share; import millions.model.Stock; import millions.model.Transaction; @@ -12,6 +16,20 @@ public final class ViewUtils { private ViewUtils() {} + public static String formatMoney(BigDecimal value) { + return value.setScale(2, RoundingMode.HALF_UP).toPlainString() + "$"; + } + + public static HBox buildTitle() { + Label million = new Label("MILLION"); + million.getStyleClass().add("title-main"); + Label dollar = new Label("$"); + dollar.getStyleClass().add("title-dollar"); + HBox box = new HBox(0, million, dollar); + box.setAlignment(Pos.CENTER_LEFT); + return box; + } + public static void configureOwnedSharesBox(ComboBox comboBox) { comboBox.setCellFactory( box -> @@ -19,12 +37,12 @@ public static void configureOwnedSharesBox(ComboBox comboBox) { @Override protected void updateItem(Share share, boolean empty) { super.updateItem(share, empty); + getStyleClass().removeAll("status-success", "status-error"); if (empty || share == null) { setText(null); - setStyle(""); } else { setText(formatOwnedShare(share)); - setStyle(getProfitStyle(share)); + getStyleClass().add(getProfitClass(share)); } } }); @@ -33,31 +51,39 @@ protected void updateItem(Share share, boolean empty) { @Override protected void updateItem(Share share, boolean empty) { super.updateItem(share, empty); + getStyleClass().removeAll("status-success", "status-error"); if (empty || share == null) { setText("Select lot"); - setStyle(""); } else { setText(formatOwnedShare(share)); - setStyle(getProfitStyle(share)); + getStyleClass().add(getProfitClass(share)); } } }); } public static String formatStock(Stock stock) { - return stock.getSymbol() + " - " + stock.getCompany() + " (" + stock.getSalesPrice() + ")"; + return stock.getSymbol() + + " - " + + stock.getCompany() + + " (" + + formatMoney(stock.getSalesPrice()) + + ")"; } public static String formatOwnedShare(Share share) { BigDecimal profit = getShareProfit(share); String sign = profit.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; - return share.getQuantity() + "|" + share.getPurchasePrice() + "|" + sign + profit; + return share.getQuantity() + + " @ " + + formatMoney(share.getPurchasePrice()) + + " " + + sign + + formatMoney(profit); } - public static String getProfitStyle(Share share) { - return getShareProfit(share).compareTo(BigDecimal.ZERO) >= 0 - ? "-fx-text-fill: green;" - : "-fx-text-fill: red;"; + public static String getProfitClass(Share share) { + return getShareProfit(share).compareTo(BigDecimal.ZERO) >= 0 ? "status-success" : "status-error"; } public static BigDecimal getShareProfit(Share share) { diff --git a/src/main/resources/styles/millions.css b/src/main/resources/styles/millions.css new file mode 100644 index 0000000..8e05692 --- /dev/null +++ b/src/main/resources/styles/millions.css @@ -0,0 +1,190 @@ +.root { + -fx-font-size: 14px; + -fx-background-color: bg; + + bg: #1e1e2e; + bg-elevated: #252535; + bg-hover: #2a2a3e; + bg-component: #2e2e42; + bg-selected: #1a3a2a; + border: #3a3a4a; + text-primary: #e8e8f0; + text-muted: #8888a0; + accent: #22c55e; + danger: #ef4444; +} + +.header-panel { + -fx-background-color: bg; + -fx-padding: 10px 16px; + -fx-border-color: accent border border border; + -fx-border-width: 2px 0 1px 0; + -fx-spacing: 20px; +} + +.toolbar-panel { + -fx-background-color: bg; + -fx-padding: 10px 14px; + -fx-border-color: border; + -fx-border-width: 1px 0 0 0; +} + +.label { -fx-text-fill: text-primary; } +.status-success { -fx-text-fill: accent; } +.status-error { -fx-text-fill: danger; } + +.title { + -fx-font-size: 32px; + -fx-font-weight: bold; + -fx-text-fill: accent; +} + +.title-main, .title-dollar, .game-over-title { + -fx-font-weight: bold; + -fx-font-family: "Georgia"; + -fx-font-style: italic; +} + +.title-main { -fx-font-size: 32px; -fx-text-fill: text-primary; } +.title-dollar { -fx-font-size: 36px; -fx-text-fill: accent; } +.game-over-title { -fx-font-size: 56px; -fx-text-fill: text-primary; } + +.title-hero .title-main { -fx-font-size: 80px; } +.title-hero .title-dollar { -fx-font-size: 88px; } + +.selected-stock-label { + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-text-fill: accent; +} + +.section-title { + -fx-font-size: 11px; + -fx-font-weight: bold; + -fx-text-fill: text-muted; +} + +.cost-preview { -fx-font-size: 11px; -fx-text-fill: text-muted; -fx-font-family: "monospace"; } +.stat-value { -fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: text-primary; } + +.button { + -fx-background-color: bg-component; + -fx-text-fill: text-primary; + -fx-background-radius: 6px; + -fx-border-color: border; + -fx-border-radius: 6px; + -fx-border-width: 1px; + -fx-padding: 6px 14px; + -fx-cursor: hand; +} + +.button:hover { -fx-background-color: #3a3a52; -fx-border-color: accent; } +.button:pressed { -fx-background-color: #22283a; } +.button:disabled { -fx-opacity: 0.4; } + +.btn-primary { + -fx-background-color: derive(accent, -20%); + -fx-text-fill: white; + -fx-border-color: accent; + -fx-font-weight: bold; +} + +.btn-primary:hover { -fx-background-color: accent; -fx-border-color: derive(accent, 25%); } +.btn-primary:pressed { -fx-background-color: derive(accent, -35%); } + +.btn-danger { + -fx-background-color: derive(danger, -65%); + -fx-text-fill: derive(danger, 40%); + -fx-border-color: danger; +} + +.btn-danger:hover { -fx-background-color: derive(danger, -55%); -fx-border-color: derive(danger, 15%); } +.btn-danger:pressed { -fx-background-color: derive(danger, -75%); } + +.text-field { + -fx-background-color: bg; + -fx-text-fill: text-primary; + -fx-prompt-text-fill: text-muted; + -fx-background-radius: 6px; + -fx-border-color: border; + -fx-border-radius: 6px; + -fx-border-width: 1px; + -fx-padding: 5px 10px; +} + +.text-field:focused, +.combo-box-base:focused { -fx-border-color: accent; } + +.combo-box, .combo-box-base { + -fx-background-color: bg-component; + -fx-background-radius: 6px; + -fx-border-color: border; + -fx-border-radius: 6px; + -fx-border-width: 1px; +} + +.combo-box-base .list-cell { -fx-text-fill: text-primary; } +.combo-box-popup .list-view { -fx-background-color: bg-elevated; -fx-border-color: border; } + +.tab-header-background { -fx-background-color: bg; } + +.tab-pane .tab { + -fx-background-color: bg-elevated; + -fx-background-radius: 6 6 0 0; + -fx-padding: 6px 16px; +} + +.tab-pane .tab:selected { + -fx-background-color: bg; + -fx-border-color: accent transparent transparent transparent; + -fx-border-width: 2px 0 0 0; +} + +.tab-pane .tab-label { -fx-text-fill: text-muted; -fx-font-size: 13px; -fx-font-weight: bold; } +.tab-pane .tab:selected .tab-label { -fx-text-fill: text-primary; } + +.slider .track { -fx-background-color: border; -fx-background-radius: 4px; } +.slider .thumb { -fx-background-color: accent; -fx-background-radius: 50%; -fx-effect: none; } + +.list-view, .table-view { + -fx-background-color: bg; + -fx-control-inner-background: bg; + -fx-border-color: border; + -fx-border-width: 1px; +} + +.list-view { -fx-background-radius: 6px; } +.table-view { -fx-table-cell-border-color: bg-component; -fx-table-header-border-color: border; } + +.table-view .column-header, +.table-view .filler { + -fx-background-color: bg-elevated; + -fx-border-color: border; + -fx-border-width: 0 1px 1px 0; + -fx-padding: 6px 8px; +} + +.table-row-cell { + -fx-background-color: bg; + -fx-text-background-color: text-primary; + -fx-border-color: transparent transparent bg-component transparent; + -fx-border-width: 1px; +} + +/* Every other cell differently collored */ +.table-row-cell:odd, .list-cell:odd { + -fx-background-color: bg-elevated; +} + +.table-row-cell:hover, .list-cell:hover { + -fx-background-color: bg-hover; +} +.table-row-cell:selected { -fx-background-color: bg-selected; } +.table-cell { -fx-text-fill: text-primary; -fx-padding: 6px 8px; } + +.list-cell { -fx-background-color: transparent; -fx-text-fill: text-primary; -fx-padding: 6px 10px; } +.list-cell:selected { -fx-background-color: bg-selected; -fx-text-fill: derive(accent, 25%); } + +.chart, .chart-plot-background, .chart-content { -fx-background-color: bg; } +.chart { -fx-padding: 10px; } +.chart-series-line { -fx-stroke: accent; -fx-stroke-width: 2px; }