From 10236cdb1694e42a628be6ffb334611096e0e41d Mon Sep 17 00:00:00 2001 From: Per Eric Trapnes Date: Fri, 15 May 2026 10:20:52 +0200 Subject: [PATCH] impliments hustlers app, version 1 --- .claude/settings.local.json | 7 - .../gruppe42/Controller/GameController.java | 2 +- .../gruppe42/View/Apps/HustlersApp.java | 398 +++++++++++++++++- .../gruppe42/View/Views/DesktopView.java | 41 +- 4 files changed, 425 insertions(+), 23 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2fd8d31..e69de29 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(del \"C:\\\\Users\\\\perer\\\\Documents\\\\GitHub\\\\Millions\\\\src\\\\main\\\\java\\\\edu\\\\ntnu\\\\idi\\\\idatt2003\\\\gruppe42\\\\View\\\\Apps\\\\DebtWarningApp.java\")" - ] - } -} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java index 201c4f3..cb4b47a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Controller/GameController.java @@ -52,7 +52,7 @@ public void setGameState(GameState gameState) { public void startGame() { timer = new Timer(true); final int delay = 0; - final int period = 1000; //TODO: This has to be 1000 (1 second) on release + final int period = 10; //TODO: This has to be 1000 (1 second) on release timer.scheduleAtFixedRate( new TimerTask() { diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/HustlersApp.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/HustlersApp.java index ec8981f..d18f6c6 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/HustlersApp.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Apps/HustlersApp.java @@ -2,18 +2,406 @@ import edu.ntnu.idi.idatt2003.gruppe42.Model.App; import edu.ntnu.idi.idatt2003.gruppe42.View.Popup; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import java.util.LinkedHashMap; +import java.util.Map; /** - * A popup for the Hustlers app. + * A popup for the Hustler's University tutorial app. + * Provides a wiki-style interface with a sidebar and scrollable content area + * covering all core game mechanics. */ public class HustlersApp extends Popup { + private static final String BG_ROOT = "#ffffff"; + private static final String BG_SIDEBAR = "#f4f4f4"; + private static final String BG_CONTENT = "#ffffff"; + private static final String ACCENT = "#a0720a"; + private static final String ACCENT_DIM = "#c9a84c"; + private static final String TEXT_PRIMARY = "#1a1a1a"; + private static final String TEXT_SEC = "#666666"; + private static final String DIVIDER = "#e0e0e0"; + private static final String HOVER_BG = "#e8e8e8"; + private static final String SELECTED_BG = "#fef9ec"; + + private Label activeNavItem = null; + private VBox contentBody; + private Label chapterTitleLabel; + private final Map chapters = new LinkedHashMap<>(); + /** - * Constructs a new Hustlers popup. + * Constructs the Hustler's University popup. */ public HustlersApp() { - super(400, 300, 140, 140, App.HUSTLERS); - // Add Hustlers specific content here + super(720, 480, 80, 60, App.HUSTLERS); + buildChapters(); + buildUI(); + } + + private void buildChapters() { + chapters.put("Welcome", new String[]{ + "HUSTLER'S UNIVERSITY", + "Knowledge is power, power is influence, influence is money.\n\n" + + "This database covers every system. Read it once, and never forget.\n\n " + + "Use the sidebar to find what you need fast.\n\n" + + "Skip a chapter and you might end up as a brokey\n" + + "Your call." + }); + + chapters.put("Trading", new String[]{ + "HOW TO TRADE", + "The market is where men is made, and babies run home to mommy.\n\n" + + "BUY LOW, SELL HIGH\n" + + "Open the Trading app and browse the available assets. " + + "Don't see it, after closing this window, it will auto install. " + + "Each asset has a current price and a trend indicator. " + + "Green means the price is climbing; red means it's falling.\n\n" + + "HOW TO BUY\n" + + "Select an asset, enter how many units you want, and tap TRADE. " + + "The cost is deducted from your wallet immediately, unless you dont have enough..\n\n" + + "HOW TO SELL\n" + + "Go to your Portfolio tab, select a held asset, and tap SELL. " + + "Profit = (sell price minus buy price) times units.\n\n" + + "RISK WARNING\n" + + "Prices move every in-game day. If a position tanks and you sell at a loss, " + + "that money is gone. Never invest money you need for rent.\n\n" + + "PRO TIPS\n" + + "Watch the trend for at least two days before buying.\n" + + "Diversify — don't put everything in one asset.\n" + + "Always keep enough cash to cover your weekly rent." + }); + + chapters.put("Bank", new String[]{ + "HOW THE BANK WORKS", + "The Bank app is your financial safety net — and leverage tool.\n\n" + + "SAVINGS ACCOUNT\n" + + "Deposit money into your savings account to earn daily interest. " + + "Interest compounds every in-game night, so the earlier you deposit, " + + "the more you earn by the end of the week.\n\n" + + "LOANS\n" + + "Need capital fast? Take out a loan. The bank will deposit the " + + "requested amount into your wallet immediately.\n" + + "WARNING: Loans accrue interest daily. A small loan can snowball " + + "if you ignore it. Pay it back before rent day.\n\n" + + "TRANSFERS\n" + + "You can transfer money between your wallet and your savings at any time. " + + "Remember: money in savings is NOT accessible for rent — " + + "you must withdraw it first.\n\n" + + "PRO TIPS\n" + + "Deposit surplus cash on Sunday so it earns all week.\n" + + "Never take a loan larger than you can repay in one week.\n" + + "Check your loan balance before rent day — it might have grown." + }); + + chapters.put("App Store", new String[]{ + "HOW THE APP STORE WORKS", + "The App Store lets you purchase new apps that unlock extra features.\n\n" + + "BROWSING APPS\n" + + "Open the App Store and scroll through the available apps. " + + "Each listing shows the app name, what it does, and its one-time price.\n\n" + + "BUYING AN APP\n" + + "Tap an app, read the description, and tap PURCHASE. " + + "The price is taken from your wallet and the app appears on your home screen.\n\n" + + "INSTALLED APPS\n" + + "Purchased apps stay with you permanently — you never pay again. " + + "Apps can include trading tools, extra income streams, " + + "and communication upgrades.\n\n" + + "FREE vs. PAID\n" + + "Some apps are free by default (like this one). " + + "Premium apps cost money but often pay for themselves quickly.\n\n" + + "PRO TIPS\n" + + "Buy income-boosting apps early — they compound over time.\n" + + "Don't blow your rent fund on apps. Timing is everything." + }); + + chapters.put("Email", new String[]{ + "HOW EMAIL WORKS", + "Email is how the game world communicates with you.\n\n" + + "RECEIVING EMAILS\n" + + "Emails arrive automatically — from employers, contacts, and the system. " + + "A badge on the Email app icon shows unread messages.\n\n" + + "IMPORTANT EMAILS\n" + + "Some emails contain offers, jobs, or warnings that expire. " + + "Check your inbox regularly so you don't miss time-sensitive opportunities.\n\n" + + "JOB OFFERS\n" + + "Employers send job offers via email. Accept them through the email itself. " + + "Ignoring an offer causes it to expire after a set number of days.\n\n" + + "RENT REMINDERS\n" + + "You will receive a rent reminder email before the end of each week. " + + "This is your cue to make sure your wallet has enough to cover the payment.\n\n" + + "PRO TIPS\n" + + "Read every email when it arrives — some are one-time chances.\n" + + "Rent reminders are not optional reading." + }); + + chapters.put("Messenger", new String[]{ + "HOW MESSENGER WORKS", + "Messenger is your direct line to contacts, NPCs, and deal-makers.\n\n" + + "CONVERSATIONS\n" + + "Each contact has their own chat thread. Tap a conversation to open it.\n\n" + + "STORY INTERACTIONS\n" + + "Some story events and side quests are triggered through Messenger. " + + "Replying (or not replying) to certain contacts affects your relationships " + + "and can unlock or close off opportunities.\n\n" + + "DEALS AND TIPS\n" + + "Certain contacts send you hot tips on the market or exclusive deals. " + + "These appear as messages — act quickly because they are time-limited.\n\n" + + "NOTIFICATIONS\n" + + "New messages show a badge on the Messenger icon. " + + "Don't let important messages pile up unread.\n\n" + + "PRO TIPS\n" + + "Maintain good relationships — ignoring contacts costs you opportunities.\n" + + "Market tips from contacts can be more reliable than raw price trends." + }); + + chapters.put("Rent", new String[]{ + "HOW RENT WORKS", + "Rent is collected automatically at the end of every in-game week.\n\n" + + "WHEN IT'S DUE\n" + + "Rent is charged on Sunday night, right before the new week begins. " + + "You will see a countdown in your home screen and receive a reminder email.\n\n" + + "HOW IT'S PAID\n" + + "The rent amount is deducted directly from your wallet — automatically, " + + "no action needed on your part. Make sure the money is in your wallet, " + + "NOT locked in savings.\n\n" + + "WHAT IF YOU CAN'T PAY?\n" + + "If your wallet balance is lower than the rent amount when Sunday night hits:\n" + + " You lose ONE heart.\n" + + " The rent debt carries over and is added to next week's bill.\n" + + " Losing all hearts ends the game.\n\n" + + "HOW MUCH IS RENT?\n" + + "Your rent amount is shown in the Rent widget on your home screen. " + + "It may increase as you progress — so keep earning.\n\n" + + "SURVIVAL CHECKLIST\n" + + "Check your wallet balance on Friday.\n" + + "Withdraw from savings if needed.\n" + + "Sell assets if you are short.\n" + + "Never go into Sunday underprepared." + }); + + chapters.put("Hearts", new String[]{ + "YOUR LIVES — HEARTS", + "Hearts represent how many chances you have left before it's game over.\n\n" + + "STARTING HEARTS\n" + + "You begin the game with a set number of hearts shown in the top bar.\n\n" + + "LOSING A HEART\n" + + "You lose one heart if your wallet cannot cover rent on Sunday night, " + + "or from certain in-game penalties triggered by events or choices.\n\n" + + "GAME OVER\n" + + "When you run out of hearts, the game ends. " + + "There is no revive. Manage your money or start over.\n\n" + + "CAN YOU GAIN HEARTS BACK?\n" + + "Hearts can sometimes be restored through special in-game events " + + "or achievements. Don't rely on it — protect the ones you have.\n\n" + + "PRO TIPS\n" + + "Treat every heart like real money. One bad week can cascade.\n" + + "If you're at one heart, play conservative — protect your wallet above all." + }); } -} + private void buildUI() { + chapterTitleLabel = new Label(); + chapterTitleLabel.setStyle( + "-fx-text-fill: " + ACCENT + ";" + + "-fx-font-size: 11px;" + + "-fx-font-weight: bold;" + + "-fx-font-family: 'Georgia';" + + "-fx-letter-spacing: 1px;" + ); + + contentBody = new VBox(14); + contentBody.setPadding(new Insets(22, 26, 22, 26)); + contentBody.setStyle("-fx-background-color: " + BG_CONTENT + ";"); + + HBox root = new HBox(); + root.setStyle("-fx-background-color: " + BG_ROOT + ";"); + root.setPrefSize(720, 480); + + VBox sidebar = buildSidebar(); + sidebar.setPrefWidth(148); + sidebar.setMinWidth(148); + sidebar.setMaxWidth(148); + + VBox contentWrapper = buildContentWrapper(); + HBox.setHgrow(contentWrapper, Priority.ALWAYS); + + root.getChildren().addAll(sidebar, contentWrapper); + content.getChildren().add(root); + + String firstKey = chapters.keySet().iterator().next(); + applyChapter(firstKey); + } + + private VBox buildSidebar() { + VBox sidebar = new VBox(); + sidebar.setStyle( + "-fx-background-color: " + BG_SIDEBAR + ";" + + "-fx-border-color: " + DIVIDER + ";" + + "-fx-border-width: 0 1 0 0;" + ); + + VBox header = new VBox(); + header.setPadding(new Insets(14, 12, 12, 12)); + header.setStyle("-fx-border-color: " + DIVIDER + "; -fx-border-width: 0 0 1 0;"); + + Label title = new Label("Hustler's\nUniversity"); + title.setStyle( + "-fx-text-fill: " + ACCENT + ";" + + "-fx-font-size: 11px;" + + "-fx-font-weight: bold;" + + "-fx-font-family: 'Georgia';" + + "-fx-line-spacing: 2;" + ); + header.getChildren().add(title); + + VBox nav = new VBox(1); + nav.setPadding(new Insets(8, 6, 8, 6)); + + for (String chapter : chapters.keySet()) { + Label item = new Label(chapter); + item.setPadding(new Insets(8, 10, 8, 10)); + item.setMaxWidth(Double.MAX_VALUE); + item.setStyle(navStyle(false)); + + item.setOnMouseEntered(e -> { + if (item != activeNavItem) { + item.setStyle(navHoverStyle()); + } + }); + item.setOnMouseExited(e -> { + if (item != activeNavItem) { + item.setStyle(navStyle(false)); + } + }); + item.setOnMouseClicked(e -> { + if (activeNavItem != null) { + activeNavItem.setStyle(navStyle(false)); + } + activeNavItem = item; + item.setStyle(navStyle(true)); + applyChapter(chapter); + }); + + nav.getChildren().add(item); + } + + ScrollPane navScroll = new ScrollPane(nav); + navScroll.setFitToWidth(true); + navScroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + navScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + VBox.setVgrow(navScroll, Priority.ALWAYS); + + sidebar.getChildren().addAll(header, navScroll); + return sidebar; + } + + private VBox buildContentWrapper() { + HBox titleBar = new HBox(); + titleBar.setPadding(new Insets(13, 20, 13, 20)); + titleBar.setAlignment(Pos.CENTER_LEFT); + titleBar.setStyle( + "-fx-background-color: " + BG_CONTENT + ";" + + "-fx-border-color: " + DIVIDER + ";" + + "-fx-border-width: 0 0 1 0;" + ); + titleBar.getChildren().add(chapterTitleLabel); + + ScrollPane scroll = new ScrollPane(contentBody); + scroll.setFitToWidth(true); + scroll.setStyle( + "-fx-background: " + BG_CONTENT + ";" + + "-fx-background-color: " + BG_CONTENT + ";" + ); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + VBox.setVgrow(scroll, Priority.ALWAYS); + + VBox wrapper = new VBox(titleBar, scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + return wrapper; + } + + private void applyChapter(String key) { + String[] content = chapters.get(key); + if (content == null) { + return; + } + chapterTitleLabel.setText(content[0]); + contentBody.getChildren().clear(); + renderContent(content[1]); + } + + private void renderContent(String rawText) { + for (String para : rawText.split("\n\n")) { + para = para.trim(); + if (para.isEmpty()) { + continue; + } + if (isSectionHeader(para)) { + Region divider = new Region(); + divider.setPrefHeight(1); + divider.setMaxWidth(32); + divider.setStyle("-fx-background-color: " + ACCENT_DIM + ";"); + VBox.setMargin(divider, new Insets(4, 0, 5, 0)); + + Label header = new Label(para); + header.setStyle( + "-fx-text-fill: " + ACCENT + ";" + + "-fx-font-size: 10px;" + + "-fx-font-weight: bold;" + + "-fx-font-family: 'Georgia';" + ); + contentBody.getChildren().addAll(divider, header); + } else { + TextFlow flow = new TextFlow(); + flow.setLineSpacing(3); + String[] lines = para.split("\n"); + for (int i = 0; i < lines.length; i++) { + Text t = new Text((i > 0 ? "\n" : "") + lines[i]); + t.setStyle("-fx-fill: " + TEXT_PRIMARY + ";"); + t.setFont(Font.font("Georgia", 12.5)); + flow.getChildren().add(t); + } + contentBody.getChildren().add(flow); + } + } + } + + private boolean isSectionHeader(String text) { + String stripped = text.replaceAll("[^a-zA-Z]", ""); + if (stripped.isEmpty()) { + return false; + } + return stripped.equals(stripped.toUpperCase()) && text.length() < 60; + } + + private String navStyle(boolean selected) { + return "-fx-text-fill: " + (selected ? ACCENT : TEXT_SEC) + ";" + + "-fx-font-size: 11px;" + + "-fx-font-family: 'Georgia';" + + "-fx-background-color: " + (selected ? SELECTED_BG : "transparent") + ";" + + "-fx-background-radius: 3;" + + "-fx-cursor: hand;" + + (selected + ? "-fx-border-color: " + ACCENT_DIM + "; -fx-border-width: 0 0 0 2; -fx-border-radius: 0;" + : ""); + } + + private String navHoverStyle() { + return "-fx-text-fill: " + TEXT_PRIMARY + ";" + + "-fx-font-size: 11px;" + + "-fx-font-family: 'Georgia';" + + "-fx-background-color: " + HOVER_BG + ";" + + "-fx-background-radius: 3;" + + "-fx-cursor: hand;"; + } +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java index 642cd6c..c324094 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/gruppe42/View/Views/DesktopView.java @@ -4,6 +4,10 @@ import edu.ntnu.idi.idatt2003.gruppe42.Controller.ViewControllers.DesktopViewController; import edu.ntnu.idi.idatt2003.gruppe42.Model.App; import edu.ntnu.idi.idatt2003.gruppe42.Model.Player; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; @@ -14,6 +18,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.effect.GaussianBlur; +import javafx.scene.input.KeyCode; import javafx.scene.layout.BorderPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; @@ -45,6 +50,7 @@ public enum ModalLayer { RAPPORT, WARNING, GAME_OVER } private static final int ROWS = 6; private static final int HEART_COUNT = 3; private static final double BACKDROP_BLUR = 8.0; + private static final String FREEDAY = "Freeday"; private final DesktopViewController controller; private final BorderPane root; @@ -89,7 +95,7 @@ public BorderPane getRoot() { if (newScene != null) root.requestFocus(); }); root.setOnKeyPressed(event -> { - if (event.getCode() == javafx.scene.input.KeyCode.R) { + if (event.getCode() == KeyCode.R) { loadStylesheets(); System.out.println("[CSS] Stylesheets reloaded"); } @@ -124,9 +130,9 @@ private void loadStylesheets() { // DEV_CSS_RELOAD: copies a CSS file to a unique temp path so JavaFX can't cache it private String devCopyToTemp(final String sourcePath) throws Exception { - java.nio.file.Path src = java.nio.file.Paths.get(sourcePath); - java.nio.file.Path tmp = java.nio.file.Files.createTempFile("css_", ".css"); - java.nio.file.Files.copy(src, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + Path src = Paths.get(sourcePath); + Path tmp = Files.createTempFile("css_", ".css"); + Files.copy(src, tmp, StandardCopyOption.REPLACE_EXISTING); tmp.toFile().deleteOnExit(); return tmp.toUri().toString(); } @@ -186,6 +192,14 @@ private void recompute() { setLocked(gridArea, anyModal); setLocked(bottomBar, anyModal); for (Node p : popupRoots) { + if (p instanceof StackPane wrapper && !wrapper.getChildren().isEmpty()) { + Node content = wrapper.getChildren().get(0); + if (content.getStyleClass().contains("popup-root")) { + setLocked(content, anyModal); + wrapper.setDisable(anyModal); + continue; + } + } setLocked(p, anyModal); } @@ -350,10 +364,10 @@ private HBox createStatusBox() { TimeAndWeatherController twc = controller.getTimeAndWeatherController(); Label weather = styledLabel("desktop-label"); - Label temp = styledLabel("desktop-label"); - Label day = styledLabel("desktop-label-bold"); - Label week = styledLabel("desktop-label-bold"); - Label clock = styledLabel("desktop-label-bold"); + Label temp = styledLabel("desktop-label"); + Label day = styledLabel("desktop-label-bold"); + Label week = styledLabel("desktop-label-bold"); + Label clock = styledLabel("desktop-label-bold"); weather.setText(twc.weatherProperty().get()); temp.setText(twc.temperatureProperty().get() + "°C"); @@ -363,8 +377,15 @@ private HBox createStatusBox() { twc.hourProperty().addListener((o, ov, nv) -> Platform.runLater(() -> clock.setText(twc.getTimeString()))); - twc.dayIndexProperty().addListener((o, ov, nv) - -> Platform.runLater(() -> day.setText(twc.getDayOfWeekString()))); + twc.dayIndexProperty() + .addListener( + (o, ov, nv) -> + Platform.runLater( + () -> { + String dayName = twc.getDayOfWeekString(); + day.setText(dayName); + nextWeekButton.setVisible(dayName.equals("SUN")); + })); twc.weekIndexProperty().addListener((o, ov, nv) -> Platform.runLater(() -> week.setText(twc.getWeekString()))); twc.weatherProperty().addListener((o, ov, nv)