Skip to content

sell portfolio og sell portfolio, save og quit. + økt min with #163

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 24 additions & 19 deletions src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,29 @@ public void start(final Stage stage) throws Exception {
gameEngineController, clickerGame, findStockGame, timeInputsGame
);

// In-game quit confirmation dialog: replaces the previous immediate
// "auto-save and return to main menu" behaviour with a popup that
// lets the player choose Continue / Save / Save and quit to main
// menu / Save and quit game / Sell portfolio, save and quit.
//
// Constructed up here (before the STATE_RESET subscriber below) so
// the subscriber can rebind it to the freshly loaded player/exchange
// alongside the other controllers.
QuitDialogView quitDialogView = new QuitDialogView();
QuitDialogController quitDialogController =
new QuitDialogController(quitDialogView, eventManager, inGameView,
gameStateLoader, saveGameService, player, exchange);
quitDialogController.setOnExitApplication(() -> {
try {
javafx.application.Platform.exit();
} finally {
// Fallback in case there are non-daemon background threads
// keeping the JVM alive after JavaFX shuts down.
System.exit(0);
}
});
topBarController.setOnQuitRequested(quitDialogController::show);

// Adds a generic event subscriber to the event manager
// that handles STATE_RESET events. These events are
// triggered when a new save file is loaded.
Expand Down Expand Up @@ -209,6 +232,7 @@ public <T> void handleEvent(final EventData<T> eventData) {
dashBoardController.handleStockPoolUpdate(dynamicStocks);
statsController.handleContextUpdate(Main.this.exchange, Main.this.player);
marketController.handleStockPoolUpdate(Main.this.exchange, Main.this.player, dynamicStocks);
quitDialogController.handleContextUpdate(Main.this.exchange, Main.this.player);

if (!dynamicStocks.isEmpty()) {
miniGamesController.setActiveStock(dynamicStocks.getFirst());
Expand All @@ -222,25 +246,6 @@ public <T> void handleEvent(final EventData<T> eventData) {
new InGameSettingsController(inGameSettingsView, eventManager, inGameView);
topBarController.setSettingsAction(inGameSettingsController::show);

// In-game quit confirmation dialog: replaces the previous immediate
// "auto-save and return to main menu" behaviour with a popup that
// lets the player choose Continue / Save / Save and quit to main
// menu / Save and quit game.
QuitDialogView quitDialogView = new QuitDialogView();
QuitDialogController quitDialogController =
new QuitDialogController(quitDialogView, eventManager, inGameView,
gameStateLoader, saveGameService);
quitDialogController.setOnExitApplication(() -> {
try {
javafx.application.Platform.exit();
} finally {
// Fallback in case there are non-daemon background threads
// keeping the JVM alive after JavaFX shuts down.
System.exit(0);
}
});
topBarController.setOnQuitRequested(quitDialogController::show);

topBarController.setOnQuitToMainMenu(() -> {
System.out.println("[auto-save] Quit triggered, attempting snapshot...");
SaveGame snapshot = gameStateLoader.snapshotActiveSave();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public enum ConfigValues {
/**
* Standard viewport width.
*/
VIEWPORT_WIDTH(800),
VIEWPORT_WIDTH(1200),

/**
* Standard viewport height.
Expand All @@ -17,7 +17,7 @@ public enum ConfigValues {
/**
* Minimum viewport width. The window cannot be resized smaller than this.
*/
MIN_VIEWPORT_WIDTH(800),
MIN_VIEWPORT_WIDTH(1200),

/**
* Minimum viewport height. The window cannot be resized smaller than this.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
*
* <p>The quit dialog is shown when the player clicks the "Quit" button
* from the in-game dashboard, and lets them choose between continuing,
* saving in place, saving and returning to the main menu, or saving
* and exiting the application.</p>
* saving in place, saving and returning to the main menu, saving and
* exiting the application, or selling the entire portfolio before
* saving and returning to the main menu.</p>
* */
public enum QuitDialogActions {
/** Closes the dialog and returns the player to the game. */
Expand All @@ -19,5 +20,11 @@ public enum QuitDialogActions {
SAVE_AND_QUIT_TO_MAIN_MENU,

/** Saves the current game state and exits the application. */
SAVE_AND_QUIT_GAME;
SAVE_AND_QUIT_GAME,

/**
* Sells every share the player owns, then saves the game and returns
* to the main menu.
* */
SELL_ALL_AND_QUIT;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.quit;

import edu.ntnu.idi.idatt2003.g40.mappe.engine.Exchange;
import edu.ntnu.idi.idatt2003.g40.mappe.model.Player;
import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame;
import edu.ntnu.idi.idatt2003.g40.mappe.model.Share;
import edu.ntnu.idi.idatt2003.g40.mappe.service.GameStateLoader;
import edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService;
import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData;
Expand All @@ -11,10 +14,14 @@
import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewEnum;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView;

import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Controller for {@link QuitDialogView}.
*
* <p>Wires the four dialog buttons to the appropriate save / scene-change
* <p>Wires the five dialog buttons to the appropriate save / scene-change
* / application-exit actions:</p>
* <ul>
* <li>{@code CONTINUE} - hides the overlay; the player stays in the
Expand All @@ -26,6 +33,10 @@
* hides the overlay, and changes the scene back to the main menu.</li>
* <li>{@code SAVE_AND_QUIT_GAME} - snapshots, writes to disk, then
* invokes the configured exit-application runnable to close the JVM.</li>
* <li>{@code SELL_ALL_AND_QUIT} - liquidates every position in the
* player's portfolio (via the same exchange API the dashboard uses),
* then snapshots, writes to disk, hides the overlay, and changes the
* scene back to the main menu.</li>
* </ul>
*
* <p>The save logic mirrors the auto-save hook in {@code Main}: a missing
Expand All @@ -45,6 +56,24 @@ public final class QuitDialogController
/** Persists save games to disk. */
private final SaveGameService saveGameService;

/**
* The {@link Player} whose portfolio is liquidated by the
* "Sell portfolio, save and quit" button.
*
* <p>Not final because the active player can be swapped out when a
* save is loaded; {@link #handleContextUpdate} is the entry point
* the host application uses to rebind it.</p>
* */
private Player player;

/**
* The {@link Exchange} used to execute the sell-all transactions.
*
* <p>Like {@link #player}, this is reassigned when a save is loaded
* via {@link #handleContextUpdate}.</p>
* */
private Exchange exchange;

/** Runnable invoked to close the application. */
private Runnable onExitApplication = () -> { };

Expand All @@ -58,23 +87,33 @@ public final class QuitDialogController
* @param inGameView the in-game view that hosts this overlay.
* @param gameStateLoader the loader used to snapshot the active save.
* @param saveGameService the service used to persist saves to disk.
* @param player the active {@link Player}, whose portfolio
* is liquidated by the sell-all-and-quit flow.
* @param exchange the active {@link Exchange}, used to execute
* the sell transactions.
*
* @throws IllegalArgumentException if any constructor argument is null.
* */
public QuitDialogController(final QuitDialogView view,
final EventManager eventManager,
final InGameView inGameView,
final GameStateLoader gameStateLoader,
final SaveGameService saveGameService)
final SaveGameService saveGameService,
final Player player,
final Exchange exchange)
throws IllegalArgumentException {
this.inGameView = inGameView;
this.gameStateLoader = gameStateLoader;
this.saveGameService = saveGameService;
this.player = player;
this.exchange = exchange;
super(view, eventManager);
eventManager.addSubscriber(this, EventType.SHOW_QUIT_OPTIONS);
if (inGameView == null
|| gameStateLoader == null
|| saveGameService == null) {
|| saveGameService == null
|| player == null
|| exchange == null) {
throw new IllegalArgumentException(
"Invalid QuitDialogController arguments!");
}
Expand Down Expand Up @@ -124,6 +163,68 @@ protected void initInteractions() {
onExitApplication.run();
}
});

getViewElement().setOnAction(QuitDialogActions.SELL_ALL_AND_QUIT, () -> {
sellEntirePortfolio();
if (performSave()) {
close();
changeScene(ViewEnum.MAIN_MENU);
}
});
}

/**
* Sells every share the player currently owns, one ticker at a time.
*
* <p>Uses the same {@link Exchange#sell(BigDecimal, String, Player)}
* entry point as the dashboard's sell button and the stats widget's
* sell-all button, so commissions, taxes and net-worth listeners all
* fire exactly as if the player had sold each ticker manually.</p>
*
* <p>If the portfolio is already empty this is a no-op.</p>
* */
private void sellEntirePortfolio() {
// Snapshot first so we don't iterate over a list that mutates
// while we sell.
Map<String, BigDecimal> totalsBySymbol = new LinkedHashMap<>();
for (Share s : player.getPortfolio().getShares()) {
String symbol = s.getStock().getSymbol();
totalsBySymbol.merge(symbol, s.getQuantity(), BigDecimal::add);
}

for (Map.Entry<String, BigDecimal> entry : totalsBySymbol.entrySet()) {
if (entry.getValue().signum() <= 0) {
continue;
}
try {
exchange.sell(entry.getValue(), entry.getKey(), player);
} catch (IllegalArgumentException e) {
System.err.println("[quit-dialog] Sell-all skipped " + entry.getKey()
+ ": " + e.getMessage());
}
}
}

/**
* Rebinds this controller to a new {@link Player} / {@link Exchange}
* pair when a save is loaded.
*
* <p>Mirrors the {@code handleContextUpdate} hook on
* {@code StatsController} - the host application reassigns its
* own player/exchange fields when a {@code STATE_RESET} event fires,
* and must call this so the sell-all-and-quit flow operates on the
* newly active save instead of the original starting state.</p>
*
* @param updatedExchange the current active exchange engine.
* @param updatedPlayer the current active player context profile.
* */
public void handleContextUpdate(final Exchange updatedExchange,
final Player updatedPlayer) {
if (updatedExchange == null || updatedPlayer == null) {
return;
}
this.exchange = updatedExchange;
this.player = updatedPlayer;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
* top bar.
*
* <p>The root of this view is a dimmer {@link StackPane} that covers the
* current center widget. Inside, a centered panel hosts four action buttons
* current center widget. Inside, a centered panel hosts five action buttons
* that let the player choose how to leave (or stay in) the game.</p>
*
* <p>Layout: a header row with the title and a close (X) button, a short
* description, and a vertical stack of four buttons: Continue, Save,
* Save and quit to main menu, Save and quit game.</p>
* description, and a vertical stack of five buttons: Continue, Save,
* Save and quit to main menu, Save and quit game, Sell portfolio save
* and quit.</p>
*
* <p>This view is purely presentational - the {@link QuitDialogController}
* wires the buttons to the actual save/quit logic.</p>
Expand All @@ -44,6 +45,11 @@ public final class QuitDialogView
/** Saves and exits the application. */
private Button saveAndQuitGameButton;

/**
* Sells the entire portfolio, then saves and returns to the main menu.
* */
private Button sellAllAndQuitButton;

/** Label used to communicate save status to the player. */
private Label statusLabel;

Expand Down Expand Up @@ -107,36 +113,43 @@ protected void initLayout() {
description.getStyleClass().add("quit-dialog-description");
description.setWrapText(true);

// The four action buttons. Each is full-width inside the panel so the
// The five action buttons. Each is full-width inside the panel so the
// longer labels do not overflow.
continueButton = new Button("Continue");
saveButton = new Button("Save");
saveAndQuitMainMenuButton =
new Button("Save and quit to main menu");
saveAndQuitGameButton = new Button("Save and quit game");
sellAllAndQuitButton =
new Button("Sell portfolio, save and quit");

continueButton.setFocusTraversable(false);
saveButton.setFocusTraversable(false);
saveAndQuitMainMenuButton.setFocusTraversable(false);
saveAndQuitGameButton.setFocusTraversable(false);
sellAllAndQuitButton.setFocusTraversable(false);

continueButton.getStyleClass().addAll(
"quit-dialog-button", "quit-dialog-button-primary");
saveButton.getStyleClass().add("quit-dialog-button");
saveAndQuitMainMenuButton.getStyleClass().add("quit-dialog-button");
saveAndQuitGameButton.getStyleClass().addAll(
"quit-dialog-button", "quit-dialog-button-danger");
sellAllAndQuitButton.getStyleClass().addAll(
"quit-dialog-button", "quit-dialog-button-danger");

continueButton.setMaxWidth(Double.MAX_VALUE);
saveButton.setMaxWidth(Double.MAX_VALUE);
saveAndQuitMainMenuButton.setMaxWidth(Double.MAX_VALUE);
saveAndQuitGameButton.setMaxWidth(Double.MAX_VALUE);
sellAllAndQuitButton.setMaxWidth(Double.MAX_VALUE);

VBox buttonColumn = new VBox(10,
continueButton,
saveButton,
saveAndQuitMainMenuButton,
saveAndQuitGameButton);
saveAndQuitGameButton,
sellAllAndQuitButton);
buttonColumn.setAlignment(Pos.CENTER);

// Status label - hidden until the controller writes something into it.
Expand All @@ -156,9 +169,9 @@ protected void initLayout() {
// shrink when the status label appears/disappears, and so it stays
// visually aligned with the settings overlay (which also has a
// fixed height).
panel.setMinHeight(420);
panel.setPrefHeight(420);
panel.setMaxHeight(420);
panel.setMinHeight(480);
panel.setPrefHeight(480);
panel.setMaxHeight(480);
panel.getStyleClass().add("quit-dialog-panel");

// Dimmer wrapper.
Expand All @@ -172,6 +185,8 @@ protected void initLayout() {
saveAndQuitMainMenuButton);
registerButton(QuitDialogActions.SAVE_AND_QUIT_GAME,
saveAndQuitGameButton);
registerButton(QuitDialogActions.SELL_ALL_AND_QUIT,
sellAllAndQuitButton);
// The X button reuses the CONTINUE action so we don't need a
// dedicated enum entry just for "close".
closeButton.setOnAction(e -> continueButton.fire());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
/**
* Enum representing all interactable actions in {@link StatsView}.
*
* <p>The stats widget is mostly an informational dashboard, so it currently
* has no top-level buttons of its own. {@code SELECT_HOLDING} is reserved
* for future use when individual holdings rows become clickable.</p>
* <p>{@code SELECT_HOLDING} is reserved for future use when individual
* holdings rows become clickable. {@code SELL_ENTIRE_PORTFOLIO} is the
* "sell everything" button shown at the bottom of the allocation panel.</p>
* */
public enum StatsActions {
/** Reserved for future holding-row click handling. */
SELECT_HOLDING;
SELECT_HOLDING,

/** Sells every share the player owns, across all tickers. */
SELL_ENTIRE_PORTFOLIO;
}
Loading