Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into 126-min-vindu-størelse
Browse files Browse the repository at this point in the history
  • Loading branch information
tommyah committed May 26, 2026
2 parents f2fba7a + 51c286c commit eb7fef7
Show file tree
Hide file tree
Showing 8 changed files with 1,002 additions and 351 deletions.
407 changes: 213 additions & 194 deletions src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.quit;

/**
* User-triggered actions available in the in-game quit dialog overlay.
*
* <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>
* */
public enum QuitDialogActions {
/** Closes the dialog and returns the player to the game. */
CONTINUE,

/** Saves the current game state without leaving the in-game session. */
SAVE,

/** Saves the current game state and returns to the main menu. */
SAVE_AND_QUIT_TO_MAIN_MENU,

/** Saves the current game state and exits the application. */
SAVE_AND_QUIT_GAME;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.quit;

import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame;
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.EventManager;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewEnum;
import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView;

/**
* Controller for {@link QuitDialogView}.
*
* <p>Wires the four dialog buttons to the appropriate save / scene-change
* / application-exit actions:</p>
* <ul>
* <li>{@code CONTINUE} - hides the overlay; the player stays in the
* current game session.</li>
* <li>{@code SAVE} - snapshots and writes the active save to disk, but
* keeps the overlay open so the player sees the confirmation message.
* They can then choose to continue, quit, or exit.</li>
* <li>{@code SAVE_AND_QUIT_TO_MAIN_MENU} - snapshots, writes to disk,
* 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>
* </ul>
*
* <p>The save logic mirrors the auto-save hook in {@code Main}: a missing
* active save (e.g. before any game has been loaded) is treated as a
* no-op rather than an error, since there's nothing meaningful to write.</p>
* */
public final class QuitDialogController
extends ViewController<QuitDialogView> {

/** The in-game view hosting this overlay. */
private final InGameView inGameView;

/** Builds {@link SaveGame} snapshots of the current player/exchange. */
private final GameStateLoader gameStateLoader;

/** Persists save games to disk. */
private final SaveGameService saveGameService;

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

/**
* Constructor.
*
* @param view the {@link QuitDialogView} this controller is
* attached to.
* @param eventManager the active {@link EventManager}, used to
* publish scene-change events.
* @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.
*
* @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)
throws IllegalArgumentException {
this.inGameView = inGameView;
this.gameStateLoader = gameStateLoader;
this.saveGameService = saveGameService;
super(view, eventManager);
if (inGameView == null
|| gameStateLoader == null
|| saveGameService == null) {
throw new IllegalArgumentException(
"Invalid QuitDialogController arguments!");
}
}

/**
* Installs the runnable used to close the application when the player
* chooses "Save and quit game".
*
* <p>Typically this is wired to {@code Platform.exit()} (plus a
* {@code System.exit(0)} fallback) from {@code Main}, so the controller
* itself stays decoupled from the JavaFX runtime.</p>
*
* @param exitRunnable runnable invoked after the save completes; a null
* value resets the hook to a no-op.
* */
public void setOnExitApplication(final Runnable exitRunnable) {
this.onExitApplication =
(exitRunnable != null) ? exitRunnable : () -> { };
}

/** {@inheritDoc} */
@Override
protected void initInteractions() {
getViewElement().setOnAction(QuitDialogActions.CONTINUE, this::close);

getViewElement().setOnAction(QuitDialogActions.SAVE, () -> {
boolean ok = performSave();
if (ok) {
getViewElement().setStatus("Game saved.", false);
}
// Failures already wrote an error to the status label inside
// performSave(); nothing more to do here.
});

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

getViewElement().setOnAction(QuitDialogActions.SAVE_AND_QUIT_GAME, () -> {
if (performSave()) {
close();
onExitApplication.run();
}
});
}

/**
* Shows the quit dialog overlay on the host {@link InGameView}.
*
* <p>Clears any stale status message left over from a previous opening
* so the player doesn't see "Game saved." from a save they made hours
* ago.</p>
* */
public void show() {
getViewElement().setStatus(null, false);
inGameView.showSettingsOverlay(getViewElement().getRootPane());
}

/**
* Hides the overlay if it's currently visible.
* */
private void close() {
inGameView.hideSettingsOverlay();
}

/**
* Snapshots the active save and writes it to disk.
*
* <p>If no save is currently active (e.g. the player got into the
* in-game view through a path that doesn't load a save), this is
* treated as a successful no-op so the quit / exit flows can proceed.
* Any I/O failure is reported back to the player via the dialog's
* status label.</p>
*
* @return {@code true} when the operation can be considered successful
* (a real save was written, or no save was needed); {@code false}
* when an actual error occurred.
* */
private boolean performSave() {
SaveGame snapshot;
try {
snapshot = gameStateLoader.snapshotActiveSave();
} catch (Exception e) {
System.err.println("[quit-dialog] Snapshot failed: "
+ e.getMessage());
getViewElement().setStatus(
"Could not prepare save: " + e.getMessage(), true);
return false;
}

if (snapshot == null) {
System.out.println(
"[quit-dialog] No active save - nothing to write.");
// Nothing to persist, but the user's intent (continue past this
// step) is still valid - treat as success.
return true;
}

try {
saveGameService.saveGame(snapshot);
System.out.println("[quit-dialog] Wrote save '"
+ snapshot.getName() + "' to disk.");
return true;
} catch (Exception e) {
System.err.println("[quit-dialog] Save failed: " + e.getMessage());
getViewElement().setStatus(
"Save failed: " + e.getMessage(), true);
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.quit;

import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewElement;
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.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;

/**
* Overlay shown when the player clicks the "Quit" button in the in-game
* 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
* 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>
*
* <p>This view is purely presentational - the {@link QuitDialogController}
* wires the buttons to the actual save/quit logic.</p>
* */
public final class QuitDialogView
extends ViewElement<StackPane, QuitDialogActions> {

/** Close button shown in the panel's top-right corner. */
private Button closeButton;

/** Closes the dialog without saving. */
private Button continueButton;

/** Saves the game in place, without leaving. */
private Button saveButton;

/** Saves and returns to the main menu. */
private Button saveAndQuitMainMenuButton;

/** Saves and exits the application. */
private Button saveAndQuitGameButton;

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

/** Modal-style card hosting the dialog controls. */
private VBox panel;

/**
* Constructor.
* */
public QuitDialogView() {
super(new StackPane(), QuitDialogActions.class);
}

/**
* Updates the status text shown below the action buttons.
*
* <p>Used by the controller to tell the player whether the last save
* succeeded or failed.</p>
*
* @param text message to display, or {@code null} / empty to hide.
* @param errorStyle when {@code true}, the message is styled as an error.
* */
public void setStatus(final String text, final boolean errorStyle) {
if (statusLabel == null) {
return;
}
statusLabel.getStyleClass().remove("quit-dialog-status-error");
if (text == null || text.isEmpty()) {
statusLabel.setText("");
statusLabel.setVisible(false);
statusLabel.setManaged(false);
return;
}
statusLabel.setText(text);
statusLabel.setVisible(true);
statusLabel.setManaged(true);
if (errorStyle) {
statusLabel.getStyleClass().add("quit-dialog-status-error");
}
}

/** {@inheritDoc} */
@Override
protected void initLayout() {
// Header row: title + close button.
Label title = new Label("Quit game?");
title.getStyleClass().add("quit-dialog-title");

Region headerSpacer = new Region();
HBox.setHgrow(headerSpacer, Priority.ALWAYS);

closeButton = new Button("X");
closeButton.setFocusTraversable(false);
closeButton.getStyleClass().add("quit-dialog-close-button");

HBox header = new HBox(12, title, headerSpacer, closeButton);
header.setAlignment(Pos.CENTER_LEFT);

Label description = new Label(
"Choose what to do with your current session.");
description.getStyleClass().add("quit-dialog-description");
description.setWrapText(true);

// The four 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");

continueButton.setFocusTraversable(false);
saveButton.setFocusTraversable(false);
saveAndQuitMainMenuButton.setFocusTraversable(false);
saveAndQuitGameButton.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");

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

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

// Status label - hidden until the controller writes something into it.
statusLabel = new Label("");
statusLabel.getStyleClass().add("quit-dialog-status");
statusLabel.setWrapText(true);
statusLabel.setVisible(false);
statusLabel.setManaged(false);

// Assemble panel.
panel = new VBox(18, header, description, buttonColumn, statusLabel);
panel.setAlignment(Pos.TOP_LEFT);
panel.setPadding(new Insets(26, 30, 28, 30));
panel.setMaxWidth(440);
panel.setPrefWidth(440);
// Lock the panel to a fixed height so it doesn't visibly grow or
// 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.getStyleClass().add("quit-dialog-panel");

// Dimmer wrapper.
getRootPane().getStyleClass().add("quit-dialog-overlay-dimmer");
getRootPane().getChildren().add(panel);
StackPane.setAlignment(panel, Pos.CENTER);

registerButton(QuitDialogActions.CONTINUE, continueButton);
registerButton(QuitDialogActions.SAVE, saveButton);
registerButton(QuitDialogActions.SAVE_AND_QUIT_TO_MAIN_MENU,
saveAndQuitMainMenuButton);
registerButton(QuitDialogActions.SAVE_AND_QUIT_GAME,
saveAndQuitGameButton);
// The X button reuses the CONTINUE action so we don't need a
// dedicated enum entry just for "close".
closeButton.setOnAction(e -> continueButton.fire());
}

/** {@inheritDoc} */
@Override
protected void initStyling() {
// Styling is applied via style classes assigned in initLayout().
}
}
Loading

0 comments on commit eb7fef7

Please sign in to comment.