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/Controller/AppControllers/StockAppController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/AppControllers/StockAppController.java
index b08777c..d0ab31e 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
@@ -11,6 +11,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;
@@ -31,6 +32,12 @@ public final class StockAppController implements AppController {
private Stock currentStock;
private AudioManager audioManager = AudioManager.getInstance();
+ /** 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.
*
@@ -164,10 +171,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;
}
@@ -210,7 +253,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 f83896a..9e554aa 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
@@ -52,7 +52,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/MarketController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java
index 478d449..41294bd 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/MarketController.java
@@ -15,7 +15,6 @@
public final class MarketController {
private Exchange exchange;
private final int stockResolution;
- private StockApp stockApp;
/**
* Constructs a new MarketController and starts the market.
@@ -64,8 +63,10 @@ public void updateMarket() {
* Starts the market by performing initial updates.
*/
public void startMarket() {
- for (int i = 0; i < stockResolution; i++) {
- updateMarket();
+ for (Stock stock : exchange.getAllStocks()) {
+ stock.fakeHistory(stockResolution);
+ stock.updateStockGraph();
+
}
}
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 4805405..b2a0df1 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,6 +5,7 @@
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.layout.BorderPane;
import javafx.scene.layout.Pane;
@@ -55,6 +56,10 @@ public List getPopups() {
return popups;
}
+ public void bindOpenButton(Button button, App type){
+ button.setOnAction(event -> show(type));
+ }
+
private void makeDraggable(Popup popup) {
BorderPane root = popup.getRoot();
double[] offset = new double[2];
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 247e217..89554bb 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
@@ -39,8 +39,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();
@@ -56,19 +54,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");
}
}
@@ -112,13 +107,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..a648674 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 gameover 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.show();
+ 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.configure(
+ "💔",
+ "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.configure(
+ "🔒",
+ "Market Closed",
+ "Trading is suspended on weekends.\nThe market reopens on Monday.",
+ "Got it"
+ );
+ warningApp.getPrimaryButton().setOnAction(event -> dismissWarning());
+ showWarning();
+ }
+
+ private void showWarning() {
+ warningApp.show();
+ 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.show();
+ 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,12 +280,8 @@ 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));
@@ -175,38 +296,17 @@ public Button createAppButton(final App type) {
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..770211e 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.configure(
+ "⚠",
+ "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.show();
+ 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/Model/Stock.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Stock.java
index 7c847ce..a8f4f09 100644
--- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Stock.java
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Model/Stock.java
@@ -16,7 +16,7 @@ public class Stock {
private final String symbol;
private final String company;
private StockGraph stockGraph;
- private final List prices = new ArrayList<>();
+ private List prices = new ArrayList<>();
/**
* Constructs a new stock with symbol, company name, and initial sales price.
@@ -124,6 +124,13 @@ public StockGraph getStockGraph() {
return stockGraph;
}
+ public void fakeHistory(int stockResolution) {
+ for (int i = 0; i < stockResolution; i++) {
+ updatePrice();
+ }
+ prices = prices.reversed();
+ }
+
/**
* Updates the stock graph if it is visible.
*/
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..033a7f9
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/GameOverApp.java
@@ -0,0 +1,59 @@
+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;
+ }
+
+ public void show(){
+ getRoot().setVisible(true);
+ getRoot().toFront();
+ }
+}
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..422e298
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WarningApp.java
@@ -0,0 +1,135 @@
+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.
+ *
+ * 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 configure(
+ 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 configure(
+ 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());
+ }
+
+ public void show(){
+ getRoot().setVisible(true);
+ getRoot().toFront();
+ }
+
+ /** 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..89dc2f5
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/WeekendRapportApp.java
@@ -0,0 +1,252 @@
+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:
+ *
+ * - Player summary: the week's net income, large and color-coded
+ * - Verdict: a one-line mood label (e.g. "Strong week")
+ * - Receipts: each transaction rendered with the shared {@link Receipt}
+ * - Footer: cash, rent, and balance — with the final balance large and colored
+ *
+ *
+ * Dismissed only via the "Continue to Sunday" button or by clicking the
+ * dim overlay (wired by {@code DesktopViewController}).
+ */
+public class WeekendRapportApp extends Popup {
+
+ private static final BigDecimal STRONG_WEEK_THRESHOLD = new BigDecimal("50.00");
+
+ private final Label playerAmountLabel = new Label();
+ private final Label playerCaptionLabel = 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);
+
+ getCloseButton().setOnAction(event -> 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() {
+ playerAmountLabel.getStyleClass().add("rapport-player-amount");
+ playerCaptionLabel.getStyleClass().add("rapport-player-caption");
+ verdictLabel.getStyleClass().add("rapport-verdict");
+
+ VBox playerAmountBlock = new VBox(2, playerAmountLabel, playerCaptionLabel);
+ playerAmountBlock.setAlignment(Pos.CENTER);
+
+ VBox playerBlock = new VBox(10, playerAmountBlock, verdictLabel);
+ playerBlock.setAlignment(Pos.CENTER);
+ playerBlock.getStyleClass().add("rapport-player-block");
+ playerBlock.setPadding(new Insets(20, 16, 20, 16));
+
+ Label transactionHeader = sectionHeader("Transactions");
+
+ ScrollPane scrollPane = new ScrollPane(receiptList);
+ scrollPane.setFitToWidth(true);
+ scrollPane.getStyleClass().add("rapport-scroll");
+ VBox.setVgrow(scrollPane, Priority.ALWAYS);
+
+ cashLabel.getStyleClass().add("rapport-line-value");
+ rentLabel.getStyleClass().addAll("rapport-line-value", "rapport-rent-value");
+
+ HBox cashRow = footerRow("Cash Available", cashLabel);
+ HBox rentRow = footerRow("Rent Due", rentLabel);
+ rentRow.getStyleClass().add("rapport-rent-row");
+
+ Region dividerRule = new Region();
+ dividerRule.getStyleClass().add("rapport-rule");
+ dividerRule.setMinHeight(1);
+ dividerRule.setMaxWidth(Double.MAX_VALUE);
+
+ Label balanceHeaderLabel = new Label("Balance After Rent");
+ balanceHeaderLabel.getStyleClass().add("rapport-balance-header");
+ balanceLabel.getStyleClass().add("rapport-balance-value");
+
+ Region balanceSpacer = new Region();
+ HBox.setHgrow(balanceSpacer, Priority.ALWAYS);
+
+ HBox balanceRow = new HBox();
+ balanceRow.getChildren().addAll(balanceHeaderLabel, balanceSpacer, balanceLabel);
+ balanceRow.setAlignment(Pos.CENTER_LEFT);
+ balanceRow.setPadding(new Insets(2, 0, 0, 0));
+
+ VBox footer = new VBox(8, cashRow, rentRow, dividerRule, balanceRow);
+
+ content.getChildren().setAll(
+ playerBlock,
+ transactionHeader,
+ scrollPane,
+ footer,
+ continueButton
+ );
+ }
+
+ private static Label sectionHeader(String text) {
+ Label label = new Label(text);
+ label.getStyleClass().add("rapport-section-header");
+ return label;
+ }
+
+ private static HBox footerRow(String labelText, Label valueLabel) {
+ Label keyLabel = new Label(labelText);
+ keyLabel.getStyleClass().add("rapport-line-key");
+
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ HBox row = new HBox(keyLabel, 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 minus 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(
+ List transactions,
+ BigDecimal netIncome,
+ BigDecimal cashBeforeRent,
+ BigDecimal rent,
+ BigDecimal cashAfterRent
+ ) {
+ playerAmountLabel.setText(formatSigned(netIncome));
+ applyMoodStyle(playerAmountLabel, moodStyleFor(netIncome));
+
+ verdictLabel.setText(computeVerdict(netIncome, cashAfterRent));
+ applyMoodStyle(verdictLabel, moodStyleFor(netIncome, cashAfterRent));
+
+ receiptList.getChildren().clear();
+
+ if (transactions.isEmpty()) {
+ Label emptyMessage = new Label("No transactions this week.");
+ emptyMessage.getStyleClass().add("rapport-line-key");
+ emptyMessage.setPadding(new Insets(20));
+ receiptList.getChildren().add(emptyMessage);
+ } else {
+ for (Transaction transaction : transactions) {
+ receiptList.getChildren().add(Receipt.forTransaction(transaction));
+ }
+ }
+
+ 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;
+ }
+
+ private static String computeVerdict(BigDecimal netIncome, BigDecimal balance) {
+ if (balance.signum() < 0) {
+ return "Week ended in debt";
+ }
+ if (netIncome.compareTo(STRONG_WEEK_THRESHOLD) >= 0) {
+ return "Strong week";
+ }
+ if (netIncome.signum() > 0) {
+ return "Made some money";
+ }
+ if (netIncome.signum() == 0) {
+ return "Breaking even";
+ }
+ return "Rough week";
+ }
+
+ private static String moodStyleFor(BigDecimal netIncome) {
+ if (netIncome.signum() > 0) {
+ return "mood-positive";
+ }
+ if (netIncome.signum() < 0) {
+ return "mood-negative";
+ }
+ return "mood-neutral";
+ }
+
+ private static String moodStyleFor(BigDecimal netIncome, BigDecimal balance) {
+ if (balance.signum() < 0) {
+ return "mood-negative";
+ }
+ if (netIncome.compareTo(STRONG_WEEK_THRESHOLD) >= 0) {
+ return "mood-positive";
+ }
+ if (netIncome.signum() >= 0) {
+ return "mood-neutral";
+ }
+ return "mood-negative";
+ }
+
+ private static void applyMoodStyle(Label label, String moodStyle) {
+ label.getStyleClass().removeAll("mood-positive", "mood-negative", "mood-neutral");
+ label.getStyleClass().add(moodStyle);
+ }
+
+ private static String formatUsd(BigDecimal amount) {
+ return "$" + String.format("%,.2f", amount.abs());
+ }
+
+ private static String formatSigned(BigDecimal amount) {
+ String sign = amount.signum() >= 0 ? "+" : "−";
+ return sign + "$" + String.format("%,.2f", amount.abs());
+ }
+
+ static String formatBalance(BigDecimal amount) {
+ if (amount.signum() < 0) {
+ return "$" + String.format("%,.2f", amount.negate()) + " in debt";
+ }
+ return "$" + String.format("%,.2f", amount);
+ }
+
+ public void show(){
+ getRoot().setVisible(true);
+ getRoot().toFront();
+ }
+}
\ No newline at end of file
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..26a4d40 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,87 @@ 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");
+ nextWeekButton.getStyleClass().add("primary-button");
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,34 +314,36 @@ 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);
+ HBox box = new HBox(10, weather, temp, clock, day, week);
box.setAlignment(Pos.CENTER_RIGHT);
box.setMinWidth(Region.USE_PREF_SIZE);
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..40680d1 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;
}