Skip to content

Commit

Permalink
Merge branch 'main' into 129-final-refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
tommyah committed May 27, 2026
2 parents 483c84a + 3e7d752 commit 77fbb99
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 40 deletions.
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

0 comments on commit 77fbb99

Please sign in to comment.