From 577fdaf45ef670a1ba1df3f00f9ba0f3e545f68b Mon Sep 17 00:00:00 2001 From: EspenTinius Date: Mon, 25 May 2026 00:47:51 +0200 Subject: [PATCH 1/3] fikset --- .../ntnu/idi/idatt2003/g40/mappe/Main.java | 17 +- .../idatt2003/g40/mappe/model/SaveGame.java | 130 +++-- .../g40/mappe/service/SaveGameService.java | 510 +++++++++++++++--- .../idatt2003/g40/mappe/view/ViewEnum.java | 5 + .../view/creategame/CreateGameActions.java | 8 + .../view/creategame/CreateGameController.java | 194 +++++++ .../mappe/view/creategame/CreateGameView.java | 381 +++++++++++++ .../view/playgame/PlayGameController.java | 88 ++- src/main/resources/saves/Halleluja.json | 6 + src/main/resources/saves/Newbie.json | 6 + src/main/resources/saves/bn.json | 6 + src/main/resources/saves/bnl.json | 6 + src/main/resources/styles.css | 122 +++++ 13 files changed, 1339 insertions(+), 140 deletions(-) create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameActions.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java create mode 100644 src/main/resources/saves/Halleluja.json create mode 100644 src/main/resources/saves/Newbie.json create mode 100644 src/main/resources/saves/bn.json create mode 100644 src/main/resources/saves/bnl.json diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java index 0e2d3cb..f477328 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java @@ -9,6 +9,8 @@ import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; import edu.ntnu.idi.idatt2003.g40.mappe.utils.ConfigValues; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewManager; +import edu.ntnu.idi.idatt2003.g40.mappe.view.creategame.CreateGameController; +import edu.ntnu.idi.idatt2003.g40.mappe.view.creategame.CreateGameView; import edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView; import edu.ntnu.idi.idatt2003.g40.mappe.view.mainmenu.MainMenuController; import edu.ntnu.idi.idatt2003.g40.mappe.view.mainmenu.MainMenuView; @@ -86,11 +88,19 @@ public void start(final Stage stage) throws Exception { // Play game (mellom hovedmeny og spillet) PlayGameView playGameView = new PlayGameView(); - new PlayGameController(playGameView, eventManager); + SaveGameService saveGameService = new SaveGameService(); + PlayGameController playGameController = + new PlayGameController(playGameView, eventManager, saveGameService); // Last lagrede spill fra disk. - SaveGameService saveGameService = new SaveGameService(); - playGameView.setSaves(saveGameService.loadSaves()); + playGameController.refresh(); + + // Create game (mellom play-game og selve spillet) + CreateGameView createGameView = new CreateGameView(); + CreateGameController createGameController = + new CreateGameController(createGameView, eventManager, saveGameService); + // Refresh save-listen etter at en ny save er skrevet til disk. + createGameController.setOnSaveCreated(playGameController::refresh); // Settings SettingsView settingsView = new SettingsView(); @@ -152,6 +162,7 @@ public void start(final Stage stage) throws Exception { // Register all views viewManager.addView(mainMenuView); viewManager.addView(playGameView); + viewManager.addView(createGameView); viewManager.addView(settingsView); viewManager.addView(inGameView); viewManager.setScene(mainMenuView); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java index e22273f..ff00e77 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java @@ -4,44 +4,102 @@ * Represents one save game entry. * *

- * Holds the display name and the current balance for a single + * Holds the display name, current balance, starting capital and + * (optionally) the path to a custom stock data file for a single * saved game. *

+ * + *

+ * When {@link #getStockDataPath()} returns {@code null} the save is + * expected to be loaded with the default bundled stock data file. + *

*/ public class SaveGame { - /** Display name of the save. */ - private final String name; - - /** Current balance in the save. */ - private final double balance; - - /** - * Constructor. - * - * @param name the display name of the save. - * @param balance the current balance value. - */ - public SaveGame(final String name, final double balance) { - this.name = name; - this.balance = balance; - } - - /** - * Getter method for the name. - * - * @return the save name. - */ - public String getName() { - return name; - } - - /** - * Getter method for the balance. - * - * @return the balance value. - */ - public double getBalance() { - return balance; - } -} \ No newline at end of file + /** Display name of the save. */ + private final String name; + + /** Current balance in the save. */ + private final double balance; + + /** The starting capital chosen when the save was created. */ + private final double startingCapital; + + /** + * Absolute path to a custom stock data file, or {@code null} + * if the save should use the default bundled stock data. + * */ + private final String stockDataPath; + + /** + * Full constructor. + * + * @param name the display name of the save. + * @param balance the current balance value. + * @param startingCapital the starting capital chosen on creation. + * @param stockDataPath absolute path to a custom stock data file, + * or {@code null} to use the default file. + */ + public SaveGame(final String name, + final double balance, + final double startingCapital, + final String stockDataPath) { + this.name = name; + this.balance = balance; + this.startingCapital = startingCapital; + this.stockDataPath = stockDataPath; + } + + /** + * Convenience constructor matching the legacy "name + balance" format. + * + *

+ * Starting capital is defaulted to the balance value and + * {@code stockDataPath} is left {@code null} so the default stock + * data file is used. + *

+ * + * @param name the display name of the save. + * @param balance the current balance value. + */ + public SaveGame(final String name, final double balance) { + this(name, balance, balance, null); + } + + /** + * Getter method for the name. + * + * @return the save name. + */ + public String getName() { + return name; + } + + /** + * Getter method for the balance. + * + * @return the balance value. + */ + public double getBalance() { + return balance; + } + + /** + * Getter method for the starting capital. + * + * @return the starting capital chosen on creation. + */ + public double getStartingCapital() { + return startingCapital; + } + + /** + * Getter method for the custom stock data path. + * + * @return the absolute path to a custom stock data file, + * or {@code null} if the save uses the default data. + */ + public String getStockDataPath() { + return stockDataPath; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java index 82b310d..2c6aaed 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java @@ -3,105 +3,459 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Stream; /** * Service for loading and saving {@link SaveGame} entries from disk. * *

- * Save file format (one entry per line): + * Each save is stored as a separate JSON file in a single directory, + * which lets several saves co-exist independently. *

- * + * + *

JSON file format:

+ * *
- * # Comment lines start with hash
- * SaveName, 1234567.89
+ * {
+ *   "name": "MySave",
+ *   "balance": 10000.00,
+ *   "startingCapital": 10000.00,
+ *   "stockDataPath": null
+ * }
  * 
* *

- * Lines that don't match the expected format are skipped. + * The service exposes both a "load all saves" entry point (used by + * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.playgame.PlayGameView}) + * and a "save one" entry point (used by the create-game flow). The + * JSON parsing logic is intentionally minimal and tailored to this + * format — invalid files are skipped rather than throwing. *

*/ public class SaveGameService { - /** Default location of the save file. */ - private static final String DEFAULT_PATH = "src/main/resources/saves.txt"; - - /** Path to the save file. */ - private final String filePath; - - /** - * Constructor with default path. - */ - public SaveGameService() { - this(DEFAULT_PATH); - } - - /** - * Constructor with custom path. - * - * @param filePath the path to the save file. - */ - public SaveGameService(final String filePath) { - this.filePath = filePath; - } - - /** - * Loads all save games from the file. - * - *

- * Returns an empty list if the file cannot be read or is empty. - *

- * - * @return the loaded {@link SaveGame} entries. - */ - public List loadSaves() { - List saves = new ArrayList<>(); - Path path = Paths.get(filePath); - try { - List lines = Files.readAllLines(path); - for (String line : lines) { - SaveGame save = parseLine(line); - if (save != null) { - saves.add(save); - } - } - } catch (IOException e) { - System.err.println("Could not read save file: " + e.getMessage()); + /** Default directory where save files are stored. */ + private static final String DEFAULT_DIRECTORY = "src/main/resources/saves"; + + /** Extension used for save files. */ + private static final String JSON_EXTENSION = ".json"; + + /** Path to the directory containing save files. */ + private final String directoryPath; + + /** + * Constructor with default directory. + */ + public SaveGameService() { + this(DEFAULT_DIRECTORY); + } + + /** + * Constructor with custom directory. + * + * @param directoryPath the path to the saves directory. + */ + public SaveGameService(final String directoryPath) { + this.directoryPath = directoryPath; + } + + /** + * Loads all save games from the directory. + * + *

+ * Walks the saves directory, parses every file with a + * {@code .json} extension and returns the resulting list. + * Files that fail to parse are silently skipped. + *

+ * + *

+ * Returns an empty list if the directory does not exist or cannot + * be read. + *

+ * + * @return the loaded {@link SaveGame} entries. + */ + public List loadSaves() { + List saves = new ArrayList<>(); + Path dir = Paths.get(directoryPath); + + if (!Files.isDirectory(dir)) { + return saves; + } + + try (Stream entries = Files.list(dir)) { + List jsonFiles = entries + .filter(p -> p.getFileName().toString() + .toLowerCase().endsWith(JSON_EXTENSION)) + .sorted(Comparator.comparing(Path::getFileName)) + .toList(); + + for (Path file : jsonFiles) { + SaveGame save = parseFile(file); + if (save != null) { + saves.add(save); } - return saves; - } - - /** - * Parses a single line into a {@link SaveGame}. - * - *

- * Returns null for invalid lines, comment lines, and blank lines. - *

- * - * @param line the raw line from the file. - * @return the parsed {@link SaveGame}, or null if the line is invalid. - */ - private SaveGame parseLine(final String line) { - if (line == null || line.isBlank() || line.trim().startsWith("#")) { - return null; + } + } catch (IOException e) { + System.err.println("Could not read saves directory: " + e.getMessage()); + } + return saves; + } + + /** + * Writes a single {@link SaveGame} to disk as a JSON file. + * + *

+ * The file name is derived from {@link SaveGame#getName()} sanitised + * to filesystem-safe characters and given the {@code .json} + * extension. The saves directory is created automatically if it + * doesn't exist. + *

+ * + * @param save the {@link SaveGame} object to write. + * + * @throws IllegalArgumentException if save is null or has a blank name. + * @throws IOException if the file cannot be written. + */ + public void saveGame(final SaveGame save) + throws IllegalArgumentException, IOException { + if (save == null) { + throw new IllegalArgumentException("Save game is null!"); + } + if (save.getName() == null || save.getName().trim().isEmpty()) { + throw new IllegalArgumentException("Save name is empty!"); + } + + Path dir = Paths.get(directoryPath); + if (!Files.isDirectory(dir)) { + Files.createDirectories(dir); + } + + String fileName = sanitiseFileName(save.getName()) + JSON_EXTENSION; + Path target = dir.resolve(fileName); + Files.writeString(target, toJson(save), StandardCharsets.UTF_8); + } + + /** + * Checks whether a save with the given name already exists on disk. + * + * @param name the save display name. + * + * @return {@code true} if a matching file is present, otherwise + * {@code false}. + */ + public boolean saveExists(final String name) { + if (name == null || name.trim().isEmpty()) { + return false; + } + String fileName = sanitiseFileName(name) + JSON_EXTENSION; + Path target = Paths.get(directoryPath).resolve(fileName); + return Files.isRegularFile(target); + } + + /** + * Loads a single save from an explicit file path. + * + *

+ * Used by the upload flow on the play-game screen so a save file + * located anywhere on disk can be added to the displayed list + * without having to live inside the saves directory. + *

+ * + * @param file the file to parse. + * @return the parsed {@link SaveGame}, or {@code null} if the file + * does not contain a valid save record. + */ + public SaveGame loadSaveFromFile(final Path file) { + if (file == null || !Files.isRegularFile(file)) { + return null; + } + return parseFile(file); + } + + /** + * Parses a single JSON file into a {@link SaveGame}. + * + *

+ * Returns {@code null} for files that don't contain a valid save + * record (missing required fields or malformed JSON). + *

+ * + * @param file path to the file to parse. + * @return the parsed {@link SaveGame}, or {@code null}. + */ + private SaveGame parseFile(final Path file) { + try { + String content = Files.readString(file, StandardCharsets.UTF_8); + Map fields = parseFlatJsonObject(content); + if (fields == null) { + return null; + } + + String name = fields.get("name"); + String balanceStr = fields.get("balance"); + if (name == null || balanceStr == null) { + return null; + } + + double balance = Double.parseDouble(balanceStr); + double startingCapital = fields.containsKey("startingCapital") + ? Double.parseDouble(fields.get("startingCapital")) + : balance; + + // stockDataPath may be absent or explicitly "null". + String stockDataPath = fields.get("stockDataPath"); + if (stockDataPath != null && stockDataPath.isEmpty()) { + stockDataPath = null; + } + + return new SaveGame(name, balance, startingCapital, stockDataPath); + } catch (IOException | NumberFormatException e) { + System.err.println("Skipping invalid save file " + + file.getFileName() + ": " + e.getMessage()); + return null; + } + } + + /** + * Serialises a {@link SaveGame} to a JSON object string. + * + *

+ * Output is pretty-printed with two-space indentation and a trailing + * newline so the files are human-readable. Numeric values are + * written via {@link BigDecimal#toPlainString()} so they never + * surface as scientific notation. + *

+ * + * @param save the save to convert. + * @return JSON object string. + */ + private String toJson(final SaveGame save) { + StringBuilder sb = new StringBuilder(); + sb.append("{\n"); + sb.append(" \"name\": ").append(quote(save.getName())).append(",\n"); + sb.append(" \"balance\": ") + .append(formatNumber(save.getBalance())).append(",\n"); + sb.append(" \"startingCapital\": ") + .append(formatNumber(save.getStartingCapital())).append(",\n"); + sb.append(" \"stockDataPath\": "); + if (save.getStockDataPath() == null) { + sb.append("null"); + } else { + sb.append(quote(save.getStockDataPath())); + } + sb.append("\n"); + sb.append("}\n"); + return sb.toString(); + } + + /** + * Formats a double as a plain (never scientific) decimal string. + * Uses {@link BigDecimal#valueOf(double)} for the conversion so the + * shortest round-tripping representation is preserved. + */ + private String formatNumber(final double value) { + return BigDecimal.valueOf(value).toPlainString(); + } + + /** + * Parses a flat (one level deep) JSON object into a map. + * + *

+ * Only supports string, number, boolean and null values, which is + * everything {@link SaveGame} needs. Returns {@code null} if the + * content can't be parsed. + *

+ * + *

+ * Values are returned as strings; {@code null} values are stored as + * an empty string so callers can distinguish "absent" from "null" + * using {@link Map#containsKey(Object)}. + *

+ * + * @param content the raw file content. + * @return a map of field name to raw value, or {@code null}. + */ + private Map parseFlatJsonObject(final String content) { + if (content == null) { + return null; + } + String trimmed = content.trim(); + if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) { + return null; + } + + String body = trimmed.substring(1, trimmed.length() - 1).trim(); + Map fields = new HashMap<>(); + int i = 0; + while (i < body.length()) { + i = skipWhitespace(body, i); + if (i >= body.length()) { + break; + } + + // Key (must be quoted string). + if (body.charAt(i) != '"') { + return null; + } + int keyEnd = findStringEnd(body, i); + if (keyEnd < 0) { + return null; + } + String key = unquote(body.substring(i, keyEnd + 1)); + i = keyEnd + 1; + + // Colon separator. + i = skipWhitespace(body, i); + if (i >= body.length() || body.charAt(i) != ':') { + return null; + } + i++; + i = skipWhitespace(body, i); + if (i >= body.length()) { + return null; + } + + // Value. + String value; + if (body.charAt(i) == '"') { + int valEnd = findStringEnd(body, i); + if (valEnd < 0) { + return null; } - String[] parts = line.split(","); - if (parts.length != 2) { - return null; + value = unquote(body.substring(i, valEnd + 1)); + i = valEnd + 1; + } else { + int valEnd = i; + while (valEnd < body.length() + && body.charAt(valEnd) != ',' + && !Character.isWhitespace(body.charAt(valEnd))) { + valEnd++; } - try { - String name = parts[0].trim(); - double balance = Double.parseDouble(parts[1].trim()); - if (name.isEmpty()) { - return null; - } - return new SaveGame(name, balance); - } catch (NumberFormatException e) { - return null; + String raw = body.substring(i, valEnd).trim(); + // Treat literal "null" as the absent-value sentinel. + value = "null".equals(raw) ? "" : raw; + i = valEnd; + } + fields.put(key, value); + + // Comma separator (optional for last entry). + i = skipWhitespace(body, i); + if (i < body.length() && body.charAt(i) == ',') { + i++; + } + } + return fields; + } + + /** + * Skips whitespace characters starting at {@code from} and returns + * the first index that holds a non-whitespace character. + */ + private int skipWhitespace(final String s, final int from) { + int i = from; + while (i < s.length() && Character.isWhitespace(s.charAt(i))) { + i++; + } + return i; + } + + /** + * Finds the index of the closing quote for a JSON string that + * begins at {@code start}, accounting for escaped quotes. Returns + * -1 if the string is unterminated. + */ + private int findStringEnd(final String s, final int start) { + int i = start + 1; + while (i < s.length()) { + char c = s.charAt(i); + if (c == '\\' && i + 1 < s.length()) { + i += 2; + continue; + } + if (c == '"') { + return i; + } + i++; + } + return -1; + } + + /** + * Wraps a string in double quotes and escapes characters that + * need to be escaped in JSON. + */ + private String quote(final String value) { + StringBuilder sb = new StringBuilder("\""); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (c) { + case '\\' -> sb.append("\\\\"); + case '"' -> sb.append("\\\""); + case '\n' -> sb.append("\\n"); + case '\r' -> sb.append("\\r"); + case '\t' -> sb.append("\\t"); + default -> sb.append(c); + } + } + sb.append('"'); + return sb.toString(); + } + + /** + * Removes surrounding double quotes from a JSON string token and + * unescapes the standard JSON escape sequences. + */ + private String unquote(final String token) { + String inner = token.substring(1, token.length() - 1); + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < inner.length()) { + char c = inner.charAt(i); + if (c == '\\' && i + 1 < inner.length()) { + char next = inner.charAt(i + 1); + switch (next) { + case '\\' -> sb.append('\\'); + case '"' -> sb.append('"'); + case 'n' -> sb.append('\n'); + case 'r' -> sb.append('\r'); + case 't' -> sb.append('\t'); + default -> sb.append(next); } + i += 2; + } else { + sb.append(c); + i++; + } + } + return sb.toString(); + } + + /** + * Sanitises a save name to a safe filesystem identifier. + * + *

+ * All characters outside {@code [A-Za-z0-9_-]} are replaced with + * underscores. This keeps the on-disk file name predictable while + * preserving the original display name in the JSON content. + *

+ */ + private String sanitiseFileName(final String name) { + String trimmed = name.trim(); + String safe = trimmed.replaceAll("[^A-Za-z0-9_-]", "_"); + if (safe.isEmpty()) { + safe = "save"; } -} \ No newline at end of file + return safe; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewEnum.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewEnum.java index b411d57..aab273e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewEnum.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/ViewEnum.java @@ -19,6 +19,11 @@ public enum ViewEnum { * */ PLAY_GAME, + /** + * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.creategame.CreateGameView}. + * */ + CREATE_GAME, + /** * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView}. * */ diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameActions.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameActions.java new file mode 100644 index 0000000..366aa7d --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameActions.java @@ -0,0 +1,8 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.creategame; + +public enum CreateGameActions { + USE_DEFAULT_STOCKS, + CHOOSE_STOCK_FILE, + CANCEL, + CREATE_GAME, +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java new file mode 100644 index 0000000..e671f6c --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java @@ -0,0 +1,194 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.creategame; + +import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; +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 java.io.File; +import java.io.IOException; + +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.stage.FileChooser; +import javafx.stage.Window; + +/** + * Controller for the {@link CreateGameView}. + * + *

Extends {@link ViewController}.

+ * + *

+ * Handles the four interactions on the create-game screen: picking + * the default stock data, choosing a custom stock data file, cancel + * (back to the play-game screen), and finally creating the save. + *

+ * + *

+ * When a save is successfully written to disk a callback can be + * notified so the play-game view can refresh its save list. + *

+ */ +public class CreateGameController extends ViewController { + + /** Service used to write new saves to disk. */ + private final SaveGameService saveGameService; + + /** + * Callback invoked after a save has been written successfully. + * Set by {@link #setOnSaveCreated(Runnable)} and defaults to a + * no-op so the controller is usable without wiring it up. + * */ + private Runnable onSaveCreated = () -> { }; + + /** + * Constructor. + * + * @param view the {@link CreateGameView} this controller + * is attached to. + * @param eventManager the active {@link EventManager}. + * @param saveGameService the {@link SaveGameService} used to write + * the new save to disk. + */ + public CreateGameController(final CreateGameView view, + final EventManager eventManager, + final SaveGameService saveGameService) { + super(view, eventManager); + if (saveGameService == null) { + throw new IllegalArgumentException("SaveGameService is null!"); + } + this.saveGameService = saveGameService; + } + + /** + * Sets the callback invoked after a save has been written to disk. + * + * @param handler callback to run on successful save creation. + */ + public void setOnSaveCreated(final Runnable handler) { + this.onSaveCreated = (handler != null) ? handler : () -> { }; + } + + /** + * {@inheritDoc} + * + *

Wires the four create-game buttons.

+ */ + @Override + protected void initInteractions() { + getViewElement().setOnAction(CreateGameActions.USE_DEFAULT_STOCKS, + () -> getViewElement().selectDefaultStocks()); + + getViewElement().setOnAction(CreateGameActions.CHOOSE_STOCK_FILE, + this::handleChooseStockFile); + + getViewElement().setOnAction(CreateGameActions.CANCEL, + () -> changeScene(ViewEnum.PLAY_GAME)); + + getViewElement().setOnAction(CreateGameActions.CREATE_GAME, + this::handleCreateGame); + } + + /** + * Opens a {@link FileChooser} so the user can pick a custom stock + * data file. The chosen file is stored on the view so the resulting + * save can reference it. + */ + private void handleChooseStockFile() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Velg aksje fil"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Stock data (*.txt)", "*.txt"), + new FileChooser.ExtensionFilter("All files", "*.*") + ); + + Window window = getOwnerWindow(); + File selectedFile = fileChooser.showOpenDialog(window); + if (selectedFile == null) { + // User cancelled the dialog. + return; + } + + getViewElement().selectCustomStockFile(selectedFile); + } + + /** + * Validates the form state, builds a {@link SaveGame} from the + * input, writes it to disk via {@link SaveGameService} and + * navigates back to the play-game screen on success. + */ + private void handleCreateGame() { + CreateGameView view = getViewElement(); + String name = view.getFileName(); + Double capital = view.getStartingCapital(); + CreateGameView.StockSelection selection = view.getStockSelection(); + + // The button should be disabled in this case, but guard anyway. + if (name.isEmpty() + || capital == null + || selection == CreateGameView.StockSelection.NONE) { + return; + } + + if (saveGameService.saveExists(name)) { + showAlert(AlertType.WARNING, + "Save finnes allerede", + "Det finnes allerede en lagret fil med navnet \"" + + name + "\". Velg et annet filnavn."); + return; + } + + String stockDataPath = null; + if (selection == CreateGameView.StockSelection.CUSTOM) { + File file = view.getCustomStockFile(); + if (file != null) { + stockDataPath = file.getAbsolutePath(); + } + } + + SaveGame newSave = new SaveGame( + name, + capital, + capital, + stockDataPath + ); + + try { + saveGameService.saveGame(newSave); + } catch (IOException | IllegalArgumentException e) { + showAlert(AlertType.ERROR, + "Kunne ikke lagre", + "Klarte ikke å skrive save-filen:\n" + e.getMessage()); + return; + } + + onSaveCreated.run(); + changeScene(ViewEnum.PLAY_GAME); + } + + /** + * Looks up the {@link Window} that hosts the view, so we can parent + * the file chooser to it. Returns null if the view is not yet + * attached to a scene (in which case the FileChooser opens + * un-parented, which still works). + */ + private Window getOwnerWindow() { + Scene scene = getViewElement().getRootPane().getScene(); + return (scene != null) ? scene.getWindow() : null; + } + + /** + * Shows a modal {@link Alert} with the given type, header and content. + */ + private void showAlert(final AlertType type, + final String header, + final String content) { + Alert alert = new Alert(type); + alert.setTitle("Create game"); + alert.setHeaderText(header); + alert.setContentText(content); + alert.showAndWait(); + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java new file mode 100644 index 0000000..4ee8f53 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameView.java @@ -0,0 +1,381 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.creategame; + +import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewElement; +import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewEnum; +import javafx.beans.value.ChangeListener; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +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; +import javafx.scene.text.Text; +import javafx.util.StringConverter; + +import java.io.File; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Objects; + +/** + * View shown after the player clicks "Create new game" in the + * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.playgame.PlayGameView}. + * + *

+ * Layout, top-down: + *

+ *
    + *
  • Filename input field.
  • + *
  • Starting-capital chooser.
  • + *
  • Two side-by-side stock-data buttons: + * "Bruk default aksje data" and "Velg egen aksje fil".
  • + *
  • Bottom row with "Avbryt" (left corner) and + * "Create game" (right corner). The "Create game" button + * stays disabled until all three values are filled in; + * the visual state is toggled via the + * {@code create-game-button-disabled} / + * {@code create-game-button-enabled} style classes.
  • + *
+ * + *

+ * Extends {@link ViewElement} with a {@link StackPane} root so a + * background image can sit behind the central panel. + *

+ */ +public class CreateGameView extends ViewElement { + + /** Filename input field. */ + private TextField fileNameField; + + /** Starting-capital chooser. */ + private ChoiceBox startingCapitalChoice; + + /** "Bruk default aksje data" button. */ + private Button useDefaultStocksButton; + + /** "Velg egen aksje fil" button. */ + private Button chooseStockFileButton; + + /** Label that mirrors the currently selected stock-data source. */ + private Label stockSelectionLabel; + + /** "Avbryt" button - bottom-left corner. */ + private Button cancelButton; + + /** "Create game" button - bottom-right corner. */ + private Button createGameButton; + + /** Background image displayed behind the panel. */ + private ImageView backgroundImage; + + /** Central VBox stacking the input rows and bottom buttons. */ + private VBox mainPanel; + + /** Tracks the user's stock-data selection mode. */ + private StockSelection stockSelection = StockSelection.NONE; + + /** Custom stock-data file chosen via the "Velg egen aksje fil" button. */ + private File customStockFile; + + /** + * Possible states of the stock-data selection on this view. + * */ + public enum StockSelection { + /** Nothing chosen yet. */ + NONE, + /** "Bruk default aksje data" pressed. */ + DEFAULT, + /** "Velg egen aksje fil" pressed and a file picked. */ + CUSTOM + } + + /** + * Constructor. + * + *

+ * Constructs with name "CreateGameView". + *

+ */ + public CreateGameView() { + super(new StackPane(), ViewEnum.CREATE_GAME, CreateGameActions.class); + } + + /** + * Returns the filename entered by the user, trimmed. + * + * @return the entered filename, or an empty string if blank. + */ + public String getFileName() { + return fileNameField.getText() == null + ? "" : fileNameField.getText().trim(); + } + + /** + * Returns the starting capital currently selected, or {@code null} + * if nothing is selected yet. + * + * @return the selected starting capital, or {@code null}. + */ + public Double getStartingCapital() { + return startingCapitalChoice.getValue(); + } + + /** + * Returns the currently active stock-data selection mode. + * + * @return the {@link StockSelection} state. + */ + public StockSelection getStockSelection() { + return stockSelection; + } + + /** + * Returns the custom stock-data file picked by the user, or + * {@code null} if the default file is selected or no file has + * been picked yet. + * + * @return the picked stock file or {@code null}. + */ + public File getCustomStockFile() { + return customStockFile; + } + + /** + * Marks the "default stock data" option as chosen and updates + * the visual indicators accordingly. + */ + public void selectDefaultStocks() { + this.stockSelection = StockSelection.DEFAULT; + this.customStockFile = null; + stockSelectionLabel.setText("Default aksje data valgt"); + refreshStockButtonStyles(); + refreshCreateButtonState(); + } + + /** + * Records the custom stock file selection and updates the + * visual indicators. + * + * @param file the file the user picked; must not be {@code null}. + */ + public void selectCustomStockFile(final File file) { + if (file == null) { + return; + } + this.stockSelection = StockSelection.CUSTOM; + this.customStockFile = file; + stockSelectionLabel.setText("Egen fil: " + file.getName()); + refreshStockButtonStyles(); + refreshCreateButtonState(); + } + + /** + * Resets all fields back to their initial state. Called when the + * view becomes active so a previous abandoned session doesn't + * leak into the next one. + */ + public void resetFields() { + if (fileNameField != null) { + fileNameField.clear(); + } + if (startingCapitalChoice != null) { + startingCapitalChoice.getSelectionModel().clearSelection(); + } + this.stockSelection = StockSelection.NONE; + this.customStockFile = null; + if (stockSelectionLabel != null) { + stockSelectionLabel.setText("Ingen aksje data valgt"); + } + refreshStockButtonStyles(); + refreshCreateButtonState(); + } + + /** {@inheritDoc} */ + @Override + protected void initLayout() { + backgroundImage = new ImageView(new Image(Objects.requireNonNull( + getClass().getResourceAsStream("/millionsbackground.png")))); + backgroundImage.setPreserveRatio(false); + + Text title = new Text("Create new game"); + title.getStyleClass().add("create-game-title"); + + // Row 1 - filename + Label fileNameLabel = new Label("Filnavn"); + fileNameLabel.getStyleClass().add("create-game-label"); + fileNameField = new TextField(); + fileNameField.setPromptText("Skriv inn filnavn..."); + fileNameField.getStyleClass().add("create-game-input"); + VBox fileNameRow = new VBox(6, fileNameLabel, fileNameField); + + // Row 2 - starting capital + Label capitalLabel = new Label("Startkapital"); + capitalLabel.getStyleClass().add("create-game-label"); + startingCapitalChoice = new ChoiceBox<>(); + startingCapitalChoice.getItems().addAll( + 1_000.0, + 10_000.0, + 100_000.0, + 1_000_000.0, + 10_000_000.0 + ); + startingCapitalChoice.setConverter(buildCapitalConverter()); + startingCapitalChoice.getStyleClass().add("create-game-input"); + VBox capitalRow = new VBox(6, capitalLabel, startingCapitalChoice); + + // Row 3 - stock data choice (two buttons side by side) + Label stockLabel = new Label("Aksje data"); + stockLabel.getStyleClass().add("create-game-label"); + + useDefaultStocksButton = new Button("Bruk default aksje data"); + chooseStockFileButton = new Button("Velg egen aksje fil"); + + useDefaultStocksButton.setMaxWidth(Double.MAX_VALUE); + chooseStockFileButton.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(useDefaultStocksButton, Priority.ALWAYS); + HBox.setHgrow(chooseStockFileButton, Priority.ALWAYS); + + HBox stockButtonsRow = new HBox(15, + useDefaultStocksButton, chooseStockFileButton); + stockButtonsRow.setAlignment(Pos.CENTER); + + stockSelectionLabel = new Label("Ingen aksje data valgt"); + stockSelectionLabel.getStyleClass().add("create-game-selection-label"); + + VBox stockRow = new VBox(6, + stockLabel, stockButtonsRow, stockSelectionLabel); + + // Bottom row - cancel (left) / create-game (right) + cancelButton = new Button("Avbryt"); + createGameButton = new Button("Create game"); + + Region bottomSpacer = new Region(); + HBox.setHgrow(bottomSpacer, Priority.ALWAYS); + + HBox bottomRow = new HBox(20, + cancelButton, bottomSpacer, createGameButton); + bottomRow.setAlignment(Pos.CENTER); + + // Push the bottom row to the bottom of the panel. + Region middleSpacer = new Region(); + VBox.setVgrow(middleSpacer, Priority.ALWAYS); + + mainPanel = new VBox(20, + title, fileNameRow, capitalRow, stockRow, + middleSpacer, bottomRow); + mainPanel.setAlignment(Pos.TOP_CENTER); + mainPanel.setPadding(new Insets(30)); + mainPanel.setMaxWidth(620); + mainPanel.setMaxHeight(560); + + HBox centerWrapper = new HBox(mainPanel); + centerWrapper.setAlignment(Pos.CENTER); + + getRootPane().getChildren().addAll(backgroundImage, centerWrapper); + + // Make the background fill the root pane. + backgroundImage.fitWidthProperty().bind(getRootPane().widthProperty()); + backgroundImage.fitHeightProperty().bind(getRootPane().heightProperty()); + + // Hook listeners so the create-game button enables only when + // every required field has a valid value. + ChangeListener enableListener = (obs, oldV, newV) -> + refreshCreateButtonState(); + fileNameField.textProperty().addListener(enableListener); + startingCapitalChoice.valueProperty().addListener(enableListener); + + registerButton(CreateGameActions.USE_DEFAULT_STOCKS, + useDefaultStocksButton); + registerButton(CreateGameActions.CHOOSE_STOCK_FILE, + chooseStockFileButton); + registerButton(CreateGameActions.CANCEL, cancelButton); + registerButton(CreateGameActions.CREATE_GAME, createGameButton); + } + + /** {@inheritDoc} */ + @Override + protected void initStyling() { + getRootPane().getStyleClass().add("main-menu-bg"); + mainPanel.getStyleClass().add("create-game-panel"); + + useDefaultStocksButton.getStyleClass().add("create-game-stock-button"); + chooseStockFileButton.getStyleClass().add("create-game-stock-button"); + + cancelButton.getStyleClass().add("create-game-cancel-button"); + // Default the create button to its disabled style; the listeners + // will swap it to "enabled" once every field is filled in. + createGameButton.getStyleClass().add("create-game-create-button"); + createGameButton.getStyleClass().add("create-game-button-disabled"); + createGameButton.setDisable(true); + } + + /** {@inheritDoc} */ + @Override + public void onUpdate() { + resetFields(); + } + + /** + * Refreshes the highlight on the two stock-source buttons so the + * currently-active choice stands out from the inactive one. + */ + private void refreshStockButtonStyles() { + useDefaultStocksButton.getStyleClass().remove("active"); + chooseStockFileButton.getStyleClass().remove("active"); + if (stockSelection == StockSelection.DEFAULT) { + useDefaultStocksButton.getStyleClass().add("active"); + } else if (stockSelection == StockSelection.CUSTOM) { + chooseStockFileButton.getStyleClass().add("active"); + } + } + + /** + * Enables or disables the "Create game" button based on whether + * all three required values are present, and swaps the style + * classes so the colour reflects the state. + */ + private void refreshCreateButtonState() { + boolean hasName = !getFileName().isEmpty(); + boolean hasCapital = getStartingCapital() != null; + boolean hasStockChoice = stockSelection != StockSelection.NONE; + boolean enable = hasName && hasCapital && hasStockChoice; + + createGameButton.setDisable(!enable); + createGameButton.getStyleClass() + .removeAll("create-game-button-disabled", + "create-game-button-enabled"); + createGameButton.getStyleClass().add(enable + ? "create-game-button-enabled" + : "create-game-button-disabled"); + } + + /** + * Builds a {@link StringConverter} that renders the starting-capital + * values in the choice box using a "1 000 kr"-style format instead + * of the default raw double output. + */ + private StringConverter buildCapitalConverter() { + DecimalFormatSymbols symbols = new DecimalFormatSymbols(); + symbols.setGroupingSeparator(' '); + final DecimalFormat format = new DecimalFormat("#,##0", symbols); + return new StringConverter<>() { + @Override + public String toString(final Double value) { + return value == null ? "" : format.format(value) + " kr"; + } + + @Override + public Double fromString(final String text) { + // Not used - the choice box is not editable. + return null; + } + }; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/playgame/PlayGameController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/playgame/PlayGameController.java index 4ca725c..de5e565 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/playgame/PlayGameController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/playgame/PlayGameController.java @@ -21,22 +21,60 @@ *

Extends {@link ViewController}

* *

- * Handles three user actions: starting a new game, going back to the - * main menu, and uploading a custom save file from disk. + * Handles four user actions: navigating to the create-game screen, + * going back to the main menu, uploading a custom save file from + * disk, and opening a save when one of the displayed rows is + * clicked. *

*/ public class PlayGameController extends ViewController { /** - * Constructor. + * Service used to read save files from disk. Kept as a field so + * {@link #refresh()} can re-load the list after the create-game + * flow has written a new file. + * */ + private final SaveGameService saveGameService; + + /** + * Constructor with a default {@link SaveGameService} pointing at + * the bundled saves directory. * - * @param view The {@link PlayGameView} object to attach - * this controller to. + * @param view the {@link PlayGameView} this controller is + * attached to. * @param eventManager the active {@link EventManager}. */ public PlayGameController(final PlayGameView view, final EventManager eventManager) { + this(view, eventManager, new SaveGameService()); + } + + /** + * Constructor accepting an explicit {@link SaveGameService} for + * tests or custom save locations. + * + * @param view the {@link PlayGameView} this controller + * is attached to. + * @param eventManager the active {@link EventManager}. + * @param saveGameService the service to load saves with. + */ + public PlayGameController(final PlayGameView view, + final EventManager eventManager, + final SaveGameService saveGameService) { super(view, eventManager); + if (saveGameService == null) { + throw new IllegalArgumentException("SaveGameService is null!"); + } + this.saveGameService = saveGameService; + } + + /** + * Re-reads all saves from disk and pushes the new list into the + * view. Used after the create-game flow writes a new file so the + * row list stays in sync with the saves directory. + */ + public void refresh() { + getViewElement().setSaves(saveGameService.loadSaves()); } /** @@ -48,34 +86,34 @@ public PlayGameController(final PlayGameView view, @Override protected void initInteractions() { getViewElement().setOnAction(PlayGameActions.NEW_GAME, () -> - changeScene(ViewEnum.IN_GAME)); + changeScene(ViewEnum.CREATE_GAME)); getViewElement().setOnAction(PlayGameActions.BACK, () -> - changeScene(ViewEnum.MAIN_MENU)); + changeScene(ViewEnum.MAIN_MENU)); getViewElement().setOnAction(PlayGameActions.UPLOAD_SAVE, - this::handleUploadSave); + this::handleUploadSave); getViewElement().setOnSaveSelected(save -> - changeScene(ViewEnum.IN_GAME)); + changeScene(ViewEnum.IN_GAME)); } /** * Opens a {@link FileChooser} for the user to pick a save file from - * disk. Parses it using {@link SaveGameService} and appends the - * loaded saves to the view's current list. + * disk. Parses it using {@link SaveGameService#loadSaveFromFile} and + * appends the resulting save to the view's current list. * *

* Shows an alert if the user picks a file that contains no valid - * save entries. + * save entry. *

*/ private void handleUploadSave() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Choose save file"); fileChooser.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("Save files (*.txt)", "*.txt"), - new FileChooser.ExtensionFilter("All files", "*.*") + new FileChooser.ExtensionFilter("Save files (*.json)", "*.json"), + new FileChooser.ExtensionFilter("All files", "*.*") ); Window window = getOwnerWindow(); @@ -85,20 +123,24 @@ private void handleUploadSave() { return; } - SaveGameService service = - new SaveGameService(selectedFile.getAbsolutePath()); - List uploadedSaves = service.loadSaves(); + SaveGame uploadedSave = + saveGameService.loadSaveFromFile(selectedFile.toPath()); - if (uploadedSaves.isEmpty()) { + if (uploadedSave == null) { showAlert(AlertType.WARNING, - "No saves found", - "The selected file did not contain any valid save entries.\n\n" - + "Expected format (one entry per line):\n" - + " SaveName, 1234567.89"); + "No saves found", + "Den valgte filen inneholdt ingen gyldig save.\n\n" + + "Forventet JSON-format:\n" + + " {\n" + + " \"name\": \"MySave\",\n" + + " \"balance\": 10000.00,\n" + + " \"startingCapital\": 10000.00,\n" + + " \"stockDataPath\": null\n" + + " }"); return; } - getViewElement().addSaves(uploadedSaves); + getViewElement().addSaves(List.of(uploadedSave)); } /** diff --git a/src/main/resources/saves/Halleluja.json b/src/main/resources/saves/Halleluja.json new file mode 100644 index 0000000..6068147 --- /dev/null +++ b/src/main/resources/saves/Halleluja.json @@ -0,0 +1,6 @@ +{ + "name": "Halleluja", + "balance": 1000650901.43, + "startingCapital": 10000.0, + "stockDataPath": null +} diff --git a/src/main/resources/saves/Newbie.json b/src/main/resources/saves/Newbie.json new file mode 100644 index 0000000..893047a --- /dev/null +++ b/src/main/resources/saves/Newbie.json @@ -0,0 +1,6 @@ +{ + "name": "Newbie", + "balance": 10000.0, + "startingCapital": 10000.0, + "stockDataPath": null +} diff --git a/src/main/resources/saves/bn.json b/src/main/resources/saves/bn.json new file mode 100644 index 0000000..f83e817 --- /dev/null +++ b/src/main/resources/saves/bn.json @@ -0,0 +1,6 @@ +{ + "name": "bn", + "balance": 1000000.0, + "startingCapital": 1000000.0, + "stockDataPath": null +} diff --git a/src/main/resources/saves/bnl.json b/src/main/resources/saves/bnl.json new file mode 100644 index 0000000..d509cfc --- /dev/null +++ b/src/main/resources/saves/bnl.json @@ -0,0 +1,6 @@ +{ + "name": "bnl", + "balance": 1000000.0, + "startingCapital": 1000000.0, + "stockDataPath": null +} diff --git a/src/main/resources/styles.css b/src/main/resources/styles.css index 3b73285..5a91a92 100644 --- a/src/main/resources/styles.css +++ b/src/main/resources/styles.css @@ -149,6 +149,128 @@ -fx-font-size: 100px; } +/* ------------- CREATE GAME VIEW ------------- */ +/* Central panel containing the create-game form. */ +.create-game-panel { + -fx-background-color: rgba(220, 220, 220, 0.85); + -fx-background-radius: 12; + -fx-border-color: rgba(135, 206, 235, 0.9); + -fx-border-width: 3; + -fx-border-radius: 12; +} + +/* Title text at the top of the panel. */ +.create-game-title { + -fx-font-family: "System"; + -fx-font-weight: bold; + -fx-font-style: italic; + -fx-font-size: 30px; + -fx-fill: black; +} + +/* Label above each input. */ +.create-game-label { + -fx-font-family: "System"; + -fx-font-weight: bold; + -fx-font-style: italic; + -fx-font-size: 16px; + -fx-text-fill: black; +} + +/* Filename TextField + starting-capital ChoiceBox. */ +.create-game-input { + -fx-background-color: rgba(255, 255, 255, 0.9); + -fx-border-color: #bbbbbb; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-padding: 6 10; + -fx-font-style: italic; + -fx-font-size: 14px; + -fx-pref-width: 360; +} + +.create-game-input:focused { + -fx-border-color: #333333; +} + +/* Stock-data label that mirrors the current selection. */ +.create-game-selection-label { + -fx-font-family: "System"; + -fx-font-style: italic; + -fx-font-size: 13px; + -fx-text-fill: #555555; +} + +/* The two stock-data choice buttons sit side by side. */ +.create-game-stock-button { + -fx-background-color: rgba(255, 255, 255, 0.85); + -fx-background-radius: 10; + -fx-border-color: #cccccc; + -fx-border-radius: 10; + -fx-padding: 10 16; + -fx-font-weight: bold; + -fx-font-style: italic; + -fx-font-size: 14px; + -fx-text-fill: black; + -fx-cursor: hand; +} + +.create-game-stock-button:hover { + -fx-background-color: white; +} + +/* Highlight on the currently-active stock choice. */ +.create-game-stock-button.active { + -fx-background-color: rgba(135, 206, 235, 0.95); + -fx-border-color: #1976d2; +} + +/* Bottom-row "Avbryt" button (left corner). */ +.create-game-cancel-button { + -fx-background-color: rgba(200, 200, 200, 0.95); + -fx-background-radius: 10; + -fx-padding: 10 24; + -fx-font-weight: bold; + -fx-font-style: italic; + -fx-font-size: 14px; + -fx-text-fill: black; + -fx-cursor: hand; +} + +.create-game-cancel-button:hover { + -fx-background-color: rgba(220, 220, 220, 1.0); +} + +/* Bottom-row "Create game" button (right corner). Shared layout + regardless of enabled/disabled state. */ +.create-game-create-button { + -fx-background-radius: 10; + -fx-padding: 10 24; + -fx-font-weight: bold; + -fx-font-style: italic; + -fx-font-size: 14px; + -fx-text-fill: black; +} + +/* Disabled state: dark grey, semi-transparent. */ +.create-game-create-button.create-game-button-disabled { + -fx-background-color: rgba(80, 80, 80, 0.5); + -fx-text-fill: rgba(255, 255, 255, 0.7); + -fx-cursor: default; + -fx-opacity: 1.0; +} + +/* Enabled state: light grey, matching the cancel button. */ +.create-game-create-button.create-game-button-enabled { + -fx-background-color: rgba(200, 200, 200, 0.95); + -fx-text-fill: black; + -fx-cursor: hand; +} + +.create-game-create-button.create-game-button-enabled:hover { + -fx-background-color: rgba(220, 220, 220, 1.0); +} + /* ------------- TOP BAR ------------- */ .top-bar { -fx-background-color: rgba(69, 69, 69, 0.7); From ac50a58b8036dbd1230828f3c00117c61baceac9 Mon Sep 17 00:00:00 2001 From: EspenTinius Date: Mon, 25 May 2026 00:53:57 +0200 Subject: [PATCH 2/3] removing bloat savefiles --- src/main/resources/saves/bn.json | 6 ------ src/main/resources/saves/bnl.json | 6 ------ 2 files changed, 12 deletions(-) delete mode 100644 src/main/resources/saves/bn.json delete mode 100644 src/main/resources/saves/bnl.json diff --git a/src/main/resources/saves/bn.json b/src/main/resources/saves/bn.json deleted file mode 100644 index f83e817..0000000 --- a/src/main/resources/saves/bn.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "bn", - "balance": 1000000.0, - "startingCapital": 1000000.0, - "stockDataPath": null -} diff --git a/src/main/resources/saves/bnl.json b/src/main/resources/saves/bnl.json deleted file mode 100644 index d509cfc..0000000 --- a/src/main/resources/saves/bnl.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "bnl", - "balance": 1000000.0, - "startingCapital": 1000000.0, - "stockDataPath": null -} From f4b7f678cafe0b943bcc42803dbda719f6dd019b Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 01:16:26 +0200 Subject: [PATCH 3/3] fix: fixed merge conflict --- .../ntnu/idi/idatt2003/g40/mappe/Main.java | 100 +++++++++++++++--- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java index c40d4f0..d6c8602 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java @@ -70,7 +70,7 @@ static void main(String[] args) { public void start(final Stage stage) throws Exception { Scene scene = new Scene(new Pane()); scene.getStylesheets() - .add(Objects.requireNonNull(getClass().getResource("/styles.css")).toExternalForm()); + .add(Objects.requireNonNull(getClass().getResource("/styles.css")).toExternalForm()); Font.loadFont(getClass().getResourceAsStream("/Fonts/Aptos.ttf"), 16); stage.setScene(scene); stage.setWidth(ConfigValues.VIEWPORT_WIDTH.getValue()); @@ -96,15 +96,14 @@ public void start(final Stage stage) throws Exception { PlayGameView playGameView = new PlayGameView(); SaveGameService saveGameService = new SaveGameService(); PlayGameController playGameController = - new PlayGameController(playGameView, eventManager, saveGameService); - - // Last lagrede spill fra disk. + new PlayGameController(playGameView, eventManager, saveGameService); playGameController.refresh(); + // Create game (mellom play-game og selve spillet) CreateGameView createGameView = new CreateGameView(); CreateGameController createGameController = - new CreateGameController(createGameView, eventManager, saveGameService); + new CreateGameController(createGameView, eventManager, saveGameService); // Refresh save-listen etter at en ny save er skrevet til disk. createGameController.setOnSaveCreated(playGameController::refresh); @@ -127,10 +126,10 @@ public void start(final Stage stage) throws Exception { // Dashboard (default center-view in-game - første siden du ser) DashBoardView dashBoardView = new DashBoardView(); new DashBoardController(dashBoardView, - eventManager, - player, - exchange, - stocksInFile); + eventManager, + player, + exchange, + stocksInFile); // Stats page (Stats-knappen i topbaren tar deg hit) StatsView statsView = new StatsView(); @@ -139,7 +138,82 @@ public void start(final Stage stage) throws Exception { // Market page (Market-knappen tar deg hit) MarketView marketView = new MarketView(); new MarketController(marketView, - eventManager, - player, - exchange, - stocksIn \ No newline at end of file + eventManager, + player, + exchange, + stocksInFile); + + // In-game (Change "topBarView" to "topBarView2" if no summary section). + // Dashboard er default center-view. + InGameView inGameView = new InGameView(topBarView, dashBoardView.getRootPane()); + InGameController inGameController = new InGameController( + inGameView, + eventManager + ); + + // Transaction history page + TransactionsView transactionsView = new TransactionsView(); + TransactionsController transactionsController = new TransactionsController( + transactionsView, + eventManager, + player.getTransactionArchive()); + + ClickerGame clickerGame = new ClickerGame(); + FindStockGame findStockGame = new FindStockGame( + stocksInFile.stream() + .map(Stock::getSymbol) + .toList() + ); + TimeInputsGame timeInputsGame = new TimeInputsGame(); + + GameEngineView gameEngineView = new GameEngineView(); + GameEngineController gameEngineController = new GameEngineController( + gameEngineView, + eventManager, + stocksInFile.getFirst() + ); + + MiniGamesView miniGamesView = new MiniGamesView(); + new MiniGamesController( + miniGamesView, + eventManager, + stocksInFile.getFirst(), + gameEngineView, + gameEngineController, + clickerGame, + inGameView, + findStockGame, + timeInputsGame + ); + + // Wire top bar buttons til å bytte mellom dashboard / stats / market / + // transactions. Stats-knappen tar deg til stats-siden. + topBarController.setMarketIntegration( + inGameView::changeCenterView, + dashBoardView.getRootPane(), + marketView.getRootPane(), + statsView.getRootPane(), + transactionsView.getRootPane(), + transactionsController::refresh, + miniGamesView.getRootPane() + ); + + // Register all views + viewManager.addView(mainMenuView); + viewManager.addView(playGameView); + viewManager.addView(createGameView); + viewManager.addView(settingsView); + viewManager.addView(inGameView); + viewManager.setScene(mainMenuView); + + // Register all widgets + inGameController.addwidget(WidgetEnum.DASHBOARD, dashBoardView.getRootPane()); + inGameController.addwidget(WidgetEnum.MARKET, marketView.getRootPane()); + inGameController.addwidget(WidgetEnum.MINIGAMES_OVERVIEW, miniGamesView.getRootPane()); + inGameController.addwidget(WidgetEnum.MINIGAMES_ENGINE, gameEngineView.getRootPane()); + inGameController.addwidget(WidgetEnum.STATS, statsView.getRootPane()); + inGameController.addwidget(WidgetEnum.TRANSACTIONS, transactionsView.getRootPane()); + + stage.show(); + } +} \ No newline at end of file